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"] = 99Python 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 errorThe 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: boolThe 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 thisCode 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: strid 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: strSubclasses 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: strThe 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.







