正在加载,请稍候…

Python Type Hints: A Practical Guide With Examples

Learn Python type hints from basics to advanced patterns. Covers Union, Optional, TypedDict, Protocol, generics, and how to use mypy for static type checking.

Why Python Type Hints Matter

Python is dynamically typed, but type hints (added in Python 3.5, with major improvements through 3.12+) let you annotate your code with expected types. They don't affect runtime behavior — Python still ignores them at execution time — but they enable:

  • Static type checking with mypy, Pyright, or similar tools
  • Better IDE support — autocomplete, refactoring, and inline documentation
  • Self-documenting APIs — function signatures communicate intent without needing docstrings
  • Catching bugs early — before your code ever runs

Basic Annotations

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

# Function parameters and return types
def greet(name: str) -> str:
    return f"Hello, {name}!"

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

def process(items: list) -> None:  # no return value
    for item in items:
        print(item)

Built-in Collection Types (Python 3.9+)

# Python 3.9+ — use built-in types directly
def get_ids() -> list[int]:
    return [1, 2, 3]

def get_config() -> dict[str, str]:
    return {"host": "localhost", "port": "5432"}

def get_scores() -> tuple[int, int, int]:
    return (95, 87, 92)

def get_unique_tags() -> set[str]:
    return {"python", "typing", "tutorial"}

# Python 3.8 and below — use typing module
from typing import List, Dict, Tuple, Set

def get_ids() -> List[int]: ...
def get_config() -> Dict[str, str]: ...

Optional and Union Types

from typing import Optional, Union  # or use | in Python 3.10+

# Optional[T] is equivalent to Union[T, None]
def find_user(user_id: int) -> Optional[str]:  # str or None
    if user_id > 0:
        return "Alice"
    return None

# Python 3.10+ syntax
def find_user(user_id: int) -> str | None:  # equivalent
    ...

# Union: can be multiple types
def process(value: Union[int, str]) -> str:
    return str(value)

# Python 3.10+ union syntax
def process(value: int | str) -> str:
    return str(value)

# Common mistake — never do this for nullable
def bad(name: str = None) -> None:  # type checkers will complain
    pass

def good(name: str | None = None) -> None:  # correct
    pass

TypedDict — Typed Dictionaries

from typing import TypedDict

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

class PartialUserDict(TypedDict, total=False):  # all keys optional
    name: str
    email: str

def create_user(data: UserDict) -> UserDict:
    return data

# Type-safe: mypy checks key names and value types
user: UserDict = {
    "id": 1,
    "name": "Alice",
    "email": "alice@example.com",
}

create_user({"id": 1, "name": "Alice"})  # ❌ mypy error: missing email
create_user({"id": "1", "name": "Alice", "email": "a@b.com"})  # ❌ id must be int

Generics

from typing import TypeVar, Generic

T = TypeVar('T')
K = TypeVar('K')
V = TypeVar('V')

# Generic function
def first(items: list[T]) -> T | None:
    return items[0] if items else None

result = first([1, 2, 3])    # inferred type: int | None
result = first(["a", "b"])   # inferred type: str | None

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

    def peek(self) -> T | None:
        return self._items[-1] if self._items else None

stack: Stack[int] = Stack()
stack.push(1)
stack.push("hello")  # ❌ mypy error: expected int, got str

Protocols — Structural Typing (Duck Typing)

from typing import Protocol

# Define what a type must be able to do
class Drawable(Protocol):
    def draw(self) -> None: ...

class Serializable(Protocol):
    def to_json(self) -> str: ...

# Any class with a draw() method satisfies Drawable — no inheritance needed
class Circle:
    def draw(self) -> None:
        print("Drawing circle")

class Square:
    def draw(self) -> None:
        print("Drawing square")

def render_all(shapes: list[Drawable]) -> None:
    for shape in shapes:
        shape.draw()

# ✅ Works because both Circle and Square have draw()
render_all([Circle(), Square()])

# Useful for third-party types you can't modify
class FileWriter(Protocol):
    def write(self, data: bytes) -> int: ...
    def close(self) -> None: ...

def save_data(writer: FileWriter, data: bytes) -> None:
    writer.write(data)
    writer.close()

Callable Types

from typing import Callable

# A function that takes int and returns str
def apply(func: Callable[[int], str], value: int) -> str:
    return func(value)

# Variable number of arguments
def run(callback: Callable[..., None]) -> None:
    callback()

# Returning a callable (higher-order function)
def make_multiplier(factor: int) -> Callable[[int], int]:
    def multiply(x: int) -> int:
        return x * factor
    return multiply

Literal Types

from typing import Literal

Direction = Literal['north', 'south', 'east', 'west']
HttpMethod = Literal['GET', 'POST', 'PUT', 'DELETE', 'PATCH']

def move(direction: Direction) -> None:
    print(f"Moving {direction}")

move('north')    # ✅
move('diagonal') # ❌ mypy error: not a valid Direction

def make_request(method: HttpMethod, url: str) -> None: ...

dataclasses and attrs with Types

from dataclasses import dataclass, field

@dataclass
class Product:
    id: int
    name: str
    price: float
    tags: list[str] = field(default_factory=list)
    active: bool = True

    def discount_price(self, pct: float) -> float:
        return self.price * (1 - pct)

p = Product(id=1, name="Widget", price=9.99)
p.discount_price(0.1)  # 8.991

Type Narrowing

def process(value: int | str) -> str:
    if isinstance(value, int):
        # mypy now knows value: int in this branch
        return str(value * 2)
    else:
        # mypy now knows value: str in this branch
        return value.upper()

# assert narrows type
def get_name(user: dict | None) -> str:
    assert user is not None, "user must not be None"
    # mypy knows user: dict here
    return user.get("name", "")

# TypeGuard for custom type narrowing functions
from typing import TypeGuard

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

def join_strings(items: list[object]) -> str:
    if is_string_list(items):
        return ", ".join(items)  # mypy knows items: list[str]
    return ""

Running mypy

# Install
pip install mypy

# Check a file
mypy script.py

# Check with strict mode (recommended for new projects)
mypy --strict script.py

# Common flags
mypy --ignore-missing-imports  # skip untyped third-party libs
mypy --check-untyped-defs      # type-check functions without annotations

# pyproject.toml config
[tool.mypy]
strict = true
ignore_missing_imports = true

Frequently Asked Questions

Q: Do type hints slow down Python? No. Python ignores annotations at runtime (they're stored as metadata, not evaluated by default). The from __future__ import annotations import makes them strings, which has even less overhead.

Q: Should I annotate every variable? Only annotate where the type isn't obvious from context. Function signatures should always be annotated. Variable assignments where the type is clear from the value (e.g., x = 5) don't need annotation.

Q: How do I handle third-party libraries without type stubs? Use # type: ignore on specific lines, py.typed stubs, or install stub packages (pip install types-requests). Check typeshed for community-maintained stubs.

→ Explore and validate Python data structures with the JSON Viewer.