Python type hints are annotations that describe the expected types of variables, function parameters, and return values. They are purely informational at runtime — Python ignores them and does not enforce them — but they are actively used by IDEs (for autocompletion and error highlighting), static type checkers (mypy, pyright), and libraries that read them at runtime (Pydantic, FastAPI). FastAPI is built on the premise that type hints carry enough information to validate request data, generate API documentation, and serialise responses automatically. Adding type hints to your FastAPI code is not optional decoration — it is how FastAPI knows what to do with your data.
Annotating Variables and Functions
# ── Variable annotations ───────────────────────────────────────────────────────
name: str = "Alice"
age: int = 30
score: float = 9.5
is_active: bool = True
# Annotation without assignment (declares but does not initialise)
user_id: int # just the type — no value yet
# ── Function parameter and return type hints ───────────────────────────────────
def greet(name: str, times: int = 1) -> str:
return (f"Hello, {name}! " * times).strip()
def add(a: int, b: int) -> int:
return a + b
def get_user(user_id: int) -> dict:
return {"id": user_id, "name": "Alice"}
# Function that returns nothing
def log_event(message: str) -> None:
print(f"[LOG] {message}")
# ── Class attribute annotations ───────────────────────────────────────────────
class Post:
id: int
title: str
body: str
published: bool = False
view_count: int = 0
def __init__(self, id: int, title: str, body: str) -> None:
self.id = id
self.title = title
self.body = body
def publish(self) -> None:
self.published = True
def word_count(self) -> int:
return len(self.body.split())
greet(123, "not-an-int") will not raise a TypeError — Python will try to run the code with those values. The hints are for developer tools. The exceptions are Pydantic models and FastAPI route handlers — these actively read type hints at runtime and enforce them. This distinction is why you can have a type hint of int on a parameter but still pass a string in plain Python, while the same type hint on a FastAPI route parameter causes an automatic 422 validation error.-> None explicitly on functions that do not return a value — it makes intent clear and helps mypy catch bugs where you accidentally return a value from a function that should not. Use -> None on __init__ methods (constructors always return None). Leave the return type unannotated only when the return type is genuinely complex to express — prefer explicit annotations everywhere.def process(data: dict) -> list: says the parameter is a dict and returns a list, but says nothing about what keys the dict must have or what items the list contains. Use more specific types: dict[str, int], list[Post], or better yet, Pydantic models that document the full structure and validate it. In FastAPI, imprecise type hints produce imprecise API documentation.Python 3.10+ Modern Syntax
# Python 3.10+ allows | for unions and built-in generics without importing typing
# This is the modern style — use it for Python 3.10+ projects
# Old style (all Python 3.x)
from typing import Optional, Union, List, Dict, Tuple
def old_style(
name: Optional[str], # Optional[X] = Union[X, None]
items: List[str],
scores: Dict[str, int],
pair: Tuple[int, str],
value: Union[int, float],
) -> Optional[str]:
...
# New style (Python 3.10+) — no typing import needed for basic cases
def new_style(
name: str | None, # | None replaces Optional
items: list[str], # built-in list works directly
scores: dict[str, int], # built-in dict works directly
pair: tuple[int, str], # built-in tuple works directly
value: int | float, # | replaces Union
) -> str | None:
...
# Python 3.9+ — built-in list, dict, tuple work in hints (no import needed)
# Python 3.10+ — | syntax for unions works (no Union import needed)
# For type aliases
PostId = int
UserName = str
def get_post(post_id: PostId) -> dict:
...
Type Hints at Runtime
import inspect
# Access type hints at runtime
def process(name: str, count: int) -> bool:
return len(name) > count
# Python stores hints as __annotations__
print(process.__annotations__)
# {"name": <class "str">, "count": <class "int">, "return": <class "bool">}
# typing.get_type_hints() resolves forward references
import typing
hints = typing.get_type_hints(process)
# {"name": str, "count": int, "return": bool}
# FastAPI reads these annotations to know what types to expect
# Pydantic reads them to build validation schemas
# mypy reads them for static checking (never at runtime)
Common Mistakes
Mistake 1 — Thinking type hints prevent wrong types at runtime
❌ Wrong — assuming hints enforce types:
def add(a: int, b: int) -> int:
return a + b
result = add("hello", " world") # no error! Returns "hello world" (str + str)
print(result) # "hello world" — type hints were ignored by Python
✅ Correct — use Pydantic or explicit validation if enforcement is needed.
Mistake 2 — Using string annotations when the class is not yet defined
❌ Wrong — NameError if class is used before it is defined:
class Node:
def add_child(self, child: Node): # NameError — Node not defined yet!
...
✅ Correct — use a string (forward reference) or from __future__ import annotations:
from __future__ import annotations # makes ALL annotations strings lazily
class Node:
def add_child(self, child: Node): # ✓ works with the future import
...
# Or without the import: def add_child(self, child: "Node"): ...
Mistake 3 — Imprecise dict/list types in FastAPI models
❌ Wrong — opaque types produce poor API docs:
class PostCreate(BaseModel):
metadata: dict # no idea what keys/values are expected
✅ Correct — specific types or nested Pydantic models:
class PostCreate(BaseModel):
metadata: dict[str, str] = {} # ✓ string keys and values
Quick Reference
| Type | Modern syntax (3.10+) | Old syntax |
|---|---|---|
| Optional string | str | None |
Optional[str] |
| String or int | str | int |
Union[str, int] |
| List of strings | list[str] |
List[str] |
| Dict str→int | dict[str, int] |
Dict[str, int] |
| Tuple (int, str) | tuple[int, str] |
Tuple[int, str] |
| No return value | -> None |
-> None |
| Any type | Any (from typing) |
Any |
| Forward reference | "ClassName" or future import |
same |