DevToolBox免费
博客

Python 类型注解完全指南:mypy、Protocols 与运行时验证

13 分钟作者 DevToolBox

Python 类型提示(PEP 484 引入,Python 3.5+)允许你用类型信息注解代码。它们不影响运行时行为——Python 仍然是动态类型——但允许 mypy、pyright 等静态分析工具和 IDE 自动补全在运行代码之前发现错误。在 2026 年,类型提示是任何认真的 Python 项目的标准实践。

基本类型注解

类型注解在变量或参数名后使用冒号,返回类型使用 ->。Python 3.9+ 允许直接使用内置泛型(list[str] 而非 List[str])。

# Python type hints — basic built-in types
def greet(name: str) -> str:
    return f"Hello, {name}!"

def add(x: int, y: int) -> int:
    return x + y

def ratio(total: int, count: int) -> float:
    return total / count

def is_valid(value: str) -> bool:
    return len(value) > 0

# Variables can be annotated too
user_id: int = 42
username: str = "alice"
scores: list[int] = [95, 87, 92]

集合和可选类型

typing 模块为集合提供泛型版本,并为可空和联合类型提供特殊形式。

from typing import Optional, Union, Any

# Python 3.9+: use built-in generics directly
names: list[str] = ["Alice", "Bob"]
counts: dict[str, int] = {"apples": 3, "bananas": 5}
coords: tuple[float, float] = (51.5, -0.1)
unique_ids: set[int] = {1, 2, 3}

# Optional: value may be None
def find_user(user_id: int) -> Optional[str]:
    db = {1: "Alice", 2: "Bob"}
    return db.get(user_id)  # may return None

# Union: multiple possible types (Python 3.10+: use X | Y)
def process(value: Union[int, str]) -> str:
    return str(value)

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

# Any: opt out of type checking
def legacy(data: Any) -> Any:
    return data

高级 typing 模块特性

typing 模块提供了描述复杂类型关系的强大构造:Callable、TypeVar、泛型类、Literal、Final 和 TypedDict。

from typing import (
    Callable, Iterator, Generator,
    TypeVar, Generic, Protocol,
    TypedDict, Literal, Final,
    overload, cast, TYPE_CHECKING
)

# Callable: function signatures
Handler = Callable[[str, int], bool]

def apply(func: Callable[[int], int], value: int) -> int:
    return func(value)

# TypeVar: generic type parameters
T = TypeVar('T')
K = TypeVar('K')
V = TypeVar('V')

def first(items: list[T]) -> T:
    return items[0]

# 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 is_empty(self) -> bool:
        return len(self._items) == 0

# Usage
stack: Stack[int] = Stack()
stack.push(1)
stack.push(2)
result: int = stack.pop()  # mypy knows this is int

# Literal: restrict to specific values
def set_direction(direction: Literal["left", "right", "up", "down"]) -> None:
    print(f"Moving {direction}")

# Final: constant that cannot be reassigned
MAX_SIZE: Final = 100

# TypedDict: typed dictionaries
class UserDict(TypedDict):
    name: str
    age: int
    email: str

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

Protocol:结构化子类型

Protocol 支持带类型安全的鸭子类型。Protocol 不要求继承,而是检查对象是否具有所需的方法和属性。

from typing import Protocol, runtime_checkable

# Protocol: structural subtyping (duck typing with types)
@runtime_checkable
class Drawable(Protocol):
    def draw(self) -> None: ...
    def get_color(self) -> str: ...

class Circle:
    def draw(self) -> None:
        print("Drawing circle")
    def get_color(self) -> str:
        return "red"

class Square:
    def draw(self) -> None:
        print("Drawing square")
    def get_color(self) -> str:
        return "blue"

def render(shape: Drawable) -> None:
    print(f"Color: {shape.get_color()}")
    shape.draw()

# Both work without inheriting from Drawable
render(Circle())
render(Square())

# Runtime check (requires @runtime_checkable)
print(isinstance(Circle(), Drawable))  # True

带类型提示的 dataclasses

dataclasses 与类型提示完美结合,根据注解字段自动生成 __init__、__repr__ 和 __eq__。

from dataclasses import dataclass, field
from typing import ClassVar

@dataclass
class Point:
    x: float
    y: float

    def distance_to(self, other: 'Point') -> float:
        return ((self.x - other.x) ** 2 + (self.y - other.y) ** 2) ** 0.5

@dataclass
class Employee:
    name: str
    department: str
    salary: float
    skills: list[str] = field(default_factory=list)
    _id_counter: ClassVar[int] = 0

    def __post_init__(self) -> None:
        Employee._id_counter += 1
        self.employee_id: int = Employee._id_counter

    def add_skill(self, skill: str) -> None:
        self.skills.append(skill)

