Python — Sets
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.
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.
| 1 | # Curly-brace literal |
| 2 | fruits = {"apple", "banana", "cherry"} |
| 3 | numbers = {1, 2, 3, 4, 5} |
| 4 | |
| 5 | # set() constructor — accepts any iterable |
| 6 | unique = set([1, 2, 2, 3, 3, 3]) # {1, 2, 3} |
| 7 | word = set("hello") # {'h', 'e', 'l', 'o'} (no duplicate 'l') |
| 8 | |
| 9 | # Empty set — must use constructor |
| 10 | empty_set = set() # ✅ correct — empty set |
| 11 | empty_dict = {} # ❌ this is a dict, not a set |
| 12 | |
| 13 | # Set comprehension — see dedicated section below |
| 14 | squares = {x ** 2 for x in range(6)} # {0, 1, 4, 9, 16, 25} |
| 15 | |
| 16 | # Heterogeneous sets (any hashable type) |
| 17 | mixed = {42, "hello", (1, 2)} # ✅ int, str, tuple — all hashable |
| 18 | # bad = {[1, 2]} # ❌ TypeError: unhashable type: 'list' |
info
The mutable setprovides methods to add, remove, and clear elements. Unlike lists, sets have no indexing — you access by membership, not position.
| 1 | s = {1, 2, 3} |
| 2 | |
| 3 | # add — insert one element (no-op if already present) |
| 4 | s.add(4) # {1, 2, 3, 4} |
| 5 | s.add(2) # {1, 2, 3, 4} — unchanged, 2 already exists |
| 6 | |
| 7 | # remove — raises KeyError if missing |
| 8 | s.remove(3) # {1, 2, 4} |
| 9 | # s.remove(99) # KeyError: 99 |
| 10 | |
| 11 | # discard — safe version, no error if missing |
| 12 | s.discard(4) # {1, 2} |
| 13 | s.discard(99) # {1, 2} — still no error |
| 14 | |
| 15 | # pop — remove and return an arbitrary element |
| 16 | val = s.pop() # 1 (or 2 — order is not guaranteed) |
| 17 | print(s) # {2} |
| 18 | |
| 19 | # clear — remove all elements |
| 20 | s.clear() # set() |
| 21 | |
| 22 | # len, in, not in |
| 23 | s = {10, 20, 30} |
| 24 | print(len(s)) # 3 |
| 25 | print(20 in s) # True |
| 26 | print(50 not in s) # True |
| 27 | |
| 28 | # copy — shallow copy |
| 29 | t = s.copy() |
| 30 | t.add(99) |
| 31 | print(s) # {10, 20, 30} — unchanged |
warning
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.
| Operation | Operator | Method | Mutating | Example | Result |
|---|---|---|---|---|---|
| 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} |
| 1 | a = {1, 2, 3, 4} |
| 2 | b = {3, 4, 5, 6} |
| 3 | |
| 4 | # Union — elements in either |
| 5 | print(a | b) # {1, 2, 3, 4, 5, 6} |
| 6 | print(a.union(b)) # same |
| 7 | |
| 8 | # Intersection — elements in both |
| 9 | print(a & b) # {3, 4} |
| 10 | print(a.intersection(b)) # same |
| 11 | |
| 12 | # Difference — elements in a but not b |
| 13 | print(a - b) # {1, 2} |
| 14 | print(a.difference(b)) # same |
| 15 | |
| 16 | # Symmetric difference — elements in a or b but not both |
| 17 | print(a ^ b) # {1, 2, 5, 6} |
| 18 | print(a.symmetric_difference(b)) # same |
| 19 | |
| 20 | # Mutating (in-place) versions |
| 21 | a = {1, 2, 3} |
| 22 | b = {3, 4, 5} |
| 23 | |
| 24 | a |= b # a = a | b |
| 25 | print(a) # {1, 2, 3, 4, 5} |
| 26 | |
| 27 | a &= {1, 2} # a = a & {1, 2} |
| 28 | print(a) # {1, 2} |
| 29 | |
| 30 | a -= {1} # a = a - {1} |
| 31 | print(a) # {2} |
| 32 | |
| 33 | a ^= {2, 3} # a = a ^ {2, 3} |
| 34 | print(a) # {3} |
| 35 | |
| 36 | # Difference between multiple sets |
| 37 | x = {1, 2, 3, 4, 5} |
| 38 | print(x - {1, 2} - {3}) # {4, 5} — chained differences |
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.
| 1 | a = {1, 2, 3} |
| 2 | b = {1, 2, 3, 4, 5} |
| 3 | c = {1, 2, 3} |
| 4 | |
| 5 | # Subset — every element of a is in b |
| 6 | print(a <= b) # True (a is a subset of b) |
| 7 | print(a <= c) # True (equal sets are subsets of each other) |
| 8 | |
| 9 | # Proper subset — subset and not equal |
| 10 | print(a < b) # True (a is a proper subset of b) |
| 11 | print(a < c) # False (equal, not proper) |
| 12 | |
| 13 | # Superset — every element of b is in a |
| 14 | print(b >= a) # True (b is a superset of a) |
| 15 | print(b >= c) # True |
| 16 | |
| 17 | # Proper superset |
| 18 | print(b > a) # True |
| 19 | print(a > c) # False |
| 20 | |
| 21 | # Disjoint — no elements in common |
| 22 | x = {1, 2} |
| 23 | y = {3, 4} |
| 24 | z = {2, 3} |
| 25 | print(x.isdisjoint(y)) # True (no overlap) |
| 26 | print(x.isdisjoint(z)) # False (2 is in both) |
| 27 | |
| 28 | # Equality — same elements regardless of order |
| 29 | print({1, 2, 3} == {3, 2, 1}) # True |
| 30 | print({1, 2} == {1, 2, 3}) # False |
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.
| 1 | # Create a frozenset |
| 2 | fs = frozenset([1, 2, 3, 3, 3]) |
| 3 | print(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 |
| 9 | registry = { |
| 10 | frozenset({"read", "write"}): "editor", |
| 11 | frozenset({"read"}): "viewer", |
| 12 | } |
| 13 | print(registry[frozenset({"read", "write"})]) # "editor" |
| 14 | |
| 15 | # Frozenset in a set |
| 16 | set_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) |
| 20 | a = frozenset({1, 2, 3}) |
| 21 | b = frozenset({3, 4, 5}) |
| 22 | print(a | b) # frozenset({1, 2, 3, 4, 5}) |
| 23 | print(a & b) # frozenset({3}) |
| 24 | print(a - b) # frozenset({1, 2}) |
| 25 | |
| 26 | # Comparison methods work too |
| 27 | print(a.isdisjoint(frozenset({4, 5}))) # False |
| 28 | print(a <= frozenset({1, 2, 3, 4})) # True |
| 29 | |
| 30 | # Use case: caching function results for set arguments |
| 31 | def expensive(dataset: frozenset) -> int: |
| 32 | return sum(dataset) # placeholder |
| 33 | |
| 34 | cache: dict[frozenset, int] = {} |
| 35 | key = frozenset([1, 2, 3]) |
| 36 | if key not in cache: |
| 37 | cache[key] = expensive(key) |
best practice
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.
| Operation | Set Complexity | List Complexity | Notes |
|---|---|---|---|
| Membership (in) | O(1) avg | O(n) | Set wins by orders of magnitude at scale |
| Add element | O(1) avg | O(1) amortized | Comparable |
| Remove element | O(1) avg | O(n) | List must find + shift |
| Union / Intersection | O(len(s) + len(t)) | N/A | No native equivalent in lists |
| Iteration | O(n) | O(n) | Comparable, but set is cache-friendlier |
| 1 | # Set membership is vastly faster at scale |
| 2 | import timeit |
| 3 | |
| 4 | n = 1_000_000 |
| 5 | search_items = list(range(10_000)) |
| 6 | |
| 7 | # Build a list and a set with the same elements |
| 8 | data_list = list(range(n)) |
| 9 | data_set = set(data_list) |
| 10 | |
| 11 | # Time membership tests |
| 12 | list_time = timeit.timeit( |
| 13 | "sum(1 for x in search_items if x in data_list)", |
| 14 | globals=globals(), number=10 |
| 15 | ) |
| 16 | |
| 17 | set_time = timeit.timeit( |
| 18 | "sum(1 for x in search_items if x in data_set)", |
| 19 | globals=globals(), number=10 |
| 20 | ) |
| 21 | |
| 22 | print(f"list: {list_time:.3f}s") # ~3-5s (O(n) per check) |
| 23 | print(f"set: {set_time:.3f}s") # ~0.01s (O(1) per check) |
| 24 | |
| 25 | # Deduplication benchmark |
| 26 | data = [i % 1000 for i in range(100_000)] |
| 27 | |
| 28 | def dedup_list(): |
| 29 | seen = [] |
| 30 | for x in data: |
| 31 | if x not in seen: |
| 32 | seen.append(x) |
| 33 | return seen |
| 34 | |
| 35 | def dedup_set(): |
| 36 | return list(set(data)) |
| 37 | |
| 38 | # set dedup is ~100x faster than manual list dedup |
info
Set comprehensions use curly braces just like dict comprehensions, but with a single expression instead of a key-value pair. They automatically discard duplicates.
| 1 | # Basic set comprehension |
| 2 | squares = {x ** 2 for x in range(6)} |
| 3 | # {0, 1, 4, 9, 16, 25} |
| 4 | |
| 5 | # With filter condition |
| 6 | evens = {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 |
| 10 | words = ["hello", "world", "hello", "python", "world"] |
| 11 | unique_lengths = {len(w) for w in words} |
| 12 | # {5, 6} — "hello" and "world" are length 5, "python" is 6 |
| 13 | |
| 14 | # Normalize to lowercase |
| 15 | names = ["Alice", "BOB", "alice", "Bob", "CHARLIE"] |
| 16 | unique_lower = {n.lower() for n in names} |
| 17 | # {"alice", "bob", "charlie"} |
| 18 | |
| 19 | # Set from nested data |
| 20 | matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]] |
| 21 | unique_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 |
| 25 | orders = [ |
| 26 | {"items": ["book", "pen"]}, |
| 27 | {"items": ["pen", "notebook"]}, |
| 28 | {"items": ["book", "ruler"]}, |
| 29 | ] |
| 30 | all_items = {item for order in orders for item in order["items"]} |
| 31 | # {"book", "pen", "notebook", "ruler"} |
| 32 | |
| 33 | # Compare with equivalent loop: |
| 34 | unique = set() |
| 35 | for order in orders: |
| 36 | for item in order["items"]: |
| 37 | unique.add(item) |
Sets shine in scenarios that align with their mathematical roots: uniqueness enforcement, fast membership queries, and algebraic operations.
| 1 | # 1. Removing duplicates (fastest approach) |
| 2 | emails = ["a@x.com", "b@x.com", "a@x.com", "c@x.com"] |
| 3 | unique_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) |
| 7 | ordered_unique = list(dict.fromkeys(emails)) |
| 8 | # ["a@x.com", "b@x.com", "c@x.com"] (first-seen order) |
| 9 | |
| 10 | # 2. Fast membership testing |
| 11 | blocked_ips: set[str] = { |
| 12 | "192.168.1.100", |
| 13 | "10.0.0.50", |
| 14 | "172.16.0.1", |
| 15 | } |
| 16 | |
| 17 | def is_blocked(ip: str) -> bool: |
| 18 | return ip in blocked_ips # O(1) — fast even with millions |
| 19 | |
| 20 | # 3. Finding common elements (intersection) |
| 21 | users_who_liked_a = {"alice", "bob", "carol"} |
| 22 | users_who_liked_b = {"bob", "dave", "eve"} |
| 23 | common_likers = users_who_liked_a & users_who_liked_b |
| 24 | # {"bob"} |
| 25 | |
| 26 | # 4. Finding unique elements (difference) |
| 27 | exclusive_to_a = users_who_liked_a - users_who_liked_b |
| 28 | # {"alice", "carol"} |
| 29 | |
| 30 | # 5. Finding unique across groups (symmetric diff) |
| 31 | either_not_both = users_who_liked_a ^ users_who_liked_b |
| 32 | # {"alice", "carol", "dave", "eve"} |
| 33 | |
| 34 | # 6. Validating unique constraints |
| 35 | def has_duplicates(items: list) -> bool: |
| 36 | return len(items) != len(set(items)) |
| 37 | |
| 38 | print(has_duplicates([1, 2, 3, 2])) # True |
| 39 | |
| 40 | # 7. Tag/category system |
| 41 | class 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 |
| 55 | def 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 |
| 64 | required = {1, 2, 3, 4, 5} |
| 65 | present = {1, 3, 5} |
| 66 | missing = required - present # {2, 4} |
| 67 | |
| 68 | # 10. Checking overlapping intervals (via set intersections) |
| 69 | active_sessions: dict[int, set[int]] = { |
| 70 | 1: {101, 102, 103}, |
| 71 | 2: {103, 104}, |
| 72 | 3: {105}, |
| 73 | } |
| 74 | |
| 75 | def find_overlapping(user_a: int, user_b: int) -> set[int]: |
| 76 | return active_sessions[user_a] & active_sessions[user_b] |
| 77 | |
| 78 | print(find_overlapping(1, 2)) # {103} |
best practice