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:

Bash
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:

Python
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.

Python
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.

Python
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:

Python
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:

Python
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:

Bash
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:

Python
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:

Python
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:

Python
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:

Code
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:

Python
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.