HeoLab
ToolsBlogAboutContact
HeoLab

Free developer tools with AI enhancement. Built for developers who ship.

Tools

  • JSON Formatter
  • JWT Decoder
  • Base64 Encoder
  • Timestamp Converter
  • Regex Tester
  • All Tools →

Resources

  • Blog
  • What is JSON?
  • JWT Deep Dive
  • Base64 Explained

Company

  • About
  • Contact
  • Privacy Policy
  • Terms of Service

© 2026 HeoLab. All rights reserved.

Tools work in your browser. Zero data retention.

HomeBlogPython Type Hints: The Complete Guide to typing, Pydantic, and mypy
Table of Contents▾
  • Basic Annotations
  • Variables
  • Functions
  • Optional (can be None)
  • Union types (Python 3.10+)
  • Never returns (raises or loops forever)
  • Collections and Generics
  • Python 3.9+ — use built-in types directly
  • Tuples — fixed length and types
  • Tuple of variable length
  • Sets
  • Callable
  • TypedDict — typed dicts
  • TypeVar and Generics
  • Generic function — return type matches input type
  • Generic class
  • Bounded TypeVar
  • Protocol — Structural Subtyping (Duck Typing)
  • Protocol defines an interface by behavior, not inheritance
  • Circle and Square satisfy Drawable without inheriting from it
  • Pydantic — Runtime Validation
  • Parse from dict (JSON API response)
  • Serialize back to dict / JSON
  • Parse directly from JSON string
  • Validation errors are descriptive
  • Pydantic for API Schemas (FastAPI)
  • mypy — Static Type Checking
  • Install and run
  • mypy.ini configuration
  • Per-module overrides (for third-party libs without stubs)
  • Common mypy errors and fixes
  • error: Argument 1 to "process" has incompatible type "Optional[str]"
  • Fix: narrow the type
  • error: Return type "None" expected "str"
  • Fix: handle the None case
  • Explicitly tell mypy you know better (use sparingly)
tutorials#python#typing#pydantic

Python Type Hints: The Complete Guide to typing, Pydantic, and mypy

Master Python type annotations — built-in types, generics, Protocol, TypeVar, runtime validation with Pydantic, and static checking with mypy.

Trong Ngo
February 25, 2026
4 min read

Python's type system is optional but powerful. Adding type hints to your code catches bugs before runtime, enables better IDE autocomplete, and makes large codebases dramatically easier to navigate.

Basic Annotations

from typing import Optional, Union

# Variables
name: str = "Alice"
age: int = 30
score: float = 9.5
active: bool = True

# Functions
def greet(name: str, times: int = 1) -> str:
    return f"Hello, {name}! " * times

# Optional (can be None)
def find_user(user_id: int) -> Optional[str]:  # str | None
    ...

# Union types (Python 3.10+)
def process(value: int | str | None) -> str:
    ...

# Never returns (raises or loops forever)
from typing import NoReturn
def crash(msg: str) -> NoReturn:
    raise RuntimeError(msg)

Collections and Generics

# Python 3.9+ — use built-in types directly
def process_names(names: list[str]) -> dict[str, int]:
    return {name: len(name) for name in names}

# Tuples — fixed length and types
def get_coords() -> tuple[float, float]:
    return (51.5, -0.1)

# Tuple of variable length
def get_all_scores() -> tuple[int, ...]:
    return (90, 85, 92)

# Sets
def unique_tags(posts: list[dict]) -> set[str]:
    return {tag for post in posts for tag in post["tags"]}

# Callable
from typing import Callable
def apply(func: Callable[[int, int], int], a: int, b: int) -> int:
    return func(a, b)

# TypedDict — typed dicts
from typing import TypedDict
class UserDict(TypedDict):
    id: int
    name: str
    email: str

def show_user(user: UserDict) -> None:
    print(user["name"])

TypeVar and Generics

from typing import TypeVar, Generic

T = TypeVar("T")

# Generic function — return type matches input type
def first(items: list[T]) -> T:
    return items[0]

x: int = first([1, 2, 3])      # inferred as int
y: str = first(["a", "b"])     # inferred as str

# Generic class
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()

stack: Stack[int] = Stack()
stack.push(42)

# Bounded TypeVar
from typing import SupportsFloat
Numeric = TypeVar("Numeric", int, float, complex)

def add(a: Numeric, b: Numeric) -> Numeric:
    return a + b

Protocol — Structural Subtyping (Duck Typing)

from typing import Protocol, runtime_checkable

# Protocol defines an interface by behavior, not inheritance
@runtime_checkable
class Drawable(Protocol):
    def draw(self) -> None: ...
    def resize(self, factor: float) -> None: ...

class Circle:
    def draw(self) -> None:
        print("Drawing circle")
    def resize(self, factor: float) -> None:
        self.radius *= factor

