Python ReadOnly: Protect TypedDict Fields

Published on: August 29, 2026
Reading time: 5 minutes
High-angle view of woman coding on a laptop, with a Python book nearby. Ideal for programming and tech content.

typing.ReadOnly marks individual TypedDict keys as read-only for static type checking. It is useful when a dictionary-shaped record contains values that consumers may read but should not modify after creation, such as IDs, timestamps, versions, audit codes, checksums, and server-computed fields.

ReadOnly does not freeze a dictionary at runtime. It documents and verifies a write contract during static analysis. This guide covers declarations, required and optional fields, inheritance, subtyping, factories, API models, runtime limitations, and the situations where a frozen dataclass or MappingProxyType is more appropriate.

The problem with mutable dictionaries

from typing import TypedDict

class User(TypedDict):
    id: int
    name: str

user: User = {"id": 1, "name": "Ana"}
user["id"] = 99

Python allows the assignment. In many domains, however, the identifier should be fixed at creation. A normal TypedDict does not distinguish editable fields from protected ones.

Declaring ReadOnly

from typing import ReadOnly, TypedDict

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

user: User = {"id": 1, "name": "Ana"}
user["name"] = "Ana Silva"  # accepted
user["id"] = 2              # static error

The id value is still readable as an integer. Assigning, deleting, or otherwise mutating that key should be rejected by the type checker.

ReadOnly works per field

class Record(TypedDict):
    created_at: ReadOnly[str]
    version: ReadOnly[int]
    title: str
    active: bool

The whole dictionary is not read-only. title and active remain writable. This models objects with stable identity and mutable state.

There is no automatic runtime protection

record: Record = {
    "created_at": "2026-07-26",
    "version": 1,
    "title": "Example",
    "active": True,
}

record["version"] = 10  # Python executes this

Code that skips static analysis can still mutate the field. For runtime immutability, use a frozen dataclass, a class with controlled setters, an immutable mapping, or MappingProxyType. The guide to Python MappingProxyType explains read-only mapping views.

ReadOnly and key presence

ReadOnly answers “may this key be written later?” Totality answers “must this key exist?” They are independent dimensions.

from typing import NotRequired, ReadOnly, Required, TypedDict

class Response(TypedDict, total=False):
    id: Required[ReadOnly[int]]
    cache: NotRequired[ReadOnly[str]]
    message: str

id is required and read-only. cache is optional and read-only when present. message is optional and writable because the class uses total=False.

Qualifier order

Modern typing tools understand combinations of Required, NotRequired, and ReadOnly. Choose one consistent style and verify it with the checker used by the project. Code review should make presence and write rules easy to understand.

Computed fields

class Order(TypedDict):
    subtotal: float
    discount: float
    total: ReadOnly[float]

A factory computes total, and downstream code treats it as stable. The contract discourages accidental edits to a derived value.

Factories define initial values

def create_order(subtotal: float, discount: float) -> Order:
    return {
        "subtotal": subtotal,
        "discount": discount,
        "total": subtotal - discount,
    }

ReadOnly does not prevent construction. It normally permits the initial dictionary to include the key and restricts later writes through a typed reference.

Use separate patch models

An update endpoint should not blindly reuse the complete response TypedDict when some fields are server-controlled.

class UpdateOrder(TypedDict, total=False):
    subtotal: float
    discount: float


def update(order_id: int, changes: UpdateOrder) -> None:
    ...

A dedicated patch type omits id and total entirely. The static interface makes invalid update payloads harder to express.

Subtyping and write safety

Read-only fields can support more flexible subtype relationships because consumers cannot replace their values. A structure containing a more specific value may be safely viewed through a more general read-only contract.

class Animal: ...
class Dog(Animal): ...

class AnimalSource(TypedDict):
    item: ReadOnly[Animal]

class DogSource(TypedDict):
    item: ReadOnly[Dog]

Exact compatibility follows the typing specification and checker implementation. The key idea is that removing write access reduces variance risks.

TypedDict inheritance

class BaseEvent(TypedDict):
    id: ReadOnly[str]
    created_at: ReadOnly[str]

class UserEvent(BaseEvent):
    user_id: int
    action: str

Subclasses inherit read-only contracts. A derived TypedDict should not silently turn a protected field into a writable one, because callers relying on the base contract would no longer be safe.

Separate public and internal representations

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

class InternalUser(TypedDict):
    id: int
    name: str
    password_hash: str

The implementation may build and modify an internal record while exposing a narrower public contract. Similar structures are not automatically interchangeable; verify assignments with the chosen checker.

ReadOnly parameters

def display(user: PublicUser) -> str:
    return f"{user['id']}: {user['name']}"

The parameter communicates that protected keys are not writable. Other unprotected keys could still be modified. When every field must be immutable at runtime, choose a stronger abstraction.

