Type hints make Python code clearer and catch bugs before runtime. Here's how to use them effectively.

Basic Annotations

def greet(name: str) -> str:
    return f"Hello, {name}"
 
age: int = 25
prices: list[float] = [9.99, 14.99, 29.99]
config: dict[str, int] = {"timeout": 30, "retries": 3}

Types go after colons for variables, after arrows for return types.

Common Types

from typing import Optional, Union, Any
 
# Basic types
x: int = 1
y: float = 3.14
z: str = "hello"
flag: bool = True
 
# Collections (Python 3.9+)
items: list[str] = ["a", "b"]
mapping: dict[str, int] = {"key": 1}
coords: tuple[float, float] = (1.0, 2.0)
unique: set[int] = {1, 2, 3}
 
# Optional - can be None
user: Optional[str] = None  # Same as str | None
 
# Union - multiple types
value: Union[int, str] = 42  # Same as int | str (3.10+)
 
# Any - escape hatch
data: Any = get_unknown_data()

Function Signatures

from typing import Callable
 
# Basic function
def add(a: int, b: int) -> int:
    return a + b
 
# Function returning None
def log(message: str) -> None:
    print(message)
 
# Function as parameter
def apply(func: Callable[[int, int], int], x: int, y: int) -> int:
    return func(x, y)
 
# Default arguments
def fetch(url: str, timeout: int = 30) -> str:
    ...
 
# *args and **kwargs
def variadic(*args: int, **kwargs: str) -> None:
    ...

Classes and Methods

class User:
    name: str
    email: str
    
    def __init__(self, name: str, email: str) -> None:
        self.name = name
        self.email = email
    
    def greet(self) -> str:
        return f"Hi, I'm {self.name}"
    
    @classmethod
    def from_dict(cls, data: dict[str, str]) -> "User":
        return cls(data["name"], data["email"])

Use string literals ("User") for forward references, or import from __future__ import annotations.

Generics

Create reusable typed containers:

from typing import TypeVar, Generic
 
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()
 
# Usage
int_stack: Stack[int] = Stack()
int_stack.push(1)
int_stack.push("wrong")  # Type error!

TypeVar Bounds

Constrain generic types:

from typing import TypeVar
 
# Must be a number type
Number = TypeVar("Number", int, float)
 
def double(x: Number) -> Number:
    return x * 2
 
# Must have specific method
from typing import Protocol
 
class Comparable(Protocol):
    def __lt__(self, other: Any) -> bool: ...
 
C = TypeVar("C", bound=Comparable)
 
def minimum(a: C, b: C) -> C:
    return a if a < b else b

Protocols (Structural Typing)

Define interfaces without inheritance:

from typing import Protocol
 
class Drawable(Protocol):
    def draw(self) -> None: ...
 
class Circle:
    def draw(self) -> None:
        print("Drawing circle")
 
class Square:
    def draw(self) -> None:
        print("Drawing square")
 
def render(shape: Drawable) -> None:
    shape.draw()
 
# Both work - they have draw()
render(Circle())
render(Square())

TypedDict

Type dictionaries with known keys:

from typing import TypedDict
 
class Movie(TypedDict):
    title: str
    year: int
    rating: float
 
movie: Movie = {
    "title": "Inception",
    "year": 2010,
    "rating": 8.8
}

Literal Types

Restrict to specific values:

from typing import Literal
 
def set_mode(mode: Literal["read", "write", "append"]) -> None:
    ...
 
set_mode("read")   # OK
set_mode("delete") # Type error

Using mypy

Install and run:

pip install mypy
mypy your_code.py

Configuration in pyproject.toml:

[tool.mypy]
python_version = "3.11"
strict = true
warn_return_any = true
warn_unused_ignores = true

Common flags:

  • --strict: Enable all strict checks
  • --ignore-missing-imports: Skip untyped libraries
  • --show-error-codes: Show error codes for targeted ignores

Type Ignore Comments

When you need to bypass checking:

# Ignore specific line
result = untyped_function()  # type: ignore[no-untyped-call]
 
# Ignore entire file (at top)
# mypy: ignore-errors

Use sparingly—most ignores indicate fixable issues.

Gradual Adoption

Start with:

  1. Function signatures in new code
  2. Public API boundaries
  3. Complex functions where bugs hide

Skip:

  • Simple scripts
  • Test files (unless helpful)
  • Prototype code

Quick Reference

from typing import (
    Optional,      # T | None
    Union,         # T | U
    Any,           # Escape hatch
    Callable,      # Function type
    TypeVar,       # Generic placeholder
    Generic,       # Base for generic classes
    Protocol,      # Structural typing
    TypedDict,     # Typed dictionaries
    Literal,       # Specific values
    Final,         # Constants
    ClassVar,      # Class-level variables
)
 
# Modern syntax (3.10+)
x: int | str           # Union
y: list[int] | None    # Optional

Type hints are documentation that the computer can check. Use them to make your code clearer and catch bugs early.

React to this post: