Python3 基础教程

Python3 类型注解 typing

🎉摘要:Python 是一门解释型、面向对象、动态类型的高级编程语言,由荷兰程序员 Guido van Rossum 于 1991 年发布,核心设计理念是优雅、明确、简单。

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+)

从 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

注意,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

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 与联合类型

联合类型指一个变量 / 参数 / 返回值,可以是多种类型中的任意一种。例如:

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。

类型收窄(Type Narrowing)

类型收窄指静态类型检查工具(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 函数类型

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

TypeVar 与泛型编程

我们为什么要使用泛型编程呢?唯一的目的是复用代码,编写一套通用代码,不绑定固定类型,可以适配 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 不满足 bound

Protocol 结构类型(鸭子类型静态化)

Python 运行时遵循鸭子类型:

看起来像鸭子、叫起来像鸭子,那它就是鸭子;不需要继承同一个父类。

但是传统静态类型系统(基于类继承)无法识别鸭子类型,必须显式继承才能匹配。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__、方法里。

自引用类型(Python 3.7+)

自引用类型指一个类的注解内部,需要引用自身这个类。典型场景:

  • 方法返回当前类的实例(工厂类方法 from_xxx)

  • 树形结构:节点包含同类型子节点(链表、树)

Python 代码自上而下执行。当解释器读到类内部注解时,类还没有定义完成,直接写类名会报 NameError。

这种“类还没创建,注解就要使用它自己” 的问题,统称为前向引用,自引用是前向引用最常见的一种。

原始方案:字符串字面量(3.7 + 通用兼容)

把类名写成字符串 "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
遍历节点: 3

现代标准方案:from __future__ import annotations

Python 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 静态类型检查实战

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 .

mypy 常见错误与修复

缺少返回值注解

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 x

Optional 未检查 None

from 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]

字典 key 类型错误

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。

说说我的看法
全部评论(
没有评论
关于
本网站专注于 Java、数据库(MySQL、Oracle)、Linux、软件架构及大数据等多领域技术知识分享。涵盖丰富的原创与精选技术文章,助力技术传播与交流。无论是技术新手渴望入门,还是资深开发者寻求进阶,这里都能为您提供深度见解与实用经验,让复杂编码变得轻松易懂,携手共赴技术提升新高度。如有侵权,请来信告知:hxstrive@outlook.com
其他应用
公众号