
Python Course
Advanced Sets in Python
You were introduced to sets in Lesson 13 — unordered collections of unique items. Now it is time to go deeper. Sets in Python are not just a way to remove duplicates. They come with a full suite of mathematical operations — unions, intersections, differences, and more — that let you compare and combine collections with remarkable speed and clarity.
This lesson covers every set operation you will encounter professionally, explains why sets dramatically outperform lists for membership testing, and covers set comprehensions and frozenset.
What Makes a Set Unique — Quick Recap
- Sets store only unique values — duplicates are silently discarded on insertion.
- Sets are unordered — you cannot index or slice them.
- Set elements must be hashable — strings, numbers, and tuples work; lists and dicts do not.
- Sets are mutable — you can add and remove items after creation.
- A frozenset is the immutable version — hashable itself, so it can be used as a dict key.
# Creating sets — several ways
tags = {"python", "data", "python", "code", "data"}
print("From literal:", tags) # duplicates removed
nums = set([1, 2, 2, 3, 3, 3, 4]) # from a list
print("From list:", nums)
letters = set("mississippi") # from a string — unique chars only
print("From string:", letters)
empty = set() # correct — {} creates an empty dict
print("Empty:", empty, type(empty))
Adding and Removing Elements
Sets are mutable — grow and shrink them after creation. Python provides several methods depending on whether you want an error when the item is missing or silent behaviour.
fruits = {"apple", "banana", "cherry"}
fruits.add("mango") # add one item
fruits.update(["kiwi", "pear"]) # add multiple items from any iterable
print("After add:", fruits)
fruits.remove("banana") # raises KeyError if not found
fruits.discard("grape") # silent — no error if not found
print("After remove:", fruits)
# pop() removes and returns an arbitrary element (sets are unordered)
item = fruits.pop()
print("Popped:", item)
fruits.clear()
print("After clear:", fruits)
add()inserts a single item.update()inserts from any iterable — a list, tuple, string, or another set.- Prefer
discard()overremove()when you are not certain the item exists. pop()removes an arbitrary element — sets have no index so you cannot control which one.
Union — Everything from Both Sets
A union produces a new set containing every element from either set, with no duplicates. This answers: "give me everything from both."
a = {"python", "sql", "excel"}
b = {"python", "tableau", "power bi"}
# Method and operator — identical results
print(a.union(b))
print(a | b)
# Union of three sets at once
c = {"spark", "sql"}
print(a | b | c)
print(a.union(b, c))
# Real-world: merge email subscriber lists from two campaigns
campaign1 = {"alice@x.com", "bob@x.com", "carol@x.com"}
campaign2 = {"bob@x.com", "dave@x.com", "eve@x.com"}
master = campaign1 | campaign2
print("Master list:", len(master), "unique addresses")
a | b | cchains as many sets as needed — clean for merging multiple groups.a.union(b, c)accepts multiple arguments — also chains cleanly.- Neither original set is modified — a new set is always returned.
Intersection — Only What Both Sets Share
An intersection returns only elements present in both sets. Answers: "what do these groups have in common?"
skills_needed = {"python", "sql", "machine learning", "excel"}
skills_you_have = {"python", "excel", "power bi", "tableau"}
# What skills match the job?
match = skills_needed & skills_you_have
print("Matching skills:", match)
# Intersection update — modify in place
test_set = {"python", "sql", "excel"}
test_set.intersection_update({"sql", "excel", "r"})
print("After intersection_update:", test_set)
# Real-world: find users who are in both a free and paid plan
free_users = {"u1", "u2", "u3", "u4", "u5"}
paid_users = {"u3", "u4", "u6", "u7"}
dual = free_users & paid_users
print("On both plans:", dual)
a.intersection(b)anda & bare equivalent.- If the sets share nothing, the result is an empty set
set(). intersection_update()modifies the set in place instead of returning a new one — useful when you want to narrow a set down.
Difference — What One Has That the Other Does Not
Difference returns elements in the first set that are not in the second. Order matters — a - b is not the same as b - a.
skills_needed = {"python", "sql", "machine learning", "excel"}
skills_you_have = {"python", "excel", "power bi", "tableau"}
# Skills you still need to learn
gaps = skills_needed - skills_you_have
print("Skills to learn:", gaps)
# Skills you have that the job doesn't require
extras = skills_you_have - skills_needed
print("Extra skills:", extras)
# Real-world: find new users this month (this month minus last month)
last_month = {"u1", "u2", "u3", "u4"}
this_month = {"u1", "u2", "u3", "u4", "u5", "u6"}
new_users = this_month - last_month
print("New users:", new_users)
# Churned users (were here last month but not this month)
lost_month = {"u1", "u2", "u3"}
churned = last_month - lost_month
print("Churned:", churned)
a - bmeans "what is in a but not b" — the direction determines the answer.difference_update()removes matching elements from the set in place.- New/churned user analysis is a standard business metric calculated exactly this way.
Symmetric Difference — What They Do Not Share
The symmetric difference returns all elements in one set or the other, but not in both — the opposite of intersection.
store_a = {"apple", "banana", "cherry", "mango"}
store_b = {"banana", "mango", "grape", "peach"}
# Items exclusive to one store (not stocked by both)
unique = store_a ^ store_b
print("Store-exclusive items:", unique)
# Commutative — order does not matter
print("Reversed:", store_b ^ store_a)
print("Same result:", (store_a ^ store_b) == (store_b ^ store_a))
# Relationship to union and intersection
print("Via union/intersection:", (store_a | store_b) - (store_a & store_b))
a ^ banda.symmetric_difference(b)are equivalent.- Unlike regular difference, symmetric difference is commutative —
a ^ b == b ^ a. - Mathematically: symmetric difference = union minus intersection.
Subset, Superset, and Disjoint Checks
Python lets you test containment relationships between sets — whether one is entirely inside another, or whether two sets share no elements at all.
basics = {"html", "css"}
frontend = {"html", "css", "javascript", "react", "typescript"}
backend = {"python", "sql", "django", "postgres"}
# Subset — is basics inside frontend?
print(basics.issubset(frontend)) # True
print(basics <= frontend) # True — operator form
# Proper subset — subset but not equal (strictly inside)
print(basics < frontend) # True — basics != frontend
print(frontend < frontend) # False — a set is not a proper subset of itself
# Superset — does frontend contain basics?
print(frontend.issuperset(basics)) # True
print(frontend >= basics) # True
# Disjoint — do they share nothing at all?
print(frontend.isdisjoint(backend)) # True — no overlap between front and back
print(basics.isdisjoint(backend)) # True
a <= b— a is a subset (may equal b).a < b— a is a proper subset (strictly inside, not equal).a >= b— a is a superset.a > b— a is a proper superset.isdisjoint()returnsTrueif two sets share no elements — equivalent tolen(a & b) == 0but faster.
Membership Testing — Why Sets Are Faster Than Lists
The single most important practical reason to use a set instead of a list is membership testing speed. Checking x in set takes constant time O(1) regardless of how large the set is. Checking x in list requires scanning every element — O(n) — and gets slower as the list grows.
import time
# Build a large dataset — 1 million items
data = list(range(1_000_000))
data_set = set(data)
target = 999_999 # near the end — worst case for list
# List membership test
start = time.time()
for _ in range(1000):
_ = target in data
list_time = time.time() - start
# Set membership test
start = time.time()
for _ in range(1000):
_ = target in data_set
set_time = time.time() - start
print(f"List : {list_time:.4f}s")
print(f"Set : {set_time:.6f}s")
print(f"Set is ~{list_time / set_time:.0f}x faster")
# Real-world: spam domain filter
blocked = {"spam.com", "junk.net", "phish.io", "malware.org"}
emails = ["user@gmail.com", "admin@spam.com", "hello@dataplexa.com", "test@junk.net"]
clean = [e for e in emails if e.split("@")[1] not in blocked]
print("Safe emails:", clean)
- Sets use a hash table internally — lookup is O(1) constant time regardless of size.
- Lists require linear scan — O(n) — the larger the list, the slower the check.
- Any time you only need to answer "is this value present?" and never need ordering or indexing, a set is almost always the right structure.
- Convert a list to a set with
set(my_list)before doing many membership checks — the conversion is O(n) but each subsequent lookup is O(1).
Set Comprehensions
Just like list and dictionary comprehensions, Python supports set comprehensions — a concise way to build a set from any iterable. Same syntax as list comprehension but with curly braces, and duplicates are collapsed automatically.
# Set comprehension — unique squares
nums = [1, 2, 2, 3, 3, 3, 4]
unique_squares = {n ** 2 for n in nums}
print("Unique squares:", unique_squares)
# Unique first letters
words = ["apple", "avocado", "banana", "blueberry", "cherry", "apricot"]
first_letters = {w[0] for w in words}
print("First letters:", first_letters)
# Filter while building — unique even numbers only
data = [1, 2, 2, 3, 4, 4, 5, 6, 6]
unique_evens = {n for n in data if n % 2 == 0}
print("Unique evens:", unique_evens)
# Clean and deduplicate tags from user input
raw_tags = [" Python ", "DATA SCIENCE", "python", " ai ", "Data Science", "AI"]
clean_tags = {t.lower().strip() for t in raw_tags}
print("Unique tags:", clean_tags)
- Structure:
{expression for item in iterable}— curly braces, no colon (no colon = set, colon = dict). - Duplicate results are collapsed automatically — no extra deduplication step needed.
- The tag-cleaning pattern is one of the most common real-world uses — normalise case/whitespace and deduplicate in one line.
frozenset — The Immutable Set
A frozenset is a set that cannot be changed after creation. Because it is immutable, it is hashable — which means it can be used as a dictionary key or stored inside another set. A regular set cannot do either of those things.
# frozenset — all set operations, no mutation
roles_admin = frozenset({"read", "write", "delete", "admin"})
roles_guest = frozenset({"read"})
roles_editor = frozenset({"read", "write"})
# Use frozensets as dictionary keys — not possible with regular sets
permissions = {
roles_admin: "full access",
roles_guest: "read only",
roles_editor: "read-write",
}
print(permissions[roles_admin])
print(permissions[roles_guest])
# All set math still works — frozenset supports all read operations
overlap = roles_admin & roles_editor
print("Shared permissions:", overlap)
print("Is editor subset of admin?", roles_editor <= roles_admin)
# frozensets can be stored inside a regular set
access_levels = {roles_guest, roles_editor} # set of frozensets
print("Access levels defined:", len(access_levels))
frozenset(iterable)accepts any iterable — list, tuple, set, or string.- Supports all read operations:
in,len(), iteration, and all set math operators. - Cannot use
add(),remove(), or any mutating method — attempting raisesAttributeError. - The permission system pattern — using frozensets as dict keys — is used in real access control systems.
Practical Real-World Example — User Analytics
# Track daily active users across three days
day1 = {"u1", "u2", "u3", "u4", "u5"}
day2 = {"u2", "u3", "u4", "u6", "u7"}
day3 = {"u1", "u3", "u5", "u6", "u8"}
# Users active all 3 days (intersection)
loyal = day1 & day2 & day3
print("Active all 3 days:", loyal)
# Users active on any day (union)
total_unique = day1 | day2 | day3
print("Total unique users:", len(total_unique), total_unique)
# Users who appeared on day 1 but NOT day 3 (churned)
churned = day1 - day3
print("Churned (day1→day3):", churned)
# New users on day 3 (not seen on day 1 or 2)
new_day3 = day3 - (day1 | day2)
print("New on day 3:", new_day3)
# Users active on exactly one day (symmetric difference of all)
one_day_only = (day1 ^ day2 ^ day3) - (day1 & day2) - (day1 & day3) - (day2 & day3)
print("One-day only:", one_day_only)
Quick Reference Table
| Operation | Method | Operator | Returns |
|---|---|---|---|
| Union | a.union(b) | a | b | All elements from both |
| Intersection | a.intersection(b) | a & b | Only shared elements |
| Difference | a.difference(b) | a - b | In a but not b |
| Symmetric Diff | a.symmetric_difference(b) | a ^ b | In one but not both |
| Subset | a.issubset(b) | a <= b | True if a inside b |
| Proper Subset | — | a < b | True if a inside b and a ≠ b |
| Superset | a.issuperset(b) | a >= b | True if a contains b |
| Disjoint | a.isdisjoint(b) | — | True if no overlap |
| Add one | a.add(x) | — | Modifies a in place |
| Add many | a.update(iterable) | — | Modifies a in place |
| Remove safe | a.discard(x) | — | No error if missing |
Practice
What operator is used for the union of two sets?
Which method removes an element from a set without raising an error if it is not found?
What is the time complexity of membership testing in a set?
What operator returns all elements that are in one set or the other, but not in both?
What is the immutable version of a set called?
Which method returns True if two sets share no elements at all?
Quick Quiz
What does {1, 2, 3} & {2, 3, 4} return?
What does {1, 2, 3} - {2, 3, 4} return?
Which of the following correctly creates an empty set?
What does {1, 2}.issubset({1, 2, 3}) return?
Why can a frozenset be used as a dictionary key but a regular set cannot?
What does {1, 2, 3} ^ {2, 3, 4} return?
re module — one of the most powerful tools for working with real-world string data.