Python Programming — Alpha School

Lesson 5: Lists, Dictionaries, and Loops

🎧 Listen to this lesson

One variable holds one value, but programs juggle thousands: a class of scores, a catalogue of products, a week's temperatures. Python's answer is collections — a list holds an ordered sequence, a dictionary maps keys to values, and loops walk through both. This lesson covers the list and dictionary toolkits, the counting pattern that powers half of all data work, tuples and sets, and list comprehensions — the compact loops professionals write daily.

Lists: ordered, changeable collections

A list literal uses square brackets: scores = [88, 92, 75, 100]. Indexing works exactly like strings — scores[0] is 88, scores[-1] is the last item (100), scores[1:3] is [92, 75]. Unlike strings, lists are mutable: you can change them in place.

scores = [88, 92, 75, 100]
scores[1] = 95          # replace an item
scores.append(67)       # add to the end
print(scores)
print(len(scores), sum(scores), max(scores), min(scores))
scores.remove(75)       # remove by value
print(scores.pop())     # remove and return the last item
print(scores)

Output:

[88, 95, 75, 100, 67]
5 425 100 67
67
[88, 95, 100]

Read the trace carefully, because three things happen in sequence: append(67) grows the list to five items, remove(75) deletes the 75 (leaving [88, 95, 100, 67]), and only then does pop() remove and return the last item — the appended 67 — leaving [88, 95, 100]. The built-ins len, sum, max and min summarise numeric lists in one call each; sort() orders a list in place (scores.sort()), while sorted(scores) returns a new sorted list and leaves the original alone — the same returns-new versus changes-in-place distinction strings taught you.

Loops over collections

The for loop visits each item directly — no index arithmetic needed:

for score in scores:
    print(score)

When you need positions as well, enumerate supplies them: for i, score in enumerate(scores) gives i = 0, 1, 2, ... alongside each value. Two loop-powered patterns matter everywhere. Filtering keeps items passing a test, and a list comprehension writes it in one line: passed = [s for s in scores if s >= 70] builds the list of passing scores. Comprehensions read as "give me each s, for s in scores, if it passes" — every comprehension is a small loop wearing a compact syntax, and Lesson 15 shows where the style comes from.

Dictionaries: lookup by key

A dictionary maps keys to values: capitals = {"France": "Paris", "Japan": "Tokyo"}. Look up with brackets — capitals["France"] is "Paris" — add or replace with capitals["Peru"] = "Lima", and iterate pairs with for k, v in capitals.items(). Looking up a missing key raises KeyError; the .get(key, default) method returns a default instead, which unlocks the counting pattern — the single most useful dictionary idiom in programming:

votes = ["a", "b", "a", "c", "a"]
counts = {}
for v in votes:
    counts[v] = counts.get(v, 0) + 1
print(counts)

Output: {'a': 3, 'b': 1, 'c': 1}. Read the key line slowly: counts.get(v, 0) means "the current count for v, or 0 if v is not there yet"; add 1 and store it back. The first "a" stores 1, the second visit finds 1 and stores 2, the third stores 3. This pattern counts votes, words in an essay, items in a basket, errors in a log — Lesson 13 uses it to total revenue by product, and Lesson 19 to find the mode. A first cousin worth knowing: tuples — (3, 4) — are immutable lists, used for fixed records like coordinates; sets — {1, 2, 3} — hold unique items only and test membership fast.

Worked example 1 — analysing quiz scores

Everything in this lesson converges on one realistic task: summarising a class's quiz results.

scores = [88, 92, 75, 100, 67, 95, 81]
average = sum(scores) / len(scores)
passed = [s for s in scores if s >= 70]
print(f"Average: {average:.1f}")
print(f"Highest: {max(scores)}, Lowest: {min(scores)}")
print(f"Passed: {len(passed)}/{len(scores)}")

grades = {}
for s in scores:
    if s >= 90:
        letter = "A"
    elif s >= 80:
        letter = "B"
    elif s >= 70:
        letter = "C"
    else:
        letter = "F"
    grades[letter] = grades.get(letter, 0) + 1
print(grades)

Output:

Average: 85.4
Highest: 100, Lowest: 67
Passed: 6/7
{'B': 2, 'A': 3, 'C': 1, 'F': 1}

Verify by hand — never trust a summary you have not checked: the scores sum to 598, and 598 / 7 = 85.43, which :.1f prints as 85.4. Six scores are 70 or above (only 67 fails). The grade tally: 88 is B, 92 is A, 75 is C, 100 is A, 67 is F, 95 is A, 81 is B — A:3, B:2, C:1, F:1, exactly what the program printed. Two habits are on display here. First, always hand-check one figure in any summary a program gives you; a program that runs is not necessarily a program that is right (Lesson 7 is built on that uncomfortable truth). Second, the dictionary printed B first because dictionaries remember the order keys were first inserted — B was stored before A, C and F, even though A ends with the largest count. Insertion order is not ranking; to rank a dictionary you sort it, which the Real-World Challenge does.

Worked example 2 — word frequency in a sentence

The counting pattern on text: how often does each word appear in a sentence?

sentence = "the cat sat on the mat and the cat slept"
counts = {}
for word in sentence.split():
    counts[word] = counts.get(word, 0) + 1
print(counts)
most_common = max(counts, key=counts.get)
print("Most common word:", most_common, counts[most_common])

Output:

{'the': 3, 'cat': 2, 'sat': 1, 'on': 1, 'mat': 1, 'and': 1, 'slept': 1}
Most common word: the 3

