Understand the contract
An annotation describes the type a name or function is intended to accept. The colon follows a parameter or variable; the arrow introduces a return annotation. list[int] means a list whose elements are integers. Python 3.11 supports these built-in generic forms without importing List.
Static checking is a separate activity performed by an editor or type checker. Python does not enforce type hints at runtime. An annotated function can receive another type, and its ordinary operations determine whether that call succeeds or fails. An annotation is neither parsing nor input validation.
Follow the runtime behavior
A union such as str | None permits either alternative. Test for None before calling string methods so both a reader and a checker can follow the narrower branch. A return annotation of None describes a function used for its side effects.
Prefer precise parameter and result contracts at public boundaries. Any disables many static checks; it is not a request to validate arbitrary values. Use explicit checks when data originates outside your program.
Apply it and practise
Use hints to document a data-processing function before it is shared with teammates. Keep validation separate so invalid external values produce intentional errors rather than accidental operator failures.
Exercise: write first_positive(values: list[int]) -> int | None. Return the first positive item, or None. Try an empty list and [-2, 0, 5], and explain why callers must handle the missing result.
Syntax
def total(prices: list[int]) -> int:
return sum(prices)Examples
Annotate a total
Use this contract for a basket total measured in whole units.
def total(prices: list[int]) -> int:
return sum(prices)
print(total([3, 7, 2]))- prices is annotated as a list of integers and the arrow describes the returned integer. sum performs the calculation when total is called; the annotation itself performs no iteration.
- Use this contract for a basket total measured in whole units.
- The contract does not reject a list of floats at runtime; validate incoming values if whole units are required.
- Expected output: 12
Handle an optional value
Use an optional parameter when a profile field may genuinely be absent.
def label(name: str | None) -> str:
if name is None:
return "Guest"
return name.upper()
print(label(None))
print(label("Ada"))- The identity check returns early for None. Only the remaining string branch reaches upper, so the method call is safe for the declared input types.
- Use an optional parameter when a profile field may genuinely be absent.
- None is different from an empty string; an empty string takes the string branch and remains empty.
- Expected output: Guest ADA
Observe the runtime boundary
Use this experiment to distinguish documentation and static diagnostics from runtime enforcement.
def repeat(value: int) -> int:
return value * 2
print(repeat(4))
print(repeat("ha"))- The second call deliberately violates the hint. Python still executes multiplication, and multiplying a string by two repeats it rather than producing an integer.
- Use this experiment to distinguish documentation and static diagnostics from runtime enforcement.
- A type checker should report the second call. This is a demonstration, not a recommended way to write a correctly typed caller.
- Expected output: 8 haha
Best practices
- Prefer precise parameter and result contracts at public boundaries. Any disables many static checks; it is not a request to validate arbitrary values. Use explicit checks when data originates outside your program.
- Use hints to document a data-processing function before it is shared with teammates. Keep validation separate so invalid external values produce intentional errors rather than accidental operator failures.
Runtime & versions
Python interpreter. Feature availability depends on the language and runtime version used by your project; consult the official reference below.
Python language reference