Python tutorials > Data Structures > Sets > What are frozen sets?

What are frozen sets?

frozenset is an immutable version of Python's set object. While you can modify a regular set by adding or removing elements, a frozenset remains constant after creation. This immutability makes frozensets suitable for use as dictionary keys or as elements of other sets, where mutability would cause problems. This tutorial will explore the characteristics, use cases, and advantages of frozensets.

Introduction to Frozensets

In Python, a set is a mutable, unordered collection of unique elements. Sometimes, you need a set-like object that cannot be changed. This is where frozenset comes in. It's an immutable set, meaning its elements cannot be added or removed after it's created. This immutability makes frozensets hashable, a crucial requirement for dictionary keys and elements of other sets.

Creating a Frozenset

You create a frozenset using the frozenset() constructor. You can pass any iterable (like a list, tuple, or set) to the constructor, which will then create a frozenset containing the unique elements from that iterable. Note that duplicate elements in the input iterable will be ignored, as with regular sets.

my_set = {1, 2, 3, 4}
my_frozenset = frozenset(my_set)
print(my_frozenset)

# Or directly from a list or tuple
my_frozenset2 = frozenset([5, 6, 7])
print(my_frozenset2)

my_frozenset3 = frozenset((8, 9, 10))
print(my_frozenset3)

Immutability in Action

Because frozenset is immutable, you cannot use methods like add(), remove(), or clear() to modify it. Attempting to do so will raise an AttributeError. This behavior is fundamentally different from mutable sets.

my_set = {1, 2, 3}
my_frozenset = frozenset(my_set)

# Attempting to add or remove elements will raise an error
# my_frozenset.add(4)  # This will raise an AttributeError

# my_set.add(4) #This is valid
# print(my_set)

Using Frozensets as Dictionary Keys

A key characteristic of dictionaries in Python is that keys must be hashable, which generally means they must be immutable. Since regular sets are mutable, they cannot be used as dictionary keys. However, frozenset, being immutable, perfectly fits this requirement. Trying to use a normal `set` as a dictionary key results in a `TypeError`. Note that order doesn't matter in a `frozenset` so `frozenset([1,2,3])` is equal to `frozenset([3,2,1])`.

data = {}
frozenset1 = frozenset([1, 2, 3])
frozenset2 = frozenset([3, 2, 1]) #order doesn't matter

data[frozenset1] = 'Value 1'
data[frozenset2] = 'Value 2'
print(data)
#This will print only one value because the frozensets are equal regardless of order of creation.  
for key in data:
    print(key)

set1 = {1, 2, 3}
#data[set1] = 'Value 3' # will cause a TypeError as sets are mutable and unhashable.

Set Operations with Frozensets

frozenset supports all the standard set operations (union, intersection, difference, symmetric difference) using operators like |, &, -, and ^, respectively. The results of these operations are new sets or frozensets, depending on the operands. When an operation involves a regular set and a frozenset, the result is a regular set.

set1 = {1, 2, 3}
frozenset1 = frozenset({3, 4, 5})

# Intersection
intersection = set1 & frozenset1
print(f'Intersection: {intersection}')

# Union
union = set1 | frozenset1
print(f'Union: {union}')

# Difference
difference = set1 - frozenset1
print(f'Difference: {difference}')

# Symmetric Difference
symmetric_difference = set1 ^ frozenset1
print(f'Symmetric Difference: {symmetric_difference}')

Concepts Behind the Snippet

The concept behind frozensets revolves around the principle of immutability. Immutability offers several benefits, including thread safety, simplified reasoning about code behavior, and the ability to use objects as dictionary keys or set elements. Frozensets leverage the underlying efficient set implementation but guarantee that their content will not change after creation, preventing unexpected side effects.

Real-Life Use Case Section

Consider a scenario where you're caching the results of a database query based on a set of criteria. The criteria themselves are represented as a set of values. To use these criteria as keys in a cache (e.g., a dictionary), you need an immutable representation of the criteria. A frozenset is perfect for this. Another example is representing the state of a chess board. A frozenset can be used to store locations of the chess pieces. This guarantees that the positions won't change.

Best Practices

Use frozensets when you need a set-like object that must not change after creation. This is especially important when using the object as a key in a dictionary or as an element of another set. Avoid unnecessary creation of frozensets if mutability is required. Use normal `set` objects in those cases. Understand performance implications as converting large sets to frozensets may take time.

Interview Tip

When discussing frozensets in an interview, highlight their immutability and the benefits it provides, particularly in scenarios involving dictionaries and sets. Be prepared to explain why mutability is problematic in these contexts and how frozensets address this issue. Also, explain the performance implication of creating a `frozenset` from a large `set`.

When to use them

Use frozensets when you need an immutable set. The most common scenario is using them as keys in dictionaries, or members of other sets. They are also useful when creating constant sets whose members should not change.

Memory footprint

The memory footprint of a frozenset is similar to that of a regular set. Both store unique elements and use hash tables for efficient lookups. The primary difference lies in the overhead associated with mutability. A regular set needs to maintain some extra metadata to allow for adding and removing elements, while a frozenset doesn't. However, this difference is usually negligible unless you're dealing with a very large number of sets. In general, immutable objects are slightly more space efficient since no memory has to be allocated to store the state information if it changes.

Alternatives

If you need a set-like object that can be modified after creation, use a regular set. If you need an immutable sequence, consider using a tuple. A tuple is ordered and can contain duplicate values, unlike sets, but it's also immutable and hashable.

Pros

  • Immutability: Guarantees that the set's contents will not change, preventing unexpected side effects.
  • Hashability: Can be used as dictionary keys or elements of other sets.
  • Thread safety: Immutable objects are inherently thread-safe, as their state cannot be modified concurrently.

Cons

  • Immutability: Cannot be modified after creation, which can be limiting in some scenarios.
  • Performance: Creating a frozenset from an existing mutable set involves copying all the elements, which can be slower than simply creating a mutable set.

FAQ

  • Can I convert a frozenset back to a regular set?

    Yes, you can convert a frozenset to a regular set using the set() constructor: my_frozenset = frozenset([1, 2, 3]); my_set = set(my_frozenset).
  • Are frozensets faster than regular sets for lookups?

    In general, lookup performance is very similar between frozensets and regular sets, as both use hash tables. The primary difference lies in the overhead associated with mutability.
  • Can a frozenset contain another frozenset?

    Yes, since frozensets are immutable and hashable, they can be elements of other sets, including other frozensets: frozenset({frozenset({1,2}), frozenset({3,4})})