Python Type Hints
Tutorial 41 of 65 · pythondeck.com Python course
Annotations describe parameter and return types: def f(x: int) -> str:. They are optional at runtime but checked by tools like mypy, pyright or pyre. Use typing for generics; from 3.9+ you can write list[int], dict[str, int] directly.
Type hints annotate intent for static checkers (mypy, Pyright) and IDEs; they do not enforce types at runtime by default. They improve maintainability in larger codebases and document function contracts. Python 3.9+ prefers built-in generics (list[int]) over typing.List.
Gradual typing lets you add hints incrementally. typing.Protocol, TypedDict, unions (X | Y), and Optional model real-world APIs without sacrificing duck typing at runtime.
Function annotations: parameters and return types after definitions.
Generics: list[str], dict[str, int], Sequence[T].
Optional[T] and unions str | None for nullable values.
typing.Protocol for structural interfaces; TypedDict for dict shapes.
type statement (3.12+) for aliases; Literal, Final, ClassVar.
Running mypy or Pyright in CI alongside tests.
Runtime cost of hints is negligible—they are stored in __annotations__ and ignored by the interpreter unless you use typing.get_type_hints or pydantic. Forward references as strings ("Node") solve mutual recursion; from __future__ import annotations postpones evaluation.
Strict optional checking catches None mishandling early. Align hint strictness with team policy—full strictness helps libraries; apps may hint only public boundaries.
Pydantic and dataclasses use hints for validation and serialization—hints become more than documentation when those tools are in play.
Expecting hints to enforce types at runtime without a validator or checker.
Using Any everywhere, defeating the purpose of gradual typing.
Mutable class attributes hinted as list without ClassVar confusion in dataclasses.
Outdated typing.List in new 3.9+ code when list suffices.
Hints that lie (wrong return type) worse than no hints—erodes trust.
Add hints to public functions first; run mypy in CI with a sensible strictness level.
Use modern syntax (X | Y, builtin generics) on supported Python versions.
Prefer Protocols and TypedDicts for flexible structural types.
Keep hints accurate; update them when refactoring function contracts.
Re-read the examples below with these ideas in mind; change variable names and inputs to match your own project.
The program below demonstrates function. Read the comments on each line, run the code, then change names or values to see how the output shifts.
# Example: Function
# Run in the REPL or save as a .py file and execute with python.
def add(a: int, b: int) -> int:
return a + b
print(add(2, 3))
This sample walks through containers / optional in a small, runnable script. Paste it into the REPL or save it as a .py file before you continue to the next block.
# Example: Containers / optional
# Run in the REPL or save as a .py file and execute with python.
from typing import Optional
def find(name: str, names: list[str]) -> Optional[int]:
return names.index(name) if name in names else None
print(find("Ada", ["Grace", "Ada"]))
Here is a hands-on illustration of dataclass typed. Follow the inline comments first; only then execute the snippet and compare the result with what you expected.
# Example: dataclass typed
# Run in the REPL or save as a .py file and execute with python.
from dataclasses import dataclass
@dataclass
class Item:
name: str
price: float
tags: list[str]
print(Item("pen", 1.5, ["office", "writing"]))
The program below demonstrates typed function. Read the comments on each line, run the code, then change names or values to see how the output shifts.
# Annotations document intent; they are not enforced at runtime
def greet(name: str, excited: bool = False) -> str: # hinted signature
msg = f"Hello, {name}" # build message
return msg + "!" if excited else msg # return str
print(greet("Ada", excited=True)) # Hello, Ada!
def total(values: list[float]) -> float: # homogeneous list
return sum(values) # numeric aggregation
print(total([1.5, 2.5])) # 4.0
from typing import Optional # optional value
def find(items: list[int], target: int) -> Optional[int]: # index or None
return items.index(target) if target in items else None
This sample walks through typed dict in a small, runnable script. Paste it into the REPL or save it as a .py file before you continue to the next block.
# TypedDict and generics help static checkers (mypy/pyright)
from typing import TypedDict # structured dict typing
class User(TypedDict): # keys with value types
name: str # required key
score: int # required key
def save(user: User) -> None: # accept typed mapping
print(user["name"], user["score"]) # key access
save({"name": "Grace", "score": 97}) # valid dict
from typing import TypeVar, Sequence # generic sequence
T = TypeVar("T") # type variable
def first(seq: Sequence[T]) -> T: # generic first element
return seq[0] # assumes non-empty
print(first([10, 20])) # 10