typing.ReadOnly: Read-Only TypedDict Fields

Published on: September 22, 2026
Reading time: 6 minutes
Developer working with static typing and Python typing.ReadOnly

typing.ReadOnly lets you declare read-only keys inside a TypedDict. It is designed for dictionary-shaped data where some values may be established during construction but should not be replaced later. Typical examples include record identifiers, creation timestamps, deployment metadata, audit fields, and values supplied by trusted infrastructure.

This guide explains what ReadOnly means, how type checkers use it, why it does not make a dictionary immutable at runtime, and how to apply it safely in real Python projects.

The problem ReadOnly solves

A TypedDict describes the expected shape of a dictionary. It tells a static analyzer which keys exist and what type each value should have. Before read-only keys were available, every declared key was generally treated as writable. That made it difficult to express a common contract: a record may be mutable overall, while selected fields must remain stable.

Consider a user record. The display name and email may change, but the internal identifier should remain the same after the record is created.

from typing import ReadOnly, TypedDict

class User(TypedDict):
    id: ReadOnly[int]
    name: str
    email: str

A type checker allows code to read user['id'], but reports an assignment such as user['id'] = 99. The other fields remain writable.

Static protection, not runtime immutability

ReadOnly does not replace the normal dictionary implementation. At runtime, the object is still a dict, and Python itself does not block an assignment to a read-only annotated key. The protection exists in static analysis performed by tools such as Pyright, mypy, IDE inspections, or a continuous integration check.

This distinction is essential. Type annotations document and verify intended usage before execution, while runtime validation enforces rules when the program is running. A robust project often needs both.

A configuration example

Application configuration often mixes fixed and adjustable values. The deployment environment and release version may be fixed, while the logging level and timeout may be changed through an administrative system.

from typing import ReadOnly, TypedDict

class AppConfig(TypedDict):
    environment: ReadOnly[str]
    version: ReadOnly[str]
    log_level: str
    timeout: float

config: AppConfig = {
    'environment': 'production',
    'version': '2.4.0',
    'log_level': 'INFO',
    'timeout': 10.0,
}

config['log_level'] = 'DEBUG'

Changing the log level is valid. Replacing the environment or version should be flagged by the type checker. The type definition therefore communicates the lifecycle of each field to every caller.

Optional read-only keys

A read-only key may also be optional. Combine the concept with NotRequired when a field can be absent but should not be replaced once supplied.

from typing import NotRequired, ReadOnly, TypedDict

class ApiResponse(TypedDict):
    request_id: ReadOnly[str]
    result: str
    cache_key: NotRequired[ReadOnly[str]]

Here, cache_key may not exist in every response. When it is present, callers are expected to treat it as stable metadata. This is useful for HTTP responses, event payloads, database projections, and messages produced by other services.

Function contracts

Read-only keys improve function signatures because they show which parts of a structure a function may inspect but should not replace.

class Order(TypedDict):
    code: ReadOnly[str]
    status: str
    total: float

def mark_paid(order: Order) -> None:
    order['status'] = 'paid'
    # order['code'] = 'other'  # typing error

The contract is more precise than a plain dictionary annotation. Reviewers can see that the order code represents identity, while status is expected to evolve.

Structural typing and aliases

TypedDict uses structural typing. Compatibility depends on the available keys and their value types, rather than only on the class name. Read-only qualifiers participate in that compatibility analysis. This matters when the same dictionary is visible through multiple aliases.

A writable alias must not be used to silently violate a promise made through a read-only view. Let the type checker evaluate assignments and avoid broad casts that erase qualifiers. When a conversion appears necessary, consider copying the data or designing a narrower input type instead.

ReadOnly versus frozen dataclasses

ReadOnly is not a substitute for @dataclass(frozen=True). A frozen dataclass provides a runtime barrier against ordinary attribute assignment. It also supports methods, properties, validation patterns, and richer domain behavior. A TypedDict, by contrast, remains ideal when the data must preserve dictionary semantics, especially for JSON, HTTP payloads, configuration mappings, and interoperability with existing APIs.

Choose read-only keys when only selected dictionary fields need static protection. Choose an immutable class when the whole object should behave as a value object and runtime enforcement is important.

Runtime validation is still required

External input must be checked even when the target type uses ReadOnly. Type annotations do not verify JSON received from a network, values read from a file, or dictionaries produced by untyped code.

