BASIC Python (Beginner Level) 📂 Basic Python Programming · 11 of 15 38 min read

Python Sets — The Complete Guide to Methods

Master Python sets from the ground up — the O(1) membership superpower, all six mutation methods, and every set operation with visual Venn diagrams. Learn union, intersection, difference, and symmetric difference by picture and by code, plus subset/superset/disjoint relationships, frozensets, and five practical patterns from deduplication to permission checks. Includes benchmarks proving 3000× speedups over lists.

Section 01

The Story That Explains Sets

The Guest List at the Door
You're running the door at a private event. Someone walks up and says "I'm Alice, I'm on the list." Two questions matter, and only two: Is Alice on the list — yes or no? That's it. You don't care what position she is on the list. You don't care if she's on it twice. You don't care who came before her.

Now imagine you had a printed 500-name list on paper. You'd scan top to bottom until you found her — slow and painful. Now imagine that list is a set. You look at Alice's name, and instantly — without scanning — you know if she's in. That's not magic. That's a hash lookup, and it's the superpower of the Python set.

A set is a bag of unique, unordered things designed for one thing: answering "is this in there?" at blinding speed.

A Python set is an unordered collection of unique, hashable elements. It borrows from mathematics — union, intersection, difference — and pairs those operations with near-constant-time membership tests. Every duplicate you throw at it is silently discarded. Every membership question is answered in one hop.

💡
The Core Insight

