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.pymypy.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 版本对比
| Feature | Python 3.8 | Python 3.9 | Python 3.10 | Python 3.12 |
|---|---|---|---|---|
| Basic annotations | typing.List, Dict | list, dict | list, dict | list, dict |
| Union types | Union[X, Y] | Union[X, Y] | X | Y | X | Y |
| Optional | Optional[X] | Optional[X] | X | None | X | None |
| TypeAlias | MyType = ... | MyType = ... | TypeAlias | type MyType = ... |
| ParamSpec | N/A | N/A | ParamSpec | ParamSpec |
| Self type | N/A | N/A | N/A | Self |
最佳实践
- 从返回类型和公共 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,然后逐步扩大覆盖范围。随着代码库增长,收益会随时间累积。