Python dictionaries are one of the most commonly used data structures in application code.
They are also mutable.
That is useful when data needs to change, but it can become a problem when a dictionary represents configuration, application metadata, fixed mappings, or shared state that should never be modified.
Python 3.15 introduces a built-in frozendict type for this purpose. Unlike a normal dict, a frozendict cannot be modified after it is created. It is also hashable when all of its keys and values are hashable.
This gives Python developers a standard immutable mapping without needing a third-party package or a custom wrapper.
But that does not mean every dictionary should become a frozendict.
The important question is: when does an immutable dictionary actually make sense?
What Is frozendict?
A normal dictionary is mutable:
config = {
"host": "localhost",
"port": 5432
}
config["port"] = 5433
print(config)
The dictionary can be changed at any time.
A frozendict works differently:
config = frozendict(
host="localhost",
port=5432
)
print(config)
Trying to modify it raises an error:
config["port"] = 5433
The operation fails because frozendict does not support item assignment.
You can also create one from an existing dictionary:
settings = {
"host": "localhost",
"port": 5432,
"debug": False
}
frozen_settings = frozendict(settings)
print(frozen_settings)
The resulting object is immutable.
Why Was frozendict Added?
Python already had ways to create read-only views of dictionaries, such as types.MappingProxyType.
However, a mapping proxy and a frozen dictionary have different semantics.
A mapping proxy is a read-only view of another dictionary. If the original dictionary changes, the proxy can reflect those changes.
A frozendict is an immutable dictionary object in its own right.
Python 3.15 documents frozendict as a built-in mapping type. It is not a subclass of dict; it inherits directly from object.
That distinction matters when writing APIs and type checks.
Creating a frozendict
There are several straightforward ways to create one.
Using Keyword Arguments
server = frozendict(
host="api.example.com",
port=443,
secure=True
)
From a Dictionary
server_config = {
"host": "api.example.com",
"port": 443,
"secure": True
}
server = frozendict(server_config)
From Key-Value Pairs
server = frozendict([
("host", "api.example.com"),
("port", 443),
("secure", True)
])
The constructor accepts a mapping or an iterable of key-value pairs, along with keyword arguments.
The Main Difference: Mutation
With dict, these operations are normal:
settings = {
"timeout": 30,
"retries": 3
}
settings["timeout"] = 60
settings["debug"] = True
settings.pop("retries")
With frozendict, mutation operations are not available.
For example:
settings = frozendict(
timeout=30,
retries=3
)
settings["timeout"] = 60
This raises:
TypeError: 'frozendict' object does not support item assignment
Several mutating dictionary methods are intentionally absent, including:
__setitem__()
__delitem__()
clear()
pop()
popitem()
setdefault()
update()
The Python documentation explicitly lists these differences from dict.
This makes the intended contract clear:
Once created, this mapping should not change.
frozendict Can Be Hashable
One of the most useful differences is that a frozendict can be hashed when all of its keys and values are hashable.
For example:
database_config = frozendict(
host="localhost",
port=5432
)
print(hash(database_config))
This makes it possible to use a frozendict as a member of a set or as a key in another mapping.
For example:
cache = {}
key = frozendict(
region="us-east",
tier="premium"
)
cache[key] = "cached-result"
This is not possible with an ordinary dictionary because a normal dict is mutable and therefore unhashable.
Hashability Has an Important Condition
The entire frozendict must contain hashable keys and values.
This works:
settings = frozendict(
timeout=30,
retries=3,
region="us-east"
)
print(hash(settings))
But consider:
settings = frozendict(
options=["fast", "safe"]
)
The value is a list.
Lists are mutable and unhashable.
Therefore, the frozendict cannot be hashed.
This distinction is important:
Immutable mapping
≠
Every contained object is immutable
A frozendict prevents changes to its own key-value structure. It does not automatically make mutable objects stored inside it immutable.
A Mutable Value Can Still Change
Consider:
settings = frozendict(
features=["search", "reports"]
)
You cannot replace the features entry:
settings["features"] = []
But the list itself is still mutable:
settings["features"].append("analytics")
The mapping has not changed its key or value reference, but the object referenced by the value has changed.
This is an important production consideration.
If you need deep immutability, the values themselves must also be immutable.
For example:
settings = frozendict(
features=("search", "reports")
)
Now the value is a tuple rather than a list.
If the tuple contains only hashable objects, the complete frozendict can also be hashable.
Equality Does Not Depend on Insertion Order
A frozendict preserves insertion order when iterated, similar to a normal dictionary.
However, equality does not depend on insertion order.
For example:
first = frozendict(
language="Python",
version=3.15
)
second = frozendict(
version=3.15,
language="Python"
)
print(first == second)
The result is:
True
The objects contain the same key-value pairs even though they were created in a different order.
This makes sense for a mapping because the relationship between a key and its value is more important than the order in which those pairs were added.
When Should You Use frozendict?
frozendict is most useful when the mapping represents data that should remain fixed after construction.
Good examples include:
Application constants
Immutable configuration
Metadata
Lookup tables
Cache keys
Function configuration
Shared application state
Plugin metadata
Serialization-oriented structures
Fixed mappings passed between components
For example:
HTTP_STATUS = frozendict(
ok=200,
created=201,
bad_request=400,
unauthorized=401,
not_found=404,
server_error=500
)
The mapping communicates an important design decision:
These values are not supposed to be changed at runtime.
Using frozendict for Configuration
Configuration is one of the easiest places to understand the value of immutability.
Consider:
DATABASE_CONFIG = {
"host": "db.internal",
"port": 5432,
"pool_size": 20
}
Any part of the application with access to this object can modify it:
DATABASE_CONFIG["pool_size"] = 100
That can create difficult-to-debug behavior.
A frozen configuration makes the contract clearer:
DATABASE_CONFIG = frozendict(
host="db.internal",
port=5432,
pool_size=20
)
Now accidental mutation is rejected immediately.
If another component needs a different configuration, create a new object rather than modifying the existing one.
Updating a frozendict Means Creating Another One
A common misconception is that immutable objects cannot be changed at all.
The more accurate way to think about them is:
You cannot modify the existing object, but you can create a new object with different values.
For example:
original = frozendict(
timeout=30,
retries=3
)
updated = frozendict(
**original,
timeout=60
)
Now:
print(original)
print(updated)
The original remains unchanged.
Conceptually:
original
|
| create new version
v
updated
This pattern can be useful when configuration objects are shared across multiple components.
Using frozendict as a Cache Key
Suppose an application caches results based on several parameters:
query_options = {
"page": 1,
"page_size": 50,
"sort": "date"
}
A normal dictionary cannot be used as a dictionary key:
cache[query_options] = result
This raises an error because dictionaries are unhashable.
Join the conversation! Your thoughts help the community grow.