from __future__ import annotations:作用、用法与 Python 3.14 变化

from __future__ import annotations 到底有什么用?本文讲清前向引用、字符串化标注、TYPE_CHECKING、get_type_hints 常见坑,以及 Python 3.14 延迟求值后的保留与删除策略。

8 分钟阅读 Python

from __future__ import annotations 有什么用

from __future__ import annotations 会改变当前 Python 模块保存类型标注的方式。在 Python 3.7 到 3.13 中,它最直接的作用是:不在函数或类定义时立刻计算标注,而是把标注保存成字符串。

from __future__ import annotations


class User:
    def __init__(self, name: str) -> None:
        self.name = name

    def rename(self, name: str) -> User:
        return User(name)


print(User.rename.__annotations__)

输出结果类似:

{'name': 'str', 'return': 'User'}

这里的 -> User 是返回值类型标注,意思是 rename() 预期返回一个 User 对象。

但 Python 执行到这行代码时,User 类还在定义过程中,名字 User 尚未正式创建。这种“类型还没定义完成,就先在标注中引用它”的写法叫作前向引用

在 Python 3.7–3.13 中,如果不使用 future import,就要把尚未定义的类型名写成字符串:

class User:
    def __init__(self, name: str) -> None:
        self.name = name

    def rename(self, name: str) -> "User":
        return User(name)

这里给 "User" 加引号,并不是说函数会返回字符串。引号只是让 Python 暂时不要查找 User 这个名字,先把标注文本保存下来,等类型真正定义好后再由类型检查器或反射工具解析。

启用 from __future__ import annotations 后,Python 会自动把当前模块的标注按字符串保存,因此可以直接写 -> User,不必再手动写成 -> "User"

它主要解决三个实际问题:

  • 引用尚未定义的类型时,可以直接写 UserNode | None,不必手动写成字符串;
  • 配合 TYPE_CHECKING,更容易拆解只由类型标注造成的循环导入;
  • Python 3.7–3.13 不必在模块加载阶段计算每个标注表达式。

这条语句只影响标注的保存和求值方式,不会让 Python 在运行时自动检查类型。想系统区分类型标注、静态检查与运行时校验,可以继续阅读Python 类型标注与类型检查:从基础语法到 mypy 实战

为什么它必须写在文件开头

future statement 是编译器指令,不是普通依赖导入。编译器需要先看到这条指令,才能决定后面的代码采用哪套语义。因此它必须位于模块顶部,前面只能出现:

  • 模块文档字符串;
  • 注释;
  • 空行;
  • 其他 from __future__ import ... 语句。

下面的写法合法:

"""用户模型。"""

from __future__ import annotations

from dataclasses import dataclass

把它放在普通 import 或可执行语句之后则会触发 SyntaxError

import os

from __future__ import annotations  # SyntaxError

它按单个模块生效。在 app.py 中启用,不会自动改变 models.py 的标注行为;需要使用时,每个模块都要单独声明。

前向引用:最常见的使用场景

方法返回当前类

链表、树、ORM 模型和领域对象经常需要在类体内引用自身:

from __future__ import annotations

from dataclasses import dataclass


@dataclass
class Node:
    value: int
    next_node: Node | None = None


head = Node(1, Node(2))

在 Python 3.10–3.13 中,去掉 future import 后,Node | None 会在 Node 尚未定义完成时求值。兼容写法只能把整个表达式写成字符串:

next_node: "Node | None" = None

future import 让复杂前向引用保持正常的类型表达式外观,可读性通常更好。

两个模块互相引用

假设 user.py 的标注需要 Orderorder.py 又需要 User。如果双方都在运行时直接导入对方,很容易形成循环导入。可以把只供类型检查器使用的导入放进 TYPE_CHECKING

from __future__ import annotations

from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from order import Order


class User:
    def latest_order(self) -> Order | None:
        return None

静态类型检查器会把 TYPE_CHECKING 视为 True,Python 运行时则把它视为 False。这样既能检查 Order,又不会为了标注在运行时导入 order.py

不过,这并没有从根本上解决所有循环依赖。如果函数体在运行时真的需要创建 Order,仍要重新设计模块边界、抽取共享类型,或在合适的位置执行局部导入。

__annotations__ 为什么变成了字符串

在 Python 3.7–3.13 中,可以用一个最小示例观察差异。

未启用 future import:

def parse(value: str) -> int:
    return int(value)


print(parse.__annotations__)
# {'value': <class 'str'>, 'return': <class 'int'>}

启用后:

from __future__ import annotations


def parse(value: str) -> int:
    return int(value)


print(parse.__annotations__)
# {'value': 'str', 'return': 'int'}

因此,依赖运行时反射的代码不能想当然地把 obj.__annotations__["value"] 当成真实类型对象。更稳妥的方式是使用 typing.get_type_hints()

from __future__ import annotations

from typing import get_type_hints


def parse(value: str) -> int:
    return int(value)


hints = get_type_hints(parse)
print(hints)
# {'value': <class 'str'>, 'return': <class 'int'>}

get_type_hints() 会尝试解析字符串和前向引用。它可能执行标注中的代码,因此不要对不可信来源构造的对象随意调用,也不要在类型标注里放带副作用的表达式。