Copying instead of mutating

def rename(user: PublicUser, name: str) -> PublicUser:
    return {"id": user["id"], "name": name}

ReadOnly restricts mutation of the existing reference; it does not prohibit constructing a new dictionary. Copy-on-write patterns can preserve published records while producing updated versions.

Serialization behavior

ReadOnly does not change JSON, pickle, database storage, or network transport. The field serializes like any other key. Servers must still validate external payloads and reject attempts to control protected values.

HTTP request and response models

A response model can mark id, created_at, and checksum as read-only. An input model should omit them instead of accepting and ignoring them. Separate request, patch, and response contracts usually produce clearer APIs.

Validation frameworks

Some frameworks may interpret ReadOnly when generating schemas or documentation; others may ignore it. Check the exact integration. Never depend on a type annotation alone for authorization, data integrity, or runtime security.

Version compatibility

On interpreters that do not provide typing.ReadOnly, import it from typing_extensions. Libraries should declare the dependency and run static tests with every supported checker and Python version.

Common mistakes

  • Assuming the dictionary is frozen: ReadOnly is a static rule.
  • Using one model for create, patch, and response: each operation often needs a different contract.
  • Confusing ReadOnly with NotRequired: writability and presence are separate.
  • Deleting a protected key: deletion is also mutation.
  • Trusting a framework without checking support: runtime behavior may not change.
  • Making an inherited protected key writable: that breaks safe substitution.

Complete example: versioned document

from typing import ReadOnly, TypedDict

class Document(TypedDict):
    id: ReadOnly[str]
    created_at: ReadOnly[str]
    version: ReadOnly[int]
    title: str
    content: str


def create_document(id_: str, title: str, content: str) -> Document:
    return {
        "id": id_,
        "created_at": "2026-07-26T12:00:00Z",
        "version": 1,
        "title": title,
        "content": content,
    }


def edit(document: Document, title: str, content: str) -> Document:
    return {
        **document,
        "version": document["version"] + 1,
        "title": title,
        "content": content,
    }

The function creates a new version instead of mutating the existing record. Depending on the checker, replacing a read-only key while constructing a new dictionary may require an internal builder type or a dedicated factory. The public goal is to keep published references stable.

When another tool is better

Use a frozen dataclass for truly immutable attribute-based objects. Use MappingProxyType for a runtime read-only mapping view. Use a normal class for validation and invariants. Use ReadOnly when the public shape must remain a dictionary and the primary requirement is static write protection.

Conclusion

typing.ReadOnly adds a valuable dimension to TypedDict: callers may read selected keys but should not reassign them. It improves response models, event records, versioned documents, and data with stable identity.

The official Python ReadOnly documentation defines the feature. Combine it with separate creation and update models, run a static checker, and add runtime protection whenever integrity genuinely depends on preventing mutation.

Share:

Facebook
WhatsApp
Twitter
LinkedIn

Article content

    Related articles

    A person typing on a laptop with a Python programming book visible, capturing technology and learning.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Required and NotRequired: Optional TypedDict Keys

    Learn Python Required and NotRequired to control mandatory and optional TypedDict keys without confusing absence with None.

    Ler mais

    Tempo de leitura: 5 minutos
    29/08/2026
    Close-up of hands typing on a laptop keyboard, Python book in sight, coding in progress.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python TypeAliasType: Runtime Type Aliases

    Learn Python TypeAliasType to create explicit aliases, inspect them at runtime, and model reusable generic APIs safely.

    Ler mais

    Tempo de leitura: 6 minutos
    29/08/2026
    Detailed view of programming code in a dark theme on a computer screen.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python overload: Precise Function Signatures

    Learn Python overload for precise signatures with Literal, None, generics, methods, and return types that depend on arguments.

    Ler mais

    Tempo de leitura: 6 minutos
    29/08/2026
    A person reads 'Python for Unix and Linux System Administration' indoors.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python ClassVar: Separate Class and Instance State

    Learn Python ClassVar to separate class and instance state in dataclasses, registries, caches, inheritance, and counters.

    Ler mais

    Tempo de leitura: 5 minutos
    29/08/2026
    A person typing on a laptop with a Python programming book visible, capturing technology and learning.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python Final: Protect Constants and Inheritance

    Learn Python Final and @final to protect constants, attributes, methods, and classes while understanding runtime limitations.

    Ler mais

    Tempo de leitura: 5 minutos
    29/08/2026
    High-angle view of woman coding on a laptop, with a Python book nearby. Ideal for programming and tech content.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python Annotated: Add Type Metadata

    Learn Python Annotated to attach metadata to types for validation, schemas, units, documentation, and framework integration.

    Ler mais

    Tempo de leitura: 6 minutos
    29/08/2026