The Frozen Collection Vault: frozenset and Set Immutability



This content originally appeared on DEV Community and was authored by Aaron Rose

Timothy’s membership registry had transformed how the library tracked visitors and members, but Professor Williams arrived with a problem that would reveal a fundamental limitation of his set system.

The Dictionary Key Crisis

“I need to catalog research groups,” Professor Williams explained, “where each group is identified by its members. Some groups overlap—Alice and Bob form one research pair, Bob and Charlie form another. I want to use the member sets as keys in my catalog.”

Timothy confidently tried to implement her request:

research_catalog = {}
group_one = {"Alice", "Bob"}
group_two = {"Bob", "Charlie"}

research_catalog[group_one] = "Quantum Physics Project"
# TypeError: unhashable type: 'set'

The system rejected his attempt. Margaret appeared and smiled knowingly. “You’ve discovered that regular sets can’t serve as dictionary keys. They’re mutable—you can add or remove members after creation. Remember the immutability rule?”

Timothy recalled his earlier lesson: only unchangeable objects could be dictionary keys. If a set could be modified after being used as a key, the entire hash table system would break.

The Frozen Alternative

Margaret led Timothy to a specialized vault labeled “Frozen Collections.” Inside were sets that had been permanently sealed—no additions, no removals, no modifications of any kind.

research_catalog = {}

# Create frozen sets - permanently immutable
group_one = frozenset({"Alice", "Bob"})
group_two = frozenset({"Bob", "Charlie"})

# These work as dictionary keys!
research_catalog[group_one] = "Quantum Physics Project"
research_catalog[group_two] = "Mathematics Collaboration"

print(research_catalog[group_one])  # "Quantum Physics Project"

The frozen sets looked and behaved like regular sets for all read operations, but they were locked forever. This immutability made them hashable and suitable as dictionary keys.

The Conversion Process

Timothy learned that converting between regular and frozen sets was straightforward:

# Regular set - mutable
active_members = {"Alice", "Bob", "Charlie"}
active_members.add("David")  # This works

# Convert to frozen - now immutable
permanent_members = frozenset(active_members)
permanent_members.add("Eve")  # AttributeError: frozenset has no 'add'

# Convert back to regular if needed
modifiable_again = set(permanent_members)
modifiable_again.add("Eve")  # This works

The frozen sets supported all the same operations as regular sets—union, intersection, difference—but rejected any attempt to modify their contents.

The Nested Collection Problem

Professor Williams revealed her true challenge: “I need sets of sets. Each research department contains multiple research groups.”

Timothy tried the obvious approach with regular sets:

quantum_dept = {
    {"Alice", "Bob"},
    {"Charlie", "David"}
}
# TypeError: unhashable type: 'set'

Sets can only contain hashable items, and regular sets aren’t hashable. Margaret showed the solution:

quantum_dept = {
    frozenset({"Alice", "Bob"}),
    frozenset({"Charlie", "David"}),
    frozenset({"Eve", "Frank"})
}

# Check if a specific group exists in the department
target_group = frozenset({"Alice", "Bob"})
group_exists = target_group in quantum_dept  # True - instant lookup

Frozen sets enabled hierarchical membership structures that would have been impossible with regular sets.

The Practical Applications

Timothy discovered several scenarios where frozen sets were essential:

Caching function results based on set inputs:

cache = {}

def analyze_group(members):
    frozen_members = frozenset(members)

    if frozen_members not in cache:
        # Expensive computation here
        result = len(frozen_members) * 10
        cache[frozen_members] = result

    return cache[frozen_members]

# Works with different argument orders
analyze_group(["Alice", "Bob"])  # Computes result
analyze_group(["Bob", "Alice"])  # Uses cached result - same frozenset

Tracking unique combinations:

seen_pairs = set()

def record_interaction(person_a, person_b):
    pair = frozenset({person_a, person_b})

    if pair in seen_pairs:
        return "Already recorded"

    seen_pairs.add(pair)
    return "New interaction recorded"

record_interaction("Alice", "Bob")  # "New interaction recorded"
record_interaction("Bob", "Alice")  # "Already recorded" - same pair

Graph edges as dictionary keys:

edge_weights = {}

# Store weights for undirected graph edges
edge_weights[frozenset({"Node A", "Node B"})] = 5.2
edge_weights[frozenset({"Node B", "Node C"})] = 3.1

# Retrieve weight regardless of node order
weight = edge_weights[frozenset({"B", "A"})]  # 5.2

The Operations Comparison

Margaret showed Timothy that frozen sets supported all read operations but rejected modifications:

regular_set = {"Alice", "Bob", "Charlie"}
frozen_set = frozenset({"Alice", "Bob", "Charlie"})

# Operations that work on both
common = regular_set & frozen_set  # Intersection works
combined = regular_set | frozen_set  # Union works
is_member = "Alice" in frozen_set  # Membership check works

# Operations only regular sets support
regular_set.add("David")  # Works
frozen_set.add("David")  # AttributeError

regular_set.remove("Alice")  # Works  
frozen_set.remove("Alice")  # AttributeError

The frozen sets provided all the power of set operations while guaranteeing immutability.

The Performance Reality

Timothy asked whether frozen sets were slower due to their immutability constraints. Margaret explained: “Frozen sets are actually slightly more efficient for operations. Because they can’t change, Python can cache their hash values permanently. Regular sets must recalculate hashes after modifications.”

# Frozen sets cache hash value on creation
frozen = frozenset(range(1000))
hash(frozen)  # Computed once
hash(frozen)  # Retrieved from cache - instant

# Regular sets can't be hashed at all
regular = set(range(1000))
hash(regular)  # TypeError: unhashable type: 'set'

This hash caching made frozen sets ideal for repeated dictionary lookups or set membership checks.

Timothy’s Frozen Set Wisdom

Through mastering frozen sets, Timothy learned key principles:

Use frozenset when immutability is required: Dictionary keys, set elements, or anywhere hashability is needed.

Convert freely between types: Use regular sets for building collections, freeze them when you need immutability.

Enable nested collections: Frozen sets allow sets of sets and other hierarchical structures.

Leverage hash caching: Frozen sets are optimized for repeated lookups.

Choose based on mutability needs: Regular sets for dynamic membership, frozen sets for fixed groups.

Timothy’s exploration of frozen sets revealed that immutability wasn’t a limitation—it was a feature that unlocked new capabilities. By making sets unchangeable, Python made them usable as dictionary keys and set elements, enabling elegant solutions to problems that would otherwise require complex workarounds.

The secret life of Python sets included this immutable variant, proving that sometimes the most powerful tool is the one that promises never to change.

Aaron Rose is a software engineer and technology writer at tech-reader.blog and the author of Think Like a Genius.


This content originally appeared on DEV Community and was authored by Aaron Rose