◐ ai-teacher

🐍 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.

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 list is O(n); x in set is 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 pair

All 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 this

Dedup 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

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

ListAn ordered, changeable sequence allowing duplicates.
DictKey-to-value mapping with O(1) lookup by key.
SetAn unordered collection of unique values with O(1) membership tests.
TupleAn 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.
HashableHas a stable hash value, so it can go in a set or be a dict key. Mutable types are not.
UnpackingAssigning several names at once from a sequence: lat, lon = point.

In two minutes

Test yourself

1. Which container makes x in collection fast?

  1. list
  2. set
  3. tuple
  4. 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?

  1. an empty set
  2. an empty dict
  3. an empty list
  4. 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?

  1. Lists are too big
  2. It is mutable, so it can't have a stable hash
  3. Dicts only accept strings
  4. 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?

  1. The file is corrupt
  2. `in` is being run against a list, making it O(n²)
  3. Memory is exhausted
  4. 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?

  1. returns None
  2. raises KeyError
  3. returns 0
  4. 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?

  1. A list of cities, then count with .count() per city
  2. A dict: city → count
  3. A set of cities
  4. 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.

Learn it so it stays. Read it, test yourself, then recall it on two different days. The app schedules that for you, free.

Start this lesson →