A set is a list without duplicates that has traded ordering for speed. You lose indexing (s[0] doesn't exist). You gain O(1) membership tests and mathematical operations that would take dozens of lines with lists. It's the perfect data structure for the questions "is X in the group?" and "what do these groups have in common?"


Section 02

The Three Laws of Every Set

Before touching syntax, internalise the three properties that define every set. Every method, every operation, every edge case flows from these rules.

🎯
Law 1 — Unique
No duplicates, ever
Adding an element that already exists is a silent no-op. {1, 1, 2, 2, 3} immediately becomes {1, 2, 3}. This is why sets are the one-line answer to deduplication.
🔄
Law 2 — Unordered
No index, no slice
Sets have no defined order. s[0] throws TypeError. Print output order isn't guaranteed. If order matters, you want a list — not a set.
🔒
Law 3 — Hashable Only
Immutable elements
Only hashable objects fit inside a set: numbers, strings, tuples, frozensets. Lists, dicts, and other sets are not hashable and will raise TypeError: unhashable type.

Section 03

Creating a Set — Four Ways

# 1) Literal — the cleanest and most common
colours = {"red", "green", "blue"}

# 2) From an iterable using the set() constructor
letters = set("hello")          # {'h', 'e', 'l', 'o'}  — 'l' deduped
nums    = set([1, 2, 2, 3, 3])   # {1, 2, 3}

# 3) Set comprehension — filter + transform in one step
squares = {n ** 2 for n in range(6)}

# 4) Empty set — the ONE case where the {} literal fails
empty = set()      # CORRECT — empty set
oops  = {}         # WRONG — this is an empty DICT, not a set!

print(colours)
print(letters)
print(type(oops).__name__)  # dict, not set
OUTPUT
{'red', 'green', 'blue'} {'h', 'e', 'l', 'o'} dict
⚠️
The Empty-Set Trap Every Beginner Hits

In Python, {} is always an empty dict — never a set. To create an empty set, you must use set(). This is because dict came first, so it got the curly braces first. Silent bugs live here — spot them early.


Section 04

Adding & Removing — The Mutation Methods

🧠 Five Methods That Change the Set
add
Adds one element. Silent if it already exists — no error, no duplicate.
update
Adds many elements from any iterable. Also silent about duplicates.
remove
Removes one element. Raises KeyError if it's not there.
discard
Removes one element. Silent if it's not there — no error.
pop
Removes and returns an arbitrary element. Raises KeyError if empty.
clear
Removes everything. Set becomes empty.
fruits = {"apple", "banana"}

fruits.add("cherry")          # {'apple', 'banana', 'cherry'}
fruits.add("apple")           # no change — silent duplicate

fruits.update(["kiwi", "mango"]) # adds both from a list
fruits.update("AB")              # adds 'A' AND 'B' — strings are iterable!

fruits.discard("kiwi")          # removes safely, silent if absent
fruits.discard("grape")         # NOT there — but no error

try:
    fruits.remove("grape")      # NOT there — raises KeyError
except KeyError as e:
    print(f"KeyError: {e}")

random_fruit = fruits.pop()      # removes one arbitrary element
print(f"Popped: {random_fruit}")

fruits.clear()                   # now empty
print(f"After clear: {fruits}")
OUTPUT
KeyError: 'grape' Popped: apple After clear: set()
🔑
remove vs discard — Pick On Purpose

Use remove when the element should be there and its absence is a bug — you want the crash. Use discard when absence is fine — "delete if present, otherwise do nothing." Choosing the right one is choosing between defensive and forgiving code.


Section 05

Membership Testing — The Superpower

This is why sets exist. Checking whether an element is inside a collection is the single most common operation in data work, and sets do it in constant time — regardless of size.

allowed_users = {"alice", "bob", "charlie", "diana"}

# The 'in' operator — O(1) on average, regardless of set size
print("alice" in allowed_users)     # True
print("eve" not in allowed_users)   # True

# Same test on a 1-million-element set — still O(1)
big_set = set(range(1_000_000))
print(999_999 in big_set)          # True — instant
OUTPUT
True True True
❌ List Membership — O(n)
SizeApprox. time
1,000~10 microseconds
100,000~1 millisecond
10,000,000~100 milliseconds
Grows linearly — painful at scale
✅ Set Membership — O(1)
SizeApprox. time
1,000~100 nanoseconds
100,000~100 nanoseconds
10,000,000~100 nanoseconds
Flat — same speed at every size

Section 06

Set Operations — Visual Reference

This is where sets show off. Four operations you'd otherwise write nested loops for become a single symbol. Below is the complete visual reference — each Venn diagram shows the highlighted region as the result.

Union — A | B — Everything from Both

Set Operation: Union (A ∪ B)
A B A | B → everything in A OR B (or both)

The highlighted region is the union — every element from either set, duplicates collapsed.

a = {1, 2, 3, 4}
b = {3, 4, 5, 6}

# Operator form — clean and Pythonic
print(a | b)              # {1, 2, 3, 4, 5, 6}

# Method form — accepts ANY iterable, not just sets
print(a.union(b, [7, 8])) # {1, 2, 3, 4, 5, 6, 7, 8}

# In-place version — modifies a
a |= b
print(a)                  # {1, 2, 3, 4, 5, 6}
OUTPUT
{1, 2, 3, 4, 5, 6} {1, 2, 3, 4, 5, 6, 7, 8} {1, 2, 3, 4, 5, 6}

Section 07

Intersection — A & B — Common Elements Only

Set Operation: Intersection (A ∩ B)
A B A & B A & B → only elements present in BOTH sets

Only the overlap is highlighted. This is the mathematical intersection.

a = {1, 2, 3, 4}
b = {3, 4, 5, 6}

# Operator form
print(a & b)                       # {3, 4}

# Method form
print(a.intersection(b))            # {3, 4}

# Real use: users who are BOTH admins AND active
admins = {"alice", "bob", "charlie"}
active = {"bob", "charlie", "diana", "eve"}
active_admins = admins & active
print(f"Active admins: {active_admins}")
OUTPUT
{3, 4} {3, 4} Active admins: {'bob', 'charlie'}

Section 08

Difference — A - B — Everything in A but Not in B

Set Operation: Difference (A − B)
A B A - B → in A, but NOT in B

The crescent on the left is highlighted — elements of A that aren't shared with B.

a = {1, 2, 3, 4}
b = {3, 4, 5, 6}

print(a - b)                       # {1, 2}  — in a, not in b
print(b - a)                       # {5, 6}  — in b, not in a
print(a.difference(b))              # {1, 2}

# Real use: which requested features are NOT yet built?
requested = {"dark_mode", "exports", "api_v2", "webhooks"}
built     = {"dark_mode", "exports"}
pending   = requested - built
print(f"Still to build: {pending}")
OUTPUT
{1, 2} {5, 6} {1, 2} Still to build: {'api_v2', 'webhooks'}
⚠️
Difference Is NOT Commutative

Unlike union and intersection, a - b and b - a give different results. This is the one set operation where order matters. Read it as "start with the left set, then subtract the right set."


Section 09

Symmetric Difference — A ^ B — In One But Not Both

Set Operation: Symmetric Difference (A ▵ B)
A B A ^ B → in A OR B, but NOT both

Both crescents are highlighted — the middle overlap is excluded. This is (A ∪ B) − (A ∩ B).

a = {1, 2, 3, 4}
b = {3, 4, 5, 6}

print(a ^ b)                              # {1, 2, 5, 6}
print(a.symmetric_difference(b))          # {1, 2, 5, 6}

# Real use: what changed between two snapshots?
yesterday = {"file_a.csv", "file_b.csv", "file_c.csv"}
today     = {"file_b.csv", "file_c.csv", "file_d.csv"}
changes = yesterday ^ today
print(f"Changed files: {changes}")   # deleted + new
OUTPUT
{1, 2, 5, 6} {1, 2, 5, 6} Changed files: {'file_a.csv', 'file_d.csv'}

Section 10

Set Relationships — Subset, Superset, Disjoint

Beyond the four operations, sets can also be compared as wholes. Is one set entirely contained in another? Do two sets share nothing at all? These relationships have one-shot method calls too.

Set Relationships — Three Patterns
A B B ⊆ A (subset) A B A ∩ B ≠ ∅ (overlap) A B A ∩ B = ∅ (disjoint)

Left: every element of B is also in A. Middle: they share some elements. Right: no shared elements at all.

a = {1, 2, 3, 4, 5}
b = {2, 3}
c = {7, 8, 9}

# Subset — is every element of b also in a?
print(b.issubset(a))          # True
print(b <= a)                  # True  (operator form)
print(b < a)                   # True  (STRICT subset — b != a)

# Superset — the mirror question
print(a.issuperset(b))        # True
print(a >= b)                  # True

# Disjoint — do they share ZERO elements?
print(a.isdisjoint(c))        # True — nothing in common
print(a.isdisjoint(b))        # False — they overlap
OUTPUT
True True True True True True False
🔑
Proper vs Non-Proper Subsets

a <= b means "a is a subset of b, and possibly equal." a < b means "a is a strict subset — everything in a is in b, and b has at least one thing extra." A set is always a non-strict subset of itself, but never a strict one.


Section 11

Frozenset — The Immutable Cousin

A regular set is mutable — you can add and remove things. Sometimes you need a hashable, unchangeable version: an element of another set, a dictionary key, or a value guaranteed not to change. That's frozenset.

# Create a frozenset from any iterable
permissions = frozenset(["read", "write", "delete"])

# All non-mutating operations still work
print("read" in permissions)               # True
print(permissions | {"admin"})            # returns NEW frozenset

# But mutation is forbidden
try:
    permissions.add("admin")
except AttributeError as e:
    print(f"Blocked: {e}")

# Because it's hashable, a frozenset can be:
# 1) an element of another set
role_bundles = {
    frozenset({"read"}),
    frozenset({"read", "write"}),
    frozenset({"read", "write", "admin"})
}
print(len(role_bundles))                    # 3

# 2) a dictionary key
cache = {
    frozenset(["a", "b"]): "result_ab",
    frozenset(["c", "d"]): "result_cd",
}
print(cache[frozenset(["b", "a"])])       # order doesn't matter
OUTPUT
True frozenset({'admin', 'read', 'write', 'delete'}) Blocked: 'frozenset' object has no attribute 'add' 3 result_ab

Section 12

Practical Real-World Examples

Example 1 — Instant Deduplication of a List

emails = ["alice@ex.com", "bob@ex.com", "alice@ex.com",
          "eve@ex.com", "bob@ex.com"]

# One line — order NOT preserved
unique_emails = list(set(emails))
print(unique_emails)

# If order matters, preserve first occurrence via dict.fromkeys()
unique_ordered = list(dict.fromkeys(emails))
print(unique_ordered)
OUTPUT
['eve@ex.com', 'alice@ex.com', 'bob@ex.com'] ['alice@ex.com', 'bob@ex.com', 'eve@ex.com']

Example 2 — Permission Checking

required_perms  = {"read", "write"}
user_perms      = {"read", "write", "delete", "share"}

# Does the user have EVERY required permission?
if required_perms <= user_perms:
    print("Access granted")
else:
    missing = required_perms - user_perms
    print(f"Missing: {missing}")
OUTPUT
Access granted

Example 3 — Tag-Based Recommendations

user_tags    = {"python", "data-science", "ml", "stats"}
article_tags = {"python", "ml", "deep-learning"}

# Match score = size of intersection
overlap = user_tags & article_tags
score = len(overlap) / len(user_tags | article_tags)

print(f"Common interests: {overlap}")
print(f"Jaccard similarity: {score:.2%}")
OUTPUT
Common interests: {'python', 'ml'} Jaccard similarity: 40.00%

Example 4 — Finding What Changed

old_snapshot = {"config.yaml", "main.py", "utils.py", "data.csv"}
new_snapshot = {"config.yaml", "main.py", "helpers.py", "data.csv"}

added   = new_snapshot - old_snapshot
removed = old_snapshot - new_snapshot
unchanged = old_snapshot & new_snapshot

print(f"Added:     {added}")
print(f"Removed:   {removed}")
print(f"Unchanged: {unchanged}")
OUTPUT
Added: {'helpers.py'} Removed: {'utils.py'} Unchanged: {'config.yaml', 'main.py', 'data.csv'}

Example 5 — Fast Filtering Against a Blocklist

# A million user IDs coming through
incoming_ids = range(1_000_000)

# 50,000 banned IDs — as a SET for O(1) lookup
banned = set(range(0, 1_000_000, 20))

# Filter — near-instant thanks to set lookup
allowed = [uid for uid in incoming_ids if uid not in banned]

print(f"Allowed: {len(allowed):,}")
print(f"Blocked: {len(banned):,}")
OUTPUT
Allowed: 950,000 Blocked: 50,000
Why This Is Fast

That last example does 1 million not in checks against a 50,000-element blocklist. With a list, that's 50 billion comparisons — minutes. With a set, it's ~1 million hashes — well under a second. Same code, one keyword changed, 60,000× speedup.


Section 13

Performance — List vs Set for Membership

import timeit

# Same 100,000 items — stored as list vs set
data_list = list(range(100_000))
data_set  = set(data_list)

# Worst case: search for last item
target = 99_999

t_list = timeit.timeit(
    lambda: target in data_list, number=10_000
)
t_set  = timeit.timeit(
    lambda: target in data_set,  number=10_000
)

print(f"List lookup: {t_list:.4f}s")
print(f"Set lookup:  {t_set:.4f}s")
print(f"Set is {t_list / t_set:.0f}x faster")
OUTPUT
List lookup: 6.4820s Set lookup: 0.0018s Set is 3601x faster
OperationListSetWinner
Access by indexO(1)Not supportedList
Membership (in)O(n)O(1)Set
InsertO(1) appendO(1) addTie
Delete by valueO(n)O(1) discardSet
DeduplicationO(n²) naiveO(n) built-inSet
Preserves orderYesNoList
Allows duplicatesYesNoDepends
Memory overheadLow~3× higher (hash table)List

Section 14

When NOT to Use a Set

🚫
You Need Order
Rankings, timelines, sequences
Sets have no reliable ordering. Print output can vary. If you need "the third item" or "the newest event first," use a list — or a dict (which preserves insertion order in Python 3.7+).
📑
You Need Counts
How many times did X appear?
Sets discard duplicates. If frequency matters — word counts, event tallies, inventory — reach for collections.Counter, not a set.
🔒
Elements Aren't Hashable
Lists, dicts, custom mutables
You can't store lists, dicts, or mutable custom objects in a set — Python will raise TypeError: unhashable type. Convert to tuples or frozensets first, or use a different structure.
# These all fail with TypeError: unhashable type
try:
    bad = {[1, 2], [3, 4]}         # lists inside a set
except TypeError as e:
    print(f"Nope: {e}")

# FIX — convert inner lists to tuples (immutable, hashable)
good = {(1, 2), (3, 4)}
print(good)                            # works!
OUTPUT
Nope: unhashable type: 'list' {(1, 2), (3, 4)}

Section 15

Complete Method Reference

Method / OperatorWhat It DoesMutates?
s.add(x)Add single elementYes
s.update(iter) / s |= tAdd many elementsYes
s.remove(x)Remove x, KeyError if absentYes
s.discard(x)Remove x, silent if absentYes
s.pop()Remove and return arbitrary elementYes
s.clear()Empty the setYes
s | t / s.union(t)Union — all elementsNo
s & t / s.intersection(t)Intersection — common onlyNo
s - t / s.difference(t)Elements in s but not tNo
s ^ t / s.symmetric_difference(t)In s or t but not bothNo
s.intersection_update(t)Keep only common elementsYes
s.difference_update(t)Remove elements found in tYes
s.symmetric_difference_update(t)Toggle: XOR into sYes
s <= t / s.issubset(t)Is s a subset of t?No
s >= t / s.issuperset(t)Is s a superset of t?No
s < tStrict subset (s ⊂ t, s ≠ t)No
s.isdisjoint(t)Do s and t share nothing?No
s.copy()Shallow copyNo
len(s)Number of elementsNo
x in sMembership test — O(1)No

Section 16

Golden Rules

🔑 Set — Non-Negotiable Rules
1
Use a set whenever you need to answer "is X in this collection?" more than once. The O(1) lookup pays for the hash-table build almost immediately. Even a few hundred lookups against a 10,000-item list is enough to justify converting to a set first.
2
Empty set is set(), never {}. Empty curly braces create a dict. This bug is silent — no error — until you try to .add() and get an AttributeError. Burn this into muscle memory.
3
Reach for set operations before writing nested loops. Finding "items in both" is a & b, not a double for loop. Finding "items in one but not the other" is a - b. Shorter, faster, and clearer.
4
Never assume iteration order. Sets look ordered when you print them for small integer inputs (because of hash coincidences), but the order is not guaranteed and can change between Python versions. If order matters, convert to a sorted list on demand: sorted(my_set).
5
Use discard for forgiving code, remove for strict code. Silent failures hide bugs — but so do unnecessary crashes. Choose deliberately based on whether the element's absence would signal a real problem.
6
Convert to frozenset when you need a set inside another set — or as a dict key. Only immutable, hashable things can be keys or elements. That's the entire purpose of the frozen variant.
7
For frequency counting, reach for collections.Counter, not a set. Sets discard the exact information — how many — that frequency problems need. Using a set here means going back to the original data twice.