Check a mapping shape before treating it as TypedDict-like

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

Comments

Popular posts from this blog

Compare Two Text Files in Python Without Modifying Them

Seven CSV Quality Checks to Run Before Importing Data

Explain why SQLite transactions cannot make an HTTP call atomic