Python Tuples, Sets and Immutability

AI-assistedDrafted with AI assistance and reviewed before publishing; examples are version-pinned. Verify against official docs when in doubt.

Python 3.13

Lists and dicts do most of the work, but two other containers remove a lot of code: tuples express fixed records, and sets answer “have I seen this?” in constant time.

Tuples: small fixed records

point = (3, 4)
x, y = point        # unpacking
print(x, y)         # 3 4

Parentheses are optional; the comma makes the tuple:

a = 1, 2            # (1, 2)
single = (1,)       # trailing comma required
not_a_tuple = (1)   # just the integer 1

Use a tuple when the number of fields is fixed and their order means something: coordinates, database rows, (key, value) pairs.

Packing and unpacking

name, age = "Ada", 36

first, *rest = [1, 2, 3, 4]   # first=1, rest=[2, 3, 4]
a, b = b, a                   # swap without a temporary

Returning several values from a function is just returning a tuple:

def min_max(values):
    return min(values), max(values)

low, high = min_max([3, 1, 9])

Named tuples

point[0] says nothing about what it holds. NamedTuple adds field names while staying a tuple:

from typing import NamedTuple

class Point(NamedTuple):
    x: float
    y: float

p = Point(3, 4)
p.x              # 3
p._asdict()      # {'x': 3, 'y': 4}

It still unpacks, compares by value, and works as a dictionary key.

What immutability actually means

A tuple cannot gain, lose, or replace elements. Its contents can still be mutable:

record = (1, [2, 3])
record[1].append(4)     # fine: mutating the list
record[1] = [2, 3, 4]   # TypeError: cannot rebind the slot

Hashability follows from the elements: a tuple is hashable only if every element is.

{(1, 2): "ok"}          # fine
{(1, [2]): "ok"}        # TypeError: unhashable type: 'list'

So a tuple is a reliable dict key only when it holds immutable values.

Sets: deduplication and membership

tags = {"python", "web", "python"}
print(tags)             # {'python', 'web'}

Sets are unordered and cannot be indexed. Their value is the average O(1) membership test:

if "python" in tags:    # O(1) on a set, O(n) on a list
    print("present")

Add and remove:

tags.add("cli")
tags.discard("web")     # no error if absent
tags.remove("web")      # KeyError if absent

Set operations

a = {"python", "go"}
b = {"go", "rust"}

a | b      # union:        {'python', 'go', 'rust'}
a & b      # intersection: {'go'}
a - b      # difference:   {'python'}
a ^ b      # symmetric:    {'python', 'rust'}

Deduplicating while preserving order needs a dict, because sets do not keep insertion order:

items = ["b", "a", "b", "c"]
list(dict.fromkeys(items))    # ['b', 'a', 'c']

frozenset is the immutable, hashable variant:

frozen = frozenset(["a", "b"])
{frozen: "usable as a key"}

Choosing a container

Need Container Why
Ordered, mutable, duplicates allowed list index access, append
Fixed record, usable as a key tuple / NamedTuple immutable, hashable
Deduplication, fast membership set O(1) lookup
Key-value mapping dict lookup by key

A useful rule: if the code says x in collection inside a loop, a set or dict is usually the right structure.

Common mistakes

1. Forgetting the comma in a one-element tuple

("a") is a string; ("a",) is a tuple. This bites when building tuples dynamically.

2. Depending on set order

set(["c", "a"]) has no guaranteed iteration order. Sort it before displaying or comparing output.

3. Putting a list inside a set or dict key

{["a"]: 1}      # TypeError: unhashable type: 'list'

Convert to a tuple or frozenset.

4. Writing {} for an empty set

{}          # empty dict
set()       # empty set

5. Assuming a tuple makes everything inside immutable

A tuple containing a list still allows mutation of that list — and becomes unhashable.

6. Using a mutable default value

The classic bug belongs to functions, but tuples do not protect you: if the tuple holds a list and you use it as a default argument, the list is shared across calls.

Summary

  • Tuples are defined by commas, support unpacking, and work as keys when all elements are hashable.
  • Immutability blocks rebinding, not mutation of nested objects.
  • Sets deduplicate and answer membership in O(1), at the cost of ordering.
  • dict.fromkeys deduplicates while preserving order.
  • Pick the container from the operation: sequence, record, membership, or mapping.

Next: functions, arguments, and how values cross the call boundary.