Frozensets: Immutable Sets for Hashable Contexts
In Lesson 3, you discovered that sets are blazingly fast because they use hashing — storing elements based on computed integer values. But there was a catch: elements must be immutable because their hash values must never change.
This lesson introduces frozensets — the immutable sibling of the mutable set. A frozenset is a set that can't be modified after creation. This simple constraint unlocks powerful capabilities: frozensets can be dictionary keys. Frozensets can be members of other sets. Frozensets can be used anywhere immutability is required.
By the end of this lesson, you'll understand when frozensets are necessary and how they enable designs that regular sets can't achieve.
💬 Why This Matters: Real-world applications often need to store collections as keys or group collections together. Regular sets can't do this. Frozensets solve this elegantly. Understanding the trade-off between mutability and hashability is a mark of intermediate Python competence.
Concept: Frozenset as Immutable Set
A frozenset is created using the frozenset() constructor. Unlike sets, frozensets cannot be modified after creation — no .add(), .remove(), or .discard() methods exist.
The trade-off is deliberate:
- Set (mutable): Can add/remove elements anytime; cannot be dict keys; cannot be in sets
- Frozenset (immutable): Cannot change after creation; CAN be dict keys; CAN be in sets
Here's how to create frozensets:
# Create from a list
coordinates: frozenset[int] = frozenset([1, 2, 3])
print(coordinates) # frozenset({1, 2, 3})
# Create from another set
my_set: set[str] = {"apple", "banana", "cherry"}
frozen: frozenset[str] = frozenset(my_set)
print(frozen) # frozenset({'apple', 'banana', 'cherry'})
# Create empty frozenset
empty: frozenset[int] = frozenset()
print(empty) # frozenset()
# Verify it's hashable
print(f"Frozenset is hashable: {hash(coordinates)}") # <some integer>
Key Observation: Notice the type hints: frozenset[int], frozenset[str]. These follow the same pattern as sets, making the distinction clear to anyone reading your code.
Attempting to modify a frozenset raises an error:
my_frozen: frozenset[int] = frozenset([1, 2, 3])
try:
my_frozen.add(4) # ❌ Method doesn't exist
except AttributeError as e:
print(f"Error: {e}") # 'frozenset' object has no attribute 'add'
try:
my_frozen.remove(1) # ❌ Method doesn't exist
except AttributeError as e:
print(f"Error: {e}") # 'frozenset' object has no attribute 'remove'
💬 Thinking Point: Notice the error is AttributeError, not TypeError. The methods literally don't exist on frozenset objects. This is Python's way of enforcing immutability at the language level.
Concept: Frozensets Are Hashable (The Superpower)
Because frozensets are immutable, they are hashable. This means you can use them in two powerful contexts:
- As dictionary keys
- As members of sets
Regular sets cannot do either of these. Let's see why:
Frozensets as Dictionary Keys
Regular sets cannot be dictionary keys because they're mutable and unhashable:
# ❌ This fails
try:
bad_dict: dict[set[int], str] = {
{1, 2}: "pair",
{3, 4}: "another"
}
except TypeError as e:
print(f"Error: {e}") # unhashable type: 'set'
But frozensets work perfectly as keys:
# ✅ This works
user_groups: dict[frozenset[str], str] = {
frozenset({"admin", "user"}): "Full access",
frozenset({"user"}): "Read-only access",
frozenset({"guest"}): "Public access"
}
# Lookup by frozenset key
admin_access: str = user_groups[frozenset({"admin", "user"})]
print(f"Admin group access: {admin_access}") # Full access
Real-World Use Case: Permission System
Imagine a permission system where user roles determine access level:
# Define permission tiers as frozenset keys
permission_levels: dict[frozenset[str], str] = {
frozenset({"admin"}): "Can do everything",
frozenset({"admin", "moderator"}): "Can moderate and administer",
frozenset({"moderator"}): "Can moderate content",
frozenset({"user"}): "Can read and comment",
frozenset({"guest"}): "Can read only"
}
# Check user permissions
def get_access_level(user_roles: frozenset[str]) -> str:
return permission_levels.get(user_roles, "Undefined access level")
alice_roles: frozenset[str] = frozenset({"admin", "moderator"})
print(get_access_level(alice_roles)) # Can moderate and administer
This pattern is elegant: the user's role set IS the key. No separate lookups needed.
Concept: Nesting Frozensets in Sets
Regular sets cannot contain sets (because sets aren't hashable). But they can contain frozensets:
# ❌ This fails
try:
nested_sets: set[set[int]] = {{1, 2}, {3, 4}}
except TypeError as e:
print(f"Error: {e}") # unhashable type: 'set'
# ✅ This works
teams: set[frozenset[str]] = {
frozenset({"Alice", "Bob"}),
frozenset({"Bob", "Charlie"}),
frozenset({"Alice", "Charlie"})
}
print(f"All teams: {teams}")
# All teams: {frozenset({'Alice', 'Bob'}), frozenset({'Bob', 'Charlie'}), frozenset({'Alice', 'Charlie'})}
Real-World Use Case: Finding All Team Members
# Find all unique members across all teams
teams: set[frozenset[str]] = {
frozenset({"Alice", "Bob", "Charlie"}),
frozenset({"David", "Eve"}),
frozenset({"Frank", "Alice"})
}
all_members: set[str] = set()
for team in teams:
all_members |= team # Union with each team's members
print(f"All team members: {all_members}") # {'Alice', 'Bob', 'Charlie', 'David', 'Eve', 'Frank'}
🎓 Learning Insight: Notice how we converted frozensets back to a regular set when needing mutability. Python allows this conversion freely — there's no performance penalty.
Code Examples
Example 1: Creating and Verifying Frozensets
Specification: Create frozensets from various sources, verify immutability and hashability.
AI Prompt Used: "Show me how to create frozensets in Python 3.14+ with type hints, and verify that they're immutable and hashable."
# Create frozensets with type hints
colors: frozenset[str] = frozenset(["red", "green", "blue"])
numbers: frozenset[int] = frozenset([1, 2, 3, 4, 5])
empty: frozenset[str] = frozenset()
# Verify type
print(f"Type of colors: {type(colors)}") # <class 'frozenset'>
# Verify hashability (can get hash value)
color_hash: int = hash(colors)
print(f"Hash of frozenset: {color_hash}") # Some integer
# Verify immutability - no modification methods
print(f"Methods available: {[m for m in dir(colors) if not m.startswith('_')]}")
# Shows: copy, difference, intersection, isdisjoint, issubset, issuperset, symmetric_difference, union
# Verify read-only operations work
union_result: frozenset[str] = colors | frozenset(["yellow"])
print(f"Union works: {union_result}") # frozenset({'red', 'green', 'blue', 'yellow'})
# Verify assignment/modification fails
try:
colors.add("yellow")
except AttributeError as e:
print(f"Cannot modify frozenset: {e}") # 'frozenset' object has no attribute 'add'
Expected Output:
Type of colors: <class 'frozenset'>
Hash of frozenset: -5483841318619854644
Methods available: ['copy', 'difference', 'intersection', 'isdisjoint', 'issubset', 'issuperset', 'symmetric_difference', 'union']
Union works: frozenset({'red', 'green', 'blue', 'yellow'})
Cannot modify frozenset: 'frozenset' object has no attribute 'add'
Example 2: Using Frozensets as Dictionary Keys
Specification: Show that regular sets fail as dict keys, but frozensets work. Demonstrate practical use case.
AI Prompt Used: "Create a Python example showing why I can't use sets as dictionary keys but CAN use frozensets, with a realistic business scenario."
# First, show the problem with regular sets
print("Attempting to use set as dictionary key:")
try:
bad_dict: dict[set[str], int] = {
{1, 2, 3}: 100 # ❌ Try to use set as key
}
except TypeError as e:
print(f" ❌ Error: {e}") # unhashable type: 'set'
# Now show the solution with frozensets
print("\nUsing frozenset as dictionary key:")
# Scenario: Coordinate lookup for locations
location_coordinates: dict[frozenset[tuple[int, int]], str] = {
frozenset([(0, 0), (1, 1)]): "diagonal_main",
frozenset([(2, 0), (0, 2)]): "corners",
frozenset([(5, 5)]): "center_point"
}
# Look up location by coordinate set
query: frozenset[tuple[int, int]] = frozenset([(0, 0), (1, 1)])
location: str = location_coordinates.get(query, "Unknown location")
print(f" Location found: {location}") # diagonal_main
# More practical: Permission levels as frozenset keys
permission_tiers: dict[frozenset[str], list[str]] = {
frozenset(["admin"]): ["read", "write", "delete", "manage_users"],
frozenset(["moderator"]): ["read", "write", "delete"],
frozenset(["user"]): ["read", "write"],
frozenset(["guest"]): ["read"]
}
# Check what a user with specific roles can do
user_bob_roles: frozenset[str] = frozenset(["user"])
bob_permissions: list[str] = permission_tiers.get(user_bob_roles, [])
print(f" Bob's permissions: {bob_permissions}") # ['read', 'write']
Expected Output:
Attempting to use set as dictionary key:
❌ Error: unhashable type: 'set'
Using frozenset as dictionary key:
Location found: diagonal_main
Bob's permissions: ['read', 'write']
Example 3: Nesting Frozensets in Sets
Specification: Show that regular sets cannot be nested, but frozensets can. Demonstrate operations on nested structures.
AI Prompt Used: "Show me how to create a set of frozensets and perform operations on nested frozenset structures."
# Show the problem with nested sets
print("Attempting to nest sets:")
try:
nested_sets: set[set[int]] = {{1, 2}, {3, 4}}
except TypeError as e:
print(f" ❌ Error: {e}") # unhashable type: 'set'
# Solution: Use frozensets
print("\nNesting frozensets in sets:")
# Represent student groups in different clubs
clubs: set[frozenset[str]] = {
frozenset({"Alice", "Bob"}), # chess club
frozenset({"Bob", "Charlie"}), # debate club
frozenset({"Alice", "Charlie"}), # math club
frozenset({"David"}) # solo club
}
print(f"Total unique groups: {len(clubs)}") # 4
# Find all students involved in clubs
all_members: set[str] = set()
for club_members in clubs:
all_members |= club_members
print(f"All club members: {all_members}") # {'Alice', 'Bob', 'Charlie', 'David'}
# Find the most popular students (in multiple clubs)
member_count: dict[str, int] = {}
for club_members in clubs:
for member in club_members:
member_count[member] = member_count.get(member, 0) + 1
popular: [str] = [name for name, count in member_count.items() if count > 1]
print(f"Members in multiple clubs: {popular}") # ['Alice', 'Bob', 'Charlie']
# Find which students are in the same clubs
alice_clubs: set[frozenset[str]] = {club for club in clubs if "Alice" in club}
bob_clubs: set[frozenset[str]] = {club for club in clubs if "Bob" in club}
# Do they share any clubs?
shared: set[frozenset[str]] = alice_clubs & bob_clubs
print(f"Alice and Bob are in {len(shared)} club(s) together") # 1
Expected Output:
Attempting to nest sets:
❌ Error: unhashable type: 'set'
Nesting frozensets in sets:
Total unique groups: 4
All club members: {'Alice', 'Bob', 'Charlie', 'David'}
Members in multiple clubs: ['Alice', 'Bob', 'Charlie']
Alice and Bob are in 1 club(s) together
Example 4: Set vs. Frozenset Comparison
Specification: Create a decision matrix showing when to use set vs. frozenset.
AI Prompt Used: "Create a comparison showing the differences between set and frozenset, and when to use each one."
from typing import Any
print("=" * 60)
print("SET vs. FROZENSET COMPARISON")
print("=" * 60)
# Create instances of each
my_set: set[int] = {1, 2, 3}
my_frozen: frozenset[int] = frozenset([1, 2, 3])
print("\n1. MUTABILITY TEST")
print("-" * 60)
# Try to modify set
print(" set.add(4):", end=" ")
my_set.add(4)
print(f"✓ Works — set is now {my_set}")
# Try to modify frozenset
print(" frozenset.add(4):", end=" ")
try:
my_frozen.add(4)
print("✓ Works")
except AttributeError:
print("✗ Fails — frozenset has no .add() method")
print("\n2. HASHABILITY TEST (Can use as dict key?)")
print("-" * 60)
# Try set as dict key
print(" dict[set[int], str]:", end=" ")
try:
test_dict_set: dict[set[int], str] = {my_set: "value"}
print("✓ Works")
except TypeError:
print("✗ Fails — set is unhashable")
# Try frozenset as dict key
print(" dict[frozenset[int], str]:", end=" ")
try:
test_dict_frozen: dict[frozenset[int], str] = {my_frozen: "value"}
print("✓ Works — frozenset is hashable")
except TypeError:
print("✗ Fails")
print("\n3. CAN BE SET MEMBER TEST")
print("-" * 60)
# Try set as set member
print(" set[set[int]]:", end=" ")
try:
set_of_sets: set[set[int]] = {my_set}
print("✓ Works")
except TypeError:
print("✗ Fails — sets are unhashable")
# Try frozenset as set member
print(" set[frozenset[int]]:", end=" ")
try:
set_of_frozen: set[frozenset[int]] = {my_frozen}
print("✓ Works — frozensets are hashable")
except TypeError:
print("✗ Fails")
print("\n4. READ-ONLY OPERATIONS (Both support these)")
print("-" * 60)
set_union: set[int] = my_set | {5, 6}
frozen_union: frozenset[int] = my_frozen | frozenset([5, 6])
print(f" Union: set={set_union}, frozen={frozen_union}")
set_inter: set[int] = my_set & {2, 3, 4}
frozen_inter: frozenset[int] = my_frozen & frozenset([2, 3, 4])
print(f" Intersection: set={set_inter}, frozen={frozen_inter}")
print("\n5. DECISION MATRIX: WHEN TO USE EACH")
print("-" * 60)
print("""
Use SET when:
✓ You need to add/remove elements
✓ No need for hashing (not a dict key, not in another set)
✓ Data is dynamic and changes frequently
Examples: tracking currently active users, building unique values
Use FROZENSET when:
✓ Data shouldn't change after creation
✓ Need to use as dictionary key
✓ Need to contain in another set
✓ Using as argument to functions that expect hashable types
Examples: permission levels, coordinate groups, immutable data caches
""")
print("=" * 60)
Expected Output:
============================================================
SET vs. FROZENSET COMPARISON
============================================================
1. MUTABILITY TEST
--------------------------------------------------------------
set.add(4): ✓ Works — set is now {1, 2, 3, 4}
frozenset.add(4): ✗ Fails — frozenset has no .add() method
2. HASHABILITY TEST (Can use as dict key?)
--------------------------------------------------------------
dict[set[int], str]: ✗ Fails — set is unhashable
dict[frozenset[int], str]: ✓ Works — frozenset is hashable
3. CAN BE SET MEMBER TEST
--------------------------------------------------------------
set[set[int]]: ✗ Fails — sets are unhashable
set[frozenset[int]]: ✓ Works — frozensets are hashable
4. READ-ONLY OPERATIONS (Both support these)
--------------------------------------------------------------
Union: set={1, 2, 3, 5, 6}, frozen=frozenset({1, 2, 3, 5, 6})
Intersection: set={2, 3}, frozen=frozenset({2, 3})
5. DECISION MATRIX: WHEN TO USE EACH
--------------------------------------------------------------
Use SET when:
✓ You need to add/remove elements
✓ No need for hashing (not a dict key, not in another set)
✓ Data is dynamic and changes frequently
Examples: tracking currently active users, building unique values
Use FROZENSET when:
✓ Data shouldn't change after creation
✓ Need to use as dictionary key
✓ Need to contain in another set
✓ Using as argument to functions that expect hashable types
Examples: permission levels, coordinate groups, immutable data caches
============================================================
Practice Exercises
Exercise 1: Create a Frozenset and Verify Hashability
Create a frozenset containing 5 integers, then:
- Print it with proper type hint syntax
- Verify it's hashable by computing its hash value
- Try to add an element (expect an error)
Exercise 2: Dictionary Keys with Frozensets
Create a dictionary where the keys are frozensets representing permission levels. Each value should be a list of allowed actions. Implement a lookup function that takes a frozenset of roles and returns the corresponding actions.
Exercise 3: Nesting Frozensets in Sets
Create a set of frozensets representing groups of students. Then:
- Find the total number of unique students
- Find which students appear in multiple groups
- Find if two specific students share any groups
Exercise 4: Convert Between Set and Frozenset
Create a mutable set, convert it to a frozenset, then convert it back to a set. Verify that the conversion preserves elements and doesn't have a performance penalty.
Try With AI
Use this section to solidify your understanding of frozensets through hands-on exploration with your AI companion.
💬 Prompt 1: Understanding Immutability Trade-offs (Understand Level)
Ask your AI: "What's the difference between a set and a frozenset? Why would I ever use frozenset instead of set? What am I trading to get immutability?"
Expected Outcome: Your AI explains the immutability constraint and highlights specific scenarios where frozensets become necessary (dict keys, set members, function arguments requiring hashable types).
AI Tool to Use: ChatGPT web (conceptual explanation with reasoning)
Follow-up Question: "Are there any performance differences between set and frozenset?"
💬 Prompt 2: Dictionary Keys Use Case (Apply Level)
Ask your AI: "Show me a practical example where frozensets as dictionary keys is the ONLY solution. Why can't I use a regular set? Why can't I use a different data structure?"
Expected Outcome: Your AI provides a realistic business scenario (permission system, caching, grouping) that demonstrates why frozensets are the right choice and why sets wouldn't work.
AI Tool to Use: Claude Code (generate example code + explain design reasoning)
Follow-up Question: "What if I try to use a tuple instead of a frozenset?"
💬 Prompt 3: Nesting Practice (Apply Level)
Ask your AI: "Write code that creates a set of frozensets representing teams in a company. Then implement a function that finds which employees are in multiple teams together."
Expected Outcome: Your AI generates working code using nested frozensets, includes type hints, and explains the structure clearly.
AI Tool to Use: Claude Code (generate + explain pattern)
Safety Note: Converting between set and frozenset is free — no performance penalty. Use frozenset when semantics require immutability, not as a performance optimization.
💬 Prompt 4: Real-World Scenario Analysis (Analyze Level)
Ask your AI: "I'm building a permission system where users have roles. Should I store user roles as a list, set, or frozenset? Compare the trade-offs and explain your choice. What if I need to check if two users have the same permissions?"
Expected Outcome: Your AI analyzes the scenario, weighs trade-offs (list: can have duplicates, set: no duplicates but mutable, frozenset: no duplicates and immutable), and recommends the best choice with reasoning.
AI Tool to Use: Claude Code or ChatGPT (design discussion with code examples)
Follow-up Question: "What if I later need to add a new role to a user?"
🎓 Reflection: After exploring with your AI, write a 2-3 sentence explanation of when you'd use frozenset instead of set. What capability unlocks? What trade-off are you making?