# Frozen dataclass (immutable)
@dataclass(frozen=True)
class Color:
    r: int
    g: int
    b: int

    def to_hex(self) -> str:
        return f"#{self.r:02x}{self.g:02x}{self.b:02x}"

red = Color(255, 0, 0)
print(red.to_hex())  # #ff0000

运行 mypy 和配置

mypy 是最流行的 Python 静态类型检查器。用 pip install mypy 安装,然后在代码上运行它。

# 安装并运行 mypy
pip install mypy
mypy src/

# 严格模式(发现更多问题)
mypy --strict src/

# 检查单个文件
mypy mymodule.py

mypy.ini Configuration

# mypy.ini
[mypy]
python_version = 3.12
warn_return_any = True
warn_unused_configs = True
disallow_untyped_defs = True
disallow_any_generics = True
check_untyped_defs = True
strict_optional = True
no_implicit_optional = True

# Ignore missing stubs for third-party packages
[mypy-requests.*]
ignore_missing_imports = True

[mypy-numpy.*]
ignore_missing_imports = True

类型提示语法:Python 版本对比

FeaturePython 3.8Python 3.9Python 3.10Python 3.12
Basic annotationstyping.List, Dictlist, dictlist, dictlist, dict
Union typesUnion[X, Y]Union[X, Y]X | YX | Y
OptionalOptional[X]Optional[X]X | NoneX | None
TypeAliasMyType = ...MyType = ...TypeAliastype MyType = ...
ParamSpecN/AN/AParamSpecParamSpec
Self typeN/AN/AN/ASelf

最佳实践

  • 从返回类型和公共 API 参数开始——以最小的工作量获得最大的价值。
  • 当值可以为 None 时,使用 Optional[X] 或 X | None(Python 3.10+)。除非与真正的动态代码接口,否则永远不要使用 Any。
  • 在 CI 中使用 --strict 或至少 --disallow-untyped-defs 运行 mypy,以防止类型注解退步。
  • 对结构化类型使用 Protocol 而非 ABC——它无需类层次结构即可工作。
  • 对字典结构使用 TypedDict,对数据对象使用 dataclasses,对轻量级不可变记录使用 NamedTuple。

常见问题

Python 类型提示会降低代码速度吗?

不会。类型提示在运行时完全被忽略(除非显式使用 typing.get_type_hints())。它们不增加任何运行时开销。Python 解释器将它们作为无关紧要的表达式处理。

mypy 和 pyright 有什么区别?

mypy 是最初的 Python 类型检查器,由 Guido van Rossum 团队开发。pyright 是微软的类型检查器,用于 VS Code(Pylance)。pyright 通常更快、更严格。两者都支持相同的 PEP 标准。对于 CI,mypy 更易配置。对于 IDE 集成,pyright(通过 Pylance)非常出色。

什么时候使用 TypeVar vs Protocol?

当需要保留类型的泛型函数或类时使用 TypeVar(例如,返回与接收相同类型的函数)。当想要类型检查对象是否具有某些方法/属性(不管其类层次结构)时使用 Protocol。

如何为 *args 和 **kwargs 添加类型提示?

使用 *args: int 表示位置可变参数(所有参数必须是相同类型),使用 **kwargs: str 表示关键字可变参数。对于混合类型,使用 *args: Any。Python 3.11+ 引入了 TypeVarTuple 用于可变泛型。

我应该为现有 Python 代码添加类型提示吗?

是的,逐步添加。从公共函数和类方法开始。谨慎使用 "# type: ignore" 来抑制复杂第三方集成中的错误。先在新文件上启用 mypy,然后逐步扩大覆盖范围。随着代码库增长,收益会随时间累积。

相关工具

𝕏 Twitterin LinkedIn
这篇文章有帮助吗?

保持更新

获取每周开发技巧和新工具通知。

无垃圾邮件,随时退订。

试试这些相关工具

{ }JSON Formatter±Text Diff Checker

相关文章

Zod 验证指南:Schema、转换、精细化与 tRPC 集成

TypeScript 中 Zod 模式验证完全指南:定义 schema、数据转换、自定义精细化与 tRPC 集成。

TypeScript 5 新特性:装饰器、const 类型参数与 satisfies 运算符

TypeScript 5 新特性完全指南:装饰器、const 类型参数、satisfies 运算符及性能改进。

Git 分支策略:GitFlow vs 主干开发 vs GitHub Flow

对比 GitFlow、主干开发和 GitHub Flow 分支策略。学习分支结构、合并工作流、CI/CD 集成,以及如何为你的团队选择合适的策略。