Back to Blog
Python

Python frozenset Type: Syntax, Hashability, and Use

python frozenset type: Understand the Python frozenset type: its syntax, hashability, read-only set operations, memory behavior, and when to choose it over set or tuple.

frozensetpython-setshashable-typesdictionary-keysimmutability
An illustration of a frozenset as a locked container holding three distinct colored shapes, positioned beside a dictionary key icon to convey immutability and hashability.

The python frozenset type exists to provide the semantics of a set—unordered, unique elements with fast membership testing—without allowing mutation. A regular set supports in-place operations like add, remove, and discard, which makes it impossible to use as a dictionary key or as an element of another set. A frozenset removes those mutating operations, which makes it hashable and therefore usable wherever an immutable, hashable value is required.

The practical consequence is that frozenset is the only built-in set-like type that can participate in hash-based lookups. If you need to group data by a set of attributes, or cache results keyed by a collection of values, frozenset is the type that makes that possible without converting to a tuple or string first.

Creating a frozenset

A frozenset is created with the frozenset() constructor, which accepts any iterable. The constructor deduplicates elements and stores them in an unspecified order, exactly like a regular set.

empty = frozenset() letters = frozenset("abracadabra") numbers = frozenset([3, 1, 4, 1, 5, 9, 2, 6])

letters contains the characters {'a', 'b', 'c', 'd', 'r'}—the duplicates are removed. numbers contains {1, 2, 3, 4, 5, 6, 9}. The iteration order is not guaranteed and should never be relied upon, just as with set.

There is no literal syntax for frozenset. You cannot write {1, 2, 3} and get a frozenset; that syntax always produces a set. The constructor is the only way to create one, which means a frozenset is always built from an existing iterable.

Hashability and Use in Dictionaries

The defining difference between set and frozenset is hashability. A set is unhashable because its contents can change after it is created, which would invalidate any hash computed before the mutation. A frozenset cannot change, so Python can compute a stable hash from its elements.

frozen = frozenset(["x", "y", "z"]) mapping = {frozen: "group A"} print(mapping[frozen]) # group A regular = {"x", "y", "z"} # mapping[regular] would raise TypeError: unhashable type: 'set'

The same property allows a frozenset to be an element of another set:

sets = {frozenset([1, 2]), frozenset([3, 4])} print(len(sets)) # 2

This is the primary reason the type exists. Any algorithm that needs to key data by a collection of unique values—such as grouping records by a set of tags, or memoizing a function whose arguments are unordered collections—can use frozenset directly.

The hash of a frozenset is computed from the hashes of its elements. This means all elements must themselves be hashable. A frozenset containing a list raises TypeError at construction time, not at hash time:

frozenset([[1, 2], [3, 4]]) # TypeError: unhashable type: 'list'

Nested frozenset values are allowed, since they are hashable, so deeply nested immutable set structures are possible.

Set Operations That Work on frozenset

All read-only set operations are available on frozenset. These include union, intersection, difference, symmetric_difference, and their operator equivalents. The result is always a new frozenset, never a mutation of the original.

a = frozenset([1, 2, 3]) b = frozenset([3, 4, 5]) print(a | b) # frozenset({1, 2, 3, 4, 5}) print(a & b) # frozenset({3}) print(a - b) # frozenset({1, 2}) print(a ^ b) # frozenset({1, 2, 4, 5}) print(a.isdisjoint(b)) # False print(a.issubset(b)) # False print(a.issuperset(frozenset([1, 2]))) # True

The comparison operators (==, !=, <, <=, >, >=) also work. A frozenset and a set with the same elements compare equal, because equality for sets is defined by membership, not by type:

print(frozenset([1, 2]) == {1, 2}) # True

This can be useful when mixing the two types in comparisons, but it also means you cannot distinguish a frozenset from a set using ==. Use isinstance(value, frozenset) when the distinction matters.

Operations That Are Not Available

Because a frozenset is immutable, every mutating method from set is absent. Attempting to call add, remove, discard, pop, clear, or any in-place update method raises AttributeError:

frozen = frozenset([1, 2, 3]) frozen.add(4) # AttributeError: 'frozenset' object has no attribute 'add'

