The previous lesson ended with a question: why is looking up catalog["Faust"] practically instantaneous? The answer lives in a sibling structure of the dictionary: the set. A set is a collection with no guaranteed order, no duplicates and no associated values — just elements, like a dictionary's keys without their values. In exchange for giving up order and repetition, it offers two superpowers: checking membership at full speed and operating with the logic of set theory (union, intersection, difference), which at Papyrus will answer questions like "which members bought this month but not last month?" in a single line.
Contents
- Creating sets (and the empty
{}trap) - Uniqueness: duplicates vanish
- Adding and removing elements
- Lightning-fast membership: why
inflies - Set operations: union, intersection, differences
- Subsets and supersets
frozenset: the immutable set- Set comprehensions
Creating sets (and the empty {} trap)
genres = {"novel", "drama", "poetry"} # literal: braces with bare elements
from_list = set(["novel", "drama", "novel"]) # set() converts iterables and DEDUPLICATES
print(from_list) # {'novel', 'drama'} → the duplicate is gone
letters = set("papyrus") # {'p', 'a', 'y', 'r', 'u', 's'} → the repeated 'p' is out
empty = set() # the ONLY way to create an empty set
trap = {} # this is an empty DICTIONARY, not a set!
print(type(empty), type(trap)) # <class 'set'> <class 'dict'>The {} trap is historical: dictionaries arrived in Python first and kept the empty braces. Memorize it: empty set = set(), always.
Elements must be immutable (hashable), like a dict's keys: strings, numbers and tuples yes; lists and dictionaries no. And there is no indexing: genres[0] is a TypeError, because a set has no positions.
Uniqueness: duplicates vanish
Uniqueness is not an annoying restriction: it is the star feature. The most frequent real-world use case — removing duplicates from a list — is one line:
# Today's orders at Papyrus: Luis has ordered Don Quixote twice by mistake
orders = ["Don Quixote", "Hamlet", "Don Quixote", "Faust", "Hamlet"]
unique_titles = set(orders)
print(unique_titles) # {'Faust', 'Hamlet', 'Don Quixote'} (order not guaranteed)
print(len(unique_titles)) # 3 distinct titles, even though there were 5 orders
# If you need a list back (e.g. sorted), convert again:
sorted_unique = sorted(set(orders))
print(sorted_unique) # ['Don Quixote', 'Faust', 'Hamlet']Careful: set() does not preserve the list's original order. If order of appearance matters, there is a dict idiom you will see in the exercises.
Adding and removing elements
Sets are mutable (an immutable version exists: frozenset, below):
genres = {"novel", "drama"}
genres.add("poetry") # adds one element (if already there, nothing happens)
genres.add("drama") # no effect and no error: it was already in
genres.update(["essay", "novel"]) # adds several at once (like extend on lists)
genres.remove("essay") # removes; KeyError if it doesn't exist
genres.discard("science") # removes if present; SILENCE if absent
element = genres.pop() # takes out an ARBITRARY element (not "the last": there's no order)
genres.clear() # empties the set| Method | If the element is absent... |
|---|---|
remove(x) |
Raises KeyError |
discard(x) |
Does nothing (the relaxed option) |
Lightning-fast membership: why in flies
With a list, x in some_list forces Python to compare element by element, from the start: if the list has a million entries, the worst case means a million comparisons. With a set (or a dict's keys), Python computes the element's hash — a numeric fingerprint — and jumps straight to the internal slot where it would be if it existed. One check, whether the set holds ten elements or ten million.
members = {"LUIS-001", "MARTA-002", "PAU-003"} # Papyrus member codes
def is_valid_member(code):
"""The papyrus_utils function, now backed by a set: immediate validation."""
return code.strip().upper() in members
print(is_valid_member("luis-001")) # True
print(is_valid_member("ANA-999")) # FalseThis is also the answer to 04-03's question: catalog["Faust"] is instantaneous because a dictionary's keys use exactly the same hash mechanism. You do not need the formal complexity theory (it will come if you ever look it up as "O(1) vs O(n)"); keep the practical rule:
Lots of "is it there or not?" checks against the same collection → turn it into a
setfirst.
Set operations: union, intersection, differences
Here sets shine in their own right. Each operation exists as an operator and as a method (the method accepts any iterable; the operator demands two sets):
| Operation | Operator | Method | Result |
|---|---|---|---|
| Union | a | b |
a.union(b) |
Elements in a or in b (or both) |
| Intersection | a & b |
a.intersection(b) |
Elements in a and in b |
| Difference | a - b |
a.difference(b) |
In a but not in b |
| Symmetric difference | a ^ b |
a.symmetric_difference(b) |
In a or in b, but not in both |
With Papyrus data: Ana wants to analyze member loyalty by comparing June's and July's buyers:
bought_june = {"Luis", "Marta", "Pau", "Julia"}
bought_july = {"Luis", "Julia", "Omar"}
print(bought_june | bought_july) # {'Luis','Marta','Pau','Julia','Omar'} → active customer base
print(bought_june & bought_july) # {'Luis', 'Julia'} → the loyal ones: back month after month
print(bought_june - bought_july) # {'Marta', 'Pau'} → bought in June but NOT July: win them back?
print(bought_july - bought_june) # {'Omar'} → new customer in July
print(bought_june ^ bought_july) # {'Marta','Pau','Omar'} → only one of the two monthsFive business questions, five lines, zero loops. Difference is not commutative (a - b != b - a); union, intersection and symmetric difference are.
Another natural use: each book's genres as sets, to answer "which books share a genre?":
book_genres = {
"The Odyssey": {"classic", "epic"},
"Hamlet": {"classic", "drama", "tragedy"},
"Faust": {"classic", "drama"},
}
shared = book_genres["Hamlet"] & book_genres["Faust"]
print(shared) # {'classic', 'drama'}Subsets and supersets
tragedies = {"Hamlet", "Faust"}
classics = {"The Odyssey", "Hamlet", "Don Quixote", "Faust"}
print(tragedies <= classics) # True: tragedies is a SUBset of classics
print(tragedies.issubset(classics)) # equivalent with a method
print(classics >= tragedies) # True: classics is a SUPERset
print(tragedies.isdisjoint({"The Iliad"})) # True: they share no elementUseful for validations: "are all the titles in the order in the catalog?" is set(order) <= set(catalog) — and yes, set(catalog) on the 04-03 dict extracts its keys.
frozenset: the immutable set
The same relationship as tuple/list: a frozenset is a frozen set. It accepts no add() or remove(), and in exchange it is hashable — it can be a dictionary key or an element of another set:
CLASSICS_PACK = frozenset({"The Odyssey", "Hamlet"}) # the shop's fixed bundle offer
offers = {
CLASSICS_PACK: 19.90, # a frozenset as a dict key: legal
}
print(offers[frozenset({"Hamlet", "The Odyssey"})]) # 19.9 → order doesn't matterWith a regular set as a key you would get TypeError: unhashable type: 'set'. Brief and occasional use, but when you need it, nothing else takes its place.
Set comprehensions
The third comprehension in the family (list → dict → set). Syntax: braces without a colon:
orders = [" hamlet ", "FAUST", "Hamlet", "don quixote"]
# Normalized, unique titles, in one go (normalization in depth comes in 04-05)
unique = {o.strip().lower() for o in orders}
print(unique) # {'hamlet', 'faust', 'don quixote'} → 4 orders, 3 real titles
# Over the canonical catalog from 04-03: titles with stock
catalog = {
"The Odyssey": {"price": 12.50, "stock": 4},
"Hamlet": {"price": 9.95, "stock": 6},
"Don Quixote": {"price": 15.90, "stock": 8},
"Faust": {"price": 21.00, "stock": 0},
}
available = {t for t, record in catalog.items() if record["stock"] > 0}
print(available) # {'The Odyssey', 'Hamlet', 'Don Quixote'}| Comprehension | Syntax | Produces |
|---|---|---|
| List | [x for x in it] |
List (ordered, with duplicates) |
| Dictionary | {k: v for ...} |
Dict (key: value) |
| Set | {x for x in it} |
Set (unique, unordered) |
Common Mistakes and Tips
{}is not an empty set: it is a dict. The mistake gives no message —trap.add(x)will simply fail later with anAttributeError. Empty set =set().- Expecting order: sets guarantee no iteration order. If you print a set and it "comes out shuffled", it is not a bug. For display,
sorted(my_set). - Indexing a set:
s[0]raisesTypeError. Without order there are no positions; if you need "the first one", you are missing a list. - Inserting mutable elements:
{["a", "b"]}raisesTypeError: unhashable type: 'list'. Convert to a tuple if you need grouping. - Confusing
remove()withdiscard(): if absence is normal,discard(); if it is a bug,remove()(let it fail early). The same criterion as brackets vsget()on dicts. - Deduplicating with a set when order matters:
set(some_list)loses the order of appearance. The idiom that preserves it:list(dict.fromkeys(some_list))— dicts deduplicate keys and remember insertion order. - Tip: if you catch yourself writing nested loops to compare two lists ("the ones here that aren't there"), it was almost certainly a set operation.
Exercises
- Order audit. From the list
orders = ["Hamlet", "Faust", "Hamlet", "Dracula", "Don Quixote", "Faust"], obtain: (a) the set of distinct titles ordered; (b) how many duplicate orders there were (total orders minus distinct titles); (c) using the canonical catalog from 04-03 and a set difference, which ordered titles are not in the catalog. - Loyalty campaign. With
june = {"Luis", "Marta", "Pau", "Julia"}andjuly = {"Luis", "Julia", "Omar"}, compute: the members to win back (bought in June but not in July), July's newcomers, and the loyal ones from both months. Then check with a subset operator whether all the loyal ones bought in June. - Available genres. With the lesson's
book_genresdict (The Odyssey, Hamlet, Faust) and theavailableset computed over the canonical catalog, build via operations and/or a comprehension the set of genres actually available today (the genres of the books with stock > 0). Hint: the union of several sets can be done withset().union(...).
Solutions
# Exercise 1
orders = ["Hamlet", "Faust", "Hamlet", "Dracula", "Don Quixote", "Faust"]
catalog = {
"The Odyssey": {"price": 12.50, "stock": 4},
"Hamlet": {"price": 9.95, "stock": 6},
"Don Quixote": {"price": 15.90, "stock": 8},
"Faust": {"price": 21.00, "stock": 0},
}
distinct = set(orders) # (a) {'Hamlet','Faust','Dracula','Don Quixote'}
duplicates = len(orders) - len(distinct) # (b) 2
unknown = distinct - set(catalog) # (c) {'Dracula'} → set(dict) takes the keys
print(distinct, duplicates, unknown)# Exercise 2
june = {"Luis", "Marta", "Pau", "Julia"}
july = {"Luis", "Julia", "Omar"}
win_back = june - july # {'Marta', 'Pau'}
newcomers = july - june # {'Omar'}
loyal = june & july # {'Luis', 'Julia'}
print(loyal <= june) # True: every loyal member bought in June (by definition)Tip: name the results with business vocabulary (win_back, loyal), not mathematical vocabulary (difference, intersection): the code explains itself.
# Exercise 3
book_genres = {
"The Odyssey": {"classic", "epic"},
"Hamlet": {"classic", "drama", "tragedy"},
"Faust": {"classic", "drama"},
}
available = {t for t, record in catalog.items() if record["stock"] > 0}
genres_today = set().union(*(book_genres[t] for t in available if t in book_genres))
print(genres_today) # {'classic', 'epic', 'drama', 'tragedy'} → Faust (sold out) contributes nothingNotice: the * unpacks the sets as arguments to union() (the same * from 03-02), and the if t in book_genres protects against books with no genres on file — "Don Quixote" is available but has no entry, and without that filter there would be a KeyError.
Conclusion
Sets complete the trio of "curly brace" collections: you know how to create them (remembering that set() is the only spelling of the empty one), exploit automatic deduplication, and express business logic with union, intersection and differences in one line where you would previously have nested loops. You also understand why in is lightning-fast on sets and dicts — the magic of the hash — and you know frozenset for when immutability matters. At Papyrus they now deduplicate orders, validate members and compare months of sales. But in almost every example an actor has kept peeking through, one we have been using since module 1 without ever studying it seriously: the string, with its strip(), lower() and friends. The next lesson finally treats it as what it is — a sequence with superpowers of its own — and systematizes the normalization of user input that find_book() and is_valid_member() have been doing halfway.
Python Programming Course
Module 1: Introduction to Python
- Introduction to Python
- Setting Up the Development Environment
- Python Syntax and Basic Data Types
- Variables and Constants
- Basic Input and Output
- Virtual Environments and Package Management
Module 2: Control Structures
Module 3: Functions and Modules
- Defining Functions
- Function Arguments
- Lambda Functions
- Modules and Packages
- Standard Library Overview
Module 4: Data Structures
Module 5: Object-Oriented Programming
Module 6: File Handling
Module 7: Error and Exception Handling
- Introduction to Exceptions
- Handling Exceptions
- Raising Exceptions
- Custom Exceptions
- Best Practices and Error Logging
Module 8: Advanced Topics
- Type Hints
- Decorators
- Generators
- Context Managers
- Concurrency: Threads and Processes
- Asyncio for Asynchronous Programming
Module 9: Testing and Debugging
- Introduction to Testing
- Unit Testing with unittest
- Testing with pytest
- Test-Driven Development
- Debugging Techniques
- Using pdb for Debugging
Module 10: Web Development with Python
- Introduction to Web Development
- Flask Framework Fundamentals
- Building REST APIs with Flask
- Introduction to Django
- Building Web Applications with Django
Module 11: Data Science with Python
- Introduction to Data Science
- NumPy for Numerical Computing
- Pandas for Data Manipulation
- Matplotlib for Data Visualization
- Introduction to Machine Learning with scikit-learn
