Required and NotRequired: Optional TypedDict Keys

Published on: August 29, 2026
Reading time: 5 minutes
A person typing on a laptop with a Python programming book visible, capturing technology and learning.

typing.Required and typing.NotRequired control whether each key must be present in a TypedDict. They solve a common modeling problem: some keys are mandatory while others may be omitted, independently of whether a value is allowed to be None.

This guide explains presence versus nullability, total=True and total=False, request and response models, patch payloads, inheritance, ReadOnly, Unpack, runtime validation, introspection, compatibility, and common design mistakes.

What total controls

By default, every declared TypedDict key is required:

from typing import TypedDict

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

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

Omitting name should produce a static error. With total=False, every key declared in that body becomes optional:

class UpdateUser(TypedDict, total=False):
    name: str
    email: str

This is useful for patch payloads because callers can send only the fields they want to change.

Required inside a partial TypedDict

from typing import Required, TypedDict

class Event(TypedDict, total=False):
    id: Required[str]
    source: Required[str]
    details: dict[str, object]

Even though the class is partial, id and source must be present. details may be omitted. Required overrides the class-level default for a specific key.

NotRequired inside a total TypedDict

from typing import NotRequired, TypedDict

class Product(TypedDict):
    id: int
    name: str
    description: NotRequired[str]

id and name are required. description may be absent. NotRequired keeps the main class total while marking selected keys as optional.

Absent is not the same as None

class Profile(TypedDict):
    nickname: NotRequired[str]
    biography: str | None

nickname may be absent, but when present it must be a string. biography must exist, but its value can be a string or None. This distinction matters in JSON, databases, and update APIs.

Three meaningful states

A patch field can be absent, present with a normal value, or present with None.

class UpdateProfile(TypedDict, total=False):
    nickname: str
    biography: str | None

An absent biography means “leave it unchanged.” A value of None means “clear it.” A string means “replace it.” Treating all three cases as the same would lose information.

Checking optional keys

def apply(profile: dict[str, object], patch: UpdateProfile) -> None:
    if "nickname" in patch:
        profile["nickname"] = patch["nickname"]
    if "biography" in patch:
        profile["biography"] = patch["biography"]

The membership test tells the checker that a NotRequired key is available in that branch. Direct access without a check can produce a warning and may raise KeyError at runtime.

get and default values

dict.get() avoids KeyError but may collapse absence and None:

value = patch.get("biography")

If the distinction matters, use membership testing or a sentinel:

MISSING = object()
value = patch.get("biography", MISSING)

Separate models for each operation

class CreateUser(TypedDict):
    name: str
    email: str
    phone: NotRequired[str]

class UpdateUser(TypedDict, total=False):
    name: str
    email: str
    phone: str | None

class UserResponse(TypedDict):
    id: int
    name: str
    email: str
    phone: str | None

The create model requires minimum input. The patch model accepts subsets. The response guarantees server-produced fields. One TypedDict rarely describes all three operations clearly.

Inheritance for shared fields

class Identity(TypedDict):
    id: int

class PublicData(TypedDict, total=False):
    nickname: str
    avatar: str

class CompleteUser(Identity, PublicData):
    name: str

Inheritance can combine key groups with different totality. Keep hierarchies shallow because many layers make the true required-key set difficult to understand.

Required and ReadOnly

from typing import ReadOnly

class Record(TypedDict, total=False):
    id: Required[ReadOnly[int]]
    created_at: Required[ReadOnly[str]]
    note: str

id and created_at must exist and should not be reassigned by consumers. Presence and writability remain separate dimensions. See the Python ReadOnly guide.

Building dictionaries dynamically

A checker may not prove that a dictionary assembled step by step contains every required key:

data = {}
data["id"] = 1
data["name"] = "Ana"
# assigning data to User may fail static analysis

Prefer complete literals, typed factories, or a variable annotated from the beginning. Use cast() only when another validation step has already guaranteed the contract.

