List and tuple both hold references, but mutability changes allocation strategy, API surface, and what callers may assume.
Core answer
Use list when an ordered collection must grow, shrink, or be updated. Use tuple when fixed length and stable sequence semantics are part of the contract.
# [CURRENT - 3.10-3.14] Works on Python 3.10+from dataclasses import dataclass@dataclass(frozen=True, slots=True)class Endpoint: host: str port: intdef deployment_targets(items: list[Endpoint]) -> tuple[Endpoint, ...]: return tuple(items)draft = [Endpoint("api-1", 443), Endpoint("api-2", 443)]published = deployment_targets(draft)draft.append(Endpoint("api-3", 443))print(draft)print(published)Why this design exists
Lists optimize mutable sequence work. Tuples give fixed-size sequence behavior and tuple-specific uses such as multiple return values, record-like grouping, and hashability when all contained objects are hashable.
Mechanics and CPython internals
CPython lists over-allocate their reference array so appends usually avoid immediate reallocation. Tuples allocate a fixed reference array for their length. Both are shallow containers over object references, so the mutability of contained objects is a separate question.
# [CURRENT - 3.10-3.14] Works on Python 3.10+from dataclasses import dataclassfrom sys import getsizeof@dataclass(frozen=True, slots=True)class Footprint: kind: str bytes: intdef compare(values: list[int]) -> list[Footprint]: return [ Footprint("list", getsizeof(values)), Footprint("tuple", getsizeof(tuple(values))), ]print(compare(list(range(1000))))Complexity and tradeoffs
Indexing is O(1) for both. List append is amortized O(1), while tuple "append" means allocating another tuple, typically O(n). Tuples can communicate stability and reduce growth overhead, but they do not make nested state immutable.
Idiomatic patterns and refactoring
Refactor mutable temporary assembly into an immutable published result when downstream code should only consume.
# [CURRENT - 3.10-3.14] Works on Python 3.10+from dataclasses import dataclass@dataclass(frozen=True, slots=True)class Column: name: strdef build_columns(raw: list[str]) -> tuple[Column, ...]: columns: list[Column] = [] for name in raw: columns.append(Column(name.casefold())) return tuple(columns)schema = build_columns(["Order_ID", "Amount_Cents"])print(schema)print(hash(schema))Common mistakes and edge cases
Do not use a tuple merely because a collection should not be reassigned; callers can still mutate contained lists or dataclass instances. Do not repeatedly concatenate tuples in a growth loop.
When to use / When NOT to use
Use tuple to publish fixed ordered data and list to build or mutate ordered data.
Do not choose tuple as a deep immutability guarantee unless the element graph also honors that guarantee.