|$ curl https://forge-ai.dev/api/markdown?path=docs/python/sets
$cat docs/python-—-sets.md
updated Recently·14 min read·published

Python — Sets

PythonIntermediate
Introduction

A set is an unordered collection of unique, hashable elements. Python offers two built-in set types: set (mutable) and frozenset(immutable and hashable). Sets implement the mathematical set operations you know from algebra — union, intersection, difference, and symmetric difference — with a clean operator syntax. Under the hood, sets are implemented as hash tables, giving O(1) average-time membership tests.

Creating Sets

Sets can be created with curly-brace literals, the set() constructor, or set comprehensions. Note that empty curly braces { } create a dict, not a set.

set_creation.py
Python
1# Curly-brace literal
2fruits = {"apple", "banana", "cherry"}
3numbers = {1, 2, 3, 4, 5}
4
5# set() constructor — accepts any iterable
6unique = set([1, 2, 2, 3, 3, 3]) # {1, 2, 3}
7word = set("hello") # {'h', 'e', 'l', 'o'} (no duplicate 'l')
8
9# Empty set — must use constructor
10empty_set = set() # ✅ correct — empty set
11empty_dict = {} # ❌ this is a dict, not a set
12
13# Set comprehension — see dedicated section below
14squares = {x ** 2 for x in range(6)} # {0, 1, 4, 9, 16, 25}
15
16# Heterogeneous sets (any hashable type)
17mixed = {42, "hello", (1, 2)} # ✅ int, str, tupleall hashable
18# bad = {[1, 2]} # ❌ TypeError: unhashable type: 'list'

info

Use set() to deduplicate any iterable in one call. This is the fastest way to remove duplicates from a list while preserving no particular order.
Basic Operations

The mutable setprovides methods to add, remove, and clear elements. Unlike lists, sets have no indexing — you access by membership, not position.

set_basic_ops.py
Python
1s = {1, 2, 3}
2
3# add — insert one element (no-op if already present)
4s.add(4) # {1, 2, 3, 4}
5s.add(2) # {1, 2, 3, 4} — unchanged, 2 already exists
6
7# remove — raises KeyError if missing
8s.remove(3) # {1, 2, 4}
9# s.remove(99) # KeyError: 99
10
11# discard — safe version, no error if missing
12s.discard(4) # {1, 2}
13s.discard(99) # {1, 2} — still no error
14
15# pop — remove and return an arbitrary element
16val = s.pop() # 1 (or 2 — order is not guaranteed)
17print(s) # {2}
18
19# clear — remove all elements
20s.clear() # set()
21
22# len, in, not in
23s = {10, 20, 30}
24print(len(s)) # 3
25print(20 in s) # True
26print(50 not in s) # True
27
28# copy — shallow copy
29t = s.copy()
30t.add(99)
31print(s) # {10, 20, 30} — unchanged

warning

s.pop() removes an arbitrary element, not the "first" one. Sets are unordered, so relying on pop order is a bug waiting to happen.
Set Algebra

Python sets support all standard mathematical set operations with intuitive operators and equivalent method calls. The operators return new sets; the mutating versions (|=, &=, -=, ^=) update the set in-place.

OperationOperatorMethodMutatingExampleResult
Union|union()|={1, 2} | {2, 3}{1, 2, 3}
Intersection&intersection()&={1, 2} & {2, 3}{2}
Difference-difference()-={1, 2, 3} - {2}{1, 3}
Symmetric Diff^symmetric_difference()^={1, 2} ^ {2, 3}{1, 3}
set_algebra.py
Python
1a = {1, 2, 3, 4}
2b = {3, 4, 5, 6}
3
4# Union — elements in either
5print(a | b) # {1, 2, 3, 4, 5, 6}
6print(a.union(b)) # same
7
8# Intersection — elements in both
9print(a & b) # {3, 4}
10print(a.intersection(b)) # same
11
12# Difference — elements in a but not b
13print(a - b) # {1, 2}
14print(a.difference(b)) # same
15
16# Symmetric difference — elements in a or b but not both
17print(a ^ b) # {1, 2, 5, 6}
18print(a.symmetric_difference(b)) # same
19
20# Mutating (in-place) versions
21a = {1, 2, 3}
22b = {3, 4, 5}
23
24a |= b # a = a | b
25print(a) # {1, 2, 3, 4, 5}
26
27a &= {1, 2} # a = a & {1, 2}
28print(a) # {1, 2}
29
30a -= {1} # a = a - {1}
31print(a) # {2}
32
33a ^= {2, 3} # a = a ^ {2, 3}
34print(a) # {3}
35
36# Difference between multiple sets
37x = {1, 2, 3, 4, 5}
38print(x - {1, 2} - {3}) # {4, 5} — chained differences
Set Comparisons