split() with no argument cuts the sentence into words; the counting loop tallies them. The line max(counts, key=counts.get) deserves a slow read: max over a dictionary normally compares the keys (alphabetical order), but key=counts.get tells it to compare by each key's count instead, so it returns the word with the highest tally. That key= trick appears again in sorting (sorted(..., key=...)) in Lesson 8 — whenever Python must rank things by something other than themselves, a key function does the translating.

Key idea. Key idea: Reach for a list when order matters and a dictionary when lookup matters. The counting pattern — counts[item] = counts.get(item, 0) + 1 — turns any sequence into a frequency table.

Common mistakes

  • Mutating a list while looping over it. Removing items from a list inside for x in my_list: makes the loop skip elements, because the list shrinks under the loop's feet. Filter into a new list instead: kept = [x for x in my_list if x >= 0].
  • Off-by-one and IndexError. A list of 5 items has indices 0 to 4; scores[5] raises IndexError. When code counts from 1 for humans, subtract 1 before indexing — or use enumerate(..., 1) for display numbers.
  • KeyError on a missing key. capitals["Spain"] crashes if Spain was never added. Use capitals.get("Spain", "unknown") when absence is possible, or test with in first.
  • Assuming dictionaries remember a helpful order for ranking. Dictionaries preserve insertion order, but "first inserted" is not "biggest" — to rank, use sorted(counts, key=counts.get, reverse=True).
  • Copying a list by assignment. b = a does not copy a list; both names point at the same list, so editing b edits a. Use b = a[:] or b = list(a) for a real copy.
Try it. Try it: Given temps = [72, 68, 75, 79, 71], print the average temperature and how many days were above 73 (answers: 73.0 and 2). Then build a dictionary mapping three friends' names to their favourite colours and print each pair with a loop over .items().

Real-World Challenge

The task. A class voted for their favourite school-lunch option. Given the list of votes, print a results table ordered by votes (most popular first), name the winner, and show each option's share of the vote as a percentage.

Worked solution.

votes = ["pizza", "pasta", "pizza", "salad", "pasta",
         "pizza", "tacos", "pasta", "salad", "pizza"]

counts = {}
for v in votes:
    counts[v] = counts.get(v, 0) + 1

ranked = sorted(counts.items(), key=lambda pair: pair[1], reverse=True)
total = len(votes)
for option, n in ranked:
    print(f"{option}: {n} votes ({100 * n / total:.0f}%)")
print("Winner:", ranked[0][0])

Output:

pizza: 4 votes (40%)
pasta: 3 votes (30%)
salad: 2 votes (20%)
tacos: 1 votes (10%)
Winner: pizza

How it works. The counting pattern tallies the votes; then sorted ranks the (option, count) pairs. Two details do the heavy lifting: counts.items() yields pairs, and key=lambda pair: pair[1] tells sorted to compare pairs by their second element — the count — while reverse=True puts the largest first. (The lambda is a tiny unnamed function; Lesson 15 gives it a full treatment.) Percentages come from 100 * n / total, formatted with :.0f. The winner is simply the first pair after sorting, ranked[0][0] — the option half of the top pair. This exact shape — count, rank, report — is an election night broadcast, a sales leaderboard and a "top tracks" chart in miniature.

Extension. A tie is possible in real elections: extend the program so that when two options share the highest count it prints Tie between: ... listing all of them. Find every option whose count equals ranked[0][1], and test your version on a vote list you design to tie exactly.

Going further — pairing data with zip, and numbering it with enumerate

Two iterators dominate tidy loop code. zip walks several sequences in lockstep, pairing their items; enumerate attaches a counter to any sequence. Together they replace the fragile for i in range(len(names)) indexing style — and its entire family of off-by-one and mismatch bugs — with loops that cannot fall out of step:

names = ["Ada", "Ben", "Cid"]
points = [30, 45, 25]
for rank, (name, pts) in enumerate(zip(names, points), 1):
    print(rank, name, pts)

Output:

1 Ada 30
2 Ben 45
3 Cid 25

Read the machinery: zip produces pairs ("Ada", 30), …, the pair is unpacked right in the loop header (name, pts), and enumerate(..., 1) starts the rank at 1 because humans rank from 1 even though Python indexes from 0. Note zip's honest contract: it stops at the *shortest* input, so mismatched lists truncate silently — when lengths must agree, check them, or the missing entries vanish without a sound. These two helpers, plus the comprehensions from the main lesson, are why experienced Python code contains so few explicit index variables: the language keeps offering loops that carry exactly the structure the problem has, and taking the offer is most of what "Pythonic" means.

🎬 Watch & Learn

Prefer video? These searches surface top tutorials on this lesson's topic:

💪 Practice Zone

Try these yourself — hints are included, answers are not. Run your code in any Python environment.

  1. Shopping list manager — Start with cart = ["milk"]. Add "eggs" and "bread", remove "milk", then print the final list and its length.
    HintUse cart.append(...), cart.remove(...), and len(cart).
  2. Phone book lookup — Build phones = {"Ava": "555-0101", "Ben": "555-0102"}. Ask for a name and print the number — or Not found without crashing.
    Hintphones.get(name, "Not found") never raises a KeyError.
  3. Squares list — Use a for loop and range() to build [1, 4, 9, 16, 25, 36, 49, 64, 81, 100].
    HintLoop for n in range(1, 11): and append n * n.
  4. Word frequency — Count how many times each word appears in "the cat sat on the mat" and print the resulting dictionary.
    HintLoop over sentence.split(); use counts[word] = counts.get(word, 0) + 1.

🌍 Real World

Your shopping cart on any store site is a list; your account profile is a dictionary; the checkout loop iterates over your items. Lists, dictionaries, and loops are the data structures behind e-commerce, social media feeds, and contact lists — billions of them running right now.

Keep reading to complete this lesson