Python 3.14 之后还需要写吗

需要先区分“推迟求值”和“字符串化标注”。它们不是同一套实现。

Python 版本 默认行为 加 future import 后
3.7–3.13 定义时立即计算标注 按 PEP 563 保存为字符串
3.14+ 按 PEP 649 延迟计算,访问时再求值 仍按 PEP 563 保存为字符串

Python 3.14 已默认延迟计算标注,所以前向引用通常不再要求这条 future import。默认模式保留了按需得到真实标注值的能力,并不等同于把所有标注直接存成字符串。

Python 3.14 官方文档已经把 from __future__ import annotations 标为弃用,并说明未来会移除,但不会早于 Python 3.13 在 2029 年结束生命周期之后。现阶段可以按项目最低版本判断:

  • 只支持 Python 3.14+ 的新项目:通常不要再新增这条 import,使用默认的延迟求值语义;
  • 仍支持 Python 3.7–3.13 的项目:可以继续使用,以获得一致的前向引用写法;
  • 依赖运行时标注的框架或库:先检查其支持的 Python 版本和标注读取方式,再决定是否移除;
  • 维护现有项目:不要只因为升级到 3.14 就批量删除,先用测试覆盖数据模型、依赖注入、序列化和反射逻辑。

Python 3.14 新增了 annotationlib。需要编写文档生成器、框架或反射工具时,可以按需求读取值、字符串或未解析的前向引用:

from annotationlib import Format, get_annotations


class Node:
    next_node: Node | None


print(get_annotations(Node, format=Format.STRING))

普通业务代码仍可优先使用 typing.get_type_hints();底层工具需要更精细控制时,再使用 annotationlib

常见报错与排查方法

SyntaxError: from __future__ imports must occur at the beginning of the file

原因是 future import 前面出现了普通 import、变量赋值或其他可执行语句。把它移动到模块文档字符串之后、所有普通 import 之前。

NameError: name 'SomeType' is not defined

启用推迟求值并不代表拼错的类型名会自动变正确。错误可能从“模块导入时”推迟到调用 get_type_hints() 或其他反射工具时才出现。

先检查名称拼写,再确认类型是否存在于正确的全局或局部命名空间。如果它只在 TYPE_CHECKING 块中导入,运行时自然找不到该名称。

get_type_hints() 解析 TYPE_CHECKING 中的类型失败

TYPE_CHECKING 能避免运行时导入,但 get_type_hints() 若要得到真实类型对象,仍然需要相应名称存在。二者存在真实取舍:

  • 只做静态检查,不做运行时反射:放进 TYPE_CHECKING 通常没问题;
  • 框架运行时必须读取真实类型:确保类型可在运行时导入,或按框架文档显式提供命名空间;
  • Python 3.14 的工具只需展示标注:可考虑使用 Format.STRING,避免强制解析未知名称。

升级 Python 后框架行为变化

dataclass、数据校验、依赖注入和 API 文档工具都可能读取标注。升级前应检查框架版本是否支持目标 Python,并为下面几类对象补充回归测试:

  1. 自引用或互相引用的模型;
  2. Annotated、泛型、联合类型和类型别名;
  3. TYPE_CHECKING 中导入的类型;
  4. 通过装饰器或元类读取标注的类。

应该保留、添加还是删除

可以用这张判断表快速决定:

项目情况 建议
最低版本为 Python 3.7–3.13,并大量使用前向引用 添加或保留
最低版本为 Python 3.14 新代码通常不添加
只是为了“开启类型检查” 不要添加,它不负责检查类型
框架直接读取 __annotations__ 先升级或修正读取方式,再改 import
需要解决真正的业务模块循环依赖 优先调整模块结构,不能只靠它遮住问题
准备从 3.13 升到 3.14 保留现状并跑回归测试,不要机械批量删除

一句话总结:在 Python 3.7–3.13 中,它是改善前向引用和类型导入体验的实用工具;在 Python 3.14+ 中,默认标注机制已经升级,新项目通常无需再写,但跨版本项目仍要根据兼容范围谨慎处理。

常见问题

它会提升程序运行速度吗?

不能笼统地保证。Python 3.7–3.13 确实不再于模块定义阶段计算标注,但实际收益取决于标注复杂度、导入方式和运行时是否再次解析标注。应以项目测量结果为准,不要把它当成通用性能开关。

它会自动检查参数类型吗?

不会。name: str 仍只是标注。运行静态检查需要 mypy、Pyright 等工具,验证外部输入则需要业务代码或运行时校验库。

使用后还要给前向引用加引号吗?

在受影响的标注表达式中通常不需要。把 "Node" 写成 Node 正是它在 Python 3.7–3.13 的主要便利之一。

能在函数内部使用吗?

不能。future statement 必须位于模块顶部,不能放进函数、类或条件分支。

Python 3.14 删除它会立刻报错吗?

不会。Python 3.14 中它的既有行为保持不变,只是已经进入弃用路径。官方承诺不会早于 Python 3.13 在 2029 年结束生命周期后移除,具体移除版本仍应以届时发布说明为准。

官方资料

Practice

读完这一节,去靶场里验证一下。

去挑战广场练习