Python checks set relationships with comparison operators and the isdisjoint() method. Subset and superset tests use the same operators as numeric comparison, but they test containment rather than value ordering.

set_comparisons.py
Python
1a = {1, 2, 3}
2b = {1, 2, 3, 4, 5}
3c = {1, 2, 3}
4
5# Subset — every element of a is in b
6print(a <= b) # True (a is a subset of b)
7print(a <= c) # True (equal sets are subsets of each other)
8
9# Proper subset — subset and not equal
10print(a < b) # True (a is a proper subset of b)
11print(a < c) # False (equal, not proper)
12
13# Superset — every element of b is in a
14print(b >= a) # True (b is a superset of a)
15print(b >= c) # True
16
17# Proper superset
18print(b > a) # True
19print(a > c) # False
20
21# Disjoint — no elements in common
22x = {1, 2}
23y = {3, 4}
24z = {2, 3}
25print(x.isdisjoint(y)) # True (no overlap)
26print(x.isdisjoint(z)) # False (2 is in both)
27
28# Equality — same elements regardless of order
29print({1, 2, 3} == {3, 2, 1}) # True
30print({1, 2} == {1, 2, 3}) # False
Frozenset

A frozenset is an immutable, hashable version of a set. Once created, its contents cannot change. This makes it eligible to be a dictionary key or an element of another set — neither of which a mutable set can do.

frozenset.py
Python
1# Create a frozenset
2fs = frozenset([1, 2, 3, 3, 3])
3print(fs) # frozenset({1, 2, 3})
4
5# Immutable — no add, remove, discard, pop, clear
6# fs.add(4) # AttributeError
7
8# Hashable — can be a dict key
9registry = {
10 frozenset({"read", "write"}): "editor",
11 frozenset({"read"}): "viewer",
12}
13print(registry[frozenset({"read", "write"})]) # "editor"
14
15# Frozenset in a set
16set_of_sets = {frozenset({1, 2}), frozenset({3, 4})}
17# {frozenset({1, 2}), frozenset({3, 4})}
18
19# All set operations still work (return new frozenset)
20a = frozenset({1, 2, 3})
21b = frozenset({3, 4, 5})
22print(a | b) # frozenset({1, 2, 3, 4, 5})
23print(a & b) # frozenset({3})
24print(a - b) # frozenset({1, 2})
25
26# Comparison methods work too
27print(a.isdisjoint(frozenset({4, 5}))) # False
28print(a <= frozenset({1, 2, 3, 4})) # True
29
30# Use case: caching function results for set arguments
31def expensive(dataset: frozenset) -> int:
32 return sum(dataset) # placeholder
33
34cache: dict[frozenset, int] = {}
35key = frozenset([1, 2, 3])
36if key not in cache:
37 cache[key] = expensive(key)

best practice

Use frozensetwhen you need an immutable, hashable collection — for example as a dictionary key, as an element of another set, or in cached function results. It signals to readers that the data should not change.
Performance Characteristics

Sets are backed by hash tables, giving O(1) average-time membership tests — dramatically faster than O(n) list or tuple scans for large collections. This is the single most important reason to choose a set over a list when order and duplicates don't matter.

OperationSet ComplexityList ComplexityNotes
Membership (in)O(1) avgO(n)Set wins by orders of magnitude at scale
Add elementO(1) avgO(1) amortizedComparable
Remove elementO(1) avgO(n)List must find + shift
Union / IntersectionO(len(s) + len(t))N/ANo native equivalent in lists
IterationO(n)O(n)Comparable, but set is cache-friendlier
set_performance.py
Python
1# Set membership is vastly faster at scale
2import timeit
3
4n = 1_000_000
5search_items = list(range(10_000))
6
7# Build a list and a set with the same elements
8data_list = list(range(n))
9data_set = set(data_list)
10
11# Time membership tests
12list_time = timeit.timeit(
13 "sum(1 for x in search_items if x in data_list)",
14 globals=globals(), number=10
15)
16
17set_time = timeit.timeit(
18 "sum(1 for x in search_items if x in data_set)",
19 globals=globals(), number=10
20)
21
22print(f"list: {list_time:.3f}s") # ~3-5s (O(n) per check)
23print(f"set: {set_time:.3f}s") # ~0.01s (O(1) per check)
24
25# Deduplication benchmark
26data = [i % 1000 for i in range(100_000)]
27
28def dedup_list():
29 seen = []
30 for x in data:
31 if x not in seen:
32 seen.append(x)
33 return seen
34
35def dedup_set():
36 return list(set(data))
37
38# set dedup is ~100x faster than manual list dedup

info

If you need to test membership repeatedly, convert your list to a set once (O(n)) and enjoy O(1) lookups thereafter. The one-time conversion cost pays for itself after just a handful of lookups.
Set Comprehensions

