Python dictionaries are one of the most frequently used data structures in application development. They store configuration values, API responses, application settings, and other key-value data. However, ordinary dictionaries are mutable, which means any code holding a reference to a dictionary can change its contents.
That flexibility is useful when data needs to change. It becomes a problem when a mapping represents fixed configuration, shared metadata, or a value that should remain consistent throughout an operation.
Python 3.15 introduces a built-in frozendict type for immutable mappings. It gives developers a standard way to represent key-value data that cannot be modified after creation.
This article explains how frozendict works, how it differs from dict and MappingProxyType, and where it fits into production Python applications.
What Is frozendict in Python?
A frozendict is an immutable mapping. Like a dictionary, it associates keys with values, but it does not allow entries to be added, removed, or replaced after the object has been created.
Consider an ordinary dictionary:
settings = {
"timeout": 30,
"retries": 3
}
settings["timeout"] = 60
print(settings["timeout"]) # 60
This is expected behavior because dictionaries are designed to support modification.
With frozendict, the mapping cannot be changed after construction:
settings = frozendict({
"timeout": 30,
"retries": 3
})
print(settings["timeout"]) # 30
settings["timeout"] = 60 # TypeError
The attempted assignment raises a TypeError.
This makes frozendict useful when an application needs to communicate that a mapping represents fixed data rather than mutable application state.
How to Create and Use frozendict
Python 3.15 provides frozendict as a built-in type. It supports the usual mapping operations, including looking up values, iterating over entries, and checking whether a key exists.
Creating a frozendict
You can construct a mapping from a dictionary or provide keyword arguments.
DEFAULT_SETTINGS = frozendict({
"timeout": 30,
"max_retries": 3,
"debug": False
})
print(DEFAULT_SETTINGS["timeout"])
print(DEFAULT_SETTINGS.get("debug"))
print("max_retries" in DEFAULT_SETTINGS)
The mapping can be read like a normal dictionary, but its entries cannot be changed.
For application configuration, this can help prevent accidental updates by components that only need read access.
Iterating Over Entries
A frozendict supports standard mapping iteration.
settings = frozendict({
"environment": "production",
"timeout": 30,
"retries": 3
})
for key, value in settings.items():
print(f"{key}: {value}")
This is useful when logging approved configuration values, generating reports, or passing settings into components that should not modify the mapping.
Be careful not to log secrets such as passwords, access tokens, or private connection strings.
Creating a New Mapping With Updated Values
Immutability does not mean the data can never be updated. It means an existing mapping cannot be modified in place.
When a change is required, create a new mapping:
original = frozendict({
"timeout": 30,
"retries": 3
})
updated = frozendict({
**original,
"timeout": 60
})
print(original["timeout"]) # 30
print(updated["timeout"]) # 60
The original object remains unchanged, while the new object contains the updated value.
This pattern is useful for configuration snapshots and other data structures where preserving the previous state matters.
Using frozendict for Application Configuration
Configuration is a natural use case because many applications load settings at startup and expect those settings to remain stable while the application is running.
For example, a service might maintain default timeout and retry settings:
DEFAULT_CONFIG = frozendict({
"request_timeout": 10,
"max_retries": 3,
"environment": "production"
})
def get_request_timeout(config):
return config["request_timeout"]
timeout = get_request_timeout(DEFAULT_CONFIG)
print(timeout)
The function can read the configuration without being able to replace its entries.
For larger applications, configuration validation should happen before constructing the immutable mapping. Validate required keys, expected value types, acceptable ranges, and environment-specific settings first.
An immutable mapping prevents later changes to its entries; it does not guarantee that the original values are valid.
frozendict Versus dict and MappingProxyType
Python already provides several ways to work with mapping data. Choosing the right one depends on whether the application needs mutability, a read-only view, or an immutable value.
Feature | dict
| frozendict
| MappingProxyType
|
|---|
Entries can be changed directly | Yes | No | No |
Reflects changes to an underlying dictionary | Yes, because it is the dictionary | No; it is an immutable mapping | Yes |
Can be used as a dictionary key | No | Yes, when its contents are hashable | No |
Useful for mutable application state | Yes | No | No |
Useful for fixed mapping values | Requires discipline | Yes | Depends on whether a live read-only view is intended |
MappingProxyType, available through types, provides a read-only view of an existing mapping. It does not make the underlying dictionary immutable.
For example:
from types import MappingProxyType
source = {"timeout": 30}
view = MappingProxyType(source)
source["timeout"] = 60
print(view["timeout"]) # 60
The proxy reflects the modification because the original dictionary changed.
By contrast, frozendict represents an immutable mapping value. This distinction matters when data is shared between components or used as a stable configuration snapshot.
Can frozendict Be Used as a Dictionary Key?
Python dictionary keys must be hashable. Ordinary dictionaries are mutable and therefore cannot be used as dictionary keys.
A frozendict can be hashable when its keys and values satisfy the required hashability constraints.
For example:
cache = {}
options = frozendict({
"format": "json",
"compressed": True
})
cache[options] = "serializer configuration"
print(cache[options])
This can be useful when a set of options identifies a particular configuration or computation.
However, do not assume that every frozendict is hashable. If it contains an unhashable value, such as a list or a regular dictionary, hashing can fail.
For example:
options = frozendict({
"columns": ["name", "email"]
})
hash(options) # TypeError
If the mapping will be used as a key or stored in a set, validate that its contents are hashable.
Important Limitation: Immutability Is Shallow
One of the most important details when using immutable mappings is that the immutability of the mapping does not automatically extend to objects stored inside it.
Consider the following example:
settings = frozendict({
"allowed_hosts": ["api.example.com"]
})
settings["allowed_hosts"].append(
"internal.example.com"
)
print(settings["allowed_hosts"])
The list can still be modified even though the mapping itself cannot be changed.
This happens because the mapping protects its key-value associations, not the internal state of every object referenced by those values.
If an application needs stronger immutability, use immutable values where appropriate.
For example:
settings = frozendict({
"allowed_hosts": (
"api.example.com",
"internal.example.com"
)
})
A tuple prevents changes to the collection's elements, although objects nested inside a tuple can still be mutable.
For deeply nested configuration, the application must choose suitable data structures at each level.
Performance Considerations
frozendict should primarily be selected for its immutability semantics, not as an automatic performance optimization.
Compared with a regular dictionary, constructing an immutable mapping has a cost. If an application repeatedly creates new mappings to update a single value, that approach may be less suitable than using a mutable dictionary.
A practical comparison should measure the operations relevant to the application:
Mapping construction time.
Lookup performance.
Memory consumption.
Frequency of configuration updates.
Cost of copying mappings.
Whether hashing is required.
Avoid claiming that immutable mappings are universally faster or more memory-efficient. The results depend on the implementation, the workload, and the size and contents of the mapping.
Best Practices for Production Applications
1. Use It for Stable Data
Choose frozendict for configuration snapshots, fixed metadata, and other mappings that should not change after construction.
Keep ordinary dictionaries for frequently updated state, counters, caches, and other mutable structures.
2. Validate Data Before Freezing It
Check required fields, types, and acceptable values before creating the immutable mapping. Immutability does not replace validation.
3. Review Existing Type Checks
Code that uses isinstance(value, dict) may reject a frozendict, because it is not a subclass of dict.
When a function only needs mapping behavior, consider using the collections.abc.Mapping interface:
from collections.abc import Mapping
def read_timeout(settings: Mapping):
return settings["timeout"]
This allows the function to work with different mapping implementations.
4. Avoid Assuming Deep Immutability
Review nested lists, dictionaries, and custom objects. Use immutable nested structures when the entire configuration must remain stable.
5. Consider Python Version Compatibility
The built-in type is a Python 3.15 feature. Applications that support earlier Python versions need a compatibility strategy.
Before upgrading, verify that the application's dependencies and deployment environments support the required interpreter version.
Common Mistakes and Troubleshooting
Problem | Cause | Recommended solution |
|---|
Assignment raises TypeError | The mapping is immutable | Create a new mapping with the desired values |
A dict type check fails | frozendict is not a subclass of dict
| Accept the Mapping interface where appropriate |
Hashing raises TypeError | A key or value is unhashable | Replace mutable nested values or avoid hashing the mapping |
Nested data changes unexpectedly | Immutability is shallow | Use immutable nested values |
Existing code breaks on older Python versions | The built-in type is unavailable | Add a compatibility layer or retain a supported mapping implementation |
Advantages and Disadvantages
Advantages
Prevents direct modification of mapping entries.
Makes configuration ownership clearer.
Supports hash-based use cases when contents are hashable.
Provides a built-in option without requiring a third-party immutable-dictionary package.
Helps communicate that a mapping represents stable data.
Disadvantages
Does not automatically make nested objects immutable.
Cannot replace dictionaries in workflows that require frequent updates.
May require changes to code that assumes every mapping is a dict.
Requires Python 3.15 or a separate compatibility approach.
Does not guarantee faster lookups or lower memory consumption.
Summary
Python 3.15's built-in frozendict gives developers a standard way to represent immutable mappings. It is useful for stable configuration, metadata, and hash-based use cases where the contents should not change after construction.
The main distinction is between an immutable mapping and a read-only view of a mutable dictionary. frozendict protects its entries, while MappingProxyType reflects changes to its underlying mapping.
For production code, validate configuration before constructing the mapping, use immutable nested values when necessary, and retain ordinary dictionaries for mutable state. These decisions make the data model clearer without introducing unnecessary complexity.
Join the conversation! Your thoughts help the community grow.