Python 是动态类型语言,但在工程化项目中,类型注解(Type Hints)能让代码意图更清晰、IDE 补全更精准,并且借助 mypy 等工具在运行前发现类型错误。
本章将从基础语法到工程实践,全面掌握类型系统。
下面从四个方面说明为什么需要类型注解:
| 场景 | 没有类型注解 | 有类型注解 |
| 阅读代码 | 需要猜测参数和返回值类型 | 签名即文档 |
| IDE 补全 | 无法推断属性,补全失效 | 精准提示方法和属性 |
| 重构 | 改一个函数签名,不知道影响哪些地方 | mypy 静态检查提前暴露调用方错误 |
| 协作 | 新成员靠试凑了解接口 | 类型即契约,减少沟通成本 |
注意:类型注解不会影响运行时行为,Python 解释器在运行时完全忽略类型标记。其价值体现在开发体验和静态检查阶段。
用于函数上的类型注解,主要用来指定参数的类型,以及函数返回类型。例如:
# 指定参数 a 和 b 均是 int 类型,返回值类型为 int
def add(a: int, b: int) -> int:
return a + b
# 指定参数 name 是字符串,age 是int类型,返回字符串类型
def greet(name: str, age: int) -> str:
return f"Hello {name}, you are {age} years old"
# 指定参数falg和返回值均为布尔类型
def is_active(flag: bool) -> bool:
return flag
# 调用(运行时完全正常,即使传 "wrong" 类型也不会报错)
print(add(1, 2))
print(greet("Alice", 30))
print(is_active(True))
# 以下代码在 mypy 检查中会报错,但运行不会报错
# add("1", "2") # mypy: Argument 1 to "add" has incompatible type "str"从 Python 3.6 开始,变量也支持定义类型,基础语法如下:
变量名: 类型 = 值注意,类型注解只是「提示」,不是强制类型检查,运行时不会报错,主要给 IDE、静态检查工具(mypy)使用。
例如:
from typing import List
# 变量注解
name: str = "Alice"
count: int = 0
prices: list[float] = [19.99, 29.99, 9.99]
# 无初始值时的声明(占位)
result: str
# 类属性注解
class User:
id: int
name: str
def __init__(self, id: int, name: str) -> None:
self.id = id
self.name = name
u = User(1, "Alice")
print(f"User({u.id}, {u.name})")注意,Python3.6 引入类型注解语法,但是原生内置类型(list/dict)早期不支持泛型写法,所以推出 typing 模块提供泛型、复合类型。
但是 Python3.9+ 支持原生泛型 list[T]、dict[K,V],typing 里对应的 List/Dict/Tuple 等逐步废弃,只是为了兼容旧项目。
要了解泛型容器,我们需要先了解两个词:
容器:能装多个数据的类型 → list、dict、tuple、set。
泛型(Generic):容器不固定里面元素是什么类型,允许你「指定容器内元素类型」。
普通容器只知道这是一个列表,但不知道里面放 int 还是 str。泛型容器给容器打上标记,明确说明这个列表只能存放 int;这个字典 key 是 str、value 是 float。理解了吧!!
注意,List、Dict、Tuple、Set 不是 Python 的基础类型,这些容器均来自 typing 模块。从 Python3.9+ 推荐使用原生内置类型:list, dict, tuple, set,无需导入;typing 里这几个类只是兼容旧代码。
下面演示 List、Dict、Tuple、Set 的泛型用法:
# 导入typing模块中的泛型容器与可选类型
# List/Dict/Tuple/Set:Python3.8及更早泛型容器注解;3.9+推荐使用原生list/dict/tuple/set
# Optional[T] 等价 Union[T, None],代表返回值可以是指定类型或者None
from typing import List, Dict, Tuple, Set, Optional
def sum_list(arr: List[int]) -> int:
"""
对整数列表求和
:param arr: List[int] 元素全部为int类型的列表
:return: int 列表所有元素累加和
"""
return sum(arr)
def build_map(keys: List[str], values: List[int]) -> Dict[str, int]:
"""
根据key列表、value列表构建字典
:param keys: List[str] 字符串类型键列表
:param values: List[int] 数字类型值列表
:return: Dict[str, int] 键为字符串、值为整型的字典
"""
return dict(zip(keys, values))
def get_point() -> Tuple[float, float]:
"""
获取二维坐标点
:return: Tuple[float, float] 定长二元元组,两个元素都是浮点型 (x, y)
"""
return (1.0, 2.0)
def unique_tags(tags: List[str]) -> Set[str]:
"""
标签列表去重
:param tags: List[str] 字符串标签列表(存在重复)
:return: Set[str] 元素为字符串的集合(自动去重、无序)
"""
return set(tags)
def find_user(users: List[Dict[str, str]], uid: str) -> Optional[Dict[str, str]]:
"""
根据用户id查找用户信息
:param users: List[Dict[str, str]] 用户列表,列表内每个元素是{字符串:字符串}结构字典
:param uid: str 需要匹配的用户编号
:return: Optional[Dict[str, str]] 找到返回用户字典;找不到返回None
"""
for u in users:
if u.get("id") == uid:
return u
return None
if __name__ == "__main__":
# 定义整型列表,使用泛型注解List[int]约束容器内元素类型
nums: List[int] = [1, 2, 3, 4]
print(sum_list(nums)) # 10
kv = build_map(["a", "b"], [1, 2])
print(kv) # {'a': 1, 'b': 2}
pt = get_point()
print(pt) # (1.0, 2.0)
tags = unique_tags(["python", "go", "python", "rust"])
print(tags) # {'python', 'go', 'rust'}
users = [{"id": "1", "name": "Alice"}, {"id": "2", "name": "Bob"}]
print(find_user(users, "2")) # 匹配到用户,输出用户字典
print(find_user(users, "99")) # 无匹配用户,输出 None嵌套泛型指泛型里面再套一层泛型。当容器内部存放的依然是另一个带类型约束的容器,就形成嵌套。
普通泛型:list[int] 列表装数字
嵌套泛型:list[list[int]] 列表里面装列表,内层列表同样约束元素类型
嵌套泛型就好比箱子里放箱子,内外两层箱子都规定好里面能装什么东西。
例如:
from typing import List, Dict, Tuple
# 二维坐标列表
points: List[Tuple[float, float]] = [(0, 0), (1, 2), (3, 4)]
# 嵌套字典
config: Dict[str, Dict[str, int]] = {
"server": {"port": 8080, "timeout": 30},
"client": {"retry": 3},
}
# 表格数据:每行是一个字典
table: List[Dict[str, str | int]] = [
{"name": "Alice", "age": 30},
{"name": "Bob", "age": 25},
]
# Python 3.9+ 可以直接用 list[dict[str, str]],不需要从 typing 导入
# 但为了兼容 3.8 及更早版本,工程上常保留 from typing import ...Optional[X] 等价于 Union[X, None],表示值可以是类型 X 或者 None。例如:
from typing import Optional
def get_name(uid: int) -> Optional[str]:
"""如果 uid > 0 返回名字,否则返回 None"""
return "Alice" if uid > 0 else None
def parse_int(s: str) -> Optional[int]:
try:
return int(s)
except ValueError:
return None
# 使用时必须检查 None,否则 mypy 会提示可能解引用 None
result = get_name(1)
if result is not None:
print(result.upper())
else:
print("未找到")联合类型指一个变量 / 参数 / 返回值,可以是多种类型中的任意一种。例如:
Union[T1, T2, T3]允许变量的类型是 T1 或者 T2 或者 T3。
例如:
from typing import Union, Optional
def process_id(uid: Union[int, str]) -> str:
"""
处理ID,将id统一格式化输出
:param uid: Union[int, str] 联合类型,参数允许传入整数 或者 字符串
:return: str 格式化后的ID字符串
"""
return f"ID-{uid}"
def fetch_data(key: str) -> Union[dict, list, None]:
"""
根据标识拉取对应数据
:param key: str 数据标识名称
:return: Union[dict, list, None] 联合类型;
key=users 返回列表;key=config 返回字典;其他情况返回None
"""
if key == "users":
return [{"id": 1, "name": "Alice"}]
if key == "config":
return {"debug": True}
return None
# Python 3.10+ 新语法:使用 | 替代 Union[类型1,类型2],语义完全等价
def modern_union(uid: int | str) -> str:
"""
联合类型新式写法演示
:param uid: int | str 等价 Union[int, str],支持int或字符串类型ID
:return: str 格式化ID文本
"""
return f"ID-{uid}"注意,Optional[T] 是 Union 的特殊简写形式。Optional[str] 完全等价 Union[str, None],允许变量/返回值为 str 或者 None。
类型收窄指静态类型检查工具(mypy / Pyright)通过代码逻辑,把宽泛的联合类型缩小成更精确的单一类型。例如:
宽泛类型:int | str(联合类型,不确定是哪一种)
收窄:经过 if isinstance()、if 判断后,编译器确定当前分支只能是 int 或者只能是 str
注意:Python 本身运行时不做,只作用于静态类型分析。
示例:
from typing import Union, Optional
def describe(value: Union[int, str, list]) -> str:
"""
描述传入数据的类型信息
:param value: Union[int, str, list] 联合类型,允许传入整数、字符串、列表三者其一
:return: str 拼接好的类型描述文本
"""
# 类型收窄:静态工具识别当前分支 value 确定为 int
if isinstance(value, int):
return f"整数: {value}"
# 类型收窄:当前分支 value 确定为 str
if isinstance(value, str):
return f"字符串: {value!r}"
# 类型收窄:当前分支 value 确定为 list
if isinstance(value, list):
return f"列表,长度 {len(value)}"
return "未知类型"
# Optional = Union[T, None]
# 通过 is None / is not None 判断实现类型收窄
def greet(name: Optional[str]) -> str:
"""
打招呼函数
:param name: Optional[str] 等价 Union[str, None];可以是字符串,也可以是 None
:return: str 问候语句
"""
# 分支收窄:满足条件时 name 类型锁定为 None
if name is None:
return "Hello, stranger"
# 经过上面 if 判断,静态类型工具完成【类型收窄】
# 确定走到此处 name 一定是 str,不再包含 None
# 可以安全调用字符串方法 title(),不会触发类型警告
return f"Hello, {name.title()}"Callable 用来标注“变量是一个可调用对象(函数、方法、lambda)”,描述函数的参数格式与返回值类型。
普通类型标注 int / str 代表数据,而 Callable 专门代表函数本身,告诉静态工具“这个变量是能被 () 调用的”。
基础语法:
from typing import Callable
# 完整格式
Callable[[参数类型1, 参数类型2, ...], 返回值类型]第一个方括号:[参数类型列表],代表入参类型
第二个类型:整个函数的返回值类型
例如:通过 Cllable 定义两个 int 操作的函数,返回值也为 int。
from typing import Callable, List
def apply_operation(a: int, b: int, op: Callable[[int, int], int]) -> int:
"""
接收两个数字和一个运算函数,执行运算并返回结果
:param a: int 参与运算的第一个数字
:param b: int 参与运算的第二个数字
:param op: Callable[[int, int], int]
Callable 代表可调用对象(函数);
[[int, int], int] 含义:接收两个int参数,返回int
:return: int 运算结果
"""
return op(a, b)
def add(x: int, y: int) -> int:
"""加法函数,匹配 Callable[[int, int], int] 类型签名"""
return x + y
def multiply(x: int, y: int) -> int:
"""乘法函数,匹配 Callable[[int, int], int] 类型签名"""
return x * y
print(apply_operation(2, 3, add)) # 5
print(apply_operation(2, 3, multiply)) # 6
# lambda匿名函数同样满足签名,可以传入
print(apply_operation(2, 3, lambda x, y: x - y)) # -1你还可以定义没有参、没有返回值的回调函数,例如:
from typing import Callable, List
def run_twice(func: Callable[[], None]) -> None:
"""
执行传入的函数两次
:param func: Callable[[], None]
无入参、无返回值的可调用对象;
[] 代表参数列表为空,返回类型为 None
:return: None
"""
func()
func()
def say_hi() -> None:
"""无参数、无返回值的函数,匹配 Callable[[], None]"""
print("Hi")
run_twice(say_hi)如果我们事先不知道函数的参数数量,以及返回类型。可以通过 Callable[..., Any] 进行表示,其中 ... 代表不限定参数数量与类型。例如:
from typing import Callable, List
from typing import Any
def log_and_call(func: Callable[..., Any], *args: Any, **kwargs: Any) -> Any:
"""
通用包装器:打印日志后调用目标函数
:param func: Callable[..., Any]
... 表示任意数量、任意类型参数;返回值为任意类型Any
代价:无法静态检查func内部参数是否匹配
:param args: Any 可变位置参数
:param kwargs: Any 可变关键字参数
:return: Any 被包装函数执行后的返回结果
"""
print(f"Calling {func.__name__} with args={args}, kwargs={kwargs}")
return func(*args, **kwargs)
def add(x: int, y: int) -> int:
"""加法函数"""
return x + y
print(log_and_call(add, 1, 2))
# 输出:
# Calling add with args=(1, 2), kwargs={}
# 3我们为什么要使用泛型编程呢?唯一的目的是复用代码,编写一套通用代码,不绑定固定类型,可以适配 int、str、list 等多种类型,同时保留类型提示。不再为 int列表、str列表 各自写一套重复函数。
而 TypeVar 则是用来定义类型变量(类似于一个类型占位符 T),相当于告诉类型检查工具:这里代表某一种未知类型,同一个函数内保持统一。
基础语法如下:
from typing import TypeVar
# 定义一个无限制的类型变量 T
T = TypeVar("T")下面演示如何通过 TypeVar 进行泛型编程:
from typing import TypeVar, List, Optional, Dict
# 定义泛型类型变量
T = TypeVar("T")
K = TypeVar("K")
V = TypeVar("V")
def first(items: List[T]) -> Optional[T]:
"""返回列表第一个元素,列表为空返回 None"""
return items[0] if items else None
def get_key(d: Dict[K, V], key: K) -> Optional[V]:
"""泛型字典取值"""
return d.get(key)
nums = [1, 2, 3]
print(first(nums)) # mypy 推断返回 Optional[int]
words = ["a", "b", "c"]
print(first(words)) # 'a',mypy 推断返回 Optional[str]
users: Dict[int, str] = {1: "Alice"}
print(get_key(users, 1)) # Alice看见了吗,first() 函数可以接收数字列表,也能接收字符串列表。
有时我们可能希望泛型类型是某一种大的类型,如数字类型,但是允许时整型、浮点型。TypeVar 提供两种方式约束允许的类型:
固定枚举约束:TypeVar("T", 类型1, 类型2, ...),例如:
T = TypeVar("T", int, str, bool)此时,T 只能是 int /str/bool 三者之一,不能是其他类型。
上界约束 bound(也称继承体系约束):TypeVar("T", bound=父类型),例如:
T = TypeVar("T", bound=Animal)此时,T 必须是 Animal 或者 Animal 的子类。
示例 1:约束泛型只能是 int 或 float 的数字类型
from typing import TypeVar
# 只允许 int 或 float 的数值类型
Number = TypeVar("Number", int, float)
def add_numbers(a: Number, b: Number) -> Number:
return a + b
print(add_numbers(1, 2)) # 3,int
print(add_numbers(1.5, 2.5)) # 4.0,float
# 以下在 mypy 中会报错(str 不在 Number 允许范围内)
# add_numbers("1", "2")示例 2:约束泛型必须是 Animal 或它的子类
from typing import TypeVar
class Animal:
def speak(self) -> str:
return "animal"
class Dog(Animal):
def speak(self) -> str:
return "woof"
class Cat(Animal):
def speak(self) -> str:
return "meow"
# bound 约束:T 必须是 Animal 或它的子类
T = TypeVar("T", bound=Animal)
def animal_speak(animal: T) -> T:
animal.speak()
return animal
if __name__ == "__main__":
dog = Dog()
cat = Cat()
animal_speak(dog) # Dog 继承 Animal
animal_speak(cat) # Cat 继承 Animal
# animal_speak(123) # int 不满足 boundPython 运行时遵循鸭子类型:
看起来像鸭子、叫起来像鸭子,那它就是鸭子;不需要继承同一个父类。
但是传统静态类型系统(基于类继承)无法识别鸭子类型,必须显式继承才能匹配。typing.Protocol 就是用来把鸭子类型静态化。
Protocol:定义一份能力契约,只要类拥有 Protocol 规定的属性 / 方法,就算实现了这个协议,不需要 class X(Protocol) 显式继承。
示例:
from typing import Protocol, Iterable
# 定义 Protocol 契约
class Readable(Protocol):
"""协议:任何有 read() -> str 方法的对象"""
def read(self) -> str:
...
# 符合契约
class FileReader:
def read(self) -> str:
return "file content"
# 符合契约
class StringReader:
def read(self) -> str:
return "string content"
# 不符合契约,不能用于 process 函数
class SocketReader:
# 缺少 read() 方法,不满足 Protocol
def recv(self) -> str:
return "socket data"
def process(reader: Readable) -> str:
return reader.read().upper()
print(process(FileReader())) # FILE CONTENT
print(process(StringReader())) # STRING CONTENT
# 以下在 mypy 中会报错:SocketReader 没有 read() 方法
# process(SocketReader())ClassVar 来自 typing,用来标记类属性,区分:
类属性:属于类本身,所有实例共享
实例属性:绑定在 self,每个对象独立
不加 ClassVar 时,mypy/pyright 会默认把类层级变量当成实例属性默认值,造成类型分析错误。
语法如下:
# 标准写法(推荐)
count: ClassVar[int] = 0
# 错误写法
count: ClassVar = 0 # 缺少内部类型
count = ClassVar[int](0) # ClassVar不是构造函数,不能这样调用例如:
from typing import ClassVar
class Demo:
# 声明:这是类属性
count: ClassVar[int] = 0
# 普通实例属性(写在 __init__ 上方预先声明)
name: str
def __init__(self, name: str):
self.name = name注意:
不能通过实例修改被 ClassVar 标记的属性。但是,运行时 Python 不会阻止,但类型层面不合法。因为赋值 self.species 会新建一个实例属性,遮蔽类属性,极易产生 bug。
ClassVar 只服务静态类型检查(mypy/pyright),运行时只是一个标记,Python 解释器本身不会拦截操作。
ClassVar 只能写在类体内部,不能出现在 __init__、方法里。
自引用类型指一个类的注解内部,需要引用自身这个类。典型场景:
方法返回当前类的实例(工厂类方法 from_xxx)
树形结构:节点包含同类型子节点(链表、树)
Python 代码自上而下执行。当解释器读到类内部注解时,类还没有定义完成,直接写类名会报 NameError。
这种“类还没创建,注解就要使用它自己” 的问题,统称为前向引用,自引用是前向引用最常见的一种。
把类名写成字符串 "ClassName",告诉类型检查器延后解析。例如:
from typing import Optional
class Node:
value: int
# 自引用:子节点同样是 Node
# 字符串字面量 "Node" 解决前向引用(类定义未完成,不能直接写 Node)
child: Optional["Node"]
def __init__(self, val: int):
self.value = val
self.child = None
# 工厂方法,返回自身实例,使用字符串自引用标注返回类型
@classmethod
def create(cls, val: int) -> "Node":
return cls(val)
if __name__ == "__main__":
# 使用构造函数实例化节点
root = Node(10)
print(f"根节点 value: {root.value}")
# 使用工厂方法 create 创建节点
child_node = Node.create(20)
print(f"子节点 value: {child_node.value}")
# 建立节点关联:root.child 类型为 Optional[Node]
root.child = child_node
print(f"根节点的子节点值: {root.child.value}")
# 把 child 置为 None(符合 Optional 允许 None 的定义)
root.child = None
print(f"清空后 child = {root.child}")
# 链式构建树形结构演示
n1 = Node.create(1)
n2 = Node.create(2)
n3 = Node.create(3)
n1.child = n2
n2.child = n3
# 遍历节点
current: Optional[Node] = n1
while current is not None:
print(f"遍历节点: {current.value}")
current = current.child运行示例,输出:
根节点 value: 10
子节点 value: 20
根节点的子节点值: 20
清空后 child = None
遍历节点: 1
遍历节点: 2
遍历节点: 3Python 3.7 引入 annotations 未来导入,3.10、3.11 广泛使用。
所有函数 / 变量注解全部延迟求值,不再立刻执行,不再触发 NameError。
文件最顶部第一行添加:from __future__ import annotations
例如:定义一个二叉树
from __future__ import annotations
from typing import Optional
class Node:
def __init__(self, value: int, left: Optional[Node] = None, right: Optional[Node] = None) -> None:
"""
二叉树节点
:param value: 节点存储数值
:param left: 左子节点,可选 Node / None(自引用类型)
:param right: 右子节点,可选 Node / None(自引用类型)
"""
self.value = value
self.left = left
self.right = right
def sum(self) -> int:
"""递归计算当前节点 + 所有子树节点数值总和"""
total = self.value
# 类型收窄:确认不为None才能调用 .sum()
if self.left is not None:
total += self.left.sum()
if self.right is not None:
total += self.right.sum()
return total
if __name__ == "__main__":
# 构建二叉树
leaf1 = Node(1)
node3 = Node(3, left=leaf1)
node15 = Node(15)
root = Node(10, left=node3, right=node15)
# 计算整棵树总和 10 + 3 + 1 + 15 = 29
tree_sum = root.sum()
print(f"整棵二叉树总和:{tree_sum}")
# 只计算左子树总和 3 + 1 = 4
left_sub_sum = root.left.sum()
print(f"根节点左子树总和:{left_sub_sum}")
# 右子树总和 15
right_sub_sum = root.right.sum()
print(f"根节点右子树总和:{right_sub_sum}")
# 叶子节点求和
print(f"叶子节点 leaf1 总和:{leaf1.sum()}")运行示例,输出:
整棵二叉树总和:29
根节点左子树总和:4
根节点右子树总和:15
叶子节点 leaf1 总和:1注意到了吗,Optional[Node] 没有将 Node 使用双引号括起来。
mypy 是 Python 官方推荐的静态类型检查工具。
Python 本身是动态语言,运行时才做类型判断,而 mypy 在代码运行之前,根据你写的类型注解,静态扫描、发现类型错误。
Python 解释器:运行时才报错
mypy:静态提前检查,不执行代码
支持标准 PEP 484 类型注解:int、str、Union、Optional、Callable、TypeVar、Protocol、泛型、类注解等,
执行 pip install mypy 命令安装 mypy:
C:\Users\Administrator> pip install mypy
Collecting mypy
Downloading mypy-2.3.0-cp313-cp313-win_amd64.whl.metadata (2.4 kB)
Requirement already satisfied: typing_extensions>=4.6.0 in .\AppData\Roaming\Python\Python313\site-packages (from mypy) (4.16.0)
Requirement already satisfied: mypy_extensions>=1.0.0 in D:\Program Files\Python313\Lib\site-packages (from mypy) (1.1.0)
Collecting pathspec>=1.0.0 (from mypy)
Downloading pathspec-1.1.1-py3-none-any.whl.metadata (14 kB)
Collecting librt>=0.13.0 (from mypy)
Downloading librt-0.15.0-cp313-cp313-win_amd64.whl.metadata (1.3 kB)
Collecting ast-serialize<1.0.0,>=0.6.0 (from mypy)
Downloading ast_serialize-0.8.0-cp39-abi3-win_amd64.whl.metadata (1.4 kB)
Downloading mypy-2.3.0-cp313-cp313-win_amd64.whl (11.2 MB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 11.2/11.2 MB 1.7 MB/s 0:00:06
Downloading ast_serialize-0.8.0-cp39-abi3-win_amd64.whl (1.1 MB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 1.1/1.1 MB 930.8 kB/s 0:00:01
Downloading librt-0.15.0-cp313-cp313-win_amd64.whl (126 kB)
Downloading pathspec-1.1.1-py3-none-any.whl (57 kB)
Installing collected packages: pathspec, librt, ast-serialize, mypy
Attempting uninstall: pathspec
Found existing installation: pathspec 0.12.1
Uninstalling pathspec-0.12.1:
Successfully uninstalled pathspec-0.12.1
Successfully installed ast-serialize-0.8.0 librt-0.15.0 mypy-2.3.0 pathspec-1.1.1
[notice] A new release of pip is available: 26.0.1 -> 26.2.1
[notice] To update, run: python.exe -m pip install --upgrade pip查看 mypy 的版本,确认安装是否成功:
C:\Users\Administrator> mypy --version
mypy 2.3.0 (compiled: yes)如果存在 demo.py 文件,代码如下:
from __future__ import annotations
from typing import Optional
class Node:
def __init__(self, value: int, left: Optional[Node] = None, right: Optional[Node] = None) -> None:
"""
二叉树节点
:param value: 节点存储数值
:param left: 左子节点,可选 Node / None(自引用类型)
:param right: 右子节点,可选 Node / None(自引用类型)
"""
self.value = value
self.left = left
self.right = right
def sum(self) -> int:
"""递归计算当前节点 + 所有子树节点数值总和"""
total = self.value
# 类型收窄:确认不为None才能调用 .sum()
if self.left is not None:
total += self.left.sum()
if self.right is not None:
total += self.right.sum()
return total
if __name__ == "__main__":
# 构建二叉树
leaf1 = Node(1)
node3 = Node(3, left=leaf1)
node15 = Node(15)
root = Node(10, left=node3, right=node15)
# 计算整棵树总和 10 + 3 + 1 + 15 = 29
tree_sum = root.sum()
print(f"整棵二叉树总和:{tree_sum}")
# 只计算左子树总和 3 + 1 = 4
left_sub_sum = root.left.sum()
print(f"根节点左子树总和:{left_sub_sum}")
# 右子树总和 15
right_sub_sum = root.right.sum()
print(f"根节点右子树总和:{right_sub_sum}")
# 叶子节点求和
print(f"叶子节点 leaf1 总和:{leaf1.sum()}")使用 mypy 检查 demo.py 文件:
E:\Users\Administrator\Desktop\python_demo> mypy demo.py
demo.py:40: error: Item "None" of "Node | None" has no attribute "sum" [union-attr]
demo.py:44: error: Item "None" of "Node | None" has no attribute "sum" [union-attr]
Found 2 errors in 1 file (checked 1 source file错误信息为 root.left 的类型是 Optional[Node] 也就是 Node | None。你直接写:
root.left.sum()root.left 有可能是 None,None 没有 sum () 方法。
修复后的代码如下:
from __future__ import annotations
from typing import Optional
class Node:
def __init__(self, value: int, left: Optional[Node] = None, right: Optional[Node] = None) -> None:
"""
二叉树节点
:param value: 节点存储数值
:param left: 左子节点,可选 Node / None(自引用类型)
:param right: 右子节点,可选 Node / None(自引用类型)
"""
self.value = value
self.left = left
self.right = right
def sum(self) -> int:
"""递归计算当前节点 + 所有子树节点数值总和"""
total = self.value
# 类型收窄:确认不为None才能调用 .sum()
if self.left is not None:
total += self.left.sum()
if self.right is not None:
total += self.right.sum()
return total
if __name__ == "__main__":
# 构建二叉树
leaf1 = Node(1)
node3 = Node(3, left=leaf1)
node15 = Node(15)
root = Node(10, left=node3, right=node15)
# 计算整棵树总和 10 + 3 + 1 + 15 = 29
tree_sum = root.sum()
print(f"整棵二叉树总和:{tree_sum}")
# 只计算左子树总和 3 + 1 = 4
if root.left is not None: # 看这里
left_sub_sum = root.left.sum()
print(f"根节点左子树总和:{left_sub_sum}")
# 右子树总和 15
if root.right is not None: # 看这里
right_sub_sum = root.right.sum()
print(f"根节点右子树总和:{right_sub_sum}")
# 叶子节点求和
print(f"叶子节点 leaf1 总和:{leaf1.sum()}")再次进行检查:
E:\Users\Administrator\Desktop\python_demo> mypy demo.py
Success: no issues found in 1 source file检查整个项目时,建议创建 mypy.ini 或 pyproject.toml 配置,将执行 mypy 中指定的参数写入配置,保证团队内部每个人执行 mypy 使用同样的参数,解决命令行参数繁琐、团队执行标准不一致两大痛点。
pyproject.toml 文件:
[tool.mypy]
python_version = "3.10"
strict = true
warn_return_any = true
warn_unused_ignores = true
ignore_missing_imports = true或者 mypy.ini 文件:
[mypy]
python_version = 3.10
strict = True
warn_return_any = True
warn_unused_ignores = True
ignore_missing_imports = True配置文件说明:
python_version = 3.10 告诉 mypy,你的代码运行在 Python 3.10 环境。mypy 会根据对应版本语法、类型特性做适配;影响语法解析、泛型写法(list[int] 是否允许)、PEP 支持;如果本地 Python 是 3.11,但这里写 3.10,mypy 会按 3.10 规则校验。建议和项目实际部署 Python 版本保持一致。
strict = True 严格模式,是一组规则集合开关,等价于一次性开启大量强类型检查:
强制函数必须写返回值注解(不能省略 -> ...)
禁止函数参数隐式变成 Any
开启 disallow_untyped_defs:不能存在完全没有注解的函数
开启 disallow_untyped_calls
严格 Optional 校验(就是你之前遇到的 union-attr 报错强制生效)
禁止隐式 Any
warn_return_any = True 开启:警告函数返回值推导为 Any,防止不知不觉扩散 Any,避免类型信息丢失,让类型链条断裂。注意,strict=True 不自带此项,必须手动打开。
warn_unused_ignores = True 检测无效的 # type: ignore[...],日常经常写这种注释屏蔽报错:
obj.some() # type: ignore[union-attr]好处是杜绝残留无效忽略注释,防止以后代码改动再次引入同类错误却被悄悄屏蔽。
ignore_missing_imports = True 忽略「找不到第三方库类型信息」的报错,典型报错:
Cannot find implementation or library stub for module named 'requests'很多第三方库(requests、pymysql 等)没有自带类型注解,也没有安装对应的 types-* 类型存根包。mypy 默认会抛出导入错误。开启后,不再提示第三方模块缺失类型。
假如项目中引入了 mypy.ini 文件,如下图:

执行 mypy . 命令,检查当前项目,如下:
E:\Users\Administrator\Desktop\python_demo\myproject> mypy .
main.py:5: error: Module "utils" does not explicitly export attribute "camel_to_snake" [attr-defined]
main.py:7: error: Module "utils" does not explicitly export attribute "read_json" [attr-defined]
main.py:8: error: Module "utils" does not explicitly export attribute "write_json" [attr-defined]
Found 3 errors in 1 file (checked 4 source files)还可以使用 --strict 开启严格模式:
mypy --strict .from typing import List, Optional, Dict
# 缺少返回值注解
# def bad_func(x): ...
# mypy: Function is missing a type annotation for one or more arguments
# 修复
def good_func(x: int) -> int:
return xfrom typing import List, Optional, Dict
def get_name() -> Optional[str]:
return None
# Optional 未检查 None
# name = get_name()
# print(name.upper()) # mypy: Item "None" of "Optional[str]" has no attribute "upper"
# 修复
def safe_upper() -> Optional[str]:
name = get_name()
if name is None:
return None
return name.upper()from typing import List, Optional, Dict
# 列表混用类型
# mypy: List item 0 has incompatible type "int"; expected "str"
# mixed: List[str] = [1, "two", 3] # 错误
# 修复:使用 Union
from typing import Union
mixed: List[Union[int, str]] = [1, "two", 3]from typing import List, Optional, Dict
# 字典 key 类型错误
# mypy: Dict entry 0 has incompatible type "str": "int"; expected "int": "str"
# mapping: Dict[int, str] = {"1": 2} # 错误
# 修复
mapping: Dict[str, int] = {"1": 2}from typing import List, Optional, Dict
# 忽略特定行(需要注释)
# reveal_type 是 mypy 调试利器,检查时使用 --warn-unused-configs
x: int = 1
# reveal_type(x) # 取消注释后 mypy 会输出 Revealed type is "builtins.int"类型注解只在静态检查阶段有效,运行时被忽略。如果需要在运行时校验,可以使用 isinstance 或第三方库如 pydantic。
示例 1:使用 isinstance 实现类型检查
from typing import List, Dict
import json
def validate_json(data: str, expected_type: type) -> bool:
"""简易运行时类型验证"""
try:
obj = json.loads(data)
return isinstance(obj, expected_type)
except json.JSONDecodeError:
return False
if __name__ == "__main__":
json_str = """
["one", "two"]
"""
flag = validate_json(json_str, List)
print(flag) # True
flag = validate_json(json_str, Dict)
print(flag) # False示例 2:使用 get_type_hints 获取函数的类型注解
from typing import get_type_hints
def my_func(a: int, b: str) -> bool:
return True
# 使用 typing.get_type_hints 获取函数的类型注解
hints = get_type_hints(my_func)
print(hints) # {'a': <class 'int'>, 'b': <class 'str'>, 'return': <class 'bool'>}
print(hints["a"]) # <class 'int'>
print(hints["b"]) # <class 'str'>示例 3:使用 pydantic 进行运行时校验,但是需要执行 pip install pydantic 安装
from pydantic import BaseModel, Field
# BaseModel:Pydantic 核心基类
# 自动实现:数据类型转换、运行时校验、序列化、字典互转
class User(BaseModel):
# 用户编号,强制 int 类型,无默认值,实例化必须传入
id: int
# 用户名称,字符串类型
# Field:附加校验规则
# min_length=1:不允许空字符串;max_length=50:最大长度限制
name: str = Field(min_length=1, max_length=50)
# 年龄,int类型
# ge=0 greater or equal 大于等于0
# le=150 less or equal 小于等于150
age: int = Field(ge=0, le=150)
# 合法构造:全部数据满足类型与校验规则
u = User(id=1, name="Alice", age=30)
print(u.model_dump()) # {'id': 1, 'name': 'Alice', 'age': 30}
# 非法示例(取消注释会抛出 ValidationError 运行时异常)
# id传字符串类型、name为空字符串、age超出0~150范围,任意一项不满足都会校验失败
u = User(id="bad", name="", age=200)运行示例,输出如下:
{'id': 1, 'name': 'Alice', 'age': 30}
Traceback (most recent call last):
File "d:\python_demo\demo.py", line 26, in <module>
u = User(id="bad", name="", age=200)
File "D:\Program Files\Python313\Lib\site-packages\pydantic\main.py", line 253, in __init__
validated_self = self.__pydantic_validator__.validate_python(data, self_instance=self)
pydantic_core._pydantic_core.ValidationError: 3 validation errors for User
id
Input should be a valid integer, unable to parse string as an integer [type=int_parsing, input_value='bad', input_type=str]
For further information visit https://errors.pydantic.dev/2.11/v/int_parsing
name
String should have at least 1 character [type=string_too_short, input_value='', input_type=str]
For further information visit https://errors.pydantic.dev/2.11/v/string_too_short
age
Input should be less than or equal to 150 [type=less_than_equal, input_value=200, input_type=int]
For further information visit https://errors.pydantic.dev/2.11/v/less_than_equal抛出校验错误 validation errors for User。