Set comprehensions use curly braces just like dict comprehensions, but with a single expression instead of a key-value pair. They automatically discard duplicates.

set_comprehensions.py
Python
1# Basic set comprehension
2squares = {x ** 2 for x in range(6)}
3# {0, 1, 4, 9, 16, 25}
4
5# With filter condition
6evens = {x for x in range(20) if x % 2 == 0}
7# {0, 2, 4, 6, 8, 10, 12, 14, 16, 18}
8
9# Operating on strings
10words = ["hello", "world", "hello", "python", "world"]
11unique_lengths = {len(w) for w in words}
12# {5, 6} — "hello" and "world" are length 5, "python" is 6
13
14# Normalize to lowercase
15names = ["Alice", "BOB", "alice", "Bob", "CHARLIE"]
16unique_lower = {n.lower() for n in names}
17# {"alice", "bob", "charlie"}
18
19# Set from nested data
20matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
21unique_vals = {val for row in matrix for val in row}
22# {1, 2, 3, 4, 5, 6, 7, 8, 9}
23
24# Flatten and deduplicate categories
25orders = [
26 {"items": ["book", "pen"]},
27 {"items": ["pen", "notebook"]},
28 {"items": ["book", "ruler"]},
29]
30all_items = {item for order in orders for item in order["items"]}
31# {"book", "pen", "notebook", "ruler"}
32
33# Compare with equivalent loop:
34unique = set()
35for order in orders:
36 for item in order["items"]:
37 unique.add(item)
Common Use Cases

Sets shine in scenarios that align with their mathematical roots: uniqueness enforcement, fast membership queries, and algebraic operations.

set_use_cases.py
Python
1# 1. Removing duplicates (fastest approach)
2emails = ["a@x.com", "b@x.com", "a@x.com", "c@x.com"]
3unique_emails = list(set(emails))
4# ["a@x.com", "b@x.com", "c@x.com"] (order not preserved)
5
6# If order matters — use dict.fromkeys (Python 3.7+ preserves order)
7ordered_unique = list(dict.fromkeys(emails))
8# ["a@x.com", "b@x.com", "c@x.com"] (first-seen order)
9
10# 2. Fast membership testing
11blocked_ips: set[str] = {
12 "192.168.1.100",
13 "10.0.0.50",
14 "172.16.0.1",
15}
16
17def is_blocked(ip: str) -> bool:
18 return ip in blocked_ips # O(1) — fast even with millions
19
20# 3. Finding common elements (intersection)
21users_who_liked_a = {"alice", "bob", "carol"}
22users_who_liked_b = {"bob", "dave", "eve"}
23common_likers = users_who_liked_a & users_who_liked_b
24# {"bob"}
25
26# 4. Finding unique elements (difference)
27exclusive_to_a = users_who_liked_a - users_who_liked_b
28# {"alice", "carol"}
29
30# 5. Finding unique across groups (symmetric diff)
31either_not_both = users_who_liked_a ^ users_who_liked_b
32# {"alice", "carol", "dave", "eve"}
33
34# 6. Validating unique constraints
35def has_duplicates(items: list) -> bool:
36 return len(items) != len(set(items))
37
38print(has_duplicates([1, 2, 3, 2])) # True
39
40# 7. Tag/category system
41class Article:
42 def __init__(self, tags: list[str]):
43 self.tags: set[str] = set(tags)
44
45 def add_tag(self, tag: str) -> None:
46 self.tags.add(tag)
47
48 def has_tag(self, tag: str) -> bool:
49 return tag in self.tags
50
51 def merge_tags(self, other: set[str]) -> None:
52 self.tags |= other
53
54# 8. Data pipeline — filter seen records
55def process_unique_records(stream):
56 seen: set[int] = set()
57 for record in stream:
58 if record["id"] in seen:
59 continue # skip duplicate
60 seen.add(record["id"])
61 yield record
62
63# 9. Finding missing elements
64required = {1, 2, 3, 4, 5}
65present = {1, 3, 5}
66missing = required - present # {2, 4}
67
68# 10. Checking overlapping intervals (via set intersections)
69active_sessions: dict[int, set[int]] = {
70 1: {101, 102, 103},
71 2: {103, 104},
72 3: {105},
73}
74
75def find_overlapping(user_a: int, user_b: int) -> set[int]:
76 return active_sessions[user_a] & active_sessions[user_b]
77
78print(find_overlapping(1, 2)) # {103}

best practice

When you need to check for duplicates or test membership, reach for a set first. It is almost always the right tool. Reserve lists for ordered, duplicate-allowed data, and tuples for immutable records.
$Blueprint — Engineering Documentation·Section ID: PYTHON-SETS·Revision: 1.0