Back to Blog
Python

Python set vs frozenset: Key Differences and Use Cases

python set vs frozenset: Compare Python set and frozenset: mutability, hashability, performance, and when to use each for keys, caching, and set operations.

setfrozensetimmutabilityhashabledata structures
Comparison of mutable set and immutable frozenset in Python, with a key symbol representing hashability.

When deciding between python set vs frozenset, the core difference is mutability: a set can be changed after creation, while a frozenset cannot. That single distinction affects hashability, dictionary keys, and where each type can be used. This article explains the practical consequences and helps you choose the right type for your data.

set and frozenset at a Glance

Both set and frozenset are built-in Python types that store an unordered collection of unique, hashable elements. They share the same underlying hash-table design, so membership tests, unions, intersections, and differences work the same way. The critical difference is that set is mutable—you can add or remove elements—while frozenset is immutable and therefore hashable.

s = {1, 2, 3} s.add(4) # works fs = frozenset([1, 2, 3]) fs.add(4) # AttributeError: 'frozenset' object has no attribute 'add'

The frozenset constructor accepts any iterable, just like set. Once created, the contents cannot change. This immutability is not a limitation but a feature that enables specific use cases, such as using a frozenset as a dictionary key or as an element of another set.

Mutability and Hashability

Python requires dictionary keys and set elements to be hashable. An object is hashable if it has a stable hash value that never changes during its lifetime. Because a set is mutable, its hash would change if its contents changed, so set is not hashable and cannot be used as a dictionary key. A frozenset is immutable, so its hash is stable and it can be used in places that require hashable objects.

# This fails key = {1, 2, 3} d = {key: "value"} # TypeError: unhashable type: 'set' # This works key = frozenset({1, 2, 3}) d = {key: "value"}

The hash of a frozenset is computed from its elements. Because the elements themselves must be hashable, a frozenset can contain other frozensets, but not sets. This makes frozenset a natural choice for representing immutable collections that need to be used as keys or stored in other sets.

When to Use frozenset as a Dictionary Key

A common scenario is caching results based on a set of parameters. Suppose you have a function that depends on a collection of configuration flags. Using a frozenset as a dictionary key lets you look up cached results without worrying about the order of the flags or about accidental mutation.

cache = {} def compute(flags): key = frozenset(flags) if key not in cache: # expensive computation cache[key] = sum(flags) return cache[key] print(compute([1, 2, 3])) # 6 print(compute({3, 2, 1})) # cached, returns 6

Because frozenset is hashable and order-independent, it is ideal for representing a set of options that should be treated as an immutable unit. If you used a regular set, you would have to convert it to a frozenset anyway to use it as a key, so using frozenset from the start avoids extra conversions.

Performance and Memory Considerations

Both set and frozenset use the same underlying hash table, so membership tests and set operations have the same average time complexity: O(1) for membership, O(n) for building the collection, and O(n) for operations like union or intersection. The performance difference is negligible for most workloads.

However, there are memory and runtime implications when you need to modify a collection frequently. A set allows in-place updates, which avoid creating new objects. A frozenset requires building a new frozenset each time you want to add or remove an element, which allocates a new hash table and copies the existing elements.

fs = frozenset(range(1000)) # Adding one element creates a whole new frozenset fs2 = fs | {1000}

If you frequently change the contents, a regular set is more efficient because it mutates in place. If the collection is static or used as a key, frozenset avoids the overhead of accidental modification and provides a stable hash. The choice is driven by how the data is used, not by raw speed.

Set Operations: Same API, Different Constraints

Both types support the same set operations: union, intersection, difference, symmetric difference, and subset/superset tests. You can mix sets and frozensets in these operations, and the result type depends on the operands. When you combine a set and a frozenset, the result is a set if either operand is a set; otherwise it is a frozenset.

a = {1, 2, 3} b = frozenset([3, 4, 5]) print(a | b) # {1, 2, 3, 4, 5} -> set print(b | a) # frozenset({1, 2, 3, 4, 5})? Actually, the result type is set if either is set.

Wait, the rule is: the result is a set if either operand is a set. So a | b returns a set, and b | a also returns a set. If both are frozensets, the result is a frozenset. This matters if you need the result to be immutable for later use as a key.

fs1 = frozenset([1, 2]) fs2 = frozenset([2, 3]) result = fs1 | fs2 # frozenset({1, 2, 3})

If you need a frozenset result from a mixed operation, you must explicitly convert: frozenset(a | b). This is a common edge case when building immutable configuration sets.

Common Pitfalls and Edge Cases

One pitfall is assuming that frozenset is always more memory-efficient. Because it is immutable, Python can sometimes reuse memory or optimize storage, but that is not guaranteed. The real benefit is safety: a frozenset cannot be accidentally modified, which prevents subtle bugs in code that expects a collection to remain unchanged.

Another edge case is that frozenset elements must themselves be hashable. If you try to create a frozenset containing a list, you get a TypeError. This is the same requirement as for set, but it becomes more visible when you try to use a frozenset as a key and the inner elements are unhashable.

# This fails try: frozenset([[1, 2]]) except TypeError as e: print(e) # unhashable type: 'list'

When working with nested immutable collections, you can use frozenset of frozensets to represent a set of sets, which is impossible with regular sets because they are not hashable.

Choosing Between set and frozenset

Use a set when you need to modify the collection during its lifetime: adding, removing, or updating elements. Use a frozenset when the collection is fixed, when you need to use it as a dictionary key or as an element of another set, or when you want to enforce immutability to prevent accidental changes.

A practical decision rule: if the data is a configuration constant, a set of allowed values, or a collection that should never change after initialization, choose frozenset. If the data is a working set that you build and modify as you process input, choose set. For example, a set of visited nodes in a graph traversal should be a mutable set because you add nodes as you go. A set of valid flags for a function should be a frozenset because it is fixed and may be used as a key in a dispatch table.

The choice also affects API design. When you write a function that accepts a collection and does not modify it, accepting a frozenset communicates that the function will not mutate the input. This can make the contract clearer and prevent accidental side effects in large codebases.

In performance-sensitive code, the difference is rarely the deciding factor. The overhead of copying a frozenset when you need to change it is only relevant if you do it in a tight loop. In most applications, correctness and clarity outweigh the micro-optimization. Choose the type that matches the intended mutability of the data, and you will avoid a class of bugs related to unexpected modification.

Frozenset also plays well with functional programming patterns where data is treated as immutable. If you are building a pipeline that transforms collections, using frozenset for intermediate results can help you reason about the flow without worrying about side effects. The hashability of frozenset makes it a natural fit for memoization and caching, as shown earlier.

One final consideration: when you serialize data, frozenset and set serialize differently in formats like JSON. JSON has no native set type, so both become lists. But if you use a custom serializer, frozenset can be treated as a tuple-like immutable structure, which may be more convenient for round-tripping. This is a minor point, but it can affect how you store and retrieve data in distributed systems.

Ultimately, the decision between set and frozenset is about the contract you want to establish with the data. A set says "this may change"; a frozenset says "this is fixed." By choosing the right type, you make your code more predictable and easier to maintain.

python set vs frozenset: Practical Usage and Code Examples | RYUSLOG DEV