def build_user(data: dict[str, object]) -> User:
    identifier = data.get('id')
    name = data.get('name')
    email = data.get('email')
    if not isinstance(identifier, int):
        raise ValueError('invalid id')
    if not isinstance(name, str) or not isinstance(email, str):
        raise ValueError('invalid user data')
    return {'id': identifier, 'name': name, 'email': email}

This factory validates the runtime values and returns data that matches the static contract. Larger systems may use a validation library, but the separation remains the same: validation checks reality, while typing checks program usage.

Version compatibility

Before adopting the feature, confirm the minimum Python version and the level of support in your selected type checker. Projects that support older Python versions may import a backport from typing_extensions. Keep both the interpreter and type-checking tools updated, because new typing features often require coordinated support.

The authoritative references are the Python typing documentation and PEP 705, which defines read-only items for TypedDict.

Good design practices

Use read-only qualifiers for fields that represent identity, provenance, generated metadata, or values whose replacement would break an invariant. Do not mark every field read-only automatically. If almost the entire structure must be immutable, a frozen dataclass or another value-object design will usually be clearer.

Avoid using cast merely to silence an error. Investigate whether the attempted mutation is actually valid. Place construction and validation in small functions, document when each stable value is assigned, and run static analysis in continuous integration.

For additional context, explore Academify guides on Python typing, Python dataclasses, Python dictionaries, and JSON in Python. Together, these topics help you choose between typed mappings, validated payloads, and dedicated classes.

Practical migration strategy

Introduce ReadOnly gradually. Start with identifiers and timestamps that are already treated as stable by convention. Run the type checker, fix genuine mutation sites, and add tests around construction and update workflows. This approach prevents a large annotation change from hiding design mistakes.

When a legitimate workflow needs a new value, create a new record instead of mutating the protected key, or redesign the type so the lifecycle is explicit. For example, a temporary draft record and a persisted record can use different TypedDict definitions.

Conclusion

typing.ReadOnly makes TypedDict contracts more expressive. It distinguishes stable keys from ordinary mutable fields, helps type checkers catch accidental replacement, and improves documentation for APIs that exchange dictionary-shaped data.

Because the annotation is static, it should be combined with runtime validation, tests, and a consistently executed type checker. Used selectively, it offers a lightweight way to preserve important invariants without giving up the compatibility and convenience of Python dictionaries.

Share:

Facebook
WhatsApp
Twitter
LinkedIn

Article content

    Related articles

    Python code representing positional arguments with functools.Placeholder
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    functools.Placeholder: Fill Middle partial Arguments

    Learn Python functools.Placeholder to reserve middle arguments in partial, build clearer callbacks, and avoid unnecessary lambda wrappers.

    Ler mais

    Tempo de leitura: 5 minutos
    22/09/2026
    Python code with a deprecated API warning using warnings.deprecated
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    warnings.deprecated: Mark Deprecated APIs

    Learn Python warnings.deprecated to mark obsolete APIs, guide migrations, and integrate deprecation with typing, tests, documentation, and CI.

    Ler mais

    Tempo de leitura: 6 minutos
    21/09/2026
    Software engineer monitoring Python code execution with sys.monitoring
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    sys.monitoring: Profiling and Observability

    Learn Python sys.monitoring for profilers, coverage, debugging, and observability with selective events and controlled overhead.

    Ler mais

    Tempo de leitura: 7 minutos
    21/09/2026
    Python code on screen representing template strings
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python Template Strings: Structured Interpolation

    Learn how template strings preserve interpolations for safer, structured rendering.

    Ler mais

    Tempo de leitura: 7 minutos
    20/09/2026
    Python code being measured for performance with perf_counter_ns
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    perf_counter_ns: Measure Performance in Nanoseconds

    Learn to measure Python performance and latency with perf_counter_ns, integer nanoseconds, repetitions, and reliable benchmarking practices.

    Ler mais

    Tempo de leitura: 4 minutos
    20/09/2026
    Python code representing a priority queue with heapq max-heap
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    heapq Max-Heap: Max-Priority Queues in Python

    Learn Python heapq max-heap functions for priority queues, rankings, schedulers, and efficient selection algorithms.

    Ler mais

    Tempo de leitura: 4 minutos
    19/09/2026