Learn the concept
Type hints describe expected inputs and outputs for readers and static analysis tools. Python does not automatically enforce annotations at runtime. Continue validating values at boundaries such as JSON parsing or user input.
An optional value represents a meaningful absence. Use str | None when a value may be missing and handle that branch explicitly. Avoid pretending that an empty string and no value are equivalent unless your domain says so.
Typed containers describe their elements, such as list[str]. Small typed functions make transformations easier to follow. Static analysis can find inconsistent uses before execution, while tests and runtime checks address actual behavior. These methods complement each other rather than replace one another.
Run and inspect
from __future__ import annotations
def label(document_id: str, title: str | None) -> str:
if title is None:
return f"{document_id}: untitled"
return f"{document_id}: {title}"
assert label("d1", None) == "d1: untitled"
assert label("d1", "") == "d1: "
Your exercise
Annotate a parsing function that can return a missing title. Create tests for None, empty text, and a valid title. Explain which invalid values annotations alone would not stop.
Check your understanding
The missing-value behavior is explicit and you do not claim that an annotation validates incoming JSON.
Guided workshop: design a function boundary
Type hints describe a contract for readers and static tools. They do not validate external JSON at runtime. A useful parsing function combines a documented return type with explicit checks, then passes clean values to simpler business logic.
# academy-check
from dataclasses import dataclass
@dataclass(frozen=True)
class Document:
id: str
text: str
def parse_document(value: object) -> Document:
if not isinstance(value, dict):
raise ValueError("record must be an object")
identifier, text = value.get("id"), value.get("text")
if not isinstance(identifier, str) or not identifier.strip():
raise ValueError("id must be nonempty text")
if not isinstance(text, str) or not text.strip():
raise ValueError("text must be nonempty text")
return Document(identifier.strip(), text.strip())
assert parse_document({"id": " a ", "text": " Hello "}) == Document("a", "Hello")
The function accepts object because any decoded value might arrive. It narrows that value through checks and returns one predictable type. The dataclass is introduced here as a compact record; the objects module explains generated methods and immutability in detail.
Exercise: split a small application into contracts.py, which defines and validates records, and main.py, which reads inputs and prints outcomes. Guard the executable entry with if __name__ == "__main__":. Importing contracts must not read files, prompt for input, or launch the application.
Answer walkthrough: keep parse_document independent of the terminal. Test it by calling it with a list, an empty dictionary, a numeric ID, whitespace-only text, and a valid record. Only the last input succeeds. A caller catches validation errors per record and continues; filesystem failure belongs to the outer application boundary.
Checkpoint: distinguish None (no value) from "" (an empty string). A str | None annotation requires a decision before treating the result as a string. Never make a required ID optional merely to silence a type checker. Save tests of both valid and invalid boundaries.