Unit 2 · Data & Control Flow

Lesson · Unit 2 · 9 min read

List methods & slicing, the operations you'll run a hundred times a day.

Once you have a list, you'll spend most of your time adding to it, removing from it, sorting it, or grabbing pieces of it. Here are the methods that matter, the ones you can mostly ignore, and the slicing tricks worth knowing.

Section · 01

Adding things to a list

Three methods. The one you reach for depends on what you’re adding and where:

queue = ["alpha", "bravo"]

# append — one item at the end
queue.append("charlie")
# ["alpha", "bravo", "charlie"]

# insert — at a specific index (slow on big lists; rare in practice)
queue.insert(0, "first")
# ["first", "alpha", "bravo", "charlie"]

# extend — merge another iterable in
queue.extend(["delta", "echo"])
# ["first", "alpha", "bravo", "charlie", "delta", "echo"]

The classic mistake: using append when you wanted extend:

tasks = ["wake up", "coffee"]
batch = ["check email", "stand-up"]

tasks.append(batch)
# ["wake up", "coffee", ["check email", "stand-up"]]   — nested!

tasks.extend(batch)
# ["wake up", "coffee", "check email", "stand-up"]    — flat

Rule of thumb: append adds the thing you pass as a single item. extend unpacks the thing and adds each element. Strings count as iterables, so list.extend("abc")adds three characters to your list. That’s usually not what you want.

Section · 02

Removing things

Four ways, each useful in a different situation:

items = ["pen", "pencil", "eraser", "ruler", "pencil"]

# pop — remove by index, returns the value
last = items.pop()           # "pencil" (the last one)
# items: ["pen", "pencil", "eraser", "ruler"]

first = items.pop(0)         # "pen"
# items: ["pencil", "eraser", "ruler"]

# remove — remove by value (first match only)
items.remove("eraser")
# items: ["pencil", "ruler"]

# del — remove by index without returning anything
del items[0]
# items: ["ruler"]

# clear — wipe everything
items.clear()
# items: []

remove() raises ValueErrorif the value isn’t in the list. pop() raises IndexError if the index is out of range. If you want safe versions, check membership first or wrap in try / except.

Section · 03

Sorting

Two ways. Pick based on whether you want to keep the original order:

scores = [88, 71, 95, 60, 82]

# sorted() — returns a NEW list, original unchanged
ranked = sorted(scores)
# ranked = [60, 71, 82, 88, 95]
# scores = [88, 71, 95, 60, 82]    (unchanged)

# .sort() — sorts IN PLACE, returns None
scores.sort()
# scores = [60, 71, 82, 88, 95]    (changed)

Both accept reverse=True and a key function:

# Descending
sorted(scores, reverse=True)
# [95, 88, 82, 71, 60]

# Sort by a derived value — here, length of each string
words = ["the", "quickest", "brown", "fox"]
sorted(words, key=len)
# ["the", "fox", "brown", "quickest"]

# Sort dicts by a field
players = [
    {"name": "Ada", "score": 92},
    {"name": "Ben", "score": 71},
    {"name": "Cal", "score": 84},
]
sorted(players, key=lambda p: p["score"], reverse=True)
# [{"name": "Ada", ...}, {"name": "Cal", ...}, {"name": "Ben", ...}]

The single most common sorting bug

# WRONG — .sort() returns None
ranked = scores.sort()
print(ranked)        # None — your scores are sorted but ranked is useless

# RIGHT — use sorted() if you want a return value
ranked = sorted(scores)

If a method’s job is to change something, it usually returns None in Python. That includes .sort(), .append(), .reverse(), .remove(), and .clear(). Don’t assign their results.

Section · 04

Slicing — the underrated tool

You met slicing in the intro lesson. The full form has a step:

numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

numbers[2:7]          # [2, 3, 4, 5, 6]       — start:stop
numbers[::2]          # [0, 2, 4, 6, 8]       — every 2nd item
numbers[1::2]         # [1, 3, 5, 7, 9]       — every 2nd starting at 1
numbers[::-1]         # [9, 8, 7, 6, 5, 4, 3, 2, 1, 0]   — reversed!
numbers[::-2]         # [9, 7, 5, 3, 1]

numbers[::-1] is the Pythonic way to reverse a list (or string) without mutating it.

Slice assignment

# Replace a chunk in place
numbers = [0, 1, 2, 3, 4]
numbers[1:3] = ["a", "b", "c"]
# [0, "a", "b", "c", 3, 4]   — the replacement can be a different length!

# Insert without replacing
numbers[2:2] = ["X", "Y"]
# [0, "a", "X", "Y", "b", "c", 3, 4]

# Clear a chunk
numbers[1:5] = []
# [0, "c", 3, 4]

Slice assignment is powerful but easy to abuse — you can usually accomplish the same thing more readably with append, extend, or insert.

Section · 05

The reference vs copy trap

This one bites every beginner exactly once:

original = [1, 2, 3]
also = original              # NOT a copy — same list, two names

also.append(4)
print(original)              # [1, 2, 3, 4]   — surprise!

Three ways to actually copy:

copy1 = original[:]          # slice copy
copy2 = original.copy()      # .copy() method (clearest)
copy3 = list(original)       # list() constructor

All three give you a shallow copy — a new outer list, but the elements inside are still shared references. That only matters when the elements are themselves mutable (lists of lists, dicts of dicts). For those cases, reach for copy.deepcopy() from the standard library:

from copy import deepcopy

grid = [[0, 0], [0, 0]]
twin = deepcopy(grid)

twin[0][0] = 9
print(grid)     # [[0, 0], [0, 0]]   — untouched

You probably won’t need deepcopyon day one. Just remember: if changing your “copy” also changes the original, you didn’t actually copy it.

Curriculum source

Lesson content is original to YorkSims. Topic structure aligns with Python for Everybody by Dr. Charles R. Severance (py4e.com), licensed under Creative Commons Attribution 3.0 Unported.