Chapter 7 Β· Data Structures

Tuples, Sets & Dictionaries

The other three built-in containers, the looping helpers that make Python loops short, and the rules behind and, or and comparing whole sequences.

You will learn

  • Tuples: packing, unpacking, and when to choose a tuple over a list.
  • Sets: removing duplicates, fast membership, and set algebra.
  • Dictionaries: lookups, get, merging, comprehensions, and the new frozendict.
  • Looping techniques: items(), enumerate(), zip(), reversed(), sorted().
  • in, is, chained comparisons, short-circuit and/or, :=, and how sequences compare.

Tuples and sequences

A tuple is an immutable sequence: a fixed group of values, written with commas and usually wrapped in parentheses.

Empty and one-item tuples need special syntax:

Packing and unpacking

t = 1, 2, 3 is tuple packing. x, y, z = t is sequence unpacking, and it works with any iterable that has the right number of items. This is how functions "return several values": they return one tuple.

listtuple
Mutable?yesno
Typical usemany items of the same kind (a list of expenses)a fixed record of different parts: ("food", 12.5), (x, y)
Can be a dict key or set member?noyes, if everything inside is hashable

β˜• Java: a tuple is like a tiny anonymous record whose fields are accessed by position. For named fields, use collections.namedtuple or a dataclass (Chapter 11).

Sets

A set is an unordered collection with no duplicates. It is great for "have I seen this?" checks and for removing duplicates. Membership tests are fast (O(1) on average), however big the set.

⚠️ Pitfall: {} is an empty dict, not an empty set. Write set() for an empty set.

Set methods: s.add(x), s.remove(x) (KeyError if x is missing), s.discard(x) (no error), and s.update(iterable). Set comprehensions use braces: {c for c in 'abracadabra' if c not in 'abc'}. A frozenset is an immutable set, so it can itself go inside sets or be a dict key.

Removing duplicates: keep order or not?

What can go in a set (or be a dict key)?

Items must be hashable: they must have a hash value that never changes. In practice that means immutable built-ins: numbers, strings, tuples of hashables, and frozenset. Lists, dicts and sets are not hashable.

Dictionaries

A dictionary (dict) maps keys to values, like a real dictionary maps words to definitions. It's like Java's HashMap or a JS object or Map. Keys must be hashable. Since Python 3.7, dicts remember insertion order.

Building dicts

Useful dict methods

CodeMeaning
d.get(k, default)the value for k, or default (None if not given)
d.setdefault(k, default)the value for k; if it is missing, first store default under k
d.pop(k[, default])remove k and return its value
d.keys(), d.values(), d.items()live views of the keys, the values, and (key, value) pairs
d.update(other) / d |= othermerge other into d
d1 | d2a new merged dict (values from d2 win)

🟨 JS: keys are not automatically strings. d[1] and d["1"] are different keys. There is no dot access: write d["name"], not d.name. β˜• Java: d[k] on a missing key raises KeyError; it doesn't return null. Use .get() for that behaviour.

πŸ“ New in 3.15 (PEP 814): frozendict is a built-in immutable dict. Because it is hashable, it can be a dict key or a set member, and it is safe to share as a constant.

Looping techniques

You want…Write
keys and values of a dictfor k, v in d.items():
index and itemfor i, x in enumerate(xs): (or enumerate(xs, start=1))
two or more sequences side by sidefor a, b in zip(xs, ys):
backwardsfor x in reversed(xs):
sorted, without changing the originalfor x in sorted(xs):
unique and sortedfor x in sorted(set(xs)):

⚠️ Pitfall: zip() stops at the shortest input without any warning. Pass strict=True to get an error when the lengths differ.

Never add or remove dict keys while looping over the dict. That raises RuntimeError: dictionary changed size during iteration. Build a new dict instead: {k: v for k, v in d.items() if v > 0}.

More on conditions

and / or return one of their operands

and and or are short-circuit operators: they stop as soon as the answer is known. They return the last value they looked at, not necessarily True/False.

⚠️ Pitfall: name = user_input or "anonymous" also replaces 0 and "", not just None. When 0 is a valid value, write x if x is not None else default.

The walrus operator :=

:= assigns inside an expression. It is named for its look, like a walrus's eyes and tusks. It is useful when you want to test a value and keep it at the same time.

β˜• Java / C: in Python, a plain = inside an if is a SyntaxError. That prevents the classic if (x = 0) bug. You must write := on purpose.

Comparing sequences and other types

