Frozenset
A frozenset is set's immutable twin — the same fast membership and set algebra, but hashable and safe to use as a dict key or set element.
A frozenset is an immutable version of a set: it supports every read-only set operation (membership, union, intersection, subset tests, and more) but has no add, remove, or other mutating method. Because it can't change after creation, a frozenset is hashable whenever its elements are, which lets it do something a plain set never can — serve as a dictionary key or live inside another set.
Creating a Frozenset
There is no frozenset literal syntax; it's built only with the frozenset() constructor, applied to any iterable.
fs = frozenset([1, 2, 3, 2])
print(fs) # frozenset({1, 2, 3})
empty = frozenset() # empty frozenset
from_set = frozenset({"a", "b"})Immutability
frozenset has no add(), remove(), discard(), pop(), or clear() — any attempt to call them raises AttributeError, since those methods simply don't exist on the type.
fs = frozenset([1, 2, 3])
fs.add(4) # AttributeError: 'frozenset' object has no attribute 'add'Hashability
Because a frozenset can't change after construction, it is hashable as long as every element it contains is itself hashable — which is exactly the property that lets it act as a dict key or live inside a regular set (something a mutable set can never do).
groups = {
frozenset(["admin", "editor"]): "elevated",
frozenset(["viewer"]): "read-only",
}
groups[frozenset(["admin", "editor"])] # "elevated"
s = {frozenset([1, 2]), frozenset([3, 4])} # a set OF frozensets -- valid
bad = {set([1, 2])} # TypeError: set is unhashableSet Operations Still Work
All the non-mutating set operations — union, intersection, difference, symmetric difference, subset/superset tests, membership — work identically on frozensets, and can mix freely with plain sets. The result type follows the left operand.
a = frozenset([1, 2, 3])
b = {2, 3, 4} # a plain set
a | b # frozenset({1, 2, 3, 4}) -- result is a frozenset
b | a # {1, 2, 3, 4} -- result is a plain set
a & b # frozenset({2, 3})
2 in a # TrueDifferences from set: Quick Reference
Feature set frozenset
Literal syntax {1, 2, 3} none -- must use frozenset(...)
Mutable yes no
add/remove/discard/pop yes no (AttributeError)
Hashable no yes (if elements are hashable)
Usable as dict key no yes
Usable as element of a set no yes
Union/intersection/etc. yes yes (read-only ops)Nested Set Use Cases
The main reason frozenset exists is to represent 'a set' in a context that itself requires hashable values: sets of sets, or dict keys that are naturally unordered groups (permission sets, tag combinations, graph edges as unordered vertex pairs).
# graph edges as unordered pairs, deduplicated automatically
edges = {frozenset([1, 2]), frozenset([2, 1]), frozenset([2, 3])}
print(len(edges)) # 2 -- {1,2} and {2,1} are the same frozenset
# memoizing a function keyed by an unordered set of arguments
cache: dict[frozenset[int], int] = {}
def cached_sum(nums: set[int]) -> int:
key = frozenset(nums)
if key not in cache:
cache[key] = sum(nums)
return cache[key]Practical Usage and When to Choose It
Reach for frozenset specifically when you need set semantics (uniqueness, fast membership, set algebra) combined with hashability or true immutability — as a dict key, as an element of another set, or simply to guarantee a collection of unique values can never be accidentally mutated after it's built.
VALID_ROLES = frozenset({"admin", "editor", "viewer"})
def check_role(role: str) -> bool:
return role in VALID_ROLES # fast, and VALID_ROLES can't be tampered withKey Takeaways
frozenset is the immutable counterpart of set: same read operations, no mutating methods.
It has no literal syntax — always built with frozenset(iterable).
It is hashable when its elements are, so it can be a dict key or live inside another set, unlike a plain set.
Union, intersection, difference, and membership tests all work the same as on a regular set.
Use it when you need an unordered, unique collection that must be hashable or must never change after creation.
fs.add(4) # AttributeError fs = fs | {4} # build a new frozenset instead frozenset has no mutating methods at all; any 'change' must produce a new frozenset via an operator or the constructor.
cache = {my_set: value} # TypeError: unhashable cache = {frozenset(my_set): value}
A plain set is unhashable and can't be a dict key; convert it to a frozenset first when the key needs to represent an unordered group.
Why can a frozenset be used as a dictionary key while a set cannot?
Dict keys must be hashable, and hashability requires the object's value to never change; a frozenset is immutable so it can compute and cache a stable hash, while a mutable set cannot.
How do you create an empty frozenset, and why is there no literal syntax for it?
With frozenset() (no arguments); there's no {} -style literal because {} is already reserved for an empty dict and set has no empty literal either -- both rely on their constructor for the empty case.
What happens if you call fs.add(x) on a frozenset fs?
AttributeError, because frozenset simply does not define an add method (or any other mutating method) -- it isn't a runtime-enforced immutability check, the method doesn't exist at all.
Give a practical example where you'd store frozensets as elements of another set.
Deduplicating undirected graph edges: representing each edge as frozenset({u, v}) makes edge (1,2) equal to edge (2,1), so storing them in a set of frozensets automatically merges duplicates.
If a = frozenset([1,2]) and b = {2,3}, what type does a | b return? What about b | a?
a | b returns a frozenset ({1,2,3}) because the left operand's type determines the result type; b | a returns a plain set with the same elements, because there b is on the left.
Write a function that groups words by their set of unique letters, using frozenset as the grouping key.
def group_by_letters(words: list[str]) -> dict[frozenset[str], list[str]]: groups: dict[frozenset[str], list[str]] = {} for word in words: key = frozenset(word.lower()) groups.setdefault(key, []).append(word) return groups result = group_by_letters(["tea", "eat", "ate", "bat"]) for key, words in result.items(): print(sorted(key), words)
'tea', 'eat', and 'ate' all share the letter set {'a','e','t'}, so they group together under one frozenset key; 'bat' gets its own group under {'a','b','t'}.
What does each line print?
True, then False, then True. a == b is True because frozensets compare by contents; a is b is False because they're two separate objects; hash(a) == hash(b) is True because equal frozensets are guaranteed to hash equally, which is what makes them safe as dict/set keys.
Write a function that returns True if a collection of tag-sets contains any exact duplicate tag-set, using frozensets.
def has_duplicate_tagset(tag_groups: list[set[str]]) -> bool: seen: set[frozenset[str]] = set() for tags in tag_groups: key = frozenset(tags) if key in seen: return True seen.add(key) return False print(has_duplicate_tagset([{"a", "b"}, {"c"}, {"b", "a"}])) # True
Converting each set of tags to a frozenset makes it hashable and order-independent, so {'a','b'} and {'b','a'} are recognized as the same tag-set when checked against the seen collection.
a = frozenset([1, 2, 3])
b = frozenset([1, 2, 3])
print(a == b)
print(a is b)
print(hash(a) == hash(b))