PythonSets

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.

Python
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}
The empty-set gotcha
You cannot create an empty set with `` — that syntax creates an empty **dict**, not a set. To get an empty set, you must call `set()`.

Python
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.

Python
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 subscriptable
Set 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 a but not in b.

Symmetric difference

a ^ b

a.symmetric_difference(b)

Elements in exactly one of the two sets.

Python
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 difference
Adding 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

remove(x)

Raises KeyError.

discard(x)

Does nothing — silently no-op.

pop()

Removes and returns an arbitrary element; raises KeyError on an empty set.

Python
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)
Note
Because sets are unordered, `pop()` does not guarantee which element you get back. Never rely on it for anything order-sensitive.
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.

Python
# 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")
Tip
Reach for a set whenever you find yourself repeatedly asking “have I seen this before?” or “is this value in that collection?” inside a loop. Converting a list to a set once, up front, can turn an O(n²) algorithm into an O(n) one.
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.

Python
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)
  • set is mutable: add, remove, discard, pop all work.

  • frozenset is immutable: no method can change its contents after creation.

  • Only frozenset (not set) can be used as a dict key or as a member of another set, because both require hashable elements.