Check a mapping shape before treating it as TypedDict-like
Also published in our Blogger archive.
TypedDict makes a mapping shape visible to type checkers, but it does not validate runtime input. At runtime, a TypedDict instance is an ordinary dict, so a boundary function must perform the checks its application needs. parse_job() first accepts only collections.abc.Mapping objects, then requires exactly the name and attempt keys. It obtains those values only after the shape check and returns a normal dictionary matching the Job annotation.
For the accepted input, the result is {"name": "compile", "attempt": 2}. A missing key is rejected as an unexpected shape. A boolean attempt is rejected because this contract uses type(attempt) is int; that deliberately excludes True, even though bool subclasses int. Choose isinstance instead if subclasses are valid for your own format.
Exact-key validation intentionally rejects extra fields. That is appropriate for a closed record but not for an extensible payload, where the check should instead allow documented optional or future keys. These checks do not sanitize values, prove a mapping is trustworthy, or replace authorization decisions.
TypedDict was added in Python 3.8. The typing documentation explains its static-only expectations, and the collections.abc documentation defines the Mapping interface used for the first runtime check.
AI-assistance disclosure: this article was drafted with AI assistance and should be adapted to the payload schema it protects.
from collections.abc import Mapping
from typing import TypedDict
class Job(TypedDict):
name: str
attempt: int
def parse_job(candidate: object) -> Job:
if not isinstance(candidate, Mapping):
raise ValueError("job must be a mapping")
if set(candidate) != {"name", "attempt"}:
raise ValueError("job has unexpected shape")
name = candidate["name"]
attempt = candidate["attempt"]
if type(name) is not str or type(attempt) is not int:
raise ValueError("job has invalid value types")
return {"name": name, "attempt": attempt}
job = parse_job({"name": "compile", "attempt": 2})
assert job == {"name": "compile", "attempt": 2}
for candidate in ({"name": "compile"}, {"name": "compile", "attempt": True}):
try:
parse_job(candidate)
except ValueError as error:
print(error)
print(f"job={job['name']} attempt={job['attempt']}")
job has unexpected shape
job has invalid value types
job=compile attempt=2