Type Hints — The Complete Notebook
Comprehensive guide on Type Hints — The Complete Notebook.
Type Hints
1. Overview#
Python remains dynamically typed at runtime — type hints don't change execution — but they let tools like mypy, IDEs, and pydantic (used heavily by FastAPI) catch mistakes before code runs and make function contracts explicit for other developers.
Type hints are purely optional and ignored by the Python interpreter at runtime (with a few framework exceptions like Pydantic/FastAPI, which do read them to validate data).
2. Basic Type Hints#
🐍 PythonInteractive WebAssemblydef greet(name: str) -> str:
return f"Hello, {name}"
age: int = 25
price: float = 19.99
is_active: bool = True
tags: list = ["python", "ai"]
2.1 Variable Annotations#
🐍 PythonInteractive WebAssemblycount: int
count = 0
user_id: str = "u-1234"
3. Generic Collection Types#
Since Python 3.9, built-in collections support subscripting directly — no need to import List/Dict from typing anymore.
🐍 PythonInteractive WebAssemblydef get_names() -> list[str]:
return ["Asha", "Ravi"]
def get_scores() -> dict[str, int]:
return {"Asha": 92, "Ravi": 85}
def get_unique_tags() -> set[str]:
return {"python", "ai"}
def get_coordinates() -> tuple[float, float]:
return (12.9716, 77.5946)
Old style (typing, pre-3.9) | Modern style (3.9+) |
|---|---|
List[str] | list[str] |
Dict[str, int] | dict[str, int] |
Tuple[int, int] | tuple[int, int] |
Set[str] | set[str] |
4. Optional and Union#
🐍 PythonInteractive WebAssemblyfrom typing import Optional, Union
def find_user(user_id: str) -> Optional[dict]:
# Optional[dict] means "dict OR None"
return database.get(user_id) # might return None if not found
def parse_id(value: Union[str, int]) -> int:
# Union[str, int] means "str OR int"
return int(value)
4.1 Modern | Syntax (Python 3.10+)#
🐍 PythonInteractive WebAssemblydef find_user(user_id: str) -> dict | None:
return database.get(user_id)
def parse_id(value: str | int) -> int:
return int(value)
5. Any, None, and Function Signatures#
🐍 PythonInteractive WebAssemblyfrom typing import Any
def process(data: Any) -> None:
# Any tells the type checker "skip checking this — could be anything"
# -> None means the function doesn't return a meaningful value
print(data)
Overusing
Anydefeats the purpose of type hints — it's an escape hatch, not a default. Reach for a precise type, or aUnion, before falling back toAny.
6. TypedDict — Typed Dictionaries#
Useful for describing the shape of dict-like data (e.g., JSON payloads) without creating a full class.
🐍 PythonInteractive WebAssemblyfrom typing import TypedDict
class UserPayload(TypedDict):
name: str
age: int
email: str
def create_user(data: UserPayload) -> None:
print(data["name"], data["age"])
create_user({"name": "Kamal", "age": 30, "email": "kamal@example.com"})
6.1 Optional Keys with TypedDict#
🐍 PythonInteractive WebAssemblyfrom typing import TypedDict, NotRequired
class UserPayload(TypedDict):
name: str
age: int
email: NotRequired[str] # this key can be omitted (Python 3.11+)
7. dataclass vs TypedDict vs NamedTuple#
| Tool | Backed By | Mutable | Best For |
|---|---|---|---|
@dataclass | Regular class | Yes (by default) | Objects with behavior/methods |
TypedDict | dict | Yes | Typing raw JSON/dict-shaped data |
NamedTuple | tuple | No | Lightweight, immutable records |
🐍 PythonInteractive WebAssemblyfrom typing import NamedTuple
class Point(NamedTuple):
x: float
y: float
p = Point(3.0, 4.0)
print(p.x, p.y) # 3.0 4.0
8. Callable — Typing Functions as Arguments#
🐍 PythonInteractive WebAssemblyfrom typing import Callable
def apply_operation(a: int, b: int, operation: Callable[[int, int], int]) -> int:
# Callable[[int, int], int] = "a function taking two ints, returning an int"
return operation(a, b)
def add(a: int, b: int) -> int:
return a + b
result = apply_operation(3, 4, add)
9. Generics with TypeVar#
Lets you write functions/classes that work with multiple types while still preserving type relationships.
🐍 PythonInteractive WebAssemblyfrom typing import TypeVar
T = TypeVar("T")
def first_item(items: list[T]) -> T:
return items[0]
print(first_item([1, 2, 3])) # inferred as int
print(first_item(["a", "b", "c"])) # inferred as str
9.1 Generic Classes#
🐍 PythonInteractive WebAssemblyfrom typing import Generic, TypeVar
T = TypeVar("T")
class Stack(Generic[T]):
def __init__(self) -> None:
self._items: list[T] = []
def push(self, item: T) -> None:
self._items.append(item)
def pop(self) -> T:
return self._items.pop()
int_stack: Stack[int] = Stack()
int_stack.push(1)
int_stack.push(2)
10. Protocol — Structural Typing ("Duck Typing" Made Explicit)#
A Protocol defines a required shape (methods/attributes) instead of a required inheritance chain — any object matching the shape satisfies the type, even without extending the Protocol class.
🐍 PythonInteractive WebAssemblyfrom typing import Protocol
class SupportsSpeak(Protocol):
def speak(self) -> str:
...
class Dog:
def speak(self) -> str:
return "Woof!"
class Robot:
def speak(self) -> str:
return "Beep boop!"
def announce(entity: SupportsSpeak) -> None:
print(entity.speak())
announce(Dog()) # valid — has a matching speak() method
announce(Robot()) # also valid — neither class inherits from SupportsSpeak
11. Type Checking with mypy#
Type hints are only enforced if you run a checker — Python itself ignores them at runtime.
bashpip install mypy mypy my_script.py
🐍 PythonInteractive WebAssemblydef add(a: int, b: int) -> int:
return a + b
add("2", "3") # mypy flags this as an error; Python itself would still run it fine
12. Common Pitfalls#
INCORRECT: Treating Type Hints as Runtime Validation#
🐍 PythonInteractive WebAssemblydef process(age: int):
return age * 2
process("25") # No error at runtime! Type hints alone don't enforce anything.
CORRECT: Fix — Validate Explicitly, or Use Pydantic#
🐍 PythonInteractive WebAssemblyfrom pydantic import BaseModel
class UserInput(BaseModel):
age: int # Pydantic DOES enforce this at runtime, raising a validation error
UserInput(age="25") # Pydantic coerces "25" -> 25 automatically
UserInput(age="abc") # raises a ValidationError
INCORRECT: Overusing Any Everywhere#
Defeats the entire purpose — a codebase full of Any gets none of the benefits of static analysis.
13. Summary & Best Practices Checklist#
- Use built-in generics (
list[str],dict[str, int]) overtyping.List/Dicton Python 3.9+. - Use
X | None(3.10+) orOptional[X]for values that might be missing. - Reach for
TypedDictwhen typing raw dict/JSON shapes, and@dataclasswhen you need behavior too. - Use
Protocolfor flexible, duck-typed interfaces instead of forcing inheritance. - Run
mypyin CI if your team relies on type hints for safety — hints alone don't stop bad data at runtime. - Use Pydantic models (not bare type hints) wherever you need runtime validation, such as API request bodies.
Python Type Hints & Static Typing Checkpoint
Finished studying this notebook?
Mark this guide as completed to update your course progress roadmap.