Lesson 07
Python Lists
The mutable cousin of the tuple — and the workhorse data structure of every Python project.
In the last lesson we worked with tuples — ordered collections that cannot be changed once created. Lists are the same thing with one major exception: lists are mutable. You can add to them, remove from them, and overwrite items in place after the list exists. That’s why lists, more than any other structure, are the workhorse of Python code: real research is iterative, and you almost always want to grow a collection as you go.
You write a list with square brackets, items separated by commas:
a_list = [1, 1.0, "one"]
nested = [[1, 2], [2, 3], [4, 2]]
print(a_list, nested)
Indexing and slicing work exactly as they do on tuples (see Lesson 06):
a_list = [1, 1.0, "one"]
nested = [[1, 2], [2, 3], [4, 2]]
print(a_list[0]) # 1
print(a_list[-1]) # "one"
print(nested[0][1]) # 2
The interesting part is what you can do after the list exists.
Growing a list — append and extend
The most common operation in any project: add an item to the end of a list. The method is append.
correspondents = [] # an empty list
correspondents.append("Voltaire")
correspondents.append("Émilie du Châtelet")
print(correspondents)
# ['Voltaire', 'Émilie du Châtelet']
Notice that append modifies the list in place and returns nothing. Don’t write correspondents = correspondents.append(...) — that will silently set correspondents to None and ruin your day.
If you want to add several items at once, use extend:
correspondents = ["Voltaire", "Émilie du Châtelet"]
correspondents.extend(["d'Alembert", "Diderot"])
print(correspondents)
extend takes an iterable (another list, a tuple, a generator) and tacks each item on. Compare:
a = [1, 2]
a.append([3, 4]) # [1, 2, [3, 4]] — one item, which happens to be a list
print(a)
a = [1, 2]
a.extend([3, 4]) # [1, 2, 3, 4] — two new items
print(a)
Removing items — remove, pop, del
There are three ways to take something out, and they answer slightly different questions:
items = ["a", "b", "c", "b"]
items.remove("b") # removes the first matching value: ['a', 'c', 'b']
print(items)
print(items.pop()) # removes and returns the LAST item: 'b' → ['a', 'c']
print(items.pop(0)) # removes and returns the item at index 0: 'a' → ['c']
del items[0] # removes by index, returns nothing
print(items)
remove is for “I know the value.” pop is for “I want it out and I want a copy of it.” del is for “I know the position and don’t need it back.”
Length, sorting, counting
A few more methods you’ll use almost daily:
items = [3, 1, 4, 1, 5, 9, 2, 6]
print(len(items)) # 8 — how many items
print(items.count(1)) # 2 — how many times 1 appears
items.sort() # sorts in place: [1, 1, 2, 3, 4, 5, 6, 9]
print(items)
print(sorted(items)) # returns a NEW sorted list, leaves items alone
items.reverse() # reverses in place
print(items)
Both sort and sorted accept a key argument — a function that says how to compare items. To sort a list of strings by length, regardless of alphabet:
words = ["the", "quality", "of", "mercy"]
words.sort(key=len)
print(words)
# ['of', 'the', 'mercy', 'quality']
For case-insensitive sorting:
names = ["Voltaire", "diderot", "Émilie"]
names.sort(key=str.lower)
print(names)
Checking membership and finding positions
correspondents = ["Voltaire", "Émilie du Châtelet", "d'Alembert", "Diderot"]
print("Voltaire" in correspondents) # True
print("Newton" not in correspondents) # True
print(correspondents.index("Diderot")) # the position of the first match
in is fast to write and reads naturally — prefer it to count(x) > 0 when you only care whether something is present.
Joining a list back into a string
This one comes up constantly when you’ve split a string apart, processed each piece, and want to glue it back together. Strings have a join method that takes a list and inserts the string between each pair of items:
words = ["the", "quality", "of", "mercy"]
print(" ".join(words)) # 'the quality of mercy'
print(", ".join(words)) # 'the, quality, of, mercy'
print("\n".join(words)) # one word per line
Note the order: it’s the separator that has the join method, and the list is the argument. This catches everyone the first time.
List comprehensions — the Python way to build lists
Once you’re comfortable with the basics, the most idiomatic way to build a new list from an old one is a list comprehension: square brackets containing an expression and a for clause.
words = ["the", "quality", "of", "mercy"]
upper = [w.upper() for w in words]
print(upper)
# ['THE', 'QUALITY', 'OF', 'MERCY']
long_only = [w for w in words if len(w) > 3]
print(long_only)
# ['quality', 'mercy']
Read it as: “for each w in words, give me w.upper().” Comprehensions are concise, easy to read once you know the pattern, and faster than the equivalent loop. We’ll see them throughout the rest of the course.
A note on clear
You’ll occasionally see a_list.clear(). It empties the list in place. It does the same thing as a_list = [], except that any other variable that was pointing at the same list will also see the change. For most projects you can prefer reassigning to a fresh empty list; reach for clear only when you specifically need the in-place behavior.
Spend some time in the console pushing items in and out of a list. Once you’re confident, continue to Lesson 08: Python Dictionaries.
Running the code
Save any snippet from this lesson to a file — say try.py — and run it from your project folder:
uv run try.py
uv run uses the project’s Python and dependencies automatically; no virtualenv to activate. If you haven’t set the project up yet, Lesson 01 walks through it.