Python dictionaries are useful because they let developers store and update data using key-value pairs. They are used for application settings, API responses, caching, configuration, and many other everyday tasks.

However, dictionaries are mutable. Once a dictionary is created, any code holding a reference to it can change its contents. That flexibility is useful in many situations, but it can also introduce bugs when data is supposed to remain unchanged.

Consider an application that loads configuration settings at startup and passes them to several services. If one component accidentally changes a setting, other components may start behaving differently. Finding the source of that change can be difficult, especially in a large application.

Python 3.15 introduces frozendict, an immutable mapping type designed to address this problem. It provides a way to represent key-value data that cannot be modified after creation.

For developers, the important questions are when immutability is useful, how frozendict differs from a regular dictionary, and what limitations to consider before using it in production code.

Why Python Dictionaries Can Cause Problems

A standard dictionary allows items to be added, updated, and removed after creation.

settings = {
    "environment": "production",
    "debug": False,
    "timeout": 30
}

settings["debug"] = True
settings["timeout"] = 60

print(settings)

Output:

{'environment': 'production', 'debug': True, 'timeout': 60}

The changes are valid Python operations. However, they may be undesirable when the dictionary represents settings that should remain fixed during application execution.

The problem becomes more complicated when several components share the same dictionary.

def start_application(settings):
    print("Starting application:", settings["environment"])


def update_settings(settings):
    settings["debug"] = True


config = {
    "environment": "production",
    "debug": False
}

start_application(config)
update_settings(config)
start_application(config)

The second call to start_application() receives a different configuration state, even though the application passed the same dictionary object both times.

In a larger application, the code that changes the dictionary might be far away from the code that depends on it. This can make configuration-related bugs difficult to diagnose.

An immutable mapping provides a way to prevent these accidental changes at the mapping level.

What Is frozendict?

Python 3.15 introduces frozendict as an immutable mapping type. Unlike a regular dictionary, its key-value associations cannot be changed after the object is created.

The following example illustrates the intended usage:

from builtins import frozendict

settings = frozendict({
    "environment": "production",
    "debug": False,
    "timeout": 30
})

print(settings["environment"])
print(settings["timeout"])

Output:

production
30

The mapping can be read like a dictionary, but attempts to change its entries are not permitted.

settings["debug"] = True

This operation raises a TypeError because the mapping is immutable.

The import and example above assume a Python 3.15 build that exposes the documented built-in frozendict type. Confirm the exact availability and API in the Python distribution used by your application before adopting it.

The main benefit is that the object communicates an important design decision: the mapping is intended to remain unchanged.

dict vs. frozendict

Both types represent mappings from keys to values, but their mutability changes how they should be used.

Feature

dict

frozendict

Add new entries

Supported

Not supported

Update existing entries

Supported

Not supported

Remove entries

Supported

Not supported

Read values by key

Supported

Supported

Suitable for frequently changing state

Yes

No

Suitable for fixed mapping data

Yes

Yes

Prevents mapping-level modification

No

Yes

A regular dictionary is appropriate for collections that change throughout normal application execution. An immutable mapping is more suitable when data should be shared without allowing callers to change its key-value associations.

Neither type is universally better. The correct choice depends on whether mutation is part of the application's intended behavior.

Using frozendict for Application Configuration

Configuration is a useful example because settings often need to be read by many components but should not be modified arbitrarily after initialization.

A service might receive settings during startup and use them to configure its behavior.

from builtins import frozendict


def create_settings():
    return frozendict({
        "environment": "production",
        "debug": False,
        "request_timeout": 30,
        "max_retries": 3
    })


def start_service(settings):
    if settings["debug"]:
        print("Debug mode is enabled.")
    else:
        print("Running with debug mode disabled.")

    print("Request timeout:", settings["request_timeout"])


settings = create_settings()
start_service(settings)

The service can read the settings without receiving a mutable dictionary.

This is useful when configuration is shared among multiple components. It reduces the chance that one component will accidentally change a value used by another.

However, immutability is not a replacement for configuration validation. The application must still check that timeouts are positive, retry counts are sensible, and required values are present before starting the service.

It is also important to understand that an immutable mapping does not automatically make every object stored inside it immutable.

Understanding Shallow Immutability

One of the most important details when working with immutable mappings is the difference between an immutable container and immutable values.

Consider a mapping that contains a list:

