Python TypedDict: Required vs NotRequired Keys
Python's typing.TypedDict allows developers to define
types for dictionary keys and values, but early versions struggled with
mixing mandatory and optional keys without cumbersome inheritance
patterns. Introduced in Python 3.11 via PEP 655 (and available in
earlier versions via typing_extensions),
typing.Required and typing.NotRequired solve
this issue. This article explains how these type qualifiers define key
optionality on a per-field basis, how they interact with the
total parameter, and how they differ from
typing.Optional.
The Limitation of the
total Parameter
Before Required and NotRequired,
optionality in a TypedDict was controlled exclusively at
the class level through the total boolean flag:
- When
total=True(the default), every defined key must be present in the dictionary. - When
total=False, all keys are optional and can be omitted.
To create a dictionary where some keys were required and others were
optional, developers had to create two separate TypedDict
classes and inherit one from the other:
from typing import TypedDict
class UserBase(TypedDict):
name: str
class User(UserBase, total=False):
email: strThis approach led to boilerplate code, reduced readability, and scaled poorly for complex schemas.
Explicit
Optionality with Required and NotRequired
PEP 655 introduced Required and NotRequired
as type qualifiers to declare optionality directly on individual keys
within a single TypedDict definition.
Using
NotRequired in a Standard TypedDict
(total=True)
Because total=True is the default behavior, every field
is required unless explicitly marked with NotRequired.
from typing import TypedDict, NotRequired
class UserProfile(TypedDict):
user_id: int # Mandatory
username: str # Mandatory
bio: NotRequired[str] # Optional key
avatar_url: NotRequired[str] # Optional key
# Valid: bio and avatar_url are omitted
user_1: UserProfile = {
"user_id": 1,
"username": "jdoe"
}
# Valid: bio is included
user_2: UserProfile = {
"user_id": 2,
"username": "asmith",
"bio": "Software engineer"
}Using
Required in a Partial TypedDict
(total=False)
When total=False is specified, all fields are optional
by default. You can use Required to specify which
individual fields must always be present.
from typing import TypedDict, Required
class APIRequest(TypedDict, total=False):
endpoint: Required[str] # Mandatory key
payload: dict # Optional key
timeout: int # Optional key
# Valid: endpoint is provided, others are omitted
request: APIRequest = {
"endpoint": "/api/v1/users"
}NotRequired[T] vs.
Optional[T]
A common point of confusion is the distinction between
NotRequired[T] and Optional[T] (or
T | None). They address different concepts in type
checking:
NotRequired[T]defines the presence of the key. The key itself can either exist in the dictionary or be omitted entirely. If the key exists, its value must match typeT.Optional[T]defines the type of the value. The key must still exist in the dictionary, but its value can be eitherTorNone.
from typing import TypedDict, Optional, NotRequired
class Settings(TypedDict):
theme: Optional[str] # Key MUST exist, value can be str or None
notifications: NotRequired[bool] # Key MAY be absent, value must be bool
# Valid
s1: Settings = {"theme": None}
# Valid
s2: Settings = {"theme": "dark", "notifications": True}
# Type Error: "theme" key is missing
s3: Settings = {"notifications": False}If you want a key that can be omitted and accept a
None value when present, you combine both:
NotRequired[Optional[str]].
Inspecting Optionality at Runtime
Python tracks Required and NotRequired keys
in special class attributes: __required_keys__ and
__optional_keys__. These frozensets allow libraries,
serializers, and runtime validation tools to inspect which fields must
be present.
print(UserProfile.__required_keys__) # frozenset({'user_id', 'username'})
print(UserProfile.__optional_keys__) # frozenset({'bio', 'avatar_url'})Using typing.Required and
typing.NotRequired eliminates the need for multi-class
inheritance workarounds and provides precise, readable control over
dictionary schemas in Python type systems.