Advanced Sets in Python | Python Course | Dataplexa

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))
From literal: {'python', 'data', 'code'} From list: {1, 2, 3, 4} From string: {'m', 'i', 's', 'p'} Empty: set() <class 'set'>

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)
After add: {'apple', 'banana', 'cherry', 'mango', 'kiwi', 'pear'} After remove: {'apple', 'cherry', 'mango', 'kiwi', 'pear'} Popped: apple After clear: set()
  • add() inserts a single item. update() inserts from any iterable — a list, tuple, string, or another set.
  • Prefer discard() over remove() 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")
{'python', 'sql', 'excel', 'tableau', 'power bi'} {'python', 'sql', 'excel', 'tableau', 'power bi'} {'python', 'sql', 'excel', 'tableau', 'power bi', 'spark'} {'python', 'sql', 'excel', 'tableau', 'power bi', 'spark'} Master list: 5 unique addresses
  • a | b | c chains 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)
Matching skills: {'python', 'excel'} After intersection_update: {'sql', 'excel'} On both plans: {'u3', 'u4'}
  • a.intersection(b) and a & b are 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)
Skills to learn: {'sql', 'machine learning'} Extra skills: {'power bi', 'tableau'} New users: {'u5', 'u6'} Churned: {'u4'}
  • a - b means "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))
Store-exclusive items: {'apple', 'cherry', 'grape', 'peach'} Reversed: {'apple', 'cherry', 'grape', 'peach'} Same result: True Via union/intersection: {'apple', 'cherry', 'grape', 'peach'}
  • a ^ b and a.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
True True True False True True True 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() returns True if two sets share no elements — equivalent to len(a & b) == 0 but 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)
List : 12.3421s Set : 0.000142s Set is ~86928x faster Safe emails: ['user@gmail.com', 'hello@dataplexa.com']
  • 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)
Unique squares: {1, 4, 9, 16} First letters: {'a', 'b', 'c'} Unique evens: {2, 4, 6} Unique tags: {'python', 'data science', 'ai'}
  • 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))
full access read only Shared permissions: frozenset({'read', 'write'}) Is editor subset of admin? True Access levels defined: 2
  • 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 raises AttributeError.
  • 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)
Active all 3 days: {'u3'} Total unique users: 8 {'u1', 'u2', 'u3', 'u4', 'u5', 'u6', 'u7', 'u8'} Churned (day1→day3): {'u2', 'u4'} New on day 3: {'u8'} One-day only: {'u7', 'u8'}

Quick Reference Table

OperationMethodOperatorReturns
Uniona.union(b)a | bAll elements from both
Intersectiona.intersection(b)a & bOnly shared elements
Differencea.difference(b)a - bIn a but not b
Symmetric Diffa.symmetric_difference(b)a ^ bIn one but not both
Subseta.issubset(b)a <= bTrue if a inside b
Proper Subseta < bTrue if a inside b and a ≠ b
Superseta.issuperset(b)a >= bTrue if a contains b
Disjointa.isdisjoint(b)True if no overlap
Add onea.add(x)Modifies a in place
Add manya.update(iterable)Modifies a in place
Remove safea.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?





NEXT UP
Regular Expressions in Python
Learn how to search, match, extract, and replace text patterns using Python's re module — one of the most powerful tools for working with real-world string data.