Python frozenset() is an immutable version of the built-in set. Once created, a frozenset cannot be modified — elements cannot be added or removed. This immutability makes frozenset hashable, which means it can be used as a dictionary key, stored inside another set, or used anywhere Python expects a hashable object.

Regular sets are mutable. They have methods like add(), remove(), and update() that change the set in place. frozenset has none of these mutation methods. The tradeoff is that frozenset gains hashability — the one thing regular sets lack. Understanding when to use each one comes down to whether the collection needs to change after creation.

TLDR

  • frozenset is an immutable, hashable version of a set
  • Use it as a dictionary key, or as an element inside another set
  • All set operations that return a new object work on frozenset
  • Mutation methods like add(), remove(), and update() are absent
  • Elements must be hashable — lists and dicts cannot be placed inside a frozenset

What is a frozenset?

A frozenset is essentially a set with one constraint lifted — mutability — and one property gained: hashability. The Python documentation describes it as “an immutable version of a set.” All set operations that produce a new object work unchanged. The only difference is that anything attempting to mutate the frozenset in place raises an error.

Here is the most direct illustration of the difference:


my_set = {1, 2, 3}
my_set.add(4)
print(my_set)

my_frozenset = frozenset([1, 2, 3])
my_frozenset.add(4)

The first block prints {1, 2, 3, 4}. The second block raises AttributeError because frozenset has no add() method.

Creating frozensets

frozenset() accepts any iterable. Python iterates over the input, extracts unique elements, and builds the frozenset. Duplicate values are discarded automatically.

From a list:


numbers = frozenset([1, 2, 3, 2, 1])
print(numbers)

Output: frozenset({1, 2, 3}). The duplicates 2 and 1 appear only once.

From a string:


letters = frozenset("abracadabra")
print(letters)

Output: frozenset({'a', 'b', 'r', 'c', 'd'}). The set constructor deduplicates characters automatically.

From a tuple:


data = frozenset((10, 20, 30, 20, 10))
print(data)

Output: frozenset({10, 20, 30}).

From an existing set or frozenset:


original = {1, 2, 3}
derived = frozenset(original)
print(derived)

Output: frozenset({1, 2, 3}). Passing one frozenset to frozenset() directly also works, though it is less common.

frozenset always produces a flat collection. If nested objects are passed, Python attempts to hash them. Unhashable types like lists raise a TypeError:


try:
    bad = frozenset([[1, 2], [3, 4]])
except TypeError as e:
    print(f"Caught: {e}")

Output: Caught: unhashable type: 'list'. Lists cannot be placed inside a frozenset because they are mutable.

frozenset Methods

frozenset supports the read and comparison methods of regular sets. All mutation methods are absent. Here is a complete reference.

copy()

Returns a shallow copy of the frozenset. Since frozensets are immutable, the copy is not strictly necessary for safety, but it is useful when passing a frozenset to a function that might hold onto the reference and clarity is desired.


original = frozenset([1, 2, 3])
copied = original.copy()
print(copied)
print(original is copied)

Output: frozenset({1, 2, 3}) and False. The copy is a distinct object.

difference()

Returns a new frozenset containing elements that exist in the first set but not in the others. Equivalent to the - operator.


a = frozenset([1, 2, 3, 4])
b = frozenset([3, 4, 5])

result = a.difference(b)
print(result)
print(a - b)

Both lines output frozenset({1, 2}). The original frozensets are never modified.

Multiple iterables can be passed to difference():


a = frozenset([1, 2, 3, 4, 5])
b = [3, 4]
c = [5, 6]

result = a.difference(b, c)
print(result)

Output: frozenset({1, 2}). Elements 3, 4, and 5 are all found in the other collections.

intersection()

Returns a new frozenset with elements that appear in all the iterables. Equivalent to the & operator.


a = frozenset([1, 2, 3, 4])
b = frozenset([3, 4, 5])

result = a.intersection(b)
print(result)
print(a & b)

Both lines output frozenset({3, 4}).

With multiple arguments:


a = frozenset([1, 2, 3, 4])
b = [2, 3, 5]
c = [2, 4, 6]

result = a.intersection(b, c)
print(result)

Output: frozenset({2}). Element 2 is the only one present in all three iterables.

union()

Returns a new frozenset with all elements from the frozenset and all iterables. Equivalent to the | operator.


a = frozenset([1, 2, 3])
b = [3, 4, 5]

result = a.union(b)
print(result)
print(a | frozenset(b))

Both lines output frozenset({1, 2, 3, 4, 5}). The | operator requires both operands to be sets or frozensets, so b must be wrapped in frozenset() when using |.

symmetric_difference()

