Patterns, not puzzles#
A cook does not invent a new way to chop an onion for every recipe. They have a few moves, chop, whisk, fold, and every dish is those moves in a different order.
Programs on lists work the same way. Nearly every exercise is built from six moves: find the best, keep and change, count, sort, pair up, and work through a table of records. Recognise the move and the code follows. Each section below shows the long loop first, because that is what you must be able to trace, and then the short Python way, because that is what you will be asked to read.
Finding the largest, and where it is#
Python has max and min, but exams often ask you to do it by hand, because the pattern matters more than the answer. Start by assuming the first item is the best, then let every other item challenge it:
temps = [-4, -9, -1, -7]
highest = temps[0]
day = 0
for i in range(1, len(temps)):
if temps[i] > highest:
highest = temps[i]
day = i
print("Warmest:", highest, "on day", day)
print(max(temps), min(temps))
words = ["fig", "banana", "kiwi"]
print(max(words), max(words, key=len))
Warmest: -1 on day 2
-1 -9
kiwi banana
Two details matter. Starting from temps[0], not from 0, keeps the code correct when every value is negative. And remembering the index as well as the value answers “which day?”. The built-ins give the same answers, and max also takes a key: with key=len it compares the lengths instead of the words themselves.
List comprehensions: keep and change in one line#
Picture a conveyor belt. Items come along one at a time; a worker checks each one, throws some away, and changes the ones that pass before putting them in a new box. A list comprehension is that belt written as one line:
new_list = [ what_to_put_in for item in old_list if keep_this_one ]
Read it as: “make a list of this, for each item in the old list, but only if the condition holds”. The if part is optional. Here is the loop way and the comprehension way side by side:
prices = [4, 15, 8, 22, 9]
cheap_with_tax = []
for p in prices:
if p < 10:
cheap_with_tax.append(p * 1.1)
print(cheap_with_tax)
cheap_with_tax = [p * 1.1 for p in prices if p < 10]
print(cheap_with_tax)
print(prices)
[4.4, 8.8, 9.9]
[4.4, 8.8, 9.9]
[4, 15, 8, 22, 9]
Both build the same list. The comprehension is shorter and, once you can read it, clearer. It always builds a new list; the original is untouched. Two more forms you will meet:
words = ["apple", "Kiwi", "banana", "Fig"]
lengths = [len(w) for w in words]
print(lengths)
capitalised = [w for w in words if w[0].isupper()]
print(capitalised)
labels = ["long" if len(w) > 4 else "short" for w in words]
print(labels)
[5, 4, 6, 3]
['Kiwi', 'Fig']
['long', 'short', 'long', 'short']
Watch where each if sits. An if at the end filters: items that fail are left out, so the new list can be shorter. An if…else at the front chooses a value for every item, so the new list is always the same length as the old one.
Counting with a dictionary and get#
To count how often each thing appears, use a dictionary: keys are the things, values are their counts. The trap is the first time you see a thing, because it has no count yet:
colors = ["red", "blue", "red"]
counts = {}
for color in colors:
counts[color] = counts[color] + 1
Traceback (most recent call last):
File "main.py", line 4, in <module>
counts[color] = counts[color] + 1
~~~~~~^^^^^^^
KeyError: 'red'
counts["red"] + 1 asks for a key that does not exist yet. The fix is get, which takes a default to use when the key is missing:
colors = ["red", "blue", "red", "green", "red", "blue"]
counts = {}
for color in colors:
counts[color] = counts.get(color, 0) + 1
print(counts)
print(max(counts, key=counts.get))
{'red': 3, 'blue': 2, 'green': 1}
red
Read counts.get(color, 0) + 1 as “the count so far, or 0 if this is the first one, plus one”. The last line finds the most common color with max(counts, key=counts.get): it looks at every key and compares them by their values.
sorted versus sort, and the key#
There are two ways to sort, and mixing them up gives one of the most common bugs in Python:
sorted(lst) | lst.sort() | |
|---|---|---|
| What it is | a built-in function | a list method |
| The original list | unchanged | sorted in place |
| Returns | a new sorted list | None |
| Works on | anything you can loop over: lists, tuples, strings, sets, dictionaries (giving the keys) | lists only |
scores = [72, 95, 88, 61]
ordered = sorted(scores)
print(ordered, scores)
scores.sort(reverse=True)
print(scores)
scores = scores.sort()
print(scores)
[61, 72, 88, 95] [72, 95, 88, 61]
[95, 88, 72, 61]
None
The last line is the trap: scores = scores.sort() sorts the list and then replaces it with the None that sort returns. Use one form or the other, never both on one line.
Both accept reverse=True and a key: a function applied to each item to decide the order. key=len sorts by length. key=str.lower sorts text ignoring case (otherwise every capital letter sorts before every lowercase one). For sorting by a value inside each item, a lambda, a tiny function written in one line, says which part to use:
names = ["bea", "Al", "Christopher", "dee"]
print(sorted(names))
print(sorted(names, key=str.lower))
print(sorted(names, key=len))
prices = {"pen": 1.5, "notebook": 4.25, "backpack": 30.0, "eraser": 0.75}
for name, price in sorted(prices.items(), key=lambda pair: pair[1]):
print(name, price)
['Al', 'Christopher', 'bea', 'dee']
['Al', 'bea', 'Christopher', 'dee']
['Al', 'bea', 'dee', 'Christopher']
eraser 0.75
pen 1.5
notebook 4.25
backpack 30.0
lambda pair: pair[1] means “given an item called pair, use its second part”. sorted(prices.items(), ...) turns the dictionary into (name, price) pairs, so this sorts the products by price.
Pairing up: zip and enumerate#
When two lists line up position by position, such as names and their scores, zip walks them together like the two sides of a zip, handing you one pair at a time. enumerate hands you each item together with its position:
names = ["Ana", "Ben", "Cy", "Dee"]
scores = [91, 78, 85]
for name, score in zip(names, scores):
print(name, score)
lookup = dict(zip(names, scores))
print(lookup)
for place, name in enumerate(names, start=1):
print(place, name)
Ana 91
Ben 78
Cy 85
{'Ana': 91, 'Ben': 78, 'Cy': 85}
1 Ana
2 Ben
3 Cy
4 Dee
zip stops when the shorter list runs out, silently: Dee had no score, so Dee simply vanished from the pairs. That is usually a sign the data is wrong, so check the lengths when it matters; from Python 3.10, zip(names, scores, strict=True) raises a ValueError instead of quietly dropping items. dict(zip(...)) turns two lists straight into a dictionary, and enumerate(..., start=1) numbers from 1 instead of 0.
A list of dictionaries: a table of records#
A list of dictionaries is the Python version of a spreadsheet: each dictionary is one row (one record), and the keys are the column headings. Every pattern above works on it:
students = [
{"name": "Ana", "score": 91, "year": 1},
{"name": "Ben", "score": 78, "year": 2},
{"name": "Cy", "score": 85, "year": 1},
]
total = 0
for s in students:
total = total + s["score"]
print("Average:", round(total / len(students), 1))
first_years = [s["name"] for s in students if s["year"] == 1]
print("First years:", first_years)
best = max(students, key=lambda s: s["score"])
print("Top:", best["name"])
for s in sorted(students, key=lambda s: s["name"], reverse=True):
print(s["name"], s["score"])
Average: 84.7
First years: ['Ana', 'Cy']
Top: Ana
Cy 85
Ben 78
Ana 91
Each line is one of the earlier moves. The loop with s["score"] reaches into each row by its column name. The comprehension keeps some rows and pulls out one field. max and sorted use a lambda to say which column to compare. This structure, a list of records, is what you get when a program reads a CSV file or data from the web, which is why courses spend time on it.
Start a search from the first item; build new lists with [expr for x in items if condition]; count with d.get(key, 0) + 1; use sorted() for a new list and .sort() to change one, never x = x.sort(); and remember zip stops at the shorter list.
Where these go wrong#
- Starting a maximum search from 0. If every value is negative, 0 wins and is wrong. Start from the first item.
counts[key] += 1on a new key.KeyError. Usecounts.get(key, 0) + 1.scores = scores.sort().sortreturnsNone, so the list is lost. Writescores.sort()orscores = sorted(scores).- Putting a filtering
ifat the front of a comprehension. At the front it needs anelseand chooses a value; to leave items out, theifgoes at the end. - Expecting
zipto complain about unequal lengths. It silently stops at the shorter list. - Sorting mixed-case words and expecting dictionary order. Capitals sort first. Use
key=str.lower. - Reaching into a record with a misspelt key, such as
s["Score"]. Keys are case-sensitive; that raisesKeyError.
Practice#
Answer each one on paper before you look at the solutions. Every answer that involves running code was checked by running it.
- Write a list comprehension that gives the squares of the even numbers in
nums = [3, 8, 12, 5, 6]. - What does
["pass" if s >= 70 else "fail" for s in [65, 90, 70]]produce, and how long is it? - Count how many times each letter appears in
"mississippi"using a dictionary andget. - Find the shortest word in a list by hand (no
min), then withminand a key. - What do
aandbhold after this code?a = [3, 1, 2],b = sorted(a),a = a.sort(). - Given a list of records like
{"name": "Ana", "score": 91}, write one line that gives the name of the student with the highest score.
Solutions#
- Filter with an
ifat the end, and square in front:nums = [3, 8, 12, 5, 6] evens_squared = [n * n for n in nums if n % 2 == 0] print(evens_squared)[64, 144, 36] ['fail', 'pass', 'pass'], three items. Anif…elseat the front chooses a value for every item, so the new list is always as long as the old one.- Each letter starts at
get’s default of 0:word = "mississippi" counts = {} for ch in word: counts[ch] = counts.get(ch, 0) + 1 print(counts){'m': 1, 'i': 4, 's': 4, 'p': 2} - Start from the first word and let each later word challenge it:
words = ["planet", "sun", "comet", "moon"] shortest = words[0] for w in words[1:]: if len(w) < len(shortest): shortest = w print(shortest) print(min(words, key=len))sun sun bis[1, 2, 3], a new list.aisNone:a.sort()sorted the list in place and returnedNone, which was then assigned toa.max(students, key=lambda s: s["score"])["name"].maxcompares the records by their score and returns the whole record, then["name"]takes its name.
Test-readiness checklist#
These are the skills a first programming course tests on this topic. Tick each one only when you can do it without looking.
- ☐ I can find the largest or smallest item, and its position, with a loop that starts from the first item.
- ☐ I can write and read list comprehensions, with a filter at the end and with an if-else at the front.
- ☐ I can count items with a dictionary and
get(key, 0), and find the most common one. - ☐ I can explain
sorted()versus.sort(), and sort withreverse=Trueand akey. - ☐ I can use
zipandenumerate, and say whatzipdoes with lists of different lengths. - ☐ I can loop over, filter, summarise and sort a list of dictionaries.
Test yourself in the free Kestrel Exams app
12 practice questions on comprehensions, zip, sorted, enumerate and counting with get. Free, offline, no account.
Frequently asked questions#
What is a list comprehension in Python?
A one-line way to build a new list from an existing one: [expression for item in items if condition]. It is equivalent to a for loop that appends to an empty list, with an optional filter. The original list is not changed.
What is the difference between sort() and sorted() in Python?
list.sort() sorts the list in place and returns None. sorted() works on anything you can loop over and returns a new sorted list, leaving the original unchanged. Writing x = x.sort() replaces the list with None.
How do you count occurrences with a dictionary in Python?
Loop over the items and write counts[item] = counts.get(item, 0) + 1. get returns 0 the first time an item is seen, which avoids the KeyError that counts[item] + 1 would raise.
What happens if you zip two lists of different lengths?
By default zip stops at the end of the shorter list, silently ignoring the extra items in the longer one, without raising an error. From Python 3.10, zip(a, b, strict=True) raises ValueError when the lengths differ.
How do you sort a list of dictionaries by a value?
Pass a key function that picks the value: sorted(records, key=lambda r: r["score"]). Add reverse=True for largest first. The same key works with max and min.
How do you find the largest value in a list without max?
Store the first item as the best so far, then loop over the rest and replace the best whenever an item is larger. Starting from the first item rather than 0 keeps it correct for negative numbers. Track the index too if you need the position.
Something here not clear? A topic you wish we covered? Tell us. We read every message, and a request is the fastest way to get a guide written — several of these exist because somebody asked.