Sets
A set is an unordered collection of unique elements. Sets are built for two things above all else: eliminating duplicates and testing membership extremely fast. If you have ever found yourself writing if x in some_list inside a loop, a set is very likely the structure you actually wanted.
Creating a Set
You can create a set with curly-brace literal syntax, or by passing an iterable to the set() constructor. Both produce the same kind of object.
primes = {2, 3, 5, 7, 11}
print(primes) # {2, 3, 5, 7, 11} (order not guaranteed)
letters = set("mississippi")
print(letters) # {'m', 'i', 's', 'p'} -- duplicates collapse away
numbers = set([1, 2, 2, 3, 3, 3])
print(numbers) # {1, 2, 3}empty_dict = {}
print(type(empty_dict)) # <class 'dict'>
empty_set = set()
print(type(empty_set)) # <class 'set'>Unordered and Unique
Two properties define a set. First, elements have no fixed position — you cannot index into a set with s[0], and the printed order may not match insertion order. Second, every element is unique: adding a value that already exists is a silent no-op rather than an error.
s = {1, 2, 3}
s.add(2) # 2 is already present, nothing changes
print(s) # {1, 2, 3}
# s[0] # TypeError: 'set' object is not subscriptableSet Operations
Sets support the mathematical set operations directly through operators, and each also has an equivalent method form that accepts any iterable (not just another set).
Operation | Operator | Method | Meaning |
|---|---|---|---|
Union | a | b | a.union(b) | All elements from both sets. |
Intersection | a & b | a.intersection(b) | Elements present in both sets. |
Difference | a - b | a.difference(b) | Elements in |
Symmetric difference | a ^ b | a.symmetric_difference(b) | Elements in exactly one of the two sets. |
a = {1, 2, 3, 4}
b = {3, 4, 5, 6}
print(a | b) # {1, 2, 3, 4, 5, 6} -> union
print(a & b) # {3, 4} -> intersection
print(a - b) # {1, 2} -> difference (in a, not in b)
print(b - a) # {5, 6} -> difference (in b, not in a)
print(a ^ b) # {1, 2, 5, 6} -> symmetric differenceAdding and Removing Elements
Sets are mutable, so you can grow and shrink them in place. add() inserts a single element, while remove() and discard() both delete one — but they disagree on what happens when the element is missing.
Method | Behavior if element is missing |
|---|---|
| Raises |
| Does nothing — silently no-op. |
| Removes and returns an arbitrary element; raises |
colors = {"red", "green", "blue"}
colors.add("yellow")
print(colors) # {'red', 'green', 'blue', 'yellow'} (order may vary)
colors.discard("purple") # not present -> no error
# colors.remove("purple") # would raise KeyError: 'purple'
removed = colors.pop() # removes some arbitrary element
print(removed, colors)Why Use a Set? Deduplication and Fast Membership
The two killer features of sets are removing duplicates and O(1) average-case membership testing, compared to O(n) for a list. Checking x in my_list has to scan every element in the worst case; checking x in my_set hashes x and jumps straight to the right bucket.
# Deduplicating a list while (mostly) preserving distinctness
raw = [1, 5, 2, 5, 1, 8, 2]
unique = list(set(raw))
print(unique) # [1, 2, 5, 8] (order not guaranteed to match raw)
# Fast membership testing
allowed_ids = {101, 102, 103, 104} # imagine this has 100,000 entries
if 103 in allowed_ids: # O(1) average case
print("access granted")
allowed_ids_list = [101, 102, 103, 104]
if 103 in allowed_ids_list: # O(n) -- scans until found
print("access granted, but slower at scale")frozenset: the Immutable Variant
A regular set is mutable, which means it is unhashable and cannot be used as a dictionary key or stored inside another set. frozenset is an immutable version of set — once created, it cannot be changed, which makes it hashable and usable anywhere an immutable value is required.
readonly = frozenset([1, 2, 3])
# readonly.add(4) # AttributeError: 'frozenset' object has no attribute 'add'
# A plain set cannot be a dict key or live inside another set:
# cache = {{1, 2}: "value"} # TypeError: unhashable type: 'set'
# But a frozenset can:
cache = {frozenset([1, 2]): "value"}
print(cache[frozenset([2, 1])]) # "value" -- order inside the frozenset doesn't matter
# Sets of sets aren't allowed, but sets of frozensets are:
grouped = {frozenset([1, 2]), frozenset([3, 4])}
print(grouped)setis mutable:add,remove,discard,popall work.frozensetis immutable: no method can change its contents after creation.Only
frozenset(notset) can be used as a dict key or as a member of another set, because both require hashable elements.