Returns a new frozenset with elements that appear in either set but not in both. Equivalent to the ^ operator.


a = frozenset([1, 2, 3, 4])
b = frozenset([3, 4, 5, 6])

result = a.symmetric_difference(b)
print(result)
print(a ^ b)

Both lines output frozenset({1, 2, 5, 6}). Elements 3 and 4 are excluded because they appear in both sets.

issubset()

Returns True if the frozenset is a subset of another set or iterable. Every element in the frozenset must exist in the other iterable for this to return True.


a = frozenset([1, 2])
b = frozenset([1, 2, 3, 4])

print(a.issubset(b))
print(b.issubset(a))

Output: True and False. Lists and tuples also work with issubset():


a = frozenset([1, 2])
print(a.issubset([1, 2, 3, 4]))

Output: True.

issuperset()

Returns True if the frozenset contains all elements of another set or iterable. This is the inverse of issubset().


a = frozenset([1, 2, 3, 4])
b = frozenset([2, 4])

print(a.issuperset(b))
print(b.issuperset(a))

Output: True and False. frozenset a contains everything in b, but b does not contain everything in a.

isdisjoint()

Returns True if the frozenset and another iterable share no common elements.


evens = frozenset([2, 4, 6, 8])
odds = frozenset([1, 3, 5, 7])
primes = frozenset([2, 3, 5, 7])

print(evens.isdisjoint(odds))
print(evens.isdisjoint(primes))

Output: True and False. evens and odds have no overlap, so they are disjoint. evens and primes share 2, so they are not disjoint.

When to Use frozenset vs Regular set

The choice comes down to whether the collection needs to change after creation. Here are the situations where frozenset is the clear choice.

As a dictionary key

Regular sets cannot be dictionary keys because they are mutable. frozenset is hashable, so it works as a dict key directly. This is the most common reason to reach for frozenset.


from collections import defaultdict

tags_by_article = {}
articles = [
    {"python", "backend", "api"},
    {"python", "data", "ml"},
    {"java", "backend", "api"},
]

for article in articles:
    tags = frozenset(article)
    if tags in tags_by_article:
        tags_by_article += 1
    else:
        tags_by_article = 1

print(tags_by_article)

This counts articles sharing the same set of tags. The frozenset serves as a perfectly valid dict key because it is hashable.

As an element inside another set

Sets cannot contain mutable objects, which means regular sets cannot contain other regular sets. frozenset is immutable, so it can be placed inside a set.


visited_paths = set()
path1 = frozenset(["a", "b", "c"])
path2 = frozenset(["a", "b"])
path3 = frozenset(["a", "b", "c"])

visited_paths.add(path1)
visited_paths.add(path2)
visited_paths.add(path3)

print(len(visited_paths))

Output: 2. path1 and path3 are identical, so the set stores only one of them.

As a signaling mechanism

Passing a frozenset to a function communicates that the collection should not change. A regular set could have .add() called on it by the receiving function, modifying the caller’s data. frozenset prevents this entirely.


def process_tags(tags):
    for tag in tags:
        print(tag)

tags = frozenset(["python", "backend"])
process_tags(tags)

The frozenset makes it unambiguous that process_tags should not modify the tags collection.

For everything else, a regular set is more convenient. The ability to use add(), remove(), discard(), update(), and other mutation methods makes regular sets the better choice when building or modifying a collection is needed.

FAQ

Q: Can a regular set be placed inside a frozenset?

No. Regular sets are mutable and therefore not hashable. Since frozenset requires all its elements to be hashable, a regular set cannot be placed inside a frozenset.

Q: Can a frozenset be converted back to a regular set?

Yes. Passing the frozenset to set() produces a regular mutable set.


fs = frozenset([1, 2, 3])
regular = set(fs)
regular.add(4)
print(regular)

Output: {1, 2, 3, 4}.

Q: Is frozenset safe to use in multi-threaded code?

frozenset itself cannot be modified after creation, so there are no race conditions related to mutating it. The objects inside the frozenset can still be modified if they are mutable. For most practical purposes, frozenset behaves safely in multi-threaded code.

Q: Does frozenset maintain element ordering?

No. Like regular sets, frozenset does not maintain any ordering of elements.

Q: What is the performance difference between frozenset and regular set?

The overhead is minimal for most use cases. frozenset is slightly faster for membership testing because the hash values of the elements never change. The real performance consideration is not frozenset vs set but whether mutability is needed at all.

frozenset occupies a specific niche in Python’s type system — immutable, hashable, set-like. It is not a replacement for regular sets but a complement for situations where immutability and hashability are required.

Last updated: 2026-04-24

Share.
Leave A Reply