DEV Community

Anna lilith
Anna lilith

Posted on

Python Type Hints Masterclass: Write Bulletproof Code

Python Type Hints Masterclass: Write Bulletproof Code

Type hints aren't just documentation — they catch bugs before they reach production. Here's how to use them effectively.

Why Type Hints Matter

  • Catch bugs at write time: IDE catches errors before runtime
  • Better documentation: Types are self-documenting
  • Performance: Tools like mypy and pyright optimize based on types
  • Refactoring safety: Change code confidently with type checking

Basic Types

from typing import Optional, Union

# Simple types
name: str = "Anna"
age: int = 25
price: float = 29.99
active: bool = True

# Collections
scores: list[float] = [95.5, 87.3, 91.0]
user_ids: set[int] = {1, 2, 3}
metadata: dict[str, str] = {"key": "value"}

# Optional (can be None)
middle_name: Optional[str] = None  # Same as str | None

# Union (multiple types)
response: Union[str, int] = "ok"  # Same as str | int
Enter fullscreen mode Exit fullscreen mode

Function Signatures

from typing import Callable, Awaitable

# Always type function signatures
def calculate_discount(price: float, percentage: float) -> float:
    return price * (1 - percentage / 100)

# Async functions
async def fetch_user(user_id: int) -> dict:
    return await db.get_user(user_id)

# Functions that accept functions
def apply_transform(data: list[int], fn: Callable[[int], int]) -> list[int]:
    return [fn(x) for x in data]
Enter fullscreen mode Exit fullscreen mode

Pydantic Models (Best Practice)

from pydantic import BaseModel, Field, EmailStr
from datetime import datetime

class User(BaseModel):
    id: int
    name: str = Field(..., min_length=1, max_length=100)
    email: EmailStr
    age: int = Field(..., ge=0, le=150)
    created_at: datetime = Field(default_factory=datetime.now)
    tags: list[str] = []

# Validation happens automatically
user = User(id=1, name="Anna", email="anna@example.com", age=25)
# User(id=1, name='Anna', email='anna@example.com', age=25, ...)

# This raises ValidationError
bad_user = User(id="not_a_number", name="", email="invalid")
Enter fullscreen mode Exit fullscreen mode

Generics

from typing import TypeVar, Generic

T = TypeVar("T")

class Repository(Generic[T]):
    def __init__(self, items: list[T] | None = None):
        self._items = items or []

    def add(self, item: T) -> None:
        self._items.append(item)

    def get(self, index: int) -> T:
        return self._items[index]

    def find(self, predicate: Callable[[T], bool]) -> T | None:
        return next((item for item in self._items if predicate(item)), None)

# Type-safe repositories
user_repo: Repository[User] = Repository()
user_repo.add(User(id=1, name="Anna", email="a@b.com", age=25))
user = user_repo.get(0)  # Type is User, not dict
Enter fullscreen mode Exit fullscreen mode

Protocols (Structural Subtyping)

from typing import Protocol

class Drawable(Protocol):
    def draw(self) -> str: ...

class Circle:
    def draw(self) -> str:
        return "🔴"

class Square:
    def draw(self) -> str:
        return "🟧"

# Any object with draw() works - no inheritance needed
def render(shape: Drawable) -> str:
    return shape.draw()

render(Circle())  # Works
render(Square())  # Works
render("hello")   # Type error: str has no draw()
Enter fullscreen mode Exit fullscreen mode

Literal Types

from typing import Literal

def set_direction(direction: Literal["north", "south", "east", "west"]) -> None:
    print(f"Moving {direction}")

set_direction("north")   # Works
set_direction("up")      # Type error

# Useful for API parameters
HTTPMethod = Literal["GET", "POST", "PUT", "DELETE", "PATCH"]

def request(method: HTTPMethod, url: str) -> dict:
    ...
Enter fullscreen mode Exit fullscreen mode

TypedDict

from typing import TypedDict

class UserDict(TypedDict):
    id: int
    name: str
    email: str

def process_user(user: UserDict) -> str:
    return f"{user['name']} ({user['email']})"

# Catches missing/extra keys at type check time
user: UserDict = {"id": 1, "name": "Anna", "email": "a@b.com"}
process_user(user)  # Works

bad_user: UserDict = {"id": 1, "name": "Anna"}  # Missing 'email'
process_user(bad_user)  # Type error
Enter fullscreen mode Exit fullscreen mode

Type Guard

from typing import TypeGuard

def is_string_list(val: list[object]) -> TypeGuard[list[str]]:
    return all(isinstance(x, str) for x in val)

def process(data: list[object]) -> None:
    if is_string_list(data):
        # Type checker knows data is list[str] here
        print(" ".join(data))
    else:
        print("Not all strings")
Enter fullscreen mode Exit fullscreen mode

Real-World Example: API Endpoint

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, Field

class CreateUserRequest(BaseModel):
    name: str = Field(..., min_length=1, max_length=100)
    email: EmailStr
    password: str = Field(..., min_length=8)

class UserResponse(BaseModel):
    id: int
    name: str
    email: str

class ErrorResponse(BaseModel):
    detail: str
    code: str

app = FastAPI()

@app.post(
    "/users",
    response_model=UserResponse,
    responses={400: {"model": ErrorResponse}},
)
async def create_user(req: CreateUserRequest) -> UserResponse:
    # Type checker knows req.name is str, req.email is EmailStr
    existing = await db.find_user(email=req.email)
    if existing:
        raise HTTPException(400, "Email already registered")

    user = await db.create_user(**req.model_dump())
    return UserResponse(id=user.id, name=user.name, email=user.email)
Enter fullscreen mode Exit fullscreen mode

MyPy Configuration

# mypy.ini
[mypy]
python_version = 3.12
strict = true
warn_return_any = true
warn_unused_configs = true
disallow_untyped_defs = true
check_untyped_defs = true
no_implicit_optional = true
warn_redundant_casts = true
warn_unused_ignores = true
Enter fullscreen mode Exit fullscreen mode

Key Takeaways

  1. Type everything: Functions, variables, return values
  2. Use Pydantic for data validation: Don't validate manually
  3. Generics for reusable code: Write type-safe containers
  4. Protocols over inheritance: Structural typing is more flexible
  5. Run mypy in CI: Catch type errors before merge
  6. Type hints are documentation: They tell other developers what your code expects

Type hints are an investment that pays dividends in code quality, maintainability, and developer productivity.


Get the Production-Ready Version

Don't want to build it yourself? We have production-ready versions of these tools at https://reply-continues-exams-confidential.trycloudflare.com.

What you get:

  • Complete, tested Python code
  • Documentation and setup guides
  • Instant delivery after crypto payment
  • Free updates

Browse the collection →

Top comments (0)