skip to content

When should you choose a tuple over a list in Python, and why?

level: juniorimportance: must knowfreq 78%

answer

  1. Two built-in sequence types, one decision
  2. Shape of the data, not its length
  3. Record versus growable collection
  4. Can it be a dict key?
  5. Immutability as an API promise

basics

~20 s

Use a tuple for a fixed-shape record whose positions each mean something, and a list for a variable-length collection of like items. Tuples are also hashable, so a tuple can key a dict or join a set; a list cannot.

solid answer

~50 s

The useful question is not "do I need to mutate this?" but "is the length part of the data or part of the code?". A list models a **collection**: homogeneous items, a length that may change, code that iterates without caring how many. A tuple models a **record**: fixed arity, each position carrying a distinct meaning, like `(name, replicas, is_primary)` — roughly array versus struct. The choice then buys three concrete things. A tuple of hashable items is itself hashable, so it can be a dict key or a set member. Returning a tuple states an interface contract: the caller cannot append to it or reassign a slot. And a tuple is a single, exactly-sized allocation, so it is smaller than the equivalent list and cheaper to build. The counter-pressure is that a tuple is a bad accumulator — growing one with `t = t + (x,)` copies every time.

code

python · 7 lines
python
shard = ("index-07", 4, True)      # name, replicas, is_primary
pending = ["doc-1", "doc-2"]       # homogeneous, grows
pending.append("doc-3")

queued = {shard: pending}          # the tuple can key the dict
print(queued[("index-07", 4, True)])
print((1, 2) == [1, 2])            # False: a tuple never equals a list

go deeper

for a junior

Be ready to state the rule in one breath: fixed-shape record with meaningful positions means tuple; growable collection of like items means list. Remember that a tuple can be a dict key and a list cannot — that single fact is often the whole screening question.

for a middle

Explain the mechanics behind the rule: what immutability enables (hashing, safe sharing), what a tuple costs in bytes versus a list, and why growing a tuple in a loop is quadratic. Know that a tuple never compares equal to a list.

for a senior

Show judgment about interfaces: what a returned tuple promises callers and what it does not, when a fixed-shape tuple should graduate to a named record type for readability, and how a wrong choice shows up later as defensive length checks or accidental mutation of shared data.

for a principal

Own the convention. Decide where in a codebase tuples are the house style for records and keys and where named types are required, so that the boundary is a rule the team applies rather than a per-file judgment call, and so that positional records never leak into long-lived public interfaces.

Python ships two general-purpose built-in sequence types, and most people learn them as "list is mutable, tuple is immutable" and stop there. That is true, but it is not a decision procedure. At the point of choice the productive question is what each type *says about the data*. ### Shape versus size A **list** models a collection whose length belongs to the data and may change: the documents still pending, the rows read from a file, the matches for a query. Every element plays the same role, and the code iterates over it without caring how many there are. A **tuple** models a record whose length belongs to the *code*: `(host, port)`, `(name, replicas, is_primary)`. Position carries meaning — element 0 is not interchangeable with element 1 — and a tuple with a different number of items is simply a different kind of thing. The old C analogy still holds: a list is an array, a tuple is a struct. This is also why a function returning several values returns a tuple: the caller is getting a fixed-shape record, not a collection. ```python shard = ("index-07", 4, True) # record: fixed arity, positional meaning pending = ["doc-1", "doc-2"] # collection: same kind of thing, grows pending.append("doc-3") ``` ### What the choice actually buys **Hashability.** A tuple whose items are all hashable is hashable, so it can key a dict or live in a set — `seen.add((doc_id, revision))` is idiomatic, `seen.add([doc_id, revision])` raises `TypeError`. That single property decides the choice more often than any performance argument: composite keys, cache keys and deduplication sets all need it. **A contract.** Handing a tuple back from a function tells the caller "this shape is fixed, do not grow it", and the interpreter enforces the shallow part of that promise: no `append`, no slot assignment. It is an expression of intent, not a security boundary — a caller can always build `list(t)` and go its own way, and a tuple holding a mutable object can still be changed through that object. **Footprint and construction.** A tuple stores its item pointers inline in one exactly-sized object; a list is a header plus a separately allocated pointer array carrying spare capacity for future growth. On a 64-bit CPython 3.14 build a five-item tuple is 88 bytes against 104 for the equivalent list literal, and 120 if that list was grown by appends. A literal tuple of constants inside a function is not even constructed at run time — the compiler stores it in the code object's constants and the body just loads it, while a list literal is rebuilt on every call, because a mutable object cannot be shared between calls. ### The API surface each one gives you Both slice, iterate, support `in`, `+`, `*` and `len`, and both unpack. What a tuple deliberately lacks is everything that mutates: it carries only `count` and `index`. That small surface is the point — there is nothing to misuse. Equality does not cross the boundary: `(1, 2) == [1, 2]` is `False`. A tuple never equals a list even with identical items, which is worth knowing before you compare a value that arrives from JSON (a list) with a constant you wrote as a tuple. ### When the heuristic bends A homogeneous sequence that never changes — a module-level table of allowed extensions, say — is legitimately a tuple: fixed length is a fact about the program, and the tuple prevents an import-time table from being edited by accident. In the other direction, a record with more than about three fields stops being readable as a tuple: `record[4]` tells the next reader nothing. At that point the answer is a named-field record type or a small class, not a longer tuple. And a tuple is the wrong accumulator: `t = t + (x,)` in a loop copies the whole tuple every iteration, so building a sequence item by item is quadratic where a list's append is not. ### How to answer in an interview Say the decision rule first — record versus collection — then name the consequences: hashable keys, a stated contract, a smaller object. Reaching for "tuples are faster" alone is the weak answer, because the difference is small and it is not why anyone chooses a tuple.

  • Does returning a tuple instead of a list really stop a caller from changing the data?
    Only the top level. The caller cannot append to it, delete from it or assign to a slot, which is usually the misuse you are guarding against. But if an item is itself mutable, the caller can change that object through the tuple, and nothing stops them building `list(t)` and working from a copy. Treat it as a stated contract and a guard against accidents, not as an enforcement mechanism.
  • Why is building a sequence with `t = t + (x,)` in a loop a bad idea?
    Tuples are fixed size, so every concatenation allocates a brand-new tuple and copies all existing item pointers into it. Over n iterations that is O(n²) work and n discarded objects. A list's `append` writes into spare capacity already held, so the same loop is linear. Accumulate in a list and call `tuple(...)` once at the end if the finished result needs to be hashable or fixed.
  • Where does a tuple stop being the right record type?
    Once positions outnumber what a reader can hold in their head — roughly past three fields — `record[4]` becomes unreadable and every call site has to remember the order. Keep the fixed-shape idea but give the fields names: a named-field record type from the standard library, a dataclass, or a small class. The failure mode of an over-long tuple is not performance, it is that nobody can review the code that indexes into it.

A list is a shopping basket — you keep adding items of the same sort. A tuple is a passport page: name, number, expiry, always those fields in that order, and altering it is not something the holder gets to do.

saying these in an interview costs you the question

  • Says a tuple is just an immutable list with no other consequence
  • Claims tuples are always faster, so use them everywhere
  • Says a tuple's contents can never change under any circumstances
  • Thinks a list can key a dict as long as you never modify it
  • Chooses by length: short means tuple, long means list
  • Believes a tuple cannot be sliced, iterated or unpacked

context