Lists, dicts, sets, and comprehensions
Picking the right container, and writing a comprehension that reads better than the loop it replaces.
After this lesson you can
- Choose between a list, a dict, and a set for a given problem
- Write a comprehension with a filter and a transform together
- Explain why dict keys and set members must be hashable
Three containers, three different jobs. A list keeps order and allows duplicates. A set has no order and no duplicates — membership is what it is for. A dict maps a key to a value, and since Python 3.7 the language guarantees it iterates in insertion order. A set makes no such promise: never rely on the order a set comes out in.
scores = [90, 75, 90, 60] # order and duplicates both matter
seen_ids = {1, 4, 4, 9} # {1, 4, 9} — duplicates collapse
by_id = {1: "Nino", 4: "Ana"} # look up by key
x in a_set and x in a_dict (checking the keys) are both O(1) on
average. x in a_list is O(n) — it has to scan. Converting a list you
check membership on repeatedly into a set is one of the cheapest
optimisations available.
Comprehensions
A comprehension is a loop and a condition read in the order you think about them: what to keep, then where it comes from, then the filter.
nums = [1, 2, 3, 4, 5, 6]
evens_doubled = [n * 2 for n in nums if n % 2 == 0] # [4, 8, 12]
by_parity = {n: "even" if n % 2 == 0 else "odd" for n in nums}
unique_lengths = {len(w) for w in ["a", "bb", "cc", "ddd"]} # {1, 2, 3}
The list, dict, and set versions are the same shape with different
brackets. Reach for a comprehension over a hand-written loop with
.append the moment the loop's only job is building a new collection —
it is usually shorter and, once the pattern is familiar, faster to read.
Try it
def run(): students = [ {"name": "Nino", "score": 90}, {"name": "Ana", "score": 55}, {"name": "Luka", "score": 70}, ] passing = [s["name"] for s in students if s["score"] >= 60] by_name = {s["name"]: s["score"] for s in students} grades = {"pass" if s["score"] >= 60 else "fail" for s in students} return {"passing": passing, "by_name": by_name, "grades": sorted(grades)}Hashability
A dict key or a set member must be hashable. Among the built-in types
that means the immutable ones: str, int, float, bool,
frozenset, and tuple (if everything inside the tuple is itself
hashable) all work. list, dict, and set do not — Python refuses
outright, because a mutable key could change after it was hashed and
silently break the lookup.
{[1, 2]: "value"} # TypeError: unhashable type: 'list'
{(1, 2): "value"} # fine — a tuple is immutable
Interviews in other languages probe the same idea: a Java HashMap key
that is mutated after insertion is just as lost, Java simply does not
stop you from using one.
Common mistakes
- Removing items from a list while looping over it. The loop's index
keeps moving while the items shift left, so the element right after
each removal is never looked at — one
2survives below. - Adding or deleting dict keys while iterating over the dict. Python
stops you with a
RuntimeError; iterate overlist(d)or build a new dict with a comprehension instead. - Checking
x in some_listinside a loop over another big list. Each check is a scan, so the whole thing is quadratic; build a set once.
def run(): nums = [1, 2, 2, 3] for n in nums: if n == 2: nums.remove(n) stock = {"apple": 0, "pear": 3} try: for name in stock: if stock[name] == 0: del stock[name] dict_result = "no error" except RuntimeError as e: dict_result = f"RuntimeError: {e}" safe = {name: qty for name, qty in {"apple": 0, "pear": 3}.items() if qty > 0} return {"list_after": nums, "dict_loop": dict_result, "comprehension": safe}What interviewers ask
- Why is
infast on a set and slow on a list? A set is a hash table, so a lookup hashes the value and checks one bucket on average; a list compares element by element. - What can be a dict key? Anything hashable whose hash never changes: strings, numbers, tuples of hashable values, frozensets — not lists.
- How would you count occurrences of each word? A dict with
.get(w, 0) + 1, orcollections.Counter; then say how ties are broken, because "the most common" is ambiguous until you do.
Try it yourself
2 visible tests · 4 hidden testsImplement entryPoint(items). items is a list of strings. Walk it
from the start and stop at the first position whose value has already
appeared earlier in the list. Return [value, index]: that value and
the 0-based index of this second occurrence. If no value repeats,
return None. Comparison is case-sensitive: "Go" and "go" are
different values.
Do it in one pass: remember what you have already seen in a set instead of searching the list again for every item.
entryPoint(["a","b","c","b","a"])entryPoint(["x","y","z"])
Sign up to run the tests, check the hidden ones and save your progress. Sign up