Python TypedDict: Required and Optional Keys
Model a dictionary record with one required key and one optional key, then use it without assuming TypedDict validates values at runtime.
Example check: Worked and guided code samples are checked against CPython 3.13. Practice uses Pyodide 0.29.5 (Python 3.13) in your browser. How we check lessons
Video lesson: Python TypedDict: Required and Optional Keys
The preview is stored on this site. YouTube loads only when you press Play. Watch on YouTube
Jump to a chapter
Read the video transcript
Select a timestamp to watch that moment on YouTube.
- 00:00
A JSON response gives us an id, but the user has never chosen a nickname. We want a Python type hint that says id must exist and nickname may be absent. We also want code that does not crash when nickname is missing. TypedDict sounds like a way to validate a dictionary, but that name can mislead us. It describes the dictionary's expected shape for a static type checker; at runtime the value remains a normal dict. We will first run the broken assumption and see a KeyError. Then we will declare the optional field with NotRequired, construct a record without an artificial None value, and read it with a fallback.
- 00:40
Keep two distinct questions in mind: which keys may be omitted, and which values are valid when the keys are present. A type hint helps answer these questions in source code, while external JSON still needs runtime validation. The class declaration marks both keys as required by default. The runtime dictionary omits nickname anyway; TypedDict does not intercept this assignment or insert a default value. The error comes when the code indexes the missing key. A static checker would also warn that the dictionary does not meet the declared Profile shape if we annotated the assignment. That warning is useful during development, but it is not a substitute for handling
- 01:22
real input. If we simply add nickname with a None value, we change the data contract. A missing field can mean the user did not provide anything; a present field containing None says something different. In an API, that difference can affect updates, serialization, and display. We should describe absence as absence and choose the fallback where the field is read. Notice also that `nickname: str | None` would still require the nickname key. It would permit a None value, which is not the same repair. NotRequired is available in Python 3.11 and later.
- 02:02
Here it marks exactly one key as potentially absent while leaving id required. The `__required_keys__` and `__optional_keys__` concepts are visible through the attributes we print, and they reflect the policy more directly than checking `total` alone. The output also shows `dict`: constructing Profile does not produce a custom validation object. In a TypedDict with `total=False`, all keys declared in that class body would be optional by default. You can then use Required on selected fields. That alternative is helpful when most fields are optional, but our record is clearer with the default required policy and a single NotRequired field.
- 02:44
If a real API returns an unexpected nickname type or an id as text, this declaration does not reject it. A static checker can catch typed source assignments; a runtime parser still has to validate untrusted values. The page's exercise checks the schema attributes and the returned dictionaries, not an external type-checker result. The function begins with the required id and adds nickname only if the caller supplied one. We do not store a None placeholder. That gives the first record exactly one key and the second record two keys. The return annotation tells a static checker what the function promises.
- 03:25
If we accidentally returned a list, used a misspelled key, or assigned an integer nickname in typed source, a checker could flag it. Python itself will not perform that check when the function returns. The runtime branch matters for our observable behavior: it determines which fields appear in the actual dictionary. This is why a type hint and a function implementation are complementary. Use this pattern when serializing optional fields to JSON or passing a record through several functions. If your interface treats an empty string as absent too, add that rule explicitly; the code here preserves an empty string because it is still a supplied string.
- 04:08
Do not silently redefine the user's input while following an annotation. The first two lines show that an optional key can be read without a KeyError. The fallback `user-7` is an application decision, not a TypedDict feature. We use direct indexing for id only because the contract requires that key; truly untrusted input should be checked before it reaches this code. The last line is intentional: Python accepts the wrong string id at runtime and prints `dict seven`. This is the precise boundary of TypedDict. It improves maintainability when combined with a static checker, but it does not parse and validate an API response.
- 04:50
Before trusting outside data, check that it is a dict, that required keys exist, and that values have acceptable types and ranges. Then pass the validated value into code annotated with Profile. On the lesson page, complete the task with both nickname paths, inspect the key sets, and answer the quiz. If you remember only one distinction, remember that an optional key is not the same as a required key whose value can be None.
Understand the concept
Python TypedDict describes the expected shape of a plain dictionary for a static type checker. It is useful when records come from JSON, API responses, or a legacy dictionary interface and a full class would add little value. A TypedDict value remains an ordinary dict at runtime. The annotation helps a checker find misspelled keys and wrong value types during development; it does not validate incoming data when the program runs.
By default, keys declared in a TypedDict are required. Mark a potentially absent key with NotRequired, available from typing in Python 3.11 and newer. Required is useful inside a total=False TypedDict when most fields may be omitted. The __required_keys__ and __optional_keys__ attributes show the declared policy. total=False alone means every field declared in that class body is optional; it does not mean that a value may be None.
A common error is to mark nickname as str and then create a record without it. A static checker should flag that omission, but Python still constructs a dict. Another error is to index an optional key directly: profile['nickname'] may raise KeyError. Use profile.get('nickname') with a deliberate fallback, or test membership before indexing. Do not confuse an absent field with a present field whose value is None; they carry different information.
The practical task defines Profile with a required integer id and optional string nickname, and returns a new profile from make_profile. An omitted nickname must stay absent rather than be stored as None. The browser checks the declared key sets and the function's runtime outputs; a separate static type checker is still needed to validate source annotations across a real project. If the input comes from an untrusted API, validate its data explicitly before treating it as a Profile.
- TypedDict describes dict shape to static checkers
- NotRequired marks a key that may be absent
- Missing differs from a value of None
- Annotations do not validate runtime input
See it step by step
Read the code, predict the output, then compare it with the result.
01. Inspect required and optional keys
from typing import TypedDict, NotRequired
class Profile(TypedDict):
id: int
nickname: NotRequired[str]
print(sorted(Profile.__required_keys__))
print(sorted(Profile.__optional_keys__))['id']
['nickname']The key sets make the schema's presence rules explicit.
02. Read an optional field safely
from typing import TypedDict, NotRequired
class Profile(TypedDict):
id: int
nickname: NotRequired[str]
def label(profile: Profile) -> str:
return profile.get('nickname', f"user-{profile['id']}")
print(label({'id': 7, 'nickname': 'Ada'}))
print(label({'id': 7}))Ada
user-7get handles absence; the fallback is application policy.
03. See the runtime boundary
from typing import TypedDict
class Profile(TypedDict):
id: int
record = Profile(id='wrong type')
print(type(record).__name__)
print(record['id'])dict
wrong typeConstruction does not validate input; run a static checker and validate external data separately.
A closer look
Follow the reasoning, inspect each result, then try the suggested changes in the console below.
Define the dictionary shape
Start with a normal record containing an id and a possibly absent nickname. TypedDict adds a precise annotation without changing that record into a new runtime object. It fits a JSON-shaped mapping that existing code already uses.
The first run prints required and optional key sets. If both fields appear required, the schema does not match the input contract. Use NotRequired for the field that may truly be omitted; use a nullable value type only when the key is always present but may contain None.
from typing import TypedDict, NotRequired
class Profile(TypedDict):
id: int
nickname: NotRequired[str]
print(sorted(Profile.__required_keys__))
print(sorted(Profile.__optional_keys__))['id']
['nickname']- Run the key-set inspection.
- Predict which keys may be omitted before reading the output.
Handle an absent key
The annotation makes a promise about the shape, but application code still needs a branch for a missing nickname. Direct indexing is appropriate for id because the schema requires it. For nickname, membership testing or get expresses the fallback deliberately.
An absent nickname is not the same as a nickname key with a None value. The former means the field was not supplied; the latter is a supplied value that violates the declared str type. The task constructs only the fields that are present, so its result is easy to serialize back to JSON.
profiles = [{'id': 7}, {'id': 8, 'nickname': 'Ada'}]
for profile in profiles:
print(profile.get('nickname', f"user-{profile['id']}"))user-7
Ada- Run the two profile values.
- Inspect which dictionary contains nickname rather than comparing only display text.
Separate typing from validation
TypedDict is aimed at static checking. Calling the TypedDict constructor does not enforce that an id is an integer. A program receiving untrusted JSON must inspect its fields and values at runtime, then return a typed record only after that check succeeds.
Use a static checker in a local project to catch assignment and missing-key mistakes during development. The browser task verifies runtime behavior and the declared key sets, not whether an external type checker has been run. Test both the omitted and supplied nickname paths so no None placeholder leaks into a result.
from typing import TypedDict
class Profile(TypedDict):
id: int
record = Profile(id='seven')
print(type(record).__name__)
print(record['id'])dict
seven- Run a deliberately incorrect runtime value.
- Explain why a successful run does not prove the type annotation is satisfied.
Try it in Python
Edit the example and run it. Python starts in your browser the first time you click Run.
Python console
Ready to runNeed input()? Add one value per line
Your output appears here.
Need a hint?
Annotate nickname with NotRequired[str]. Build a dict containing id, then add nickname only if the argument is not None.
Complete the task and select Check task to verify your code.
Quick quiz
Three questions. You can change your answers and try again.
Typical mistakes
Everyone meets these errors. See what causes them and how to fix them.
Treating NotRequired as nullable
nickname: str | Nonenickname: NotRequired[str]What happens: A checker still expects the key to be present.
A nullable value and an absent key are different contracts.
Assuming a runtime validator
profile = Profile(id='seven')Validate untrusted input, then annotate the result as ProfileWhat happens: This constructs a dict despite the incorrect value type.
TypedDict supports static analysis but does not reject bad runtime values.