Docs
type hints
🏷️ Type Hints and Static Typing in Python
📌 What You'll Learn
- •What type hints are and why they matter
- •Basic and advanced type annotations
- •Generics, TypeVar, and Protocol
- •Using mypy for static type checking
- •Best practices for typed Python code
🔍 What are Type Hints?
Type hints (introduced in Python 3.5) add optional static typing to Python. They don't change how your code runs but provide metadata for tools and humans.
# Without type hints
def greet(name):
return f"Hello, {name}"
# With type hints - much clearer!
def greet(name: str) -> str:
return f"Hello, {name}"
Why Use Type Hints?
- •Better IDE support - Autocomplete, refactoring, error detection
- •Self-documenting code - Types tell you what functions expect
- •Bug prevention - Catch type errors before runtime
- •Team collaboration - Clear interfaces between components
- •Easier refactoring - IDE can find all usages safely
📝 Basic Type Hints
Variable Annotations
# Basic types
name: str = "Alice"
age: int = 30
price: float = 19.99
is_active: bool = True
# Python 3.9+ - use built-in types directly
numbers: list[int] = [1, 2, 3]
scores: dict[str, int] = {"math": 95, "english": 88}
unique_ids: set[str] = {"a", "b", "c"}
coords: tuple[float, float] = (3.14, 2.71)
# Python 3.8 and earlier - use typing module
from typing import List, Dict, Set, Tuple
numbers: List[int] = [1, 2, 3]
scores: Dict[str, int] = {"math": 95}
Function Annotations
def add(a: int, b: int) -> int:
"""Add two integers."""
return a + b
def greet(name: str) -> str:
"""Return a greeting message."""
return f"Hello, {name}!"
def process_items(items: list[str]) -> None:
"""Process items (returns nothing)."""
for item in items:
print(item)
# Multiple return types with tuple
def divmod_custom(a: int, b: int) -> tuple[int, int]:
return a // b, a % b
🔀 Union Types and Optional
Union Types (Multiple Possible Types)
# Python 3.10+ syntax
def process(value: int | str) -> str:
return str(value)
# Python 3.9 and earlier
from typing import Union
def process(value: Union[int, str]) -> str:
return str(value)
# Common use: accepting multiple types
def get_length(item: str | list | tuple) -> int:
return len(item)
Optional Types (Can be None)
from typing import Optional
# Optional[X] is shorthand for X | None
def find_user(user_id: int) -> Optional[dict]:
"""Return user dict or None if not found."""
users = {1: {"name": "Alice"}, 2: {"name": "Bob"}}
return users.get(user_id)
# Python 3.10+ - cleaner syntax
def find_user(user_id: int) -> dict | None:
users = {1: {"name": "Alice"}, 2: {"name": "Bob"}}
return users.get(user_id)
# Important: Optional doesn't mean "optional parameter"!
# This parameter is required but can be None:
def set_name(name: Optional[str]) -> None:
pass
# This parameter is optional (has default):
def set_name(name: str = "Unknown") -> None:
pass
📦 Collection Types
Lists, Sets, and Dictionaries
# Homogeneous collections
names: list[str] = ["Alice", "Bob", "Charlie"]
unique_names: set[str] = {"Alice", "Bob"}
scores: dict[str, int] = {"Alice": 95, "Bob": 88}
# Nested collections
matrix: list[list[int]] = [[1, 2], [3, 4]]
nested_dict: dict[str, list[int]] = {"evens": [2, 4], "odds": [1, 3]}
# Dict with complex values
users: dict[int, dict[str, str]] = {
1: {"name": "Alice", "email": "alice@example.com"},
2: {"name": "Bob", "email": "bob@example.com"}
}
Tuples - Fixed Size vs Variable Size
# Fixed-size tuple (specific types in order)
point: tuple[int, int] = (3, 4)
rgb: tuple[int, int, int] = (255, 128, 0)
mixed: tuple[str, int, bool] = ("Alice", 30, True)
# Variable-size tuple (homogeneous)
numbers: tuple[int, ...] = (1, 2, 3, 4, 5) # Any number of ints
📞 Callable Types
Functions as Parameters
from typing import Callable
# Function that takes a function as parameter
def apply_twice(func: Callable[[int], int], value: int) -> int:
"""Apply a function twice."""
return func(func(value))
def double(x: int) -> int:
return x * 2
result = apply_twice(double, 5) # 20
# More complex callable
def process_data(
data: list[int],
transformer: Callable[[int], int],
filter_func: Callable[[int], bool]
) -> list[int]:
return [transformer(x) for x in data if filter_func(x)]
# Callable with no arguments
def run_callback(callback: Callable[[], None]) -> None:
callback()
# Callable with any arguments (use ...)
def log_call(func: Callable[..., any]) -> None:
print(f"Calling {func.__name__}")
func()
🔤 TypeVar and Generics
Creating Generic Functions
from typing import TypeVar
# TypeVar creates a type placeholder
T = TypeVar('T') # Can be any type
def first(items: list[T]) -> T:
"""Return first item from list."""
return items[0]
# Type checker knows the return type matches input type
num = first([1, 2, 3]) # num is int
name = first(["a", "b"]) # name is str
# Constrained TypeVar - limited to specific types
Numeric = TypeVar('Numeric', int, float)
def add_numbers(a: Numeric, b: Numeric) -> Numeric:
return a + b
# Bound TypeVar - must be subclass of bound
from typing import Sequence
T = TypeVar('T', bound=Sequence) # Must be Sequence or subclass
def get_length(seq: T) -> int:
return len(seq)
Generic Classes
from typing import TypeVar, Generic
T = TypeVar('T')
class Stack(Generic[T]):
"""A generic stack implementation."""
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()
def peek(self) -> T:
return self._items[-1]
def is_empty(self) -> bool:
return len(self._items) == 0
# Usage with specific type
int_stack: Stack[int] = Stack()
int_stack.push(1)
int_stack.push(2)
value = int_stack.pop() # Type checker knows this is int
str_stack: Stack[str] = Stack()
str_stack.push("hello") # Only strings allowed
🦆 Protocol (Structural Subtyping)
Protocol defines interfaces based on structure (duck typing with type checking).
from typing import Protocol, runtime_checkable
class Drawable(Protocol):
"""Any class with a draw method is Drawable."""
def draw(self) -> None:
...
class Circle:
def draw(self) -> None:
print("Drawing circle")
class Square:
def draw(self) -> None:
print("Drawing square")
# No inheritance needed - just matching structure
def render(shape: Drawable) -> None:
shape.draw()
render(Circle()) # ✓ Works - Circle has draw()
render(Square()) # ✓ Works - Square has draw()
# Runtime checkable protocol
@runtime_checkable
class Sized(Protocol):
def __len__(self) -> int:
...
print(isinstance([1, 2, 3], Sized)) # True
print(isinstance("hello", Sized)) # True
print(isinstance(42, Sized)) # False
Common Protocol Patterns
from typing import Protocol
class Comparable(Protocol):
"""Objects that can be compared."""
def __lt__(self, other: 'Comparable') -> bool:
...
class Hashable(Protocol):
"""Objects that can be hashed."""
def __hash__(self) -> int:
...
class Iterator(Protocol[T]):
"""Generic iterator protocol."""
def __next__(self) -> T:
...
def __iter__(self) -> 'Iterator[T]':
...
🎨 Type Aliases
Create readable names for complex types.
from typing import TypeAlias
# Type alias for complex types
UserId: TypeAlias = int
UserData: TypeAlias = dict[str, str | int | None]
UserList: TypeAlias = list[UserData]
def get_user(user_id: UserId) -> UserData:
return {"id": user_id, "name": "Alice", "age": 30}
def get_all_users() -> UserList:
return [
{"id": 1, "name": "Alice", "age": 30},
{"id": 2, "name": "Bob", "age": None}
]
# For callbacks - makes code much cleaner
EventHandler: TypeAlias = Callable[[str, dict], None]
Middleware: TypeAlias = Callable[[dict], dict]
def register_handler(event: str, handler: EventHandler) -> None:
pass
def add_middleware(mw: Middleware) -> None:
pass
✨ Special Types
Any - Disable Type Checking
from typing import Any
def process(data: Any) -> Any:
"""Accept and return anything - no type checking."""
return data
# Use sparingly - defeats purpose of type hints!
Literal - Exact Values
from typing import Literal
def set_mode(mode: Literal["read", "write", "append"]) -> None:
"""Only these exact strings are allowed."""
print(f"Mode: {mode}")
set_mode("read") # ✓ OK
set_mode("write") # ✓ OK
set_mode("delete") # ✗ Type error!
# Useful for status codes, options, etc.
def respond(status: Literal[200, 404, 500]) -> str:
return f"Status: {status}"
Final - Prevent Reassignment
from typing import Final
MAX_SIZE: Final = 100
API_URL: Final[str] = "https://api.example.com"
MAX_SIZE = 200 # Type error: cannot reassign Final
TypedDict - Typed Dictionaries
from typing import TypedDict, Required, NotRequired
class User(TypedDict):
"""A typed dictionary for user data."""
id: int
name: str
email: str
age: NotRequired[int] # Optional key
def create_user(data: User) -> None:
print(f"Creating user: {data['name']}")
# Type checker validates keys and value types
create_user({"id": 1, "name": "Alice", "email": "alice@example.com"})
create_user({"id": 1, "name": "Alice"}) # ✗ Missing 'email'!
🔧 Using mypy
Installation and Basic Usage
# Install mypy
pip install mypy
# Check a file
mypy my_script.py
# Check with strict mode
mypy --strict my_script.py
# Check entire project
mypy src/
Configuration (pyproject.toml)
[tool.mypy]
python_version = "3.12"
warn_return_any = true
warn_unused_ignores = true
disallow_untyped_defs = true
disallow_incomplete_defs = true
# Per-module options
[[tool.mypy.overrides]]
module = "tests.*"
disallow_untyped_defs = false
Common mypy Errors and Fixes
# Error: Incompatible return value type
def get_name() -> str:
return None # Error!
# Fix: Use Optional
def get_name() -> str | None:
return None # OK
# Error: Item of list has incompatible type
def process(items: list[int]) -> None:
pass
process([1, 2, "3"]) # Error: str in list[int]
# Error: Missing return statement
def calculate(x: int) -> int:
if x > 0:
return x * 2
# Error: missing return for x <= 0
# Fix: Add all return paths
def calculate(x: int) -> int:
if x > 0:
return x * 2
return 0
📋 Best Practices
Do's ✓
# DO: Type all public APIs
def public_function(name: str, age: int) -> dict[str, any]:
pass
# DO: Use specific types
def process_users(users: list[User]) -> None:
pass
# DO: Use TypeAlias for complex types
Callback: TypeAlias = Callable[[str, int], bool]
# DO: Use Protocol for duck typing
class Sendable(Protocol):
def send(self, data: bytes) -> None:
...
Don'ts ✗
# DON'T: Overuse Any
def process(data: Any) -> Any: # Too loose!
pass
# DON'T: Type internal/private helper functions excessively
def _helper(x): # OK to skip for internal functions
return x * 2
# DON'T: Use wrong collection types
def process(items: list[int]) -> None: # Too specific
pass
# Better: Accept any sequence
def process(items: Sequence[int]) -> None:
pass
📚 Quick Reference
| Type | Example | Description |
|---|---|---|
int, str, float, bool | x: int = 5 | Basic types |
list[T] | items: list[str] | List with type |
dict[K, V] | d: dict[str, int] | Dictionary |
tuple[T, ...] | t: tuple[int, int] | Fixed tuple |
X | Y | x: int | str | Union type |
X | None | x: str | None | Optional |
Callable[[Args], Ret] | f: Callable[[int], str] | Function type |
TypeVar('T') | T = TypeVar('T') | Generic type |
Protocol | class P(Protocol): | Structural type |
Literal["a", "b"] | mode: Literal["r", "w"] | Exact values |
Final | MAX: Final = 100 | Constant |
TypedDict | class User(TypedDict): | Typed dict |
🎯 Next Steps
After mastering type hints, proceed to 26_cli_applications to learn how to build professional command-line tools!