The essentials
Quick reference
One focused task per row. Jump to the related section for complete, working examples.
| Use | Syntax | Examples |
|---|---|---|
| Annotate a function | def total(values: list[float]) -> float: ... | View examples |
| No return value | def log(message: str) -> None: ... | View examples |
| Union type | identifier: int | str | View examples |
| Optional value | name: str | None = None | View examples |
| Narrow by runtime type | if isinstance(value, str): return value.casefold() | View examples |
| Read-only collection input | def first(values: Sequence[str]) -> str: ... | View examples |
| Read-only mapping input | def render(values: Mapping[str, object]) -> str: ... | View examples |
| Callable signature | transform: Callable[[str], str] | View examples |
| Declare a type alias | type UserId = int | View examples |
| Typed dictionary shape | class UserRow(TypedDict): ... | View examples |
| Generic type parameter | def first[T](values: Sequence[T]) -> T: ... | View examples |
| Structural protocol | class SupportsClose(Protocol): ... | View examples |
| Runtime protocol check | @runtime_checkable | View examples |
| User-defined narrowing | def is_str_list(value: list[object]) -> TypeGuard[list[str]]:
... | View examples |
Type hints describe expected values for static analyzers, IDEs, and readers; Python normally does not enforce them at runtime. Annotate public boundaries and domain concepts, prefer precise structural interfaces over broad object types, and keep annotations maintainable enough that the checker remains trusted.
Step by step
Detailed examples
Annotate contracts, not implementation trivia
Annotations are stored for introspection and consumed by external tools; calls still accept arbitrary values unless code or a framework validates them. Use None for procedures, avoid Any where a real interface exists, and annotate public functions before local variables whose types are already obvious.
def mean(values: list[float]) -> float:
if not values:
raise ValueError('values cannot be empty')
return sum(values) / len(values)
print(mean([2.0, 4.0, 6.0]))
print(mean.__annotations__) 4.0
{'values': list[float], 'return': <class 'float'>}Narrow alternatives before using specific operations
A union permits only operations valid for every member until control flow narrows it. isinstance, identity comparison with None, literal comparisons, match patterns, and TypeGuard functions can narrow. Do not use casts to silence evidence of a real unchecked boundary; validate external data first.
def normalize(value: int | str | None) -> str:
if value is None:
return 'missing'
if isinstance(value, int):
return str(value)
return value.casefold()
print([normalize(value) for value in [42, 'READY', None]]) ['42', 'ready', 'missing']Accept abstract read-only interfaces when mutation is unnecessary
Sequence and Mapping widen callers while documenting supported operations. Mutable containers are invariant in common checkers because writing a broader type could violate a narrower collection. Callable describes argument and result types; Protocol is better when callbacks have named attributes or overloaded call forms.
from collections.abc import Callable, Sequence
def transform_all(values: Sequence[str], transform: Callable[[str], str]) -> list[str]:
return [transform(value) for value in values]
print(transform_all((' Ada ', ' Lin '), str.strip)) ['Ada', 'Lin']Distinguish aliases, new domain types, and mapping records
A type alias gives a readable alternate spelling but does not create a distinct runtime type. NewType creates a checker-visible distinction with near-zero runtime wrapping cost. TypedDict describes dictionary keys and value types; it performs no runtime validation and is best for dict-shaped compatibility boundaries.
from typing import NotRequired, TypedDict
class UserRow(TypedDict):
id: int
name: str
nickname: NotRequired[str]
row: UserRow = {'id': 7, 'name': 'Ada'}
print(row['name'])
print(type(row).__name__) Ada
dictRelate types across a reusable operation
A generic parameter preserves relationships that object or Any would erase, such as returning the same element type accepted. Modern parameter syntax requires Python 3.12; TypeVar supports older versions. Bounds require subtype compatibility, while constraints choose from a closed set of alternatives.
from collections.abc import Sequence
from typing import TypeVar
T = TypeVar('T')
def first(values: Sequence[T]) -> T:
if not values:
raise ValueError('empty sequence')
return values[0]
print(first([10, 20]))
print(first(('a', 'b'))) 10
aExpress behavior structurally
A Protocol declares members an implementation must provide without requiring inheritance or registration. This reduces coupling for repositories, streams, clocks, and adapters. runtime_checkable tests only attribute presence and does not validate signatures or type arguments, so it is not a full runtime contract checker.
from typing import Protocol
class SupportsClose(Protocol):
def close(self) -> None: ...
class Resource:
def close(self) -> None:
print('closed')
def finish(resource: SupportsClose) -> None:
resource.close()
finish(Resource()) closedLocal code tester
Explore structural typing
Define a protocol and use an unrelated implementation that satisfies the same behavior.
Press Run to load Python locally.
Sources and further reading
References
Authoritative documentation used to verify and expand this cheat sheet.
Help us improve
Found a typo or missing example?
Tell us what would make this cheat sheet clearer, more complete, or more useful.



