Python 类型注解完全指南:从入门到实践

Python 3.5+ 引入类型注解,配合 mypy/pyright 让 Python 也能享受静态类型检查的好处。

$1.1k 字/约 6 min👁— views

Python 类型注解完全指南:从入门到实践

Python 是动态类型语言,但从 3.5 开始引入了类型注解(Type Hints),配合 mypy 或 pyright,可以在不牺牲动态特性的前提下获得静态类型检查的好处。

为什么要用类型注解

  1. IDE 补全更智能:VS Code / PyCharm 能精准推断方法和属性
  2. 重构更安全:改函数签名时工具帮你找到所有调用点
  3. 文档即代码:类型注解就是最好的参数说明
  4. 提前发现 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.inipyproject.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 开始加注解,逐步扩展到整个项目。