Module 2 · Core Data Structures ⏱ 18 min

Sets & Tuples

By the end of this lesson you will be able to:
  • Choose a set when you need unique items or fast membership tests
  • Choose a tuple for a fixed, ordered, immutable record, and unpack one into named variables
  • Apply set operations and avoid the single-element-tuple and unhashable-item traps

Two more containers round out the basics, and each fills a job a list does poorly. A set stores unique items and throws duplicates away; a tuple is an ordered, immutable record - like a list you cannot change after building it.

tags = {"python", "code", "python"}   # {'python', 'code'}
point = (3, 4)                       # a fixed pair

A list keeps every value in order and lets you edit it, which is wonderful until you do not want repeats, or until a value should be locked in place. Use a set to remove repeats or test membership in a flash; use a tuple for things that belong together and should not be edited - a coordinate, a fixed row of values, several results handed back from one function.

flowchart TD
  A["[1, 1, 2, 3, 3]"] --> S["set(...)"]
  S --> B["{1, 2, 3}"]
  style S fill:#3776ab,color:#fff
A set drops duplicates; a tuple keeps a fixed, ordered record.

Sets: uniqueness and fast membership

A set's defining trick is that it holds each value at most once. Wrap a list in set(...) and the repeats simply vanish, which is the easiest way to dedupe anything. The other gift is speed: testing x in some_set is effectively instant, even when the set holds millions of items, because a set looks items up by hash rather than scanning one by one. A list has to check every element until it finds a match.

Sets are mutable - you grow and shrink them with .add() to insert and .remove() or .discard() to delete. The difference matters: .discard() stays quiet if the item is absent, while .remove() raises an error. The price for all that hashing magic is that a set is unordered (you cannot rely on the order you visit its items, and s[0] raises a TypeError) and it only accepts hashable values.

Dedupe with set(...), test membership with in, and unpack a tuple into names.
tags = ["python", "code", "python", "fun", "code"]
unique = set(tags)
print(sorted(unique))
print(len(unique))
print("python" in unique)

point = (3, 4)
x, y = point
print("x =", x, "y =", y)

Set operations: combining groups

Because sets model mathematical sets, they come with the operations you remember from that class. The union a | b holds everything in either set; the intersection a & b holds only what appears in both; the difference a - b holds what is in a but not b. Each builds a fresh set and leaves the originals alone.

These turn awkward loops into a single expression. "Which tags do these two articles share?" becomes set(tags_a) & set(tags_b). "Which customers bought but never left a review?" becomes buyers - reviewers. Whenever a question is really about overlap or difference, reach for a set before you reach for a loop.

flowchart LR
  A["{1, 2, 3}"] --> U["union<br/>everything in either"]
  B["{3, 4, 5}"] --> U
  U --> R1["{1, 2, 3, 4, 5}"]
  A --> I["intersection<br/>only the overlap"]
  B --> I
  I --> R2["{3}"]
  style U fill:#3776ab,color:#fff
  style I fill:#b45309,color:#fff
Union joins everything; intersection keeps only the overlap; difference subtracts.
Union, intersection, and difference - one operator each.
a = {1, 2, 3}
b = {3, 4, 5}

print("union:", a | b)            # {1, 2, 3, 4, 5}
print("intersection:", a & b)     # {3}
print("difference:", a - b)       # {1, 2}

print(4 in b)                     # True - fast membership

Tuples: ordered and immutable

A tuple is the fixed cousin of a list. Once you build (3, 4), you can read point[0] and ask for len(point), but you cannot reassign a position - there is no .append() and no in-place sort. That rigidity is a feature, not a limitation: a tuple promises its values will not change, which makes it safe to use as a dictionary key or a set member (lists cannot serve either role, because they are mutable). Tuples are also how a function returns several values at once - return low, high packs two results into one tuple the caller can unpack.

point = (3, 4)
print(point[0])        # 3
print(len(point))      # 2
# point[0] = 5         # TypeError - tuples are immutable
flowchart LR
  T["tuple ('Ada', 36, 'London')"] --> P["unpack into names"]
  P --> N1["name = 'Ada'"]
  P --> N2["age = 36"]
  P --> N3["city = 'London'"]
  style P fill:#3776ab,color:#fff
Unpacking assigns each position of a tuple to its own name in one line.

Unpacking, and choosing the right container

Tuples shine with unpacking - assigning each position to its own name in a single line, which beats indexing for readability:

name, age, city = ("Ada", 36, "London")

Picking the container comes down to what you need. Reach for a list when order matters and the contents will change. Reach for a set when you want uniqueness or fast membership tests. Reach for a tuple when the values form a fixed record that should not move - a coordinate, an RGB colour, a pair of results. Matching the container to the job makes your intent obvious to the next reader, instead of leaving them to guess why you reached for a list yet again.

Why some values can live in a set and others cannot

A set and a dictionary key share one rule: the value must be hashable, meaning Python can compute a single fixed number that identifies it. That number is the index the set uses to file the value away. A hash only works when the value can never change, because a change would move it to a different slot and leave the old lookup stranded.

That is why an immutable value qualifies and a list does not. A list can be appended to or reordered at any moment, so its hash would be a moving target. A tuple, locked at creation, keeps a stable hash - which is why it passes the same test a list fails.

Exercise

Write unique_sorted(values) that returns a sorted list of the unique values. (Example: unique_sorted([3, 1, 2, 3, 1])[1, 2, 3].)

def unique_sorted(values):
    # use set(...) to dedupe, then sorted(...)
    pass
Exercise

Write in_both(list_a, list_b) that returns a sorted list of the values that appear in BOTH lists. Use set intersection (&) to find the overlap.

def in_both(list_a, list_b):
    # set(list_a) & set(list_b), then sorted(...)
    pass
Exercise

What does this print? A tuple is unpacked into three names.

person = ("Ada", 36, "London")
name, age, city = person
print(name)
print(age)
print(city)
Exercise

single() is meant to return a one-item tuple containing the number 42, but return (42) just returns the bare number - parentheses group an expression, they do not make a tuple. Add the trailing comma that tells Python this really is a tuple.

def single():
    return (42)
Exercise

A function can pack several values into one tuple and let the caller unpack them. Write min_max(numbers) that returns the smallest and largest values as a tuple (low, high) - return them separated by a comma, with no parentheses. Assume numbers is a non-empty list.

def min_max(numbers):
    # return two values separated by a comma
    pass

Recap

  • A set holds unique, unordered items - wrap a collection in set(...) to dedupe, and test membership with in at hash speed.
  • Sets combine with | (union), & (intersection), and - (difference).
  • A tuple is ordered and immutable - safe as a dictionary key, ideal for fixed records.
  • Unpack a tuple into names in one line: name, age = record.
  • Mind the traps: (5,) not (5) for a one-item tuple, and sets hold only hashable values.

Next you will store key-value pairs in dictionaries, where tuples often serve as keys.

Checkpoint quiz

What does len({1, 1, 2, 2, 3}) return?

Which expression gives only the values that appear in BOTH set a and set b?

Why does len((42)) raise a TypeError instead of returning 1?

Go deeper — technical resources