from builtins import frozendict

settings = frozendict({
    "allowed_hosts": ["api.example.com", "admin.example.com"]
})

The mapping's entries cannot be reassigned. However, the list stored under allowed_hosts is a separate mutable object.

If the implementation accepts this value, the following operation illustrates the distinction:

settings["allowed_hosts"].append("unexpected.example.com")

The list itself can still be modified, even though the mapping is immutable.

This matters for security-sensitive configuration. A developer might assume that an immutable mapping guarantees that every nested setting is protected from change. That assumption is incorrect unless the nested values are also immutable.

For fixed collections, use immutable values where appropriate, such as tuples instead of lists:

from builtins import frozendict

settings = frozendict({
    "allowed_hosts": (
        "api.example.com",
        "admin.example.com"
    )
})

This example avoids the mutable-list problem. The precise immutability guarantees of nested values still depend on the types stored in the mapping.

Using Immutable Mappings in Shared Application Code

Immutable data structures can also improve the design of libraries and service interfaces.

For example, a function that receives a mutable dictionary can modify the original object:

def configure_client(options):
    options["timeout"] = 60

If the caller passes a dictionary, the function changes the caller's data as well.

An immutable mapping makes that operation invalid:

from builtins import frozendict

options = frozendict({
    "timeout": 30,
    "retries": 3
})

def configure_client(options):
    print("Timeout:", options["timeout"])
    print("Retries:", options["retries"])

configure_client(options)

The function can inspect the configuration without modifying its entries.

This can make library interfaces easier to reason about. Callers know that passing the mapping does not permit the receiving function to update its key-value associations.

However, a function that genuinely needs to modify its configuration should use a mutable structure or explicitly create a new mapping. Immutability should reflect the intended contract, not be introduced merely because it is available.

Advantages and Disadvantages

Advantages

Prevents accidental mapping changes: Code cannot add, remove, or replace entries after the mapping has been created.

Makes shared data easier to reason about: Multiple components can read the same mapping without changing its associations.

Clarifies API contracts: An immutable mapping communicates that the supplied configuration is intended for reading rather than modification.

Can simplify debugging: Some unexpected state changes become impossible, reducing the number of mutation paths that developers need to investigate.

Disadvantages

Not suitable for changing state: Applications that frequently update mappings still need mutable dictionaries or another appropriate data structure.

Nested values may remain mutable: An immutable mapping does not automatically freeze lists, dictionaries, or custom objects stored inside it.

Some code may need refactoring: Functions that expect to modify dictionaries must be redesigned or supplied with a mutable copy.

Immutability does not validate data: A mapping can contain incorrect values even when those values cannot be reassigned.

Common Mistakes and Troubleshooting

Problem

Explanation

Recommended approach

Assigning a new value raises TypeError

The mapping is immutable

Create a new mapping if the configuration must change

A nested list still changes

The mapping is immutable, but the list is not

Use immutable nested values where appropriate

Existing code expects a dictionary to be modified

The code depends on mutable behavior

Review the interface and decide whether mutation is actually required

Configuration contains invalid values

Immutability does not validate data

Validate settings before constructing the mapping

The type is unavailable in the deployed interpreter

The runtime does not expose the expected feature

Verify the Python version and the documented API

Best Practices for Production Applications

  • Use frozendict when a mapping's entries should remain fixed after initialization.

  • Keep ordinary dictionaries for mutable application state.

  • Validate configuration values before creating immutable mappings.

  • Review nested objects carefully; immutability of the outer mapping does not guarantee deep immutability.

  • Avoid forcing immutable mappings into APIs that genuinely require mutation.

  • Test the behavior using the exact Python interpreter and runtime configuration used in production.

  • Document whether a function expects mutable configuration or a read-only mapping.

Summary

Python 3.15's frozendict provides an immutable alternative to the standard dictionary for situations where key-value associations should not change after creation.

It can be useful for application configuration, shared settings, and library interfaces that should expose read-only mapping data. By preventing entries from being reassigned, it reduces accidental changes and makes the intended behavior clearer.

However, an immutable mapping does not automatically make its nested objects immutable, validate configuration values, or replace dictionaries in every situation.

For production applications, choose the data structure according to the required behavior. Use mutable dictionaries when changes are expected, and use immutable mappings when the data should remain fixed. This small design decision can make shared configuration easier to maintain and debug.