← All notes

Field notes

Python 3.15 frozendict: Built-in Immutable Dictionaries

A look at Python 3.15's new built-in frozendict type (PEP 814)—shallow immutability, hashability, cache keys, and practical differences from standard dicts.

2 min read
  • python
  • backend
  • data-structures

What is frozendict?

Python 3.15 introduces a new built-in type called frozendict (PEP 814). It is an immutable dictionary: once created, entries cannot be added, removed, or modified.

Until Python 3.15, achieving immutable mapping behavior required third-party packages, types.MappingProxyType (which is only a read-only view over a mutable dictionary), or custom tuple-of-tuples wrappers. frozendict makes immutable mappings first-class.

Basic usage

You cannot create a frozendict with curly-brace literal syntax {}; you instantiate it using the frozendict() constructor:

  config = frozendict({
      "host": "api.example.com",
      "port": 8000,
  })

  print(config["host"])   # api.example.com

  config["port"] = 9000   # Raises TypeError
  config["debug"] = True  # Raises TypeError
  del config["host"]      # Raises TypeError

Because mutation operations raise TypeError, configuration objects and shared constants remain protected from accidental in-place modifications.

Hashability and dictionary keys

A standard dict cannot be used as a dictionary key or placed inside a set because it is mutable and unhashable (TypeError: unhashable type: 'dict').

In contrast, if every key and value in a frozendict is hashable, the frozendict itself is hashable. This allows you to use complex structured configurations as dictionary keys, in sets, and as arguments to @functools.lru_cache:

  from functools import lru_cache

  settings = frozendict({"env": "prod", "region": "ap-south-1"})

  # Use as dictionary keys
  cache = {settings: "production-config"}
  print(cache[settings])

  # Use with lru_cache
  @lru_cache(maxsize=128)
  def compute_pipeline(parameters: frozendict):
      # Safe from parameter mutation across cache hits
      return run_complex_calculation(parameters)

Important implementation details

  • Order preservation & equality: Like standard dictionaries, frozendict preserves insertion order. However, equality comparisons (==) check matching key-value pairs while ignoring order.
  • Inheritance: frozendict is not a subclass of dict; it inherits directly from object.
  • Type checking: Because it does not subclass dict, isinstance(x, dict) evaluates to False. If your function needs to accept either mapping, write isinstance(x, (dict, frozendict)), or use collections.abc.Mapping.
  • Shallow immutability: Just like Python tuple, immutability is shallow. If a value inside a frozendict is a mutable collection (such as a list), the nested object can still be modified:
  data = frozendict({"tags": ["ai", "python"]})

  # Allowed: The frozendict mapping is fixed, but the list itself is mutable
  data["tags"].append("fastapi")

For completely immutable deep structures, ensure nested values are also immutable types (like tuple or nested frozendict).

Where frozendict is useful

  • Immutable configuration and constants: Application settings, database connection profiles, and environment metadata that should never change at runtime.
  • Safe cache keys: Clean compound keys for memoization without having to manually convert dictionaries into sorted tuple pairs.
  • Function defaults: Avoids the infamous "mutable default argument" footgun in Python function definitions.
  • AI agent & pipeline metadata: Passing immutable state, prompt metadata, or tool definitions through multi-stage agent workflows where intermediate handlers should not mutate upstream contracts.

Python 3.15 also introduces a sentinel builtin (PEP 661), but frozendict is the most significant addition to Python's core collection primitives.