Tuples and sets

Pick the right container: a tuple when the shape is fixed, a set when the question is "have I seen this".

05of 24
≈ 12Min read
5Parts

A tuple is a fixed record

A tuple is immutable, which is why it can be a dictionary key or a set member, and why it is the natural return type for a function that answers more than one thing at once. The parentheses are usually optional: return x, y builds a tuple.

When a tuple grows past three fields, reach for a NamedTuple or a dataclass — person[2] is unreadable two weeks later.

py
point = (3, 4)
x, y = point              # unpacking — this is the point of tuples
a, b = b, a               # swap, with no temporary

single = (5,)             # the comma makes it a tuple; (5) is just 5
empty = ()

first, *rest = [1, 2, 3, 4]      # first=1, rest=[2, 3, 4]
name, _, age = ("ada", "x", 36)  # _ is the convention for "not used"

Sets: membership and de-duplication

A set is an unordered collection of unique, hashable values. Membership is constant time rather than a scan, which is the reason to use one: the difference between a script that takes a minute and one that takes an hour.

It has no order. list(set(items)) de-duplicates but scrambles; when order matters use dict.fromkeys(items), which keeps the first occurrence of each.

py
seen = {"a", "b", "c"}     # a set literal
empty = set()              # {} is an empty DICT, not a set

seen.add("d")
seen.discard("z")          # no error if missing; .remove() raises
"a" in seen                # O(1), however big the set gets

list(set(items))           # de-duplicated — but the order is gone

Set algebra reads like the mathematics

"Which users are in both groups", "which files are new since last time", "which permissions is this role missing" — all of them are one operator, and all of them are fast.

py
a = {1, 2, 3}
b = {3, 4, 5}

a | b      # {1, 2, 3, 4, 5}   union            a.union(b)
a & b      # {3}               intersection     a.intersection(b)
a - b      # {1, 2}            difference       a.difference(b)
a ^ b      # {1, 2, 4, 5}      symmetric difference

a <= b     # is a a subset of b?
a.isdisjoint(b)               # nothing in common?

Hashable means usable as a key

A value may go in a set, or be a dictionary key, only if it is hashable — and in practice that means immutable. Lists, dicts and sets are not; numbers, strings, tuples of hashables and frozenset are.

The rule behind it: if a key could change after it was stored, the structure could no longer find it.

py
{(1, 2), (3, 4)}          # fine — tuples are hashable
{[1, 2]}                  # TypeError: unhashable type: 'list'

frozenset({1, 2})         # an immutable set — this one IS hashable

Choosing

Getting this right once, at the start, is usually worth more than any later optimisation.

QuestionContainer
Does the order matter, and will it change?list
Is it a fixed record of a few fields?tuple — or a NamedTuple
Do I only need "is this in here?"set
Am I looking things up by a key?dict
Do I keep taking from the front?collections.deque

Practice