Python sets are unordered collections of unique, hashable elements. They excel at two things that lists cannot do efficiently: fast membership testing (O(1) average time regardless of size, vs O(n) for lists) and mathematical set operations (union, intersection, difference). In FastAPI applications, sets are used for permission checking against allowed roles, deduplicating tags submitted in post creation, finding common elements between two user-selected lists, and building lookup structures for fast validation. Understanding sets completes your collection toolkit and enables you to write significantly more efficient code for certain common patterns.
Creating and Using Sets
# Create with curly braces (values, not key:value pairs)
roles = {"user", "editor", "admin"}
primes = {2, 3, 5, 7, 11, 13}
tags = {"python", "fastapi", "postgresql"}
# Create from a list — automatically deduplicates
raw_tags = ["python", "fastapi", "python", "react", "fastapi"]
unique = set(raw_tags)
print(unique) # {"python", "fastapi", "react"} — order not guaranteed
# Empty set — must use set(), not {} (that creates an empty dict)
empty_set = set()
empty_dict = {} # this is a dict, not a set!
type(empty_set) # <class 'set'>
type(empty_dict) # <class 'dict'>
# Membership — O(1) average time
"admin" in roles # True
"superuser" in roles # False
"superuser" not in roles # True
# Length
len(roles) # 3
# Iteration (order is not guaranteed)
for role in roles:
print(role) # printed in arbitrary order
roles[0] raises TypeError). If you need both uniqueness and order, use a dict (since Python 3.7 dicts maintain insertion order) with the items as keys, or use a list and manually check for duplicates before inserting. Python 3.7+ also has dict.fromkeys() as a way to deduplicate while preserving order.if role in ["user", "editor", "admin"]: (O(n) list scan), use if role in {"user", "editor", "admin"}: (O(1) set lookup). For a small number of roles this difference is negligible, but it becomes important when checking against large permission lists or when the check happens on every request. It also communicates that these are unordered alternatives, not a sequence.frozenset type for immutable sets that can themselves be set members or dict keys.Adding and Removing Elements
permissions = {"read", "write"}
# add — add one element (no-op if already present)
permissions.add("delete")
permissions.add("read") # already there — no error, no duplicate
# update — add multiple elements from an iterable
permissions.update(["admin", "export"])
permissions.update({"import", "archive"})
# remove — raises KeyError if not present
permissions.remove("export")
# discard — does NOT raise error if not present (safe)
permissions.discard("nonexistent") # no error ✓
permissions.discard("write") # removes if present
# pop — removes and returns an arbitrary element
any_perm = permissions.pop() # unpredictable which one
# clear
permissions.clear() # set()
Set Operations
python_tags = {"python", "fastapi", "sqlalchemy", "postgresql"}
js_tags = {"javascript", "react", "fastapi", "nodejs"}
# Union — all elements from both sets
all_tags = python_tags | js_tags # operator
all_tags = python_tags.union(js_tags) # method
# {"python", "fastapi", "sqlalchemy", "postgresql", "javascript", "react", "nodejs"}
# Intersection — elements in BOTH sets
shared = python_tags & js_tags # operator
shared = python_tags.intersection(js_tags) # method
# {"fastapi"}
# Difference — in first set but NOT in second
py_only = python_tags - js_tags # operator
py_only = python_tags.difference(js_tags) # method
# {"python", "sqlalchemy", "postgresql"}
# Symmetric difference — in one set but NOT both
unique_to_each = python_tags ^ js_tags # operator
unique_to_each = python_tags.symmetric_difference(js_tags) # method
# {"python", "sqlalchemy", "postgresql", "javascript", "react", "nodejs"}
# Subset and superset checks
{"read"} <= {"read", "write", "delete"} # True — is subset
{"read", "write"}.issubset({"read", "write", "delete"}) # True
{"read", "write", "delete"} >= {"read"} # True — is superset
# Disjoint — no elements in common
{"a", "b"}.isdisjoint({"c", "d"}) # True (no overlap)
Practical FastAPI Patterns with Sets
# ── Permission checking ────────────────────────────────────────────────────────
ADMIN_ONLY = {"delete_user", "change_role", "view_audit_log"}
EDITOR_PERMS = {"create_post", "edit_post", "delete_own_post"}
USER_PERMS = {"read_post", "create_comment"}
def check_permission(user_role: str, action: str) -> bool:
all_perms = {
"admin": ADMIN_ONLY | EDITOR_PERMS | USER_PERMS,
"editor": EDITOR_PERMS | USER_PERMS,
"user": USER_PERMS,
}
return action in all_perms.get(user_role, set())
# ── Tag deduplication ──────────────────────────────────────────────────────────
def process_tags(raw_tags: list) -> list:
# Clean and deduplicate in one step
return list({tag.strip().lower() for tag in raw_tags if tag.strip()})
# ── Find posts to notify about (intersection) ─────────────────────────────────
user_subscribed_tags = {"python", "fastapi", "react"}
post_tags = {"python", "postgresql", "tutorial"}
relevant_tags = user_subscribed_tags & post_tags
if relevant_tags:
send_notification(f"New post covers: {', '.join(relevant_tags)}")
# ── Validate submitted tags against allowed list ───────────────────────────────
ALLOWED_TAGS = {"python", "fastapi", "postgresql", "react", "javascript", "sql"}
submitted = {"python", "fastapi", "unknown_tag"}
invalid = submitted - ALLOWED_TAGS # {"unknown_tag"}
if invalid:
raise ValueError(f"Unknown tags: {invalid}")
frozenset — Immutable Sets
# frozenset is immutable — can be used as dict key or set member
READ_ONLY_PERMS = frozenset({"read", "list"})
# Use as dict key
cache = {frozenset({"read", "write"}): True}
# Use as set member
permission_groups = {
frozenset({"read"}),
frozenset({"read", "write"}),
frozenset({"read", "write", "delete"}),
}
# Supports all set operations but not add/remove
READ_ONLY_PERMS | frozenset({"write"}) # new frozenset {"read", "list", "write"}
Common Mistakes
Mistake 1 — Creating an empty set with {}
❌ Wrong — {} creates an empty dict, not a set:
empty = {}
type(empty) # <class 'dict'> — not a set!
✅ Correct — use set() for an empty set:
empty = set()
type(empty) # <class 'set'> ✓
Mistake 2 — Putting unhashable items in a set
❌ Wrong — list is not hashable:
s = {[1, 2], [3, 4]} # TypeError: unhashable type: 'list'
✅ Correct — use tuples (immutable, hashable):
s = {(1, 2), (3, 4)} # ✓
Mistake 3 — Indexing a set
❌ Wrong — sets have no order or index:
roles = {"user", "admin"}
roles[0] # TypeError: 'set' object is not subscriptable
✅ Correct — convert to list if indexing needed:
role_list = list(roles)
role_list[0] # valid, but order is arbitrary ✓
Quick Reference
| Operation | Code |
|---|---|
| Create | {"a", "b"} or set(iterable) |
| Empty set | set() — NOT {} |
| Add element | s.add(x) |
| Safe remove | s.discard(x) |
| Membership | x in s — O(1) |
| Union | a | b or a.union(b) |
| Intersection | a & b or a.intersection(b) |
| Difference | a - b or a.difference(b) |
| Symmetric diff | a ^ b |
| Deduplicate list | list(set(items)) |