Lists and Tuples in Python
Ordered collections in Python: creating and changing lists, searching and sorting, aliasing and copying, list comprehensions, nested lists, and immutable tuples with unpacking.
Lists: ordered, changeable collections
A list holds any number of values in order, written between square brackets and separated by commas. It is Python's general-purpose collection: a shopping list, the scores of a game, the lines of a file.
Lists share the sequence operations you met with strings: len(), indexing from 0, negative indexes, slicing and the in operator. The big difference is that lists are mutable: you can change, add and remove items after the list is created.
A list can hold values of different types, but in practice a list usually holds items of one kind, which is what makes looping over it simple.
1scores = [72, 95, 88, 61]2print(scores)3print(len(scores), scores[0], scores[-1])4print(scores[1:3])5print(95 in scores, 100 in scores)6empty = []7print(len(empty), empty)
[72, 95, 88, 61] 4 72 61 [95, 88] True False 0 []
Changing a list
Because lists are mutable, their methods usually change the list in place and return None:
append(x)adds one item at the end;extend(items)adds every item of another iterable.insert(i, x)putsxbefore positioni.remove(x)deletes the first item equal tox(and raisesValueErrorif there is none).pop()removes and returns the last item;pop(i)does the same for positioni.items[i] = xreplaces one item, anddel items[i]deletes one.
Adding at the end (append, pop()) is fast. Inserting or removing near the front moves every later item, which gets slow for very long lists.
1tasks = ["write tests", "fix bug"]2tasks.append("deploy")3tasks.insert(0, "read issue")4print(tasks)5tasks.remove("fix bug")6last = tasks.pop()7print(last, tasks)8tasks[0] = "reread issue"9tasks.extend(["review", "merge"])10del tasks[1]11print(tasks)
['read issue', 'write tests', 'fix bug', 'deploy'] deploy ['read issue', 'write tests'] ['reread issue', 'review', 'merge']
Searching and summarising
For questions about the contents of a list, reach for the built-ins first: in (is it there?), items.index(x) (where is the first one? raises ValueError if absent), items.count(x) (how many?), and sum, min, max for numbers.
in on a list checks the items one by one, so it gets slower as the list grows. When you test membership many times on a large collection, a set is much faster; the next lesson covers sets.
1rolls = [4, 6, 2, 6, 1, 6, 3]2print(rolls.index(6), rolls.count(6))3print(sum(rolls), min(rolls), max(rolls))4print(f"Average roll: {sum(rolls) / len(rolls):.2f}")5if 5 not in rolls:6 print("No five was rolled")
1 3 28 1 6 Average roll: 4.00 No five was rolled
Sorting
There are two ways to sort:
items.sort()sorts the list in place and returnsNone.sorted(iterable)returns a new sorted list and leaves the original unchanged. It works on any iterable, including strings and tuples.
Both accept reverse=True and a key function that says what to compare. key=str.lower sorts text without regard to case; key=lambda p: p[1] sorts pairs by their second item. Python's sort is stable: items that compare equal keep their original order, which lets you sort by several criteria in steps.
`numbers = numbers.sort()` sets numbers to None. Either call numbers.sort() on its own line or write numbers = sorted(numbers).
1numbers = [5, 2, 9, 1]2ordered = sorted(numbers)3print(numbers, ordered)4numbers.sort(reverse=True)5print(numbers)6print(numbers.sort())78people = [("Ada", 36), ("Linus", 21), ("Grace", 36), ("Tim", 30)]9print(sorted(people, key=lambda p: p[1]))10print(sorted(people, key=lambda p: (-p[1], p[0])))
[5, 2, 9, 1] [1, 2, 5, 9]
[9, 5, 2, 1]
None
[('Linus', 21), ('Tim', 30), ('Ada', 36), ('Grace', 36)]
[('Ada', 36), ('Grace', 36), ('Tim', 30), ('Linus', 21)]Two names, one list: aliasing and copying
Assignment never copies. After b = a, both names refer to the same list object, so a change made through b is visible through a. With immutable values (numbers, strings) this never mattered; with lists it matters a lot.
To get an independent list, make a copy: a[:], list(a) or a.copy(). These are shallow copies: a new outer list holding the same inner objects. For lists of lists, use copy.deepcopy when the inner lists must be independent too.
Step through the trace and watch which lists change.
1a = [1, 2]2b = a3c = a.copy()4print(b is a, c is a, c == a)56def add_bonus(scores):7 scores.append(10)89mine = [5]10add_bonus(mine)11print(mine)
True False True [5, 10]
Aliasing versus copying
a = [1, 2]b = ab.append(3)c = a[:]c.append(4)| Step | a | b | c | State Evolution |
|---|---|---|---|---|
| 1 | [1, 2] | — | — | a refers to a new list [1, 2]. |
List comprehensions
Building a new list from an existing one is so common that Python has a dedicated syntax, the list comprehension:
[expression for item in iterable if condition]
It reads almost like English: "the expression, for each item, keeping those where the condition holds". The if part is optional. A comprehension replaces the three-line pattern empty list, loop, append with one line, and it is usually faster too.
Comprehensions can contain several for clauses, which read in the same order as nested loops. Keep them short: if a comprehension needs more than one condition and two loops, a normal loop is easier to read.
1prices = [12.5, 3.0, 8.75, 20.0]23with_tax = []4for p in prices:5 with_tax.append(round(p * 1.1, 2))6print(with_tax)78print([round(p * 1.1, 2) for p in prices])9print([p for p in prices if p >= 10])10print([w.upper() for w in "keep it short".split()])11print([(x, y) for x in range(2) for y in "ab"])
[13.75, 3.3, 9.62, 22.0] [13.75, 3.3, 9.62, 22.0] [12.5, 20.0] ['KEEP', 'IT', 'SHORT'] [(0, 'a'), (0, 'b'), (1, 'a'), (1, 'b')]
Lists of lists
A list can contain other lists, which is the simplest way to represent a table or a grid: grid[row][col]. Loop over the rows, then over the items in each row.
One trap catches almost everyone once: [[0] * 3] * 3 creates three references to the same row, so changing one cell changes a whole column. Build each row separately with a comprehension: [[0] * 3 for _ in range(3)].
zip(*grid) transposes a grid: it pairs up the first items of every row, then the second items, and so on.
1grid = [[1, 2, 3], [4, 5, 6]]2print(grid[1][2])3for row in grid:4 print(sum(row), end=" ")5print()67wrong = [[0] * 3] * 38wrong[0][0] = 99print(wrong)10right = [[0] * 3 for _ in range(3)]11right[0][0] = 912print(right)1314print([list(col) for col in zip(*grid)])
6 6 15 [[9, 0, 0], [9, 0, 0], [9, 0, 0]] [[9, 0, 0], [0, 0, 0], [0, 0, 0]] [[1, 4], [2, 5], [3, 6]]
Tuples: fixed sequences
A tuple is an ordered sequence like a list, written with parentheses: (3, 4). The difference is that a tuple is immutable: once created, its items cannot be replaced, added or removed. A tuple with one item needs a trailing comma: ("only",).
Use a tuple for a small, fixed group of values that belong together, where position has meaning: a coordinate (x, y), an RGB colour, a (name, score) record, or several values returned from a function. Because they are immutable, tuples can be used as dictionary keys and set members, which lists cannot.
Unpacking assigns the items of any sequence to several names at once. A starred name collects whatever is left over.
1point = (3, 4)2x, y = point3print(x, y, len(point))45record = ("Ada", 1815, "London")6name, year, city = record7print(f"{name} was born in {city} in {year}")89first, *rest = [10, 20, 30, 40]10print(first, rest)1112single = ("only",)13print(type(single).__name__, type(("only")).__name__)1415distances = {("A", "B"): 12, ("B", "C"): 7}16print(distances[("A", "B")])
3 4 2 Ada was born in London in 1815 10 [20, 30, 40] tuple str 12
List or tuple?
Both are ordered sequences; choose by how the data behaves.
- ✓Use a list for a collection of similar items that grows, shrinks or gets sorted.
- ✓Use a tuple for a fixed record whose positions have meanings, such as (x, y).
- ✓Use tuples when you need a sequence as a dictionary key.
- ✓Return several values from a function as a tuple and unpack them.
- ✕Rely on a function not changing a list you passed to it; pass a copy if it must stay intact.
- ✕Build a grid with [[0] * n] * n.
- ✕Use a list as a dictionary key; it raises TypeError: unhashable type.
- •Lists are mutable and most list methods change the list in place and return None.
- •sorted() returns a new list; .sort() sorts in place.
- •Assignment creates an alias, not a copy; use .copy() or a slice to copy.
- •Tuples are immutable sequences, ideal for fixed records and unpacking.
Check your understanding
After `a = [1, 2]`, `b = a`, `b.append(3)`, what is `a`?
Practice
Remove duplicates while keeping order, find the second largest value in one pass, summarise a grid with comprehensions, and implement NumPy-style matrix operations with plain lists.
List Operations: Remove Duplicates Preserving Order
Reveal Solution & AnalysisHide Solution
Second Largest Value
Reveal Solution & AnalysisHide Solution
List Comprehension Challenges
Reveal Solution & AnalysisHide Solution
NumPy-Style Array Operations
Reveal Solution & AnalysisHide Solution
Quick Syntax & Reference Sheet
| Concept / Rule | Syntax / Usage |
|---|---|
| Create / add | items = [3, 1]; items.append(2) |
| Remove | items.remove(x); last = items.pop() |
| Sort in place / copy | items.sort(); new = sorted(items, key=len) |
| Copy | copy = items.copy() # or items[:] |
| Comprehension | [x * 2 for x in items if x > 0] |
| Grid | [[0] * cols for _ in range(rows)] |
| Tuple and unpacking | x, y = (3, 4); first, *rest = items |
Common Pitfalls & Developer Mistakes
items = items.append(x)
Why it happens: append returns None, so the list is lost.
Copying with b = a
Why it happens: Both names share one list, so changes leak.
grid = [[0] * 3] * 3
Why it happens: All rows are the same list object.
Removing items while looping over the list
Why it happens: Items shift left and some are skipped.