Python - Frozen Set with Examples
A frozen set is an immutable set in Python created using the `frozenset()` built-in function. Unlike regular sets, once created, you cannot add, remove, or modify elements. This immutability makes...
Key Insights
- Frozen sets are immutable, hashable versions of regular sets that can be used as dictionary keys or elements of other sets, enabling complex data structures impossible with mutable sets
- They provide the same O(1) membership testing and set operations as regular sets while guaranteeing thread-safety and preventing accidental modifications in production code
- Frozen sets consume less memory than regular sets and offer performance advantages in scenarios requiring repeated hash computations or set comparisons
What is a Frozen Set
A frozen set is an immutable set in Python created using the frozenset() built-in function. Unlike regular sets, once created, you cannot add, remove, or modify elements. This immutability makes frozen sets hashable, allowing them to serve as dictionary keys or elements within other sets.
# Creating frozen sets
fs1 = frozenset([1, 2, 3, 4, 5])
fs2 = frozenset(['a', 'b', 'c'])
fs3 = frozenset() # empty frozen set
# From other iterables
fs4 = frozenset("hello") # frozenset({'h', 'e', 'l', 'o'})
fs5 = frozenset(range(5)) # frozenset({0, 1, 2, 3, 4})
print(fs1) # frozenset({1, 2, 3, 4, 5})
print(fs4) # frozenset({'h', 'e', 'l', 'o'})
Immutability and Hashability
The defining characteristic of frozen sets is immutability. This enables their use in contexts where regular sets fail.
# Regular sets are not hashable
regular_set = {1, 2, 3}
try:
hash(regular_set)
except TypeError as e:
print(f"Error: {e}") # Error: unhashable type: 'set'
# Frozen sets are hashable
frozen = frozenset([1, 2, 3])
print(hash(frozen)) # Returns a hash value, e.g., -7558908597238377754
# Using frozen sets as dictionary keys
cache = {
frozenset([1, 2, 3]): "result_1",
frozenset([4, 5, 6]): "result_2"
}
print(cache[frozenset([1, 2, 3])]) # result_1
# Nested sets using frozen sets
nested = {frozenset([1, 2]), frozenset([3, 4]), frozenset([5, 6])}
print(nested) # {frozenset({1, 2}), frozenset({3, 4}), frozenset({5, 6})}
Set Operations
Frozen sets support all standard set operations: union, intersection, difference, and symmetric difference. These operations return new frozen sets without modifying the originals.
fs1 = frozenset([1, 2, 3, 4])
fs2 = frozenset([3, 4, 5, 6])
# Union
union = fs1 | fs2
print(union) # frozenset({1, 2, 3, 4, 5, 6})
# Intersection
intersection = fs1 & fs2
print(intersection) # frozenset({3, 4})
# Difference
difference = fs1 - fs2
print(difference) # frozenset({1, 2})
# Symmetric difference
sym_diff = fs1 ^ fs2
print(sym_diff) # frozenset({1, 2, 5, 6})
# Method-based operations
print(fs1.union(fs2)) # frozenset({1, 2, 3, 4, 5, 6})
print(fs1.intersection(fs2)) # frozenset({3, 4})
print(fs1.difference(fs2)) # frozenset({1, 2})
print(fs1.symmetric_difference(fs2)) # frozenset({1, 2, 5, 6})
Membership Testing and Comparisons
Frozen sets provide O(1) average-case membership testing and support subset/superset comparisons.
fs = frozenset([1, 2, 3, 4, 5])
# Membership testing
print(3 in fs) # True
print(10 in fs) # False
# Subset and superset checks
fs1 = frozenset([1, 2, 3])
fs2 = frozenset([1, 2, 3, 4, 5])
print(fs1.issubset(fs2)) # True
print(fs1 <= fs2) # True
print(fs2.issuperset(fs1)) # True
print(fs2 >= fs1) # True
# Disjoint sets
fs3 = frozenset([6, 7, 8])
print(fs1.isdisjoint(fs3)) # True
Practical Use Case: Graph Algorithms
Frozen sets excel in graph representations where edges need to be stored in sets or used as dictionary keys.
class Graph:
def __init__(self):
self.edges = {}
self.edge_weights = {}
def add_edge(self, node1, node2, weight=1):
# Use frozen set as key for undirected edges
edge = frozenset([node1, node2])
if node1 not in self.edges:
self.edges[node1] = set()
if node2 not in self.edges:
self.edges[node2] = set()
self.edges[node1].add(node2)
self.edges[node2].add(node1)
self.edge_weights[edge] = weight
def get_weight(self, node1, node2):
edge = frozenset([node1, node2])
return self.edge_weights.get(edge)
def find_triangles(self):
"""Find all triangles in the graph"""
triangles = set()
for node in self.edges:
neighbors = self.edges[node]
for n1 in neighbors:
for n2 in neighbors:
if n1 != n2 and n2 in self.edges.get(n1, set()):
triangle = frozenset([node, n1, n2])
triangles.add(triangle)
return triangles
# Usage
g = Graph()
g.add_edge('A', 'B', 5)
g.add_edge('B', 'C', 3)
g.add_edge('A', 'C', 7)
g.add_edge('C', 'D', 2)
print(g.get_weight('A', 'B')) # 5
print(g.get_weight('B', 'A')) # 5 (same edge)
triangles = g.find_triangles()
print(triangles) # {frozenset({'A', 'B', 'C'})}
Memoization with Frozen Sets
Frozen sets enable efficient memoization when function arguments include collections.
from functools import lru_cache
@lru_cache(maxsize=128)
def subset_sum(numbers, target):
"""
Find if any subset of numbers sums to target.
numbers must be a frozenset for caching.
"""
if target == 0:
return True
if not numbers or target < 0:
return False
# Try including or excluding first element
num = next(iter(numbers))
remaining = frozenset(numbers - {num})
return (subset_sum(remaining, target - num) or
subset_sum(remaining, target))
# Usage
nums = frozenset([3, 5, 7, 11, 13])
print(subset_sum(nums, 18)) # True (5 + 13 or 7 + 11)
print(subset_sum(nums, 4)) # False
# Cache info shows memoization working
print(subset_sum.cache_info())
Configuration Management
Frozen sets provide immutable configuration sets that prevent accidental modifications.
class FeatureFlags:
def __init__(self, enabled_features):
self._enabled = frozenset(enabled_features)
@property
def enabled_features(self):
return self._enabled
def is_enabled(self, feature):
return feature in self._enabled
def with_feature(self, feature):
"""Return new instance with additional feature"""
return FeatureFlags(self._enabled | {feature})
def without_feature(self, feature):
"""Return new instance without feature"""
return FeatureFlags(self._enabled - {feature})
# Usage
base_config = FeatureFlags(['api_v2', 'caching', 'logging'])
print(base_config.is_enabled('api_v2')) # True
print(base_config.is_enabled('analytics')) # False
# Create variant configurations
premium_config = base_config.with_feature('analytics')
print(premium_config.is_enabled('analytics')) # True
print(base_config.is_enabled('analytics')) # False (original unchanged)
# Multiple environments
dev_features = frozenset(['debug_mode', 'verbose_logging', 'hot_reload'])
prod_features = frozenset(['caching', 'compression', 'cdn'])
configs = {
'development': FeatureFlags(dev_features),
'production': FeatureFlags(prod_features)
}
Performance Considerations
Frozen sets offer performance benefits in specific scenarios due to their immutability.
import sys
import time
# Memory comparison
regular_set = {1, 2, 3, 4, 5}
frozen = frozenset([1, 2, 3, 4, 5])
print(f"Regular set size: {sys.getsizeof(regular_set)} bytes")
print(f"Frozen set size: {sys.getsizeof(frozen)} bytes")
# Hash computation performance
large_frozen = frozenset(range(10000))
start = time.perf_counter()
for _ in range(100000):
hash(large_frozen)
end = time.perf_counter()
print(f"Hash computation (cached): {end - start:.4f}s")
# Set operations on frozen sets
fs1 = frozenset(range(1000))
fs2 = frozenset(range(500, 1500))
start = time.perf_counter()
for _ in range(10000):
_ = fs1 & fs2
end = time.perf_counter()
print(f"Intersection operations: {end - start:.4f}s")
Converting Between Sets and Frozen Sets
Conversion between mutable and immutable sets is straightforward but creates new objects.
# Set to frozen set
mutable = {1, 2, 3, 4, 5}
immutable = frozenset(mutable)
# Frozen set to set
frozen = frozenset([1, 2, 3])
mutable_copy = set(frozen)
# Modifying the mutable copy doesn't affect frozen set
mutable_copy.add(4)
print(frozen) # frozenset({1, 2, 3})
print(mutable_copy) # {1, 2, 3, 4}
# Working with nested structures
nested_sets = [
frozenset([1, 2]),
frozenset([3, 4]),
frozenset([5, 6])
]
# Convert all to regular sets
mutable_nested = [set(fs) for fs in nested_sets]
print(mutable_nested) # [{1, 2}, {3, 4}, {5, 6}]
Frozen sets are essential when you need immutable, hashable set collections. Use them for dictionary keys, set elements, configuration management, and any scenario requiring guaranteed immutability. The performance characteristics and thread-safety make them ideal for concurrent applications and cached computations.