PYTHON / DICTIONARIES AND SETS
Sets and set operations
Build sets in Python, test membership, and combine them with union, intersection, difference, and subset comparisons.
What you will learn
- Create sets with {..} or set(iterable) and know why {} is not an empty set
- Use | & - ^ to combine sets, and the method forms to accept any iterable
- Test membership with in and containment with <= / < instead of comparing sizes
- Choose add/discard/update for mutation and the operators for fresh sets
Understanding Sets and set operations
A set stores each element once, in no defined order, using the same hash-based lookup that dictionary keys use. That single implementation detail explains almost everything about sets: duplicates collapse because equal elements hash to the same slot, `x in s` is fast because Python jumps straight to that slot instead of scanning, and there is no index or ordering because position is decided by hash values, not by insertion. Write a set literally as `{3, 1, 3}`, which becomes `{1, 3}`, or build one from any iterable with `set("banana")`, which yields `{'b', 'a', 'n'}`.
The four core operations mirror set algebra. `a | b` is union (in either), `a & b` is intersection (in both), `a - b` is difference (in a, not in b), and `a ^ b` is symmetric difference (in exactly one). Each operator has a named method — `union`, `intersection`, `difference`, `symmetric_difference` — and the difference matters: the operators require both operands to be sets and raise `TypeError` otherwise, while the methods accept any iterable, so `tags.union(["a"])` works where `tags | ["a"]` fails.
Every operation comes in a copying form and a mutating form. `a | b` returns a new set and leaves both alone; `a |= b` and `a.update(b)` modify `a` in place and return `None`. Comparisons follow the same containment logic rather than ordering: `a <= b` asks whether every element of `a` is in `b`, and `a < b` adds that `b` has something more. Because containment is only a partial order, two overlapping-but-distinct sets like `{1, 2}` and `{2, 3}` make both `a < b` and `b < a` false, which surprises anyone who expects number-like comparison.
seen = {"alice", "bob", "carol", "alice"}
print(len(seen))
editors = {"bob", "dave"}
print(sorted(seen | editors))
print(sorted(seen & editors))
print(sorted(seen - editors))
print(sorted(seen ^ editors))
print(editors <= seen)
print("dave" in seen)A set is an unordered hash-based collection of unique elements, so uniqueness, fast membership, and the algebra of union/intersection/difference all follow from how it stores items.
Worked examples
Operators versus methods
Shows that the named methods accept any iterable while the operators demand a set on both sides.
tags = {"python", "sets"}
print(sorted(tags.union(["sets", "hashing"])))
try:
tags | ["hashing"]
except TypeError as e:
print("TypeError:", e)
tags.update("abc")
print(sorted(tags))Example explained
Line 1`tags.union([...])` converts the list to set semantics internally and returns a new set; `tags` is untouched.
Line 2`tags | ["hashing"]` refuses the list because `|` is only defined between two set-like objects.
Line 3`tags.update("abc")` iterates the string, so it adds three separate characters rather than the word `abc`.
Line 4`sorted(...)` is used for printing only, because the set itself has no stable display order.
Deduplicate while keeping order, and remove safely
Uses a set as a fast membership tracker alongside a list, then contrasts discard with remove.
raw = [3, 1, 3, 7, 1, 9]
seen = set()
unique = []
for n in raw:
if n not in seen:
seen.add(n)
unique.append(n)
print(unique)
print(type({}).__name__, type(set()).__name__)
seen.discard(42)
try:
seen.remove(42)
except KeyError as e:
print("KeyError:", e)Example explained
Line 1`set(raw)` alone would lose the original order, so the list keeps order while the set answers `not in` quickly.
Line 2`type({})` reports `dict`: the empty-set literal does not exist, and `set()` is the only way to build one.
Line 3`discard(42)` silently does nothing when the element is absent.
Line 4`remove(42)` raises `KeyError` for the same absent element, which is why it is the wrong default choice.
Important notes
Set elements must be hashable, so `{[1, 2]}` raises TypeError: unhashable type: 'list'; use a tuple when you need a compound element.
`a <= b` is a subset test, not a size test: `{1, 2} <= {1, 2}` is True, while `{1, 2} < {1, 2}` is False because a proper subset must be strictly smaller.
Common mistakes
Writing `s = {}` for an empty set: you actually get a dict, and the next `s.add(1)` fails with AttributeError: 'dict' object has no attribute 'add'.
Expecting insertion order or indexing, then writing `s[0]`: raises TypeError: 'set' object is not subscriptable, because sets store by hash, not position.
Calling `remove()` on an element that may not be present, which raises KeyError and kills the loop where `discard()` would have been a no-op.
Assigning the result of an in-place method, as in `s = s.update(other)`: `update` returns None, so the set is silently replaced by None.
Try it yourself
Change, predict, then run
Given `monday = ["ana", "raj", "lee", "ana"]` and `tuesday = ["lee", "kim", "raj"]`, print the sorted names who attended both days, the sorted names who came only on Monday, and the total count of distinct attendees.
Open the Python workspaceCheck your understanding
With `a = {1, 2, 3}` and `b = {3, 4}`, which expression evaluates to `{1, 2}` and leaves `a` unchanged?
- a -= b
- a.difference_update(b)
- a.difference(b)
- a.symmetric_difference(b)
Show answer
`a.difference(b)` returns a new set `{1, 2}` while `a` still holds `{1, 2, 3}`. `a.difference_update(b)` computes the same elements but mutates `a` in place and evaluates to None, so it fails the 'unchanged' half of the question; `a -= b` mutates as well, and `a.symmetric_difference(b)` returns `{1, 2, 4}` because it also keeps the element unique to `b`.