Tuples and Immutable Sequence Records

Updated

September 7, 2026

Tuples and Immutable Sequence Records

After reading this chapter, you will master the internal architecture of Python tuples, distinguish when to use immutable records over mutable lists, leverage composite tuples as hashable dictionary keys, enforce defensive API boundaries, and prevent singleton comma syntax traps.

Mental model

While a list is designed for homogeneous, dynamically resizing collections, a tuple is designed for heterogeneous, fixed-length records.

In CPython, a tuple (PyTupleObject) is fixed in size at allocation time: - It has no allocated capacity field. - Its memory buffer contains exactly the number of pointer slots required for its elements. - CPython maintains a memory cache (“free list”) for small tuples (up to 20 elements), making tuple allocation and deallocation exceptionally fast.

CPython PyTupleObject Architecture:
┌────────────────────────────────────────────────────────┐
│ PyTupleObject Header (40 bytes on 64-bit)              │
│  ├── ob_refcnt: 1                                      │
│  ├── ob_type:   &PyTuple_Type                          │
│  ├── ob_size:   3   (Exact length, fixed forever)      │
│  └── ob_item    ───────────────────────────┐           │
└────────────────────────────────────────────┼───────────┘
                                             │
      ┌──────────────────────────────────────┘
      ▼
┌───────────┬───────────┬───────────┐
│ Slot 0    │ Slot 1    │ Slot 2    │  (Zero spare capacity; no reallocation)
│ &"us-east"│ &8080     │ &"active" │
└─────┬─────┴─────┬─────┴─────┬─────┘
      │           │           │
      ▼           ▼           ▼
  "us-east"      8080      "active"

The Hashability Rule for Tuples

An object is hashable if it has an immutable hash value that remains constant throughout its lifetime. A tuple is hashable if and only if every element contained within it is also hashable:

( "10.0.0.1", 443 )          ──▶ All elements immutable ──▶ Hashable (Valid dict key)
( "10.0.0.1", [ 80, 443 ] )  ──▶ Contains mutable list  ──▶ TypeError: unhashable type: 'list'

Minimal example

Save as tuple_mechanics.py:

# tuple_mechanics.py
import sys

def main() -> None:
    # 1. Memory footprint comparison
    items_list = [10, 20, 30, 40]
    items_tuple = (10, 20, 30, 40)

    print(f"List  size (4 items): {sys.getsizeof(items_list)} bytes (includes over-allocation buffer)")
    print(f"Tuple size (4 items): {sys.getsizeof(items_tuple)} bytes (lean, fixed struct)")

    # 2. Singleton tuple syntax: Trailing comma is REQUIRED
    not_a_tuple = ("standalone")      # Evaluates to a plain string!
    real_tuple  = ("standalone",)     # Comma defines the tuple!

    print(f"\nType of ('standalone'):  {type(not_a_tuple).__name__}")
    print(f"Type of ('standalone',): {type(real_tuple).__name__}")

    # 3. Tuples as composite dictionary keys
    # Map (datacenter, cluster_id) -> primary VIP
    cluster_vips: dict[tuple[str, int], str] = {
        ("us-east", 1): "10.100.1.1",
        ("us-east", 2): "10.100.2.1",
        ("eu-west", 1): "10.200.1.1",
    }

    target = ("us-east", 2)
    print(f"\nVIP for cluster {target}: {cluster_vips[target]}")

if __name__ == "__main__":
    main()

Run via uv run python tuple_mechanics.py:

List  size (4 items): 88 bytes (includes over-allocation buffer)
Tuple size (4 items): 80 bytes (lean, fixed struct)

Type of ('standalone'):  str
Type of ('standalone',): tuple

VIP for cluster ('us-east', 2): 10.100.2.1

Worked examples

Case 1: Composite Coordinates and Multi-Dimensional Indexing

In network engineering and cloud orchestration, routing tables and security groups frequently require multi-attribute indexing (e.g. (source_ip, destination_port, protocol)). Tuples make natural, zero-dependency composite keys:

# firewall_lookup.py
def main() -> None:
    # Rule table: (dest_port, protocol) -> action
    security_rules: dict[tuple[int, str], str] = {
        (22, "tcp"):   "ALLOW_ADMIN",
        (80, "tcp"):   "REDIRECT_HTTPS",
        (443, "tcp"):  "ALLOW_PUBLIC",
        (53, "udp"):   "ALLOW_DNS",
        (123, "udp"):  "ALLOW_NTP",
    }

    inbound_probes = [
        (443, "tcp"),
        (22, "tcp"),
        (3389, "tcp"),
        (53, "udp"),
    ]

    for port, proto in inbound_probes:
        # Direct O(1) hash lookup on composite tuple
        action = security_rules.get((port, proto), "DENY_LOG")
        print(f"Probe to Port {port:4d}/{proto:3} -> Decision: {action}")

if __name__ == "__main__":
    main()

Run:

uv run python firewall_lookup.py

Output:

Probe to Port  443/tcp -> Decision: ALLOW_PUBLIC
Probe to Port   22/tcp -> Decision: ALLOW_ADMIN
Probe to Port 3389/tcp -> Decision: DENY_LOG
Probe to Port   53/udp -> Decision: ALLOW_DNS

Case 2: Defensive API Design (Immutable Return Values)

When an internal service or class exposes its collected state to external consumers, returning a mutable list allows callers to accidentally or maliciously corrupt the internal state. Returning a tuple guarantees immutability:

