PAYATHON 2026

Python 3.14+ 能否复用全局类型别名和约束元组定义泛型函数?

数据小陈

可以复用模块级类型别名作为 bound,例如 T: Scalar。T: SCALAR_TYPES 不符合 PEP 695 对 constraints 的规定,即使它在某个环境中能够运行或通过检查,也不能认为它是受规范支持的约束声明。

对于这里的字典取值校验,建议保留 Scalar 类型别名,并采用第一种函数签名。

两种写法为什么不同?

问题中的两个公共定义分别是类型别名和普通运行时元组:

type Scalar = bool | int | str |float
SCALAR_TYPES = (bool, int, str, float)

Scalar 可以用于类型表达式,所以下面的声明有效:

def extract_scalar[T: Scalar](
    data: dict, key: str, expected: type[T]
) -> T: ...

这里的 T 必须满足 Scalar 这个上界。bound 允许保留更具体的子类型;constraints 则会将类型变量求解为列出的约束类型之一,两者的推断行为不同。Python 类型规范:类型变量上界

下面的声明则存在问题:

def extract_scalar[T: SCALAR_TYPES](
    data: dict, key: str, expected: type[T]
) -> T: ...

新语法通过冒号后的表达式结构区分 bound 和 constraints。直接写出的元组表达式表示 constraints;SCALAR_TYPES 是一个名称,即使它的求值结果是元组,也不会被转换成约束列表。在这个声明中,它处于 bound 的位置,但普通元组变量不能作为有效的类型上界。

PEP 695 给出了同类反例:将类型元组赋给变量后,在类型参数声明中引用该变量,应产生类型检查错误。有效的约束声明应直接写出元组:

def extract_scalar[T: (bool, int, str, float)](
    data: dict, key: str, expected: type[T]
) -> T: ...

依据:PEP 695:Constrained Type Specification。

延迟求值会改变这些规则吗?

不会。延迟求值决定表达式何时求值,类型规范决定哪些表达式可以作为 bound 或 constraints。

通过 PEP 695 类型参数语法创建的类型变量,其 bound 和 constraints 从 Python 3.12 起就已采用延迟求值,并非到 Python 3.14 才有这一行为。相关值在访问 __bound__、__constraints__ 等接口时才求值。Python 3.14:TypeVar 文档

因此,Python 3.14 默认延迟求值注解,不会让普通全局变量自动成为合法的类型表达式,也不会取消约束元组的语法要求。

字典取值校验可以怎样实现?

可以将类型别名放在公共模块中,再在提取函数里复用。下面用 scalar_types.py 保存公共定义:

# scalar_types.py
type Scalar = bool | int | str | float
SCALAR_TYPES = (bool, int, str, float)
# extract.py
from collections.abc import Mapping

from scalar_types import Scalar


def extract_scalar[T: Scalar](
    data: Mapping[str, object],
    key: str,
    expected: type[T],
) -> T:
    value = data[key]
    if not isinstance(value, expected):
        raise TypeError(
            f"{key!r}: expected {expected.__name__}, "
            f"got {type(value).__name__}"
        )
    return value

调用示例:

data: dict[str, object] = {"name": "Alice", "count": 3}

name = extract_scalar(data, "name", str)
count = extract_scalar(data, "count", int)

Mapping[str, object] 表示函数只读取映射,键为字符串,读出的值需要进一步检查。expected: type[T] 将预期类型与返回类型关联起来,isinstance 执行实际的运行时校验。

在这个实现中,键不存在时会抛出 KeyError,值不符合预期类型时会抛出 TypeError。函数假设传入对象已经是符合接口要求的映射;JSON 顶层对象是否为字典,应在读取边界另行检查。

SCALAR_TYPES 可以保留给 isinstance(value, SCALAR_TYPES) 这样的运行时检查使用。类型注解本身不会自动校验函数实参或 JSON 内容。Python typing 文档

如果确实需要复用 constraints 呢?

可以使用传统 TypeVar 声明,将约束集中定义一次,再在多个函数中复用该类型变量:

from collections.abc import Mapping
from typing import TypeVar

ScalarT = TypeVar("ScalarT", bool, int, str, float)


def extract_scalar(
    data: Mapping[str, object],
    key: str,
    expected: type[ScalarT],
) -> ScalarT:
    value = data[key]
    if not isinstance(value, expected):
        raise TypeError(f"Unexpected type for {key!r}")
    return value

这里使用传统泛型函数写法,函数名后不再重新声明 [ScalarT]。约束允许接收相应类型的子类,但推断结果会归到列出的约束类型,不能保证保留具体子类。Python 类型规范:泛型与约束类型变量

对于当前需求,如果希望返回类型跟随 expected 并保留子类型信息,T: Scalar 通常更合适。

如何看待 mypy 和 pyright 都通过的结果?

原问题没有提供检查器版本、配置及完整的跨模块示例,因此无法确定它们为什么没有报告第二种写法的问题。可以用只包含公共定义和函数签名的最小示例复查,并确认相关文件实际参与了检查。

运行成功说明解释器接受了相应代码路径;静态检查没有报错说明该次检查未发现问题。这两点都不能取代类型规范对合法声明形式的要求。Python 语言参考也区分了 bound 与元组形式的 constraints,并说明部分要求不会在运行时强制执行。Python 3.14:类型参数列表

还需注意,isinstance 接受子类。如果业务要求值的类型必须与 expected 完全一致,应另外设计精确类型检查;改用 constraints 不会自动改变运行时校验方式。

备注:内容仅供参考。