Runtime validation

TypedDict qualifiers do not validate external data. A JSON object missing required keys is still an ordinary dictionary at runtime.

def is_user(value: object) -> bool:
    if not isinstance(value, dict):
        return False
    return (
        isinstance(value.get("id"), int)
        and isinstance(value.get("name"), str)
    )

To narrow the type after validation, use TypeGuard or TypeIs. The Python TypeGuard guide demonstrates that pattern.

OpenAPI and schema generation

Frameworks may translate Required and NotRequired into required and optional schema fields. Support varies with inheritance and nested qualifiers. Inspect generated schemas and maintain contract tests instead of assuming every tool follows the same rules.

Unpack for keyword arguments

from typing import Unpack

class Options(TypedDict, total=False):
    timeout: float
    cache: bool


def run(**options: Unpack[Options]) -> None:
    ...

Required and NotRequired determine which keyword arguments are mandatory when a TypedDict is expanded through Unpack.

TypedDict compatibility

A structure where a key is required is not automatically compatible with another type where the same key is optional. A consumer of the optional type may remove the key or perform writes that violate the required contract. Compatibility considers presence, value type, writability, and inheritance.

Functional syntax for unusual keys

Headers = TypedDict(
    "Headers",
    {
        "content-type": Required[str],
        "x-request-id": NotRequired[str],
    },
)

Functional syntax supports keys that are not valid Python identifiers, including names with hyphens.

Introspection

print(Product.__required_keys__)
print(Product.__optional_keys__)

These sets describe effective key presence. Frameworks can inspect them, but runtime introspection is not a replacement for validating values and nested structures.

__total__ does not tell the whole story

__total__ only reports the totality declared on the current class body. A class with __total__ == True may still contain NotRequired keys or inherit optional keys. Use the required and optional key sets for the effective contract.

Evolving a contract

Changing a required key to optional may break consumers that access it without checking. Changing an optional key to required breaks old producers that omit it. Use schema versions, gradual validation, migration defaults, and compatibility tests.

Common mistakes

  • Using Optional to mean absent: T | None controls values, not key presence.
  • Reading NotRequired keys without checking: runtime KeyError remains possible.
  • Reusing response models for patches: callers are forced to send fields they should not control.
  • Trusting TypedDict at runtime: external data still needs validation.
  • Building deep inheritance trees: the real key contract becomes hard to see.
  • Using get when None and absence differ: semantics are lost.

Complete example: task configuration

from typing import NotRequired, Required, TypedDict

class Task(TypedDict, total=False):
    name: Required[str]
    command: Required[list[str]]
    directory: str
    timeout: float
    retries: int
    environment: dict[str, str]
    description: NotRequired[str]


def execute(task: Task) -> None:
    name = task["name"]
    command = task["command"]
    directory = task.get("directory", ".")
    timeout = task.get("timeout", 30.0)
    retries = task.get("retries", 1)
    print(name, command, directory, timeout, retries)

The two essential keys are Required. Every other option may be omitted and receive a default. The contract remains readable without splitting the configuration into many classes.

Best practices

Model presence separately from nullability. Define distinct create, update, and response types. Use membership tests when absence carries meaning. Validate data at system boundaries. Run mypy, pyright, or another checker in CI. Test minimum, complete, null-containing, and invalid payloads.

Conclusion

Required and NotRequired make TypedDict contracts more precise by controlling each key independently. They represent real payloads without confusing missing data with None and without forcing every key to follow the same totality.

The official Python Required and NotRequired documentation defines the rules. Use the qualifiers for clear static contracts and complement them with runtime validation whenever data comes from an untrusted boundary.

Share:

Facebook
WhatsApp
Twitter
LinkedIn

Article content

    Related articles

    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 ReadOnly: Protect TypedDict Fields

    Learn Python ReadOnly to protect TypedDict fields, model stable response contracts, and prevent accidental writes during static analysis.

    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