# defensive_service.py
class NodeCluster:
    def __init__(self, cluster_name: str) -> None:
        self.cluster_name = cluster_name
        self._active_nodes: list[str] = []

    def register_node(self, node_id: str) -> None:
        self._active_nodes.append(node_id)

    @property
    def nodes(self) -> tuple[str, ...]:
        """Expose nodes as an immutable tuple snapshot."""
        return tuple(self._active_nodes)

def main() -> None:
    cluster = NodeCluster("prod-compute")
    cluster.register_node("node-01")
    cluster.register_node("node-02")

    current_nodes = cluster.nodes
    print(f"Registered nodes: {current_nodes}")

    # Attempting to mutate external view raises AttributeError
    try:
        current_nodes.append("rogue-node")  # type: ignore
    except AttributeError as err:
        print(f"Caught expected error: {err}")

    # Internal state remains completely pristine
    print(f"Internal cluster state verified intact: {cluster.nodes}")

if __name__ == "__main__":
    main()

Run:

uv run python defensive_service.py

Output:

Registered nodes: ('node-01', 'node-02')
Caught expected error: 'tuple' object has no attribute 'append'
Internal cluster state verified intact: ('node-01', 'node-02')

Case 3: Tuple Struct Packing and Structural Records

Tuples represent fixed records where position defines semantics. When paired with standard sequence unpacking, they avoid the overhead of heavy class instances for lightweight data exchange:

# metrics_pipeline.py
def fetch_telemetry_sample() -> tuple[str, float, int]:
    """Return a fixed telemetry packet: (sensor_id, temperature_c, timestamp)."""
    return ("sensor-temp-04", 42.8, 1725705600)

def main() -> None:
    sample = fetch_telemetry_sample()

    # Clean unpacking
    sensor_id, temp_c, ts = sample
    print(f"Sensor ID:   {sensor_id}")
    print(f"Temperature: {temp_c:.1f} C")
    print(f"Timestamp:   {ts}")

    # Comparison mechanics: Tuples compare element-by-element lexicographically
    version_a = (2, 4, 1)
    version_b = (2, 5, 0)
    print(f"\nSemantic version comparison:")
    print(f"  {version_a} < {version_b} -> {version_a < version_b}")

if __name__ == "__main__":
    main()

Run:

uv run python metrics_pipeline.py

Output:

Sensor ID:   sensor-temp-04
Temperature: 42.8 C
Timestamp:   1725705600

Semantic version comparison:
  (2, 4, 1) < (2, 5, 0) -> True

Pitfalls

Pitfall 1: The Missing Comma in Singleton Tuples

Parentheses in Python serve double duty: grouping expressions and delimiting tuples. Without a trailing comma, parentheses simply group an expression:

# THE TRAP:
single_item = ("config.yaml")  # BUG: This is a string, NOT a tuple!
print(type(single_item))       # <class 'str'>

# Calling a function expecting an iterable of files:
# for file in single_item: iterates character-by-character: 'c', 'o', 'n', 'f', ...!

# THE FIX: Always include a trailing comma for singletons
real_tuple = ("config.yaml",)
print(type(real_tuple))        # <class 'tuple'>

Pitfall 2: The Mutable-Element-in-Tuple Trap

A tuple’s immutability means its pointer array cannot be modified. However, if a tuple contains a mutable object (like a list), the contents of that object can still be mutated, and the tuple cannot be hashed:

# THE TRAP:
tricky = (1, 2, ["alpha", "beta"])

# 1. Immutability is shallow: the inner list CAN be modified!
tricky[2].append("gamma")
print(f"Mutated inner list: {tricky}")  # (1, 2, ['alpha', 'beta', 'gamma'])

# 2. Hashability fails:
try:
    bad_dict = {tricky: "payload"}
except TypeError as err:
    print(f"Hash failure: {err}")  # TypeError: unhashable type: 'list'

Output:

Mutated inner list: (1, 2, ['alpha', 'beta', 'gamma'])
Hash failure: unhashable type: 'list'
# THE FIX: Ensure all nested elements are immutable if hashing is required
safe_tuple = (1, 2, ("alpha", "beta", "gamma"))
safe_dict = {safe_tuple: "payload"}
print(f"Safe dict lookup: {safe_dict[safe_tuple]}")

Pitfall 3: The Augmented Assignment Trap on Tuples

Attempting += on a mutable object inside a tuple both raises TypeError AND modifies the object:

t = (1, [2, 3])
try:
    t[1] += [4]
except TypeError as e:
    print(f"Raised: {e}")

print(f"State after exception: {t}")

Output:

Raised: 'tuple' object does not support item assignment
State after exception: (1, [2, 3, 4])

Why this happens: += mutates the list in place (adding 4), and then attempts to assign the mutated list back to slot t[1], which the tuple forbids. Avoid storing mutable objects inside tuples.


Exercises

  1. Create a 1-element tuple containing the integer 42. Confirm with type() that it is a tuple, not an int.
  2. Given a list of (host, port) tuples, write a function that finds all unique ports using a set comprehension.
  3. Write a version comparison function that takes two release strings (e.g. "1.14.2" and "1.9.5"), converts them to tuples of integers, and determines which version is newer.
  4. Construct a dictionary where keys are 3D grid coordinates (x, y, z) and values are block types. Query coordinate (10, 5, 2) using .get().
  5. Demonstrate why (1, [2, 3]) cannot be added to a Python set, and rewrite it so it can.

Further reading

  • Python Documentation: Standard Types – Tuples.
  • CPython Source Code: Objects/tupleobject.c (tuple implementation and small-tuple caching).
  • Raymond Hettinger: Transforming Code into Beautiful, Idiomatic Python.