Sequences of the same type compare lexicographically, like words in a dictionary: compare the first items; if they are equal, compare the second; and so on. If one sequence runs out first, the shorter one is smaller.

Strings compare by Unicode code point, so every uppercase letter comes before every lowercase letter: 'Z' < 'a'. Comparing unrelated types with < (like 1 < "a") raises TypeError. With == they are simply not equal.

More examples

1. Word frequencies

(Chapter 13 shows collections.Counter, which does this in one line.)

2. Grouping expenses by category

3. Who is in both teams?

4. Inverting a dictionary

5. Sorting records by several keys with tuples

Tuples compare item by item, so a tuple key sorts by several fields at once.

Recap

  • Tuples are immutable records. (x,) is a one-item tuple. Unpacking (a, b = pair) needs the right count.
  • Sets have no duplicates and no order. They support | & - ^. set() is empty; {} is an empty dict.
  • Dicts keep insertion order. d[k] raises KeyError and d.get(k, default) doesn't. d1 | d2 merges. frozendict is new in 3.15.
  • Loop with items(), enumerate(), zip(strict=True), reversed() and sorted().
  • and/or return an operand. Chains like 0 <= x < 10 work. := assigns in an expression. Sequences compare item by item.

Exercises

Exercise 1 🟒 easy predict the output

What are type((5)) and type((5,))?

Show solution

<class 'int'> and <class 'tuple'>. It is the comma, not the parentheses, that makes a tuple.

Exercise 2 🟒 easy build it

Given emails = ["a@x.com", "b@x.com", "a@x.com"], count how many unique addresses there are.

Show solution

Converting to a set drops the duplicates.

Exercise 3 🟒 easy fix the bug

This crashes with KeyError on the first word. Fix it.

Show solution

counts[w] += 1 has to read counts[w] first, and that key doesn't exist yet. get(w, 0) supplies a starting value.

Exercise 4 🟒 easy predict the output

Evaluate: "" or "x", "a" and "", None or 0, 3 and 4.

Show solution

'x', '', 0, 4. or returns the first true value, or the last value if none are true. and returns the first false value, or the last value if all are true.

Exercise 5 🟑 medium refactor it

Rewrite this using zip, with no indexes.

Show solution

strict=True also catches lists of different lengths.

Exercise 6 🟑 medium predict the output

True or False? (2, 1) < (2, 1, 0), "apple" < "Banana", [3] > [2, 99], 1 < 2 < 2.

Show solution

True (the shorter prefix is smaller), False ('a' is 97 and 'B' is 66), True (3 > 2 decides it immediately), and False (it means 1 < 2 and 2 < 2).

Exercise 7 🟑 medium build it

Given prices = {"apple": 0.5, "pear": 0.75, "kiwi": 0.2}, use a dict comprehension to build a dict of only the items under 0.6, with the prices raised by 10% and rounded to 2 decimals.

Show solution

A dict comprehension has the same shape as a list comprehension, with key: value in front.

Exercise 8 🟑 medium build it · playground

Create playground/summary.py. From expenses = [("food", 12.5), ("rent", 900), ("food", 7.5), ("fun", 60), ("fun", 15)], print each category with its total and its number of entries, sorted by total (highest first). Use a dict of totals, a dict of counts, and items().

Show solution

Two dicts share the same keys. The format codes <6 and >8.2f pad and align (Chapter 9).

Exercise 9 πŸ”΄ hard explain why

Why does {["food"]: 1} fail, while {("food",): 1} works? What would go wrong if lists were allowed as keys?

Show solution

A dict stores each key in a slot chosen by its hash. If you could mutate a list key after inserting it, its hash would change, and the dict would look in the wrong slot and "lose" the entry. So mutable built-ins are unhashable. Tuples are immutable, so they are hashable, as long as their items are hashable too.

Exercise 10 πŸ”΄ hard predict the output

What does this print?

Show solution

{'a': 1, 'b': 20, 'c': 3} 100 3. |= updates the dict in place, and e and d are the same dict. | creates a new dict f and leaves d unchanged.

Exercise 11 πŸ”΄ hard mini-project Β· playground

Create playground/budget.py. Given budget = {"food": 100, "fun": 50, "rent": 900} and a list of expenses, produce a report that shows, for every category in either the budget or the expenses (use a set union), the amount spent, the budget, and a status of OK, OVER or NO BUDGET, in alphabetical order.

Show solution

keys() views act like sets, so | gives every category exactly once. get with a default handles categories that only appear on one side.

Official docs