Lists & Sequences
1 · The lesson
readA list is an ordered, mutable collection of anything. It's the workhorse container in Python — when you don't know which data structure to reach for, you reach for a list and refactor later.
You met indexing and slicing in Strings. Lists use the same syntax but with one enormous difference: you can change them in place.
1. What a List Is
nums = [1, 2, 3, 4] # all the same type — common mixed = [1, "two", 3.0, True, None] # heterogeneous — also legal empty = [] # zero items
Three things to internalise:
- Ordered — items keep the order you put them in.
nums[0]is always1. - Mutable — you can change, add, and remove items after creation.
- Heterogeneous — items don't have to share a type. Use this sparingly; mixed lists are harder to reason about.
2. Creating Lists
a = [1, 2, 3] # literal — most common b = list("hello") # ['h', 'e', 'l', 'l', 'o'] — from any iterable c = list(range(5)) # [0, 1, 2, 3, 4] d = [0] * 5 # [0, 0, 0, 0, 0] — repeat a value
The [x] * n trick is great for fixed-size buffers of immutable values — but it has a sharp edge with mutable values. See Common Mistakes below.
3. Indexing & Slicing
Indexing and slicing work exactly as they do for strings.
letters = ['a', 'b', 'c', 'd', 'e'] print(letters[0]) # 'a' — first print(letters[-1]) # 'e' — last print(letters[1:4]) # ['b', 'c', 'd'] — slice [start:stop) print(letters[::2]) # ['a', 'c', 'e'] — every other print(letters[::-1]) # ['e', 'd', 'c', 'b', 'a'] — reversed copy
The difference from strings: slots are writable.
letters[0] = 'X' print(letters) # ['X', 'b', 'c', 'd', 'e'] letters[1:3] = ['Y', 'Z', 'Q'] # slice assignment can resize print(letters) # ['X', 'Y', 'Z', 'Q', 'd', 'e']
setup added so this can run · defines letters
letters = ["alpha", "beta", "gamma"]
Strings would throw TypeError on either of those. Lists welcome it.
4. Mutating Methods
These methods change the list in place and return None — they don't give you back a new list.
nums = [1, 2, 3] nums.append(4) # [1, 2, 3, 4] — add one item to the end nums.extend([5, 6]) # [1, 2, 3, 4, 5, 6] — add every item from an iterable nums.insert(0, 0) # [0, 1, 2, 3, 4, 5, 6] — insert at index i nums.remove(3) # [0, 1, 2, 4, 5, 6] — remove first occurrence of value 3 last = nums.pop() # last == 6, nums == [0, 1, 2, 4, 5] — remove + return last first = nums.pop(0) # first == 0, nums == [1, 2, 4, 5] — remove + return at index nums.clear() # [] — empty the list
append(x) vs extend(iter) trips up beginners — see Common Mistakes.
5. Sorting
Two ways. Pick deliberately.
nums = [3, 1, 4, 1, 5, 9, 2, 6] nums.sort() # in-place — nums is now [1, 1, 2, 3, 4, 5, 6, 9] nums.sort(reverse=True) # in-place descending snapshot = [3, 1, 4, 1, 5] new = sorted(snapshot) # returns a new list — snapshot unchanged print(snapshot, new) # [3, 1, 4, 1, 5] [1, 1, 3, 4, 5]
The key= argument is the real power. It takes a function that produces the sort key for each item.
words = ["banana", "fig", "cherry", "apple"] print(sorted(words, key=len)) # ['fig', 'apple', 'banana', 'cherry'] print(sorted(words, key=str.lower)) # case-insensitive alphabetical people = [("Ada", 36), ("Linus", 54), ("Grace", 85)] print(sorted(people, key=lambda p: p[1])) # sort by age
key= is lazy — it calls your function once per item, not once per comparison. Cheap.
6. Reverse — Two Flavours
nums = [1, 2, 3] nums.reverse() # in-place — nums is now [3, 2, 1] original = [1, 2, 3] for x in reversed(original): print(x) # 3, 2, 1 — but original is untouched
reversed() returns an iterator — fine for looping, but wrap it in list(...) if you need a list.
7. Built-ins That Take a List
nums = [4, 1, 7, 3, 9] print(len(nums)) # 5 print(min(nums)) # 1 print(max(nums)) # 9 print(sum(nums)) # 24
min and max accept a key= argument too, mirroring sorted.
8. Membership and Searching
fruits = ["apple", "banana", "cherry", "banana"] print("apple" in fruits) # True print("kiwi" not in fruits) # True print(fruits.count("banana")) # 2 print(fruits.index("cherry")) # 2 — first index where it lives
.index(x) raises ValueError if x isn't there. If you're not sure, check with in first or use try/except.
9. Copying — The Aliasing Trap
This is where beginners lose a weekend.
a = [1, 2, 3] b = a # NOT a copy — b is another name for the same list b.append(4) print(a) # [1, 2, 3, 4] — a changed too
b = a binds the name b to the same list object. Two names, one list. To get an independent copy:
a = [1, 2, 3] b = a.copy() # or list(a), or a[:] b.append(4) print(a, b) # [1, 2, 3] [1, 2, 3, 4]
.copy() is a shallow copy — nested lists are still shared. For deeply nested structures, use copy.deepcopy() from the standard library.
10. The Immutable Cousin — Tuples
A tuple is a list that can't change.
point = (3, 4) # parentheses, comma-separated print(point[0]) # 3 # point[0] = 9 # TypeError: 'tuple' object does not support item assignment
Use tuples for fixed-shape records (a coordinate, an RGB colour, a database row) where mutation would be a bug. They're also hashable, which means they can be dictionary keys — lists can't. Full treatment in Tuples.
11. A Quick Taste of Comprehensions
You'll write a lot of "take a list, transform every item, get a new list" code. The verbose way:
nums = [1, 2, 3, 4, 5] squares = [] for n in nums: squares.append(n * n) print(squares) # [1, 4, 9, 16, 25]
The Pythonic way is a list comprehension — one line, faster, more readable once your eyes adjust:
squares = [n * n for n in nums] evens = [n for n in nums if n % 2 == 0]
setup added so this can run · defines nums
nums = [3, -1, 4, -1, 5]
Read it left-to-right: give me n*n, for each n in nums. We'll dig in properly in a later lesson.
Common Mistakes
1. [x] * n of a mutable item — they all share the same object.
grid = [[]] * 3 # looks like three empty lists grid[0].append("oops") print(grid) # [['oops'], ['oops'], ['oops']] — all three changed!
All three slots point to the same inner list. To get three independent lists:
grid = [[] for _ in range(3)] # comprehension creates a fresh list each iteration
2. Mutating a list while iterating over it.
nums = [1, 2, 3, 4, 5] for n in nums: if n % 2 == 0: nums.remove(n) # skips items, gives wrong result
Iterate over a copy (
for n in nums[:]:) or build a new list with a comprehension.
3. .append([1, 2]) vs .extend([1, 2]).
a = [0] a.append([1, 2]) # [0, [1, 2]] — one new item, which is a list a = [0] a.extend([1, 2]) # [0, 1, 2] — two new items, unpacked
append adds one thing. extend consumes an iterable.
4. Expecting .sort() to return the sorted list.
nums = [3, 1, 2] result = nums.sort() # result is None — nums was sorted in place print(result) # None
Use
sorted(nums) when you want the return value. Use nums.sort() when you want to mutate.
🎯 Your Turn — Move Zeros to the End
Write a function move_zeros(lst) that returns a new list with every 0 moved to the end, preserving the original order of the non-zero elements.
[1, 0, 3, 0, 5, 0, 7] → [1, 3, 5, 7, 0, 0, 0] [0, 0, 1] → [1, 0, 0] [1, 2, 3] → [1, 2, 3] [] → []
Skeleton:
def move_zeros(lst): # TODO 1: collect non-zero items in their original order # TODO 2: count how many zeros there were # TODO 3: return non-zeros followed by that many zeros ... print(move_zeros([1, 0, 3, 0, 5, 0, 7])) # [1, 3, 5, 7, 0, 0, 0]
Hint 1 — Two passes is fine
You don't need anything clever. One pass to collect non-zero items, then append the right number of zeros. A list comprehension with a filter handles the first pass in one line:[x for x in lst if x != 0].
Hint 2 — Count what's missing
The number of zeros islen(lst) - len(non_zeros), or just lst.count(0). Then non_zeros + [0] * zeros gives you the answer — list concatenation builds a new list.
Show full solution
def move_zeros(lst): non_zeros = [x for x in lst if x != 0] zeros = len(lst) - len(non_zeros) return non_zeros + [0] * zeros # Sanity checks print(move_zeros([1, 0, 3, 0, 5, 0, 7])) # [1, 3, 5, 7, 0, 0, 0] print(move_zeros([0, 0, 1])) # [1, 0, 0] print(move_zeros([1, 2, 3])) # [1, 2, 3] print(move_zeros([])) # []
Two lines of real work. The comprehension filters; concatenation rebuilds. We return a fresh list rather than mutating the input — generally the safer default, since the caller still has their original.
A purely in-place version is possible (swap non-zeros forward, fill the tail with zeros) and is the classic interview answer when memory matters. For everyday code, the version above reads better and runs in linear time too.
What You Learned
- A list is an ordered, mutable, heterogeneous sequence — Python's default container.
- Build with
[],list(iterable), or[x] * n(careful with mutables). - Indexing and slicing match strings, but list slots are writable — including via slice assignment.
- Mutating methods (
append,extend,insert,remove,pop,clear) returnNone. They change the list in place. .sort()mutates;sorted()returns a new list. Both acceptkey=andreverse=.b = ais aliasing, not copying. Use.copy(),list(a), ora[:]for an independent list.- Tuples are the immutable, hashable cousin — reach for them when data shouldn't change.
- List comprehensions are the idiomatic way to transform a list into another list.
Next: Conditionals — making decisions, then Loops to put your lists to work.
Practice this
on practicepython.inShort exercises that run in your browser and tell you what your code actually did, not just whether a test passed.