Python 类型注解完全指南:从入门到实践
Python 是动态类型语言,但从 3.5 开始引入了类型注解(Type Hints),配合 mypy 或 pyright,可以在不牺牲动态特性的前提下获得静态类型检查的好处。
为什么要用类型注解
- IDE 补全更智能:VS Code / PyCharm 能精准推断方法和属性
- 重构更安全:改函数签名时工具帮你找到所有调用点
- 文档即代码:类型注解就是最好的参数说明
- 提前发现 Bug:mypy strict 模式能在运行前抓住大量错误
# 安装 mypy
pip install mypy
# 检查文件
mypy main.py
# strict 模式(推荐)
mypy --strict main.py
基础类型
# 变量注解
name: str = "Alice"
age: int = 30
pi: float = 3.14
active: bool = True
nothing: None = None
# 函数注解
def greet(name: str) -> str:
return f"Hello, {name}"
def add(a: int, b: int) -> int:
return a + b
# 无返回值
def log(msg: str) -> None:
print(msg)
容器类型
Python 3.9+ 可以直接用内置类型,旧版本需要从 typing 导入。
from typing import List, Dict, Tuple, Set # Python 3.8 及以下
# Python 3.9+ 写法(推荐)
def process(items: list[int]) -> dict[str, int]:
return {"count": len(items), "sum": sum(items)}
# Tuple:固定长度
def get_point() -> tuple[int, int]:
return (1, 2)
# Tuple:可变长度(同类型)
def get_scores() -> tuple[int, ...]:
return (90, 85, 92)
# Set
def unique_tags(tags: list[str]) -> set[str]:
return set(tags)
Python 3.10+ 新写法
# Union 用 | 代替
def parse(value: str | int) -> str:
return str(value)
# Optional[T] 等价于 T | None
def find_user(uid: int) -> str | None:
if uid == 1:
return "Alice"
return None
# 旧写法(仍然有效)
from typing import Optional, Union
def old_style(x: Optional[int]) -> Union[str, int]:
return x or 0
TypedDict:给字典加类型
from typing import TypedDict
class UserInfo(TypedDict):
name: str
age: int
email: str
class PartialUser(TypedDict, total=False):
name: str
age: int # 可选字段
def create_user(info: UserInfo) -> str:
return f"{info['name']} ({info['age']})"
# 使用
user: UserInfo = {"name": "Bob", "age": 25, "email": "bob@example.com"}
print(create_user(user))
Protocol:结构化子类型
Protocol 比 ABC 更灵活,不需要显式继承,只要"鸭子类型"匹配就行。
from typing import Protocol
class Drawable(Protocol):
def draw(self) -> None: ...
def get_area(self) -> float: ...
class Circle:
def __init__(self, r: float):
self.r = r
def draw(self) -> None:
print(f"Circle r={self.r}")
def get_area(self) -> float:
return 3.14 * self.r ** 2
class Square:
def __init__(self, side: float):
self.side = side
def draw(self) -> None:
print(f"Square side={self.side}")
def get_area(self) -> float:
return self.side ** 2
# Circle 和 Square 都没有继承 Drawable,但都满足 Protocol
def render(shape: Drawable) -> None:
shape.draw()
print(f"Area: {shape.get_area():.2f}")
render(Circle(5)) # OK
render(Square(4)) # OK
TypeVar:泛型函数
from typing import TypeVar
T = TypeVar("T")
def first(items: list[T]) -> T | None:
return items[0] if items else None
# 带约束的 TypeVar
Number = TypeVar("Number", int, float)
def double(x: Number) -> Number:
return x * 2
print(first([1, 2, 3])) # 1,类型推断为 int
print(first(["a", "b"])) # "a",类型推断为 str
print(double(3)) # 6
print(double(2.5)) # 5.0
dataclass + 类型注解
from dataclasses import dataclass, field
from typing import ClassVar
@dataclass
class Product:
name: str
price: float
tags: list[str] = field(default_factory=list)
_id: int = field(default=0, repr=False)
# 类变量(不是实例字段)
category: ClassVar[str] = "general"
def discounted_price(self, pct: float) -> float:
return self.price * (1 - pct)
p = Product("Laptop", 999.99, ["tech", "computer"])
print(p)
print(p.discounted_price(0.1)) # 899.991
# frozen=True 让 dataclass 不可变
@dataclass(frozen=True)
class Point:
x: float
y: float
pt = Point(1.0, 2.0)
# pt.x = 3.0 # FrozenInstanceError
mypy 配置
在项目根目录创建 mypy.ini 或 pyproject.toml:
# mypy.ini
[mypy]
python_version = 3.12
strict = true
warn_return_any = true
warn_unused_configs = true
# pyproject.toml
[tool.mypy]
python_version = "3.12"
strict = true
# 运行 strict 检查
mypy --strict src/
# 常见输出
# error: Function is missing a return type annotation
# error: Argument 1 to "foo" has incompatible type "str"; expected "int"
实际项目中的细节
py.typed 标记
如果你发布了一个库,需要在包根目录放一个空的 py.typed 文件,告诉类型检查器这个包支持类型注解:
touch mypackage/py.typed
# pyproject.toml
[tool.setuptools.package-data]
mypackage = ["py.typed"]
type: ignore 注释
遇到无法修复的类型错误(比如第三方库没有 stub),可以用 # type: ignore 抑制:
import untyped_lib # type: ignore[import]
result = untyped_lib.do_something() # type: ignore[no-any-return]
cast:强制类型转换
from typing import cast
value: object = get_some_value()
# 你确定它是 str,但类型推断不知道
s = cast(str, value)
print(s.upper())
总结
| 特性 | 场景 |
|---|---|
| 基础注解 | 所有函数参数/返回值 |
| TypedDict | 字典结构固定时 |
| Protocol | 需要鸭子类型约束 |
| TypeVar | 泛型函数/类 |
| dataclass | 数据容器类 |
| mypy strict | 项目级别质量把控 |
类型注解不是强制的,渐进式引入即可。从最重要的公共 API 开始加注解,逐步扩展到整个项目。