🐍 Python for Data Engineers · Chapter 4
Lists, dicts, sets, tuples — choosing the right one
Four containers. Pick by the question you'll ask of the data — order, lookup, membership, or 'never change this'.
Study this lesson free in the app →
The basics
Python's four built-in containers — list, dict, set, tuple — all hold many values. They differ in which question is cheap to ask of the data.
The problem it solves: at 1,000 rows any choice works. At 1,000,000, checking membership in a list instead of a set turns a 4-second job into one that never finishes, silently.
The mental model: four ways to keep a wedding guest list.
- list — the arrival log. Ordered, duplicates allowed. Great for "who came third?", slow for "is Priya here?".
- dict — the reception index. Look up a key, get a value in one step.
- set — the hand stamp. Only "stamped or not?" — instant, no duplicates.
- tuple — the printed invitation. Fixed; because it can't change, it can be a dict key:
(city, date).
Words you must know 4
- O(1) vs O(n)
- constant time (same cost at any size) versus linear (cost grows with size).
x in listis O(n);x in setis O(1). - O(n²)
- a linear scan inside a loop. The usual cause of a job that never finishes.
- Hashable
- has a stable hash, so it can go in a set or be a dict key. Mutable things (lists, dicts) are not.
- Mutable
- changeable in place. Lists, dicts and sets are; tuples and strings aren't.
Rule: if you write in inside a loop, the right-hand side should be a set or dict.
Explained
events = [e1, e2, e3] # list — order matters
by_id = {"e1": e1, "e2": e2} # dict — find one instantly
seen = {"e1", "e2"} # set — have I seen this?
point = (25.57, 91.89) # tuple — a fixed pairAll four hold things. The difference is which question is cheap, and at a million rows that difference is the whole job.
Four ways to keep a guest list
The list is the arrival log — names in the order they walked in. Perfect for "who came third?". Terrible for "is Priya here?", because you read from the top every time.
The dict is the reception desk index — look up a name, get their table number instantly. One step, whether there are 10 guests or 10 million.
The set is the hand stamp. You don't care about order or details, only: stamped or not? Instant, and it can't hold duplicates by construction.
The tuple is the printed invitation. Fixed, unchangeable, and because it can't change, it can itself be used as a dict key or dropped into a set.
The cost table — memorise this
| Operation | list | dict | set | tuple | | --- | --- | --- | --- | --- | | Get by position | O(1) | — | — | O(1) | | Get by key | O(n) | O(1) | — | O(n) | | x in ... | O(n) | O(1) | O(1) | O(n) | | Add | O(1) append | O(1) | O(1) | can't | | Keeps order | yes | yes (3.7+) | no | yes | | Allows duplicates | yes | keys no | no | yes | | Can be a dict key | no | no | no | yes |
The row that matters most is x in .... Checking membership against a list scans every element; against a set it's one hash lookup.
Inside a loop over a million events, that is the difference between four seconds and four hours. It is the most common real performance bug in Python data code, and it never raises an error — the program just stops finishing.
What you'll actually type
# ---- list: ordered, changeable ----
events = []
events.append(e) # add to the end
events.extend(more) # add many
events[0], events[-1] # first, last
events[:5] # first five (a new list)
sorted(events, key=lambda e: e["time"])
len(events)
# ---- dict: key → value ----
by_id = {}
by_id["e1"] = event # set
by_id.get("e9") # None instead of KeyError
by_id.get("e9", 0) # with a fallback
"e1" in by_id # checks KEYS, instantly
for key, value in by_id.items():
...
# ---- set: membership and dedup ----
seen = set() # NOT {} — that's an empty dict
seen.add("e1")
"e1" in seen # instant
unique_cities = set(all_cities)
set(a) & set(b) # in both
set(a) - set(b) # in a, not b
# ---- tuple: fixed ----
point = (25.57, 91.89)
lat, lon = point # unpacking
by_cell = {(city, date): count} # a compound key — only tuples can do thisDedup with a set, step by step
Swap seen = set() for seen = [] and the logic is identical — but a million events would take hours instead of seconds.
Why is this slow?
seen = []
unique = []
for e in events: # 1,000,000 events
if e["id"] not in seen:
seen.append(e["id"])
unique.append(e)It works perfectly on a 1,000-row test file — 0.01 seconds. On the real file it hasn't finished in an hour.
Gotchas
{}is an empty dict, not an empty set. Useset()inagainst a list inside a loop — the quadratic trap aboved["missing"]raisesKeyError— used.get(key)ord.get(key, default)when absence is normal- Sets have no order — never rely on the order you get back; sort if you need one
- A list can't be a dict key — it's mutable, so it can't be hashed. Use a tuple
list2 = list1doesn't copy — both names tag the same list. Uselist1.copy()orlist(list1)- Sorting a dict returns its keys —
sorted(d)gives keys; you probably wantedsorted(d.items(), key=...)
Order → list. Lookup → dict. "Seen it?" → set. Fixed → tuple.
If you write in inside a loop, the right-hand side must be a set or a dict.
{} is a dict. set() is a set.
Words to know
| List | An ordered, changeable sequence allowing duplicates. |
|---|---|
| Dict | Key-to-value mapping with O(1) lookup by key. |
| Set | An unordered collection of unique values with O(1) membership tests. |
| Tuple | An immutable, fixed-length sequence — hashable, so usable as a dict key. |
| O(1) | Constant time — cost doesn't grow with the size of the collection. |
| O(n) | Linear time — cost grows in proportion to the collection. |
| O(n²) | Quadratic — a linear scan inside a loop. The usual cause of a job that never finishes. |
| Hashable | Has a stable hash value, so it can go in a set or be a dict key. Mutable types are not. |
| Unpacking | Assigning several names at once from a sequence: lat, lon = point. |
In two minutes
- Pick the container by the question you'll ask: order → list, lookup by key → dict, 'seen it?' → set, fixed → tuple.
- x in list scans every element (O(n)); x in set or dict is one hash lookup (O(1)).
- A list membership check inside a loop is accidental O(n²) — fine on samples, endless on production data.
- Use .get(key, default) when a key may be missing; d[key] raises KeyError.
- {} is an empty dict; an empty set is set().
- Only hashable values can be dict keys or set members — use a tuple for compound keys like (city, date).
- b = a never copies a list; both names tag the same object. Use a.copy() or list(a).
- Sets have no order; dedup with a seen-set plus a list when order matters.
Test yourself
1. Which container makes x in collection fast?
- list
- set
- tuple
- all the same
Show the answer
set — Sets (and dict keys) hash the value — one lookup regardless of size. Lists and tuples scan every element.
2. What does {} create?
- an empty set
- an empty dict
- an empty list
- an empty tuple
Show the answer
an empty dict — An empty dict. For an empty set you must write set() — a genuinely common beginner trip-up.
3. Why can't a list be used as a dict key?
- Lists are too big
- It is mutable, so it can't have a stable hash
- Dicts only accept strings
- It can, actually
Show the answer
It is mutable, so it can't have a stable hash — Hashing requires immutability — if the key changed, the dict could never find it again. Use a tuple for compound keys.
4. Your dedup loop is fast on 1,000 rows and never finishes on 1,000,000. Most likely?
- The file is corrupt
- `in` is being run against a list, making it O(n²)
- Memory is exhausted
- The loop has a break
Show the answer
`in` is being run against a list, making it O(n²) — Quadratic cost is invisible at small n. Scaling input 1000x scales the work 1,000,000x. Swap the list for a set.
5. d["missing_key"] on a dict that lacks the key does what?
- returns None
- raises KeyError
- returns 0
- adds the key
Show the answer
raises KeyError — It raises. Use d.get(key) for None, or d.get(key, default), when a missing key is expected rather than exceptional.
6. You need ride counts per city from 2M events. Which structure fits?
- A list of cities, then count with .count() per city
- A dict: city → count
- A set of cities
- A tuple of (city, count) pairs
Show the answer
A dict: city → count — One pass, O(1) update per event. Calling list.count() per city rescans 2M items each time; a set loses the counts; tuples can't be updated.
There are 11 questions, 3 puzzles and 6 recall cards for this lesson in the app — the recall cards come back on later days, just before you would forget.
Start this lesson →