class Square:
    def draw(self) -> None:
        print("Drawing square")
    def resize(self, factor: float) -> None:
        self.side *= factor

# Circle and Square satisfy Drawable without inheriting from it
def render_all(shapes: list[Drawable]) -> None:
    for shape in shapes:
        shape.draw()

render_all([Circle(), Square()])  # works!
isinstance(Circle(), Drawable)   # True (runtime_checkable)

Pydantic — Runtime Validation

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

class User(BaseModel):
    id: int
    name: str = Field(..., min_length=2, max_length=50)
    email: EmailStr
    age: int = Field(..., ge=0, le=150)  # >= 0, <= 150
    role: str = "user"
    created_at: datetime = Field(default_factory=datetime.utcnow)

    @field_validator("name")
    @classmethod
    def name_must_not_contain_spaces(cls, v: str) -> str:
        if "  " in v:
            raise ValueError("Name cannot have consecutive spaces")
        return v.strip()

# Parse from dict (JSON API response)
user = User.model_validate({
    "id": 1,
    "name": "Alice",
    "email": "alice@example.com",
    "age": 30
})

# Serialize back to dict / JSON
user.model_dump()        # dict
user.model_dump_json()   # JSON string

# Parse directly from JSON string
user = User.model_validate_json('{"id": 1, "name": "Bob", ...}')

# Validation errors are descriptive
try:
    User(id="not-an-int", name="x", email="bad", age=200)
except ValidationError as e:
    print(e.json())  # structured error list

Pydantic for API Schemas (FastAPI)

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

class CreateUserRequest(BaseModel):
    name: str
    email: EmailStr
    role: Literal["admin", "user"] = "user"

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

    model_config = {"from_attributes": True}  # allow ORM model input

@app.post("/users", response_model=UserResponse, status_code=201)
async def create_user(body: CreateUserRequest) -> UserResponse:
    # body is already validated — safe to use
    user = await db.users.create(**body.model_dump())
    return UserResponse.model_validate(user)

mypy — Static Type Checking

# Install and run
pip install mypy
mypy src/ --strict

# mypy.ini configuration
[mypy]
strict = true
python_version = 3.12
warn_return_any = true
disallow_untyped_defs = true

# Per-module overrides (for third-party libs without stubs)
[mypy-boto3.*]
ignore_missing_imports = true
# Common mypy errors and fixes

# error: Argument 1 to "process" has incompatible type "Optional[str]"
# Fix: narrow the type
if value is not None:
    process(value)  # value is str here

# error: Return type "None" expected "str"
# Fix: handle the None case
def get_name(user: dict) -> str:
    return user.get("name") or "Unknown"  # always returns str

# Explicitly tell mypy you know better (use sparingly)
result = some_func()  # type: ignore[return-value]

Try These Tools

JSON Formatter & Validator

Format, validate, and beautify JSON data instantly. Detect errors with precise line numbers.

JSON → TypeScript Interface

Paste JSON and instantly get a typed TypeScript interface. Handles nested objects and arrays.

Related Articles

PostgreSQL Performance Tuning: Indexes, Query Plans, and EXPLAIN ANALYZE

5 min read

WebSockets: Building Real-Time Apps from Scratch

4 min read

REST API Design Best Practices in 2025

4 min read

Back to Blog

Table of Contents

  • Basic Annotations
  • Variables
  • Functions
  • Optional (can be None)
  • Union types (Python 3.10+)
  • Never returns (raises or loops forever)
  • Collections and Generics
  • Python 3.9+ — use built-in types directly
  • Tuples — fixed length and types
  • Tuple of variable length
  • Sets
  • Callable
  • TypedDict — typed dicts
  • TypeVar and Generics
  • Generic function — return type matches input type
  • Generic class
  • Bounded TypeVar
  • Protocol — Structural Subtyping (Duck Typing)
  • Protocol defines an interface by behavior, not inheritance
  • Circle and Square satisfy Drawable without inheriting from it
  • Pydantic — Runtime Validation
  • Parse from dict (JSON API response)
  • Serialize back to dict / JSON
  • Parse directly from JSON string
  • Validation errors are descriptive
  • Pydantic for API Schemas (FastAPI)
  • mypy — Static Type Checking
  • Install and run
  • mypy.ini configuration
  • Per-module overrides (for third-party libs without stubs)
  • Common mypy errors and fixes
  • error: Argument 1 to "process" has incompatible type "Optional[str]"
  • Fix: narrow the type
  • error: Return type "None" expected "str"
  • Fix: handle the None case
  • Explicitly tell mypy you know better (use sparingly)

Related Articles

PostgreSQL Performance Tuning: Indexes, Query Plans, and EXPLAIN ANALYZE

5 min read

WebSockets: Building Real-Time Apps from Scratch

4 min read

REST API Design Best Practices in 2025

4 min read