Sets: Unordered, Unique, Lightning Fast
1 · The lesson
readA set is Python's collection type for "a bag of unique things, in no particular order". You met sets briefly in the Dictionaries lesson. This lesson covers them in detail.
Sets are the right tool for three jobs: removing duplicates, checking if something exists, and comparing groups (intersections, unions, differences). For these jobs, sets are much faster than lists.
1. Creating a Set
# Set literal: curly braces around the items fruits = {"apple", "banana", "cherry"} print(fruits) # {'banana', 'cherry', 'apple'} (the order can vary) # From a list (any iterable works) unique_numbers = set([1, 2, 2, 3, 3, 3]) print(unique_numbers) # {1, 2, 3} # Empty set: you must use set(), NOT {} empty_set = set() # {} would create an empty DICT print(type(empty_set)) # <class 'set'> print(type({})) # <class 'dict'>
Many people make this mistake: {} looks like an empty set, but it is a dict. Always use set() for an empty set.
2. The Two Properties That Define a Set
Unique: duplicates are removed without any warning:
votes = ["yes", "no", "yes", "yes", "no", "maybe"] unique_votes = set(votes) print(unique_votes) # {'yes', 'no', 'maybe'} print(len(unique_votes)) # 3
Unordered: items have no fixed position:
s = {"c", "a", "b"}
print(s) # order is unpredictable
# print(s[0]) # TypeError: sets have no index positionsIf you need order, use a list. If you need unique items and the order doesn't matter, a set is faster and cleaner.
3. Membership Checks: What Sets Do Best
Checking x in collection is the operation sets are fastest at. For a list, Python compares the items one by one, so a list of 1 million items may need 1 million comparisons. A set uses a hash, so it takes about the same time whatever its size.
allowed_users = {"alice", "bob", "carol", "dave"}
# Very fast: the check takes the same time however big the set is
print("alice" in allowed_users) # True
print("eve" in allowed_users) # FalseThis is why sets are the right choice for questions like "is this in the blocklist?" or "have I already seen this username?".
4. Adding and Removing Items
tags = {"python", "tutorial"}
tags.add("beginner") # add one item
print(tags) # {'python', 'tutorial', 'beginner'}
tags.update(["free", "ide"]) # add many items
print(tags)
tags.remove("tutorial") # remove: raises KeyError if the item is missing
tags.discard("not_there") # remove: does nothing if the item is missing
tags.pop() # remove and return one item (you can't choose which)
# Empty the whole set
tags.clear()
print(tags) # set()discard is like remove, but it doesn't raise an error. Use it when you don't care whether the item was there.
5. Set Math
Sets support the operations from mathematical set theory. They are much easier to read than the same logic written with list comprehensions.
team_a = {"alice", "bob", "carol", "dave"}
team_b = {"carol", "dave", "eve", "frank"}
# UNION: in either set
print(team_a | team_b)
# {'alice', 'bob', 'carol', 'dave', 'eve', 'frank'}
# INTERSECTION: in both sets
print(team_a & team_b)
# {'carol', 'dave'}
# DIFFERENCE: in A but not in B
print(team_a - team_b)
# {'alice', 'bob'}
# SYMMETRIC DIFFERENCE: in either set, but not in both
print(team_a ^ team_b)
# {'alice', 'bob', 'eve', 'frank'}The same operations exist as named methods if you prefer (team_a.union(team_b), .intersection(), .difference(), .symmetric_difference()).
Subset / superset tests:
required = {"name", "email"}
provided = {"name", "email", "age"}
print(required.issubset(provided)) # True
print(provided.issuperset(required)) # True
print(required <= provided) # True (a short way to write issubset)6. Real-World Patterns
Remove duplicates from a list (the order is not kept):
emails = ["a@b.com", "c@d.com", "a@b.com", "e@f.com"] unique_emails = list(set(emails)) print(unique_emails) # the 3 unique emails, in any order (it can change between runs)
If you need to deduplicate AND keep the original order, use dict.fromkeys() instead (dicts preserve insertion order in Python 3.7+):
ordered_unique = list(dict.fromkeys(emails)) print(ordered_unique) # ['a@b.com', 'c@d.com', 'e@f.com']: original order kept
setup added so this can run · re-runs 1 earlier block on this page first
# Lightweight mock for objects whose attributes/methods aren't critical class _AutoMock: def __init__(self, name='mock'): self._name = name def __getattr__(self, k): return _AutoMock(self._name + '.' + k) def __call__(self, *a, **kw): print('-> ' + self._name + '() called') return _AutoMock(self._name + '()') def __repr__(self): return '<mock ' + self._name + '>' def __str__(self): return '<mock ' + self._name + '>' def __bool__(self): return True def __iter__(self): return iter([]) def __len__(self): return 0 def __getitem__(self, k): return _AutoMock(self._name + '[...]') def __setitem__(self, k, v): pass def __enter__(self): return self def __exit__(self, *a): return False async def __aenter__(self): return self async def __aexit__(self, *a): return False def __add__(self, o): return self def __radd__(self, o): return self def __sub__(self, o): return self def __mul__(self, o): return self def __rmul__(self, o): return self def __truediv__(self, o): return self def __eq__(self, o): return isinstance(o, _AutoMock) def __hash__(self): return hash(self._name) def __lt__(self, o): return True def __le__(self, o): return True def __gt__(self, o): return False def __ge__(self, o): return False def __mro_entries__(self, bases): return (object,) emails = _AutoMock('emails') # Earlier blocks on this page that this one builds on, re-run quietly first. _pm_earlier = [ 'emails = ["a@b.com", "c@d.com", "a@b.com", "e@f.com"]\nunique_emails = list(set(emails))\nprint(unique_emails) # the 3 unique emails, in any order (it can change between runs)', ] def _pm_run_earlier(_srcs, _g): import sys as _s, io as _io, warnings as _w for _src in _srcs: _o, _e = _s.stdout, _s.stderr _s.stdout = _s.stderr = _io.StringIO() try: with _w.catch_warnings(): _w.simplefilter('ignore') exec(compile(_src, '<earlier block>', 'exec'), _g) except (Exception, SystemExit): pass finally: _s.stdout, _s.stderr = _o, _e if 'matplotlib.pyplot' in _s.modules: _s.modules['matplotlib.pyplot'].close('all') # their plots belong to their own blocks _pm_run_earlier(_pm_earlier, globals()) del _pm_earlier, _pm_run_earlier
Find who's new vs returning:
yesterday = {"alice", "bob", "carol"}
today = {"bob", "carol", "dave", "eve"}
new_users = today - yesterday
print(new_users) # {'dave', 'eve'}
returning_users = today & yesterday
print(returning_users) # {'bob', 'carol'}
churned = yesterday - today
print(churned) # {'alice'}That gives three useful business numbers in three lines.
Find duplicates in a list:
items = ["a", "b", "c", "a", "d", "b"] seen = set() duplicates = set() for x in items: if x in seen: duplicates.add(x) seen.add(x) print(duplicates) # {'a', 'b'}
7. frozenset: The Immutable Version
A normal set is mutable. frozenset is the read-only version. It supports the same operations that read a set, but you can't add or remove items. The benefit is that a frozenset is hashable, so it can be a dictionary key or an item in another set.
flavors = frozenset(["vanilla", "chocolate"]) flavors_with_strawberry = frozenset(["vanilla", "chocolate", "strawberry"]) # Use frozensets as dict keys. A normal set CAN'T do this order_count = { flavors: 5, flavors_with_strawberry: 2, } print(order_count[flavors]) # 5
You won't need frozenset often, but it is there when you need a hashable set.
8. When to Use What
| Need | Use |
|---|---|
| Ordered items, possibly repeating, items might change | List |
| Ordered items, fixed once created, used as a "record" | Tuple |
| Key → value lookup | Dictionary |
| Unique items, fast membership, set math | Set |
| Unique items, immutable, hashable | frozenset |
9. Mistakes You'll Hit
1. {} is a dict, not a set
x = {} # dict
y = set() # set2. Trying to put a mutable thing in a set
# {[1, 2], [3, 4]} # TypeError: lists aren't hashable {(1, 2), (3, 4)} # tuples are fine
3. Indexing a set
# s = {"a", "b"} # s[0] # TypeError: sets aren't ordered
If you need indexing, convert to a list first:
list(s)[0] (but the "first" item is unpredictable).
Mini-Program: Vocabulary Overlap Between Two Texts
text_a = "the quick brown fox jumps over the lazy dog" text_b = "the lazy fox sleeps under a brown tree" words_a = set(text_a.split()) words_b = set(text_b.split()) shared = words_a & words_b only_a = words_a - words_b only_b = words_b - words_a print(f"Shared words: {sorted(shared)}") print(f"Unique to A: {sorted(only_a)}") print(f"Unique to B: {sorted(only_b)}") print(f"Vocab overlap: {len(shared) / len(words_a | words_b):.1%}")
Set math does a real piece of text analysis here in 8 lines.
🎯 Your Turn: Who Dropped the Course?
You have two attendance registers: the students enrolled in week 1, and those
still enrolled in week 5. Answer three questions with set operations rather than
loops.
Write course_changes(week1, week5) returning a tuple of three sorted lists:
students who dropped, students who joined late, and students who stayed
throughout.
week1 = ["asha", "biju", "chitra", "deepak"] week5 = ["asha", "chitra", "elango"] dropped → ["biju", "deepak"] joined → ["elango"] stayed → ["asha", "chitra"]
Skeleton:
def course_changes(week1, week5): a = set(week1) b = set(week5) # TODO 1: dropped = in week1 but not week5 # TODO 2: joined = in week5 but not week1 # TODO 3: stayed = in both # TODO 4: return all three as sorted lists ... dropped, joined, stayed = course_changes( ["asha", "biju", "chitra", "deepak"], ["asha", "chitra", "elango"], ) print("dropped:", dropped) # ['biju', 'deepak'] print("joined :", joined) # ['elango'] print("stayed :", stayed) # ['asha', 'chitra']
Hint 1: Three operators, three answers
a - b is everything in a that is not in b.
b - a is the reverse. a & b is what appears in
both. Each question in this exercise is exactly one of those.
Hint 2: Sets have no order, so sort before you return
Printing a set gives you an arbitrary order that can change between runs, which makes the output impossible to test.sorted(some_set) returns a list in
a predictable order. Sort once, just before the data leaves your
function.
Show full solution
def course_changes(week1, week5): a = set(week1) b = set(week5) return sorted(a - b), sorted(b - a), sorted(a & b) dropped, joined, stayed = course_changes( ["asha", "biju", "chitra", "deepak"], ["asha", "chitra", "elango"], ) print("dropped:", dropped) # ['biju', 'deepak'] print("joined :", joined) # ['elango'] print("stayed :", stayed) # ['asha', 'chitra']
Written with loops, this would take many more lines. With set operations it is one
line, and it reads almost exactly like the question you were asked. That is the
main reason to use sets. The answer also handles duplicates in the input
registers correctly, because building a set removes
them.
What You Learned
- A set is an unordered collection of unique items.
- Create with
{1, 2, 3}orset(...). Useset()for an empty set, because{}is a dict. - Sets are the fastest way to check membership (
x in s): a lookup is O(1) on average. - Set math operators:
|union,&intersection,-difference,^symmetric difference. - Use frozenset when you need an immutable, hashable set.
Once you know sets, you will use them often. "Are these emails unique?" "Which people are in both groups?" "Have I seen this user before?" A set answers each of these questions.
Next: Errors & Exceptions.
Source: adapted from Python official documentation Section 5.4 (Sets). PSF License.
Practice this
on practicepython.inShort exercises that run in your browser and tell you what your code did, as well as whether a test passed.
Found this useful? Share it with a friend who’s learning Python.