open atlas
↑ Back to track
Python for JS/TS developers PY · 01 · 02

Lists, dicts, sets, tuples

Python ships four core containers — list, dict, set, tuple. Picking the right one is a question of mutability and hashability, and the wrong pick costs O(n) where O(1) was free.

PY Middle ◷ 17 min
Level
FoundationsJuniorMiddleSenior

A reviewer flags your function: it scans a 50,000-entry list with if user_id in seen on every request, and the endpoint crawls. You wrote it like JS — an array, a loop, .includes() in spirit. The fix is one character of intent: make seen a set. Membership drops from O(n) to O(1) and the p99 collapses. Same data, same code shape — a different container, because in Python the container is the performance decision. Reaching for a list by reflex, the way you’d reach for [] in JavaScript, is the most common way to write accidentally quadratic Python.

Two questions decide everything: mutable? hashable?

Python gives you four built-in containers, and choosing between them is not taste — it is two yes/no questions. Is it mutable? — can you change it in place after creation. Is it hashable? — can Python compute a stable hash so the object can be a dict key or a set member. These two properties are linked: an object is hashable only if its value can’t change underneath the hash, so mutable built-ins are not hashable. That single rule explains why a list cannot be a dict key but a tuple can.

  • list — ordered, mutable, the workhorse sequence. Closest to a JS Array, but the methods differ (.append(), not .push(); .sort() mutates in place and returns None). Append is amortized O(1); insert(0, x), del lst[0], and x in lst are all O(n). Not hashable.
  • dict — a hash map with guaranteed insertion order since Python 3.7. Keys must be hashable; lookup, insert, and delete are O(1) average. It is JS’s Object and Map unified into one type: string-keyed and arbitrary-hashable-keyed, ordered, fast.
  • set — an unordered collection of unique, hashable members. O(1) membership, plus first-class | union, & intersection, - difference. Like JS Set, but with real set algebra built in.
  • tuple — ordered, immutable, and hashable if every element is hashable. JavaScript has no equivalent. It is the type for fixed records, multiple return values, and — crucially — composite dict/set keys.

Together these four types cover every data-shaping task, but they are not interchangeable. When you pick the wrong one — a list for membership or a tuple where a dict belongs — the cost shows up as real algorithmic complexity, not a style warning.

point = (3, 4)              # tuple: fixed record, hashable
grid = {point: "origin"}   # tuple as a dict key — works
grid[[3, 4]]               # TypeError: unhashable type: 'list'

x, y = point               # unpacking a tuple into two names
def bounds(): return 0, 100  # returning a tuple (the comma makes it)
lo, hi = bounds()

The cheat sheet, and the JS translation

When you come from JavaScript, the mistake isn’t syntax — it’s mapping Array onto everything. JS makes you fake a set with an object ({}) or a Map, fake set algebra with filters, and has no immutable record at all. Python splits those jobs across four purpose-built types, and the cost of using the wrong one is real algorithmic complexity, not just style.

TypeMutable?Ordered?Hashable?Reach for it when…JS analog
listyesyesnoan ordered, growable sequence you iterate or indexArray
dictyesyes (insertion)nokey→value lookup, counting, grouping, any mappingObject / Map
setyesnonomembership tests, dedup, union/intersectionSet
tuplenoyesyes*fixed records, multi-return, composite dict/set keys(none)

The asterisk on tuple matters: a tuple is hashable only if all its elements are. (1, 2) is a fine key; (1, [2]) is not, because it nests a list. Hashability is recursive.

Pick the best fit

You process an event stream and must answer 'have I already seen this (user_id, day) pair?' millions of times. Pick the structure that holds the seen pairs.

Identity vs equality, and the mutable-default trap

Two pitfalls bite engineers crossing from JS. First, identity (is) vs equality (==). == compares values; is compares object identity (same object in memory) and is what None/True/False checks should use. They diverge in ways that look like magic: [1,2] == [1,2] is True but [1,2] is [1,2] is False (two distinct lists). CPython caches small integers and short strings, so 256 is 256 may be True while 257 is 257 is False — an implementation detail you must never rely on. Rule: use == for value checks, reserve is for None and other singletons.

Second, the mutable default argument. A default value is evaluated once, at function definition, and shared across every call — so a mutable default like [] or {} accumulates state between calls, a bug that looks like haunting.

def add(item, bucket=[]):     # bucket is created ONCE, shared forever
    bucket.append(item)
    return bucket

add("a")        # ['a']
add("b")        # ['a', 'b']  ← surprise: the same list persisted

def add(item, bucket=None):   # the fix: sentinel + fresh object per call
    if bucket is None:
        bucket = []
    bucket.append(item)
    return bucket
Why this works

Assignment in Python binds a name to an object; it never copies. So b = a for a list means a and b are the same list, and b.append(1) mutates both. Copying is explicit: a.copy() (or a[:], list(a)) makes a shallow copy — a new outer container holding the same inner objects, so mutating a nested list still shows through both. For a fully independent clone of nested data, reach for copy.deepcopy(). This is the same reference-semantics model as JS objects and arrays — the trap is assuming = duplicates.

Quiz

Why does `d = {[1, 2]: 'x'}` raise TypeError, while `d = {(1, 2): 'x'}` works?

Quiz

Membership testing `if x in c` runs millions of times in a hot loop. Which container keeps it O(1)?

Order the steps

A function should record each unique (user, action) pair once and answer membership fast. Order the decision from the deciding question down to the final pick:

  1. 1 Do I need fast membership / uniqueness? → yes, so a hash container (set/dict), not a list
  2. 2 Is it pure membership (seen-or-not) or do I store a value? → seen-or-not, so set over dict
  3. 3 What is each member? → a compound (user, action) identity, so a composite key
  4. 4 Is that composite hashable? → use a tuple (immutable, hashable), not a list
  5. 5 Result: set of (user, action) tuples — O(1) membership, automatic dedup
Recall before you leave
  1. 01
    Why can a tuple be a dict key but a list cannot, and what links that to mutability?
  2. 02
    What is the mutable-default-argument trap, and what's the correct fix?
Recap

Python’s four core containers split across two questions: mutability and hashability. A list is your ordered, mutable, growable sequence — the JS Array, but in is an O(n) scan and .append() not .push(). A dict is an insertion-ordered hash map with O(1) lookup, unifying JS’s Object and Map. A set holds unique hashable members with O(1) membership and real union/intersection algebra. A tuple is immutable and ordered, hashable when its elements are — the type for fixed records, multiple returns, and composite dict/set keys, with no JS equivalent. Mutability gates hashability: a list can’t be a key, a tuple can. The recurring senior mistakes are writing accidentally quadratic code by testing membership against a list instead of a set, leaning on is where == is meant (CPython’s small-int and string caching makes is lie), and the mutable-default-argument trap — fixed with a None sentinel and a fresh object per call. Now when you see a list in a hot membership check or a list used as a dict key, you’ll know which question it fails — and which container to reach for instead.

Practice

Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.

recallapplystretch0 of 5 done

Something unclear?

Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.

shortcuts expand
search
K
prev piece
k
next piece
j
cycle tier
t
this menu
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.