This is not a runtime guard; the methods simply do not exist on the type. The design is intentional: the absence of mutation is what makes the type hashable, and Python enforces this at the type level rather than at the call level.

To create a modified version, build a new frozenset from the result of a set operation:

frozen = frozenset([1, 2, 3]) extended = frozen | frozenset([4, 5]) reduced = frozen - frozenset([2])

Each operation allocates a new object. If you need to apply many changes in a loop, it is often cheaper to convert to a regular set, mutate it, and convert back once:

working = set(frozen) working.add(4) working.remove(1) frozen = frozenset(working)

This avoids repeated allocation of intermediate frozenset objects.

Memory and Performance Characteristics

A frozenset has the same underlying hash-table storage as a set. Membership testing is O(1) on average, and the same hash-table resizing behavior applies. The main performance cost is the hash computation: the hash of a frozenset is computed by combining the hashes of its elements, which is O(n) for a set of n elements.

This has a practical consequence for dictionary lookups. When a frozenset is used as a dictionary key, every lookup must recompute the hash of the key. For small sets this is negligible, but for large sets the cost grows linearly with the number of elements. If the key set is large and lookups are frequent, consider whether a tuple of sorted elements would be cheaper to hash, or whether the set is small enough that the difference does not matter.

Memory usage is similar to a set of the same size, with a small additional overhead for the type object. The immutability does not reduce memory consumption; it only changes what operations are permitted.

One important detail is that a frozenset cannot be pickled more efficiently than a set, and there is no compact binary representation that saves space. If memory is the constraint, a tuple or a bytes value may be a better choice for storing the same data.

Practical Use Cases

The most common use of frozenset is as a dictionary key for a collection of unordered values. Consider grouping records by a set of tags:

from collections import defaultdict records = [ ("alpha", {"fast", "stable"}), ("beta", {"stable", "fast"}), ("gamma", {"slow", "stable"}), ] groups = defaultdict(list) for name, tags in records: groups[frozenset(tags)].append(name) print(dict(groups)) # {frozenset({'fast', 'stable'}): ['alpha', 'beta'], # frozenset({'slow', 'stable'}): ['gamma']}

Because frozenset ignores element order, {"fast", "stable"} and {"stable", "fast"} map to the same key. A tuple would not give this behavior unless you sorted the elements first.

Another use is memoization for functions that accept unordered collections:

def count_common(left, right): return len(frozenset(left) & frozenset(right)) cache = {} def cached_count_common(left, right): key = (frozenset(left), frozenset(right)) if key not in cache: cache[key] = count_common(left, right) return cache[key]

The frozenset values serve as a stable cache key regardless of the order of the input lists.

frozenset is also useful for representing a fixed set of allowed values in configuration or validation logic. Because it cannot be mutated, accidental modification by another part of the program is impossible:

VALID_STATUSES = frozenset({"pending", "active", "closed"}) def validate_status(status): return status in VALID_STATUSES

This gives the same membership-testing speed as a set while preventing accidental modification of the constant.

Choosing Between frozenset, set, and tuple

The decision between these three types depends on what you need to do with the data.

Use set when you need to mutate the collection during its lifetime, such as accumulating unique values while processing a stream.

Use tuple when the order of elements matters, or when you need to store duplicate values, or when you need the lowest possible memory overhead for a fixed sequence.

Use frozenset when you need set semantics—unordered, unique elements with fast membership testing—and the value must be hashable. The two concrete triggers are: using the collection as a dictionary key, or using it as an element of another set.

There is one more consideration. A frozenset is not always the best hashable representation of a collection. If the order of elements is meaningful, a tuple is the correct choice. If the elements are strings and the set is large, a frozenset of strings hashes more slowly than a tuple of the same strings, because the set hash combines element hashes in a way that is sensitive to the number of elements. Measure the actual cost if the key is large and lookups dominate the workload.

A final edge case: frozenset is not a subclass of set, and set is not a subclass of frozenset. Code that checks isinstance(value, set) will not accept a frozenset. If your code needs to accept either type, check isinstance(value, (set, frozenset)), or rely on duck typing and use only the read-only operations that both types support.

python frozenset type: Practical Usage and Code Examples | RYUSLOG DEV