Python 类型注解
大约 12 分钟约 3557 字
Python 类型注解
简介
类型注解是 Python 3.5+ 引入的静态类型系统,通过在函数签名和变量声明中添加类型信息,配合 mypy/pyright 等工具实现编译期类型检查。类型注解不影响运行时行为,但能显著提升代码可读性、IDE 支持和重构安全性。
类型注解的设计哲学是"渐进式类型化"(gradual typing)——Python 不要求所有代码都有类型注解,你可以在项目中逐步添加。没有注解的代码默认为 Any 类型,类型检查器会跳过检查。这种设计使得 Python 既能享受静态类型的安全保障,又能保持动态语言的灵活性。
从实现角度看,类型注解本质上是函数和变量的"元数据",存储在 __annotations__ 字典中。Python 运行时不会强制执行这些类型约束(除非你使用 @typechecked 等运行时检查装饰器)。类型检查是由外部工具(mypy、pyright、pytype)在代码运行之前完成的。
特点
基础类型注解
变量与函数注解
# 变量注解
name: str = "张三"
age: int = 30
score: float = 95.5
is_active: bool = True
tags: list[str] = ["python", "go"]
config: dict[str, int] = {"timeout": 30, "retries": 3}
items: tuple[str, int, float] = ("hello", 42, 3.14)
# 纯注解(不赋值,类型检查器会要求后续赋值时类型匹配)
username: str # 必须在后续被赋值
# 函数注解
def greet(name: str, times: int = 1) -> str:
"""生成问候语"""
return (f"你好, {name}! " * times).strip()
def calculate_area(width: float, height: float) -> float:
"""计算矩形面积"""
return width * height
def process_data(
data: list[dict[str, Any]],
*,
filter_key: str | None = None,
sort_desc: bool = False,
) -> list[dict[str, Any]]:
"""处理数据的函数"""
if filter_key:
data = [d for d in data if filter_key in d]
if sort_desc:
data = sorted(data, key=lambda d: d.get("score", 0), reverse=True)
return data
from typing import AnyOptional、Union 与 None 处理
from typing import Optional, Union
# Optional[X] 等价于 Union[X, None]
# Python 3.10+ 推荐使用 X | None 语法
def find_user(user_id: int) -> dict | None:
"""查找用户,可能不存在"""
users = {1: {"name": "Alice"}, 2: {"name": "Bob"}}
return users.get(user_id)
# 使用时需要处理 None
result = find_user(99)
if result is not None:
print(result["name"]) # 类型收窄:这里 result 是 dict
# Union 类型:多种可能的类型
def parse_value(value: str) -> int | float | str:
"""尝试解析为数字,失败则返回字符串"""
try:
return int(value)
except ValueError:
try:
return float(value)
except ValueError:
return value
# 类型收窄(Type Narrowing)
def process(value: int | str | None):
"""根据类型执行不同操作"""
if value is None:
print("值为空")
elif isinstance(value, int):
print(f"整数: {value * 2}") # 这里 value 被收窄为 int
elif isinstance(value, str):
print(f"字符串: {value.upper()}")实现
基础类型注解与泛型
from typing import Optional, Union, List, Dict, Tuple, Generic, TypeVar
from collections.abc import Callable, Iterator
# 基础类型注解
def search_users(
name: str,
age: Optional[int] = None,
roles: list[str] | None = None, # Python 3.10+ 语法
) -> dict[str, Any]:
"""搜索用户"""
result: dict[str, Any] = {"name": name}
if age is not None:
result["age"] = age
if roles:
result["roles"] = roles
return result
# 泛型:创建类型安全的容器
T = TypeVar("T")
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:
if not self._items:
raise IndexError("栈为空")
return self._items.pop()
def peek(self) -> T:
if not self._items:
raise IndexError("栈为空")
return self._items[-1]
def __len__(self) -> int:
return len(self._items)
# 使用
int_stack: Stack[int] = Stack()
int_stack.push(1)
int_stack.push(2)
value: int = int_stack.pop() # 类型检查器知道 value 是 int
# Callable 类型
from typing import Any
Handler = Callable[[dict[str, Any]], bool]
def process_event(event: dict[str, Any], handler: Handler) -> bool:
return handler(event)高级泛型:有界 TypeVar 与约束
from typing import TypeVar, Generic, Protocol, runtime_checkable
# 有界 TypeVar:限定为某个类型的子类
T = TypeVar("T", bound="Comparable")
class Comparable(Protocol):
def __lt__(self, other: Any) -> bool: ...
def __gt__(self, other: Any) -> bool: ...
def find_max(items: list[T]) -> T:
"""找到列表中的最大值"""
if not items:
raise ValueError("空列表")
maximum = items[0]
for item in items[1:]:
if item > maximum:
maximum = item
return maximum
# 约束 TypeVar:限定为几个具体类型之一
T = TypeVar("T", int, float, str)
def first_item(collection: list[T]) -> T:
"""获取第一个元素"""
return collection[0]
# 多类型参数的泛型
K = TypeVar("K")
V = TypeVar("V")
class Pair(Generic[K, V]):
"""键值对"""
def __init__(self, key: K, value: V):
self.key = key
self.value = value
def __repr__(self) -> str:
return f"Pair({self.key!r}, {self.value!r})"
pair1: Pair[str, int] = Pair("age", 30)
pair2: Pair[int, list[str]] = Pair(1, ["a", "b"])Protocol 与结构化类型
from typing import Protocol, runtime_checkable
@runtime_checkable
class Closeable(Protocol):
"""可关闭资源的协议"""
def close(self) -> None: ...
@runtime_checkable
class Renderable(Protocol):
"""可渲染的协议"""
def render(self) -> str: ...
class JsonRenderer:
"""实现 Renderable 协议(无需显式继承)"""
def __init__(self, data: dict):
self.data = data
def render(self) -> str:
import json
return json.dumps(self.data, ensure_ascii=False)
class HtmlRenderer:
def __init__(self, data: dict):
self.data = data
def render(self) -> str:
items = "".join(f"<li>{k}: {v}</li>" for k, v in self.data.items())
return f"<ul>{items}</ul>"
def display(renderable: Renderable) -> None:
"""接受任何实现了 render() 方法的对象"""
print(renderable.render())
# 鸭子类型 + 静态检查
display(JsonRenderer({"name": "张三", "age": 30}))
display(HtmlRenderer({"name": "李四", "age": 25}))
# 组合协议
class PersistentRenderable(Renderable, Closeable, Protocol):
"""同时满足渲染和关闭的协议"""
def save(self, path: str) -> None: ...TypedDict、Literal 与高级类型
from typing import TypedDict, Literal, TypeGuard, overload
from typing import NotRequired, Required
# TypedDict:精确的字典结构
class UserInfo(TypedDict):
name: str
age: int
email: NotRequired[str] # 可选字段
role: Required[Literal["admin", "user", "guest"]]
def create_user(name: str, age: int, role: Literal["admin", "user", "guest"] = "user") -> UserInfo:
return {"name": name, "age": age, "role": role}
# 类型守卫
def is_admin(user: UserInfo) -> TypeGuard[UserInfo]:
return user.get("role") == "admin"
# 函数重载
@overload
def get_value(data: dict, key: str) -> str: ...
@overload
def get_value(data: dict, key: str, default: str) -> str: ...
@overload
def get_value(data: dict, key: str, default: None) -> str | None: ...
def get_value(data: dict, key: str, default: str | None = None) -> str | None:
return data.get(key, default)
# TypeAlias 和类型组合
from typing import TypeAlias
UserId: TypeAlias = int
JsonData: TypeAlias = dict[str, str | int | float | bool | None]
ApiResult: TypeAlias = dict[str, JsonData | list[JsonData]]
def fetch_user(user_id: UserId) -> ApiResult:
return {
"user": {"id": user_id, "name": "张三", "score": 95.5, "active": True}
}深入 TypeGuard 类型守卫
from typing import TypeGuard, Union
# TypeGuard 让类型检查器在条件判断后收窄类型
class Dog:
def __init__(self, name: str):
self.name = name
def bark(self) -> str:
return f"{self.name}: 汪汪!"
class Cat:
def __init__(self, name: str):
self.name = name
def meow(self) -> str:
return f"{self.name}: 喵~"
def is_dog(animal: Dog | Cat) -> TypeGuard[Dog]:
"""类型守卫:判断是否为 Dog"""
return isinstance(animal, Dog)
def interact(animal: Dog | Cat) -> str:
if is_dog(animal):
# 类型收窄:这里 animal 是 Dog
return animal.bark()
else:
# 类型收窄:这里 animal 是 Cat
return animal.meow()
# 自定义类型守卫
def is_non_empty_string(value: Any) -> TypeGuard[str]:
return isinstance(value, str) and len(value) > 0
def is_dict_of_strings(value: Any) -> TypeGuard[dict[str, str]]:
return (isinstance(value, dict) and
all(isinstance(k, str) and isinstance(v, str) for k, v in value.items()))
# TypeGuard vs isinstance
# isinstance 只支持运行时类型检查
# TypeGuard 告诉类型检查器收窄后的类型(可以比实际运行时类型更精确)dataclass 与类型驱动的模型设计
from dataclasses import dataclass, field
from typing import ClassVar
from enum import Enum
class Status(str, Enum):
ACTIVE = "active"
INACTIVE = "inactive"
PENDING = "pending"
@dataclass(frozen=True, slots=True) # Python 3.10+ slots
class Address:
city: str
district: str
detail: str
def full_address(self) -> str:
return f"{self.city}{self.district}{self.detail}"
@dataclass
class Employee:
name: str
age: int
status: Status = Status.ACTIVE
address: Address | None = None
skills: list[str] = field(default_factory=list)
_id_counter: ClassVar[int] = 0
def __post_init__(self):
if self.age < 18:
raise ValueError(f"年龄不能小于 18,当前: {self.age}")
def add_skill(self, skill: str) -> None:
if skill not in self.skills:
self.skills.append(skill)
def deactivate(self) -> None:
self.status = Status.INACTIVE
# 使用
addr = Address(city="北京", district="海淀区", detail="中关村大街1号")
emp = Employee(name="张三", age=30, address=addr, skills=["Python", "Go"])
emp.add_skill("Rust")
print(emp)
# Employee(name='张三', age=30, status=<Status.ACTIVE: 'active'>, ...)Pydantic 模型与运行时验证
from pydantic import BaseModel, Field, validator, EmailStr
from typing import Optional
from datetime import datetime
class UserCreate(BaseModel):
"""用户创建请求模型"""
name: str = Field(min_length=1, max_length=50, description="用户名")
email: EmailStr = Field(description="邮箱地址")
age: int = Field(ge=0, le=150, description="年龄")
role: str = Field(default="user", pattern="^(admin|user|guest)$")
@validator("name")
def name_must_not_contain_space(cls, v: str) -> str:
if " " in v:
raise ValueError("用户名不能包含空格")
return v.strip()
class UserResponse(BaseModel):
"""用户响应模型"""
id: int
name: str
email: str
age: int
role: str
created_at: datetime
class Config:
from_attributes = True # 允许从 ORM 对象创建
# 使用 —— 运行时自动验证
try:
user = UserCreate(name="", email="invalid", age=200, role="super")
except Exception as e:
print(f"验证失败: {e}")
# 自动生成详细的错误信息,包含每个字段的具体问题
# 正确数据
user = UserCreate(name="张三", email="zhangsan@example.com", age=30)
print(f"验证通过: {user.model_dump()}")TYPE_CHECKING 与循环导入解决
# 在类型注解中引用尚未定义或会导致循环导入的类型
from typing import TYPE_CHECKING, Optional
if TYPE_CHECKING:
# 这段代码只在类型检查时执行,运行时不会执行
# 可以安全地引用任何模块而不会导致循环导入
from models import User
from services import DatabaseService
class UserService:
def __init__(self, db: "DatabaseService"):
self.db = db
def get_user(self, user_id: int) -> "User | None":
"""返回值类型引用了通过 TYPE_CHECKING 导入的 User"""
return self.db.find_by_id(user_id)
# 另一种方式:使用字符串形式的类型注解(前向引用)
class TreeNode:
def __init__(self, value: int):
self.value = value
self.left: "TreeNode | None" = None
self.right: "TreeNode | None" = None
def add_left(self, node: "TreeNode") -> None:
self.left = node
def add_right(self, node: "TreeNode") -> None:
self.right = node回调函数与 Callable 类型
from typing import Callable, ParamSpec, Concatenate
from collections.abc import Awaitable
# 基础 Callable
def apply_operation(
value: int,
operation: Callable[[int], int]
) -> int:
return operation(value)
result = apply_operation(5, lambda x: x ** 2) # 25
# 多参数 Callable
MathFunc = Callable[[float, float], float]
def compute(a: float, b: float, func: MathFunc) -> float:
return func(a, b)
print(compute(3.0, 4.0, lambda x, y: x + y)) # 7.0
print(compute(3.0, 4.0, lambda x, y: x * y)) # 12.0
# ParamSpec(Python 3.10+)—— 保留被装饰函数的签名
P = ParamSpec("P")
R = TypeVar("R")
def log_calls(func: Callable[P, R]) -> Callable[P, R]:
"""记录函数调用的装饰器,保留原始签名"""
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
print(f"调用 {func.__name__}")
return func(*args, **kwargs)
return wrapper
@log_calls
def add(a: int, b: int) -> int:
return a + b
# 类型检查器知道 add 仍然接受 (int, int) -> int
# 异步回调
AsyncCallback = Callable[[str], Awaitable[bool]]
async def on_message(message: str) -> bool:
print(f"处理消息: {message}")
return True工具链配置
mypy 配置
# mypy.ini 或 pyproject.toml 中配置
[mypy]
# 基本设置
python_version = 3.11
warn_return_any = True
warn_unused_configs = True
disallow_untyped_defs = True
disallow_any_generics = True
# 严格模式
strict_optional = True
strict_equality = True
check_untyped_defs = True
# 第三方库
ignore_missing_imports = False
# 路径设置
mypy_path = src
files = src/**/*.py
# 按模块配置
[mypy.tests.*]
disallow_untyped_defs = False
[mypy.third_party.*]
ignore_missing_imports = Truepyproject.toml 集成
[tool.mypy]
python_version = "3.11"
strict = true
warn_return_any = true
warn_unused_ignores = true
disallow_untyped_defs = true
check_untyped_defs = true
[[tool.mypy.overrides]]
module = ["tests.*"]
disallow_untyped_defs = false
[tool.pyright]
pythonVersion = "3.11"
typeCheckingMode = "strict"优点
缺点
总结
类型注解是 Python 工程化的基础能力,它通过静态类型检查在编码阶段捕获错误,显著提升代码可维护性和团队协作效率。建议从函数签名开始逐步添加注解,配合 mypy 集成到 CI/CD 流程中。Protocol、TypedDict、Generic 是最实用的高级特性,值得优先掌握。
关键知识点
- Optional[X] 等价于 X | None,Python 3.10+ 推荐使用后者
- Protocol 实现结构化类型(鸭子类型的静态检查版本)
- TypedDict 为 JSON/字典数据提供精确的类型约束
- Generic 和 TypeVar 用于创建类型安全的通用组件
- TypeGuard 用于自定义类型收窄逻辑
- TYPE_CHECKING 用于解决类型注解中的循环导入问题
- ParamSpec 用于保留被装饰函数的参数签名
项目落地视角
- CI/CD 中集成 mypy --strict 检查,防止类型错误合入主分支
- 新项目从第一天就添加类型注解,老项目采用渐进式策略逐步补齐
- 使用 py.typed 标记和 stub 文件(.pyi)为公共 API 提供类型信息
- 统一 typing 的使用风格,编写类型注解规范文档
常见误区
- 把 type: ignore 当作万能药,不区分是误报还是代码问题
- 使用 Any 放弃类型检查,导致类型系统形同虚设
- 忽略第三方库的类型支持,需要安装 types-xxx 包或编写 stub
- 过度使用复杂类型嵌套,降低代码可读性
- 在 Union 类型使用后忘记处理所有可能的分支
- TypedDict 中使用可变默认值(应该用 NotRequired)
进阶路线
- 学习 mypy 的高级配置:disallow_untyped_defs、strict 等
- 研究 TypeVarTuple(Python 3.11+)和 ParamSpec 的高级用法
- 了解 pyright 和基于 LSP 的类型检查工作流
- 探索类型驱动开发(Type-Driven Development)在 Python 中的实践
适用场景
- 中大型项目需要提升代码可维护性和团队协作效率
- 公共库和 SDK 需要为使用者提供清晰的 API 类型信息
- 微服务接口需要严格的输入输出类型约束
落地建议
- 从核心模块的公共 API 开始添加类型注解,逐步向内扩展
- 配置 pre-commit hook 在提交前自动运行 mypy
- 为项目编写 py.typed 标记文件,确保类型信息对使用者可见
- 团队统一使用 Python 3.10+ 语法(X | None 替代 Optional[X])
排错清单
- 检查 mypy 版本是否与 Python 版本匹配
- 确认第三方库的类型存根(types-requests 等)是否已安装
- 排查循环导入导致的类型检查失败,考虑使用 TYPE_CHECKING 守卫
- 确认 mypy 是否正确读取了配置文件
复盘问题
- 你的项目中类型注解的覆盖率是多少?哪些模块最需要补齐?
- mypy 的 strict 模式在项目中开启了多少规则?有多少 type: ignore 需要清理?
- 类型注解是否真正帮助团队减少了运行时类型错误?有哪些数据支撑?
