โ† Back to Blog

Python Type Hints Masterclass: Write Bulletproof Code

Python Type Hints Masterclass ยท 853 words

---

title: "Python Type Hints Masterclass: Write Bulletproof Code"

description: "Master Python type hints with practical examples. Learn protocols, generics, literals, and advanced typing patterns for production code."

date: "2026-07-03"

category: "python"

tags: ["python", "type-hints", "typing", "best-practices", "clean-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

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

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]

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="[email protected]", age=25)
# User(id=1, name='Anna', email='[email protected]', age=25, ...)

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

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="[email protected]", age=25))
user = user_repo.get(0)  # Type is User, not dict

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()

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:
    ...

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": "[email protected]"}
process_user(user)  # Works

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

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")

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)

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

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](https://reply-continues-exams-confidential.trycloudflare.com).

What you get:

[Browse the collection โ†’](https://reply-continues-exams-confidential.trycloudflare.com)

๐Ÿ›’ Ready to deploy?

Browse 120+ Python tools with crypto payments and instant delivery.

Browse Products โ†’