You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何阻止存根文件中定义的类型别名被其他模块调用?

如何阻止存根文件中定义的类型别名被其他模块调用?

我现在在做一个基于Python 3.13.1的项目,用mypy 1.14.0做静态类型检查。我有个module.py模块,其中的function函数会返回一个名字超长的类型——Type_whose_name_is_so_long_that_we_do_not_want_to_call_it_over_and_over_again。为了让存根文件module.pyi的代码更简洁,我定义了类型别名T来指代这个长名字类型,存根文件的简化版本如下:

# module.pyi
T = Type_whose_name_is_so_long_that_we_do_not_want_to_call_it_over_and_over_again

def function() -> T: ...

class Type_whose_name_is_so_long_that_we_do_not_want_to_call_it_over_and_over_again: ...

但我遇到了一个问题:其他模块(比如下面的illegal_usage_of_T.py)可以直接导入并使用这个T别名,这是我不希望看到的:

# illegal_usage_of_T.py
import module

foo: module.T = module.function()

我期望运行mypy illegal_usage_of_T.py时,mypy能报错提示T未定义,阻止这种非法用法。

我试过的方法

谷歌搜索

我搜了关键词“mypy type alias only used in stub file”,但没找到能精准解决问题的方案——我原本以为只在存根文件里定义别名,它的作用域就会被限制在存根内部,但mypy还是会让其他模块识别到这个别名。

尝试的修复手段

我试了几种方法,但都没能达到预期:

  1. 重命名类型别名:把T改成_T,想靠Python的私有命名约定隐藏它,但没用;
  2. 使用if TYPE_CHECKING:包裹:把别名定义放在这个代码块里,结果其他模块还是能访问到;
  3. 添加__all__限制导出:在module.pyi里添加了__all__,只明确导出function和那个长名字类,但T(或_T)依然能被外部模块导入使用。

修改后的module.pyi是这样的:

# 修改后的module.pyi
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    _T = Type_whose_name_is_so_long_that_we_do_not_want_to_call_it_over_and_over_again

def function() -> _T: ...

class Type_whose_name_is_so_long_that_we_do_not_want_to_call_it_over_and_over_again: ...

__all__ = ["function", "Type_whose_name_is_so_long_that_we_do_not_want_to_call_it_over_and_over_again"]

对应的非法使用代码改成了:

# illegal_usage_of_T.py
import module

foo: module._T = module.function()  # 依然能被mypy认可,没有报错

解决方案

要实现“存根内部能用别名,外部模块访问时mypy报错”的需求,你需要让mypy严格区分存根文件的内部成员和对外导出成员,具体步骤如下:

1. 调整存根文件的定义

  • 把类型别名改成私有命名(比如_T),遵循Python的私有成员命名规范;
  • 保留__all__,只列出你希望外部模块能访问的成员(function和长名字类)。

调整后的module.pyi:

# 最终版module.pyi
# 私有类型别名,仅存根内部使用
_T = Type_whose_name_is_so_long_that_we_do_not_want_to_call_it_over_and_over_again

def function() -> _T: ...

class Type_whose_name_is_so_long_that_we_do_not_want_to_call_it_over_and_over_again: ...

# 明确对外导出的成员,排除私有别名_T
__all__ = ["function", "Type_whose_name_is_so_long_that_we_do_not_want_to_call_it_over_and_over_again"]

2. 开启mypy的no_implicit_reexport选项

mypy默认允许隐式导出所有非下划线开头的成员(即使不在__all__里),下划线开头的私有成员也只是会给出警告而非错误。开启no_implicit_reexport后,mypy会严格按照__all__的定义来判断哪些成员可以被外部访问,不在__all__里的成员(包括私有命名的_T),外部模块访问时会直接报错。

你可以通过两种方式开启这个选项:

方式一:全局开启(在mypy配置文件中)

在项目的pyproject.toml或mypy.ini中添加配置:

# pyproject.toml
[mypy]
no_implicit_reexport = true

方式二:仅针对目标模块开启

如果不想全局修改配置,可以只对module模块开启该规则:

# pyproject.toml
[mypy-module]
no_implicit_reexport = true

3. 验证效果

现在运行mypy illegal_usage_of_T.py,mypy会输出类似这样的错误,完美符合预期:

error: Module has no attribute "T"  [attr-defined]

为什么之前的方法没用?

  • 仅重命名为_T:mypy默认不会严格限制私有成员的访问,只是会给出弱警告,不会直接报错;
  • 用if TYPE_CHECKING:包裹:存根文件本身就是给类型检查器用的,这个代码块在mypy处理存根时会被完全解析,无法隐藏里面的成员;
  • 仅加__all__:mypy默认的隐式导出规则会忽略__all__,依然允许访问不在列表里的成员,必须配合no_implicit_reexport才能生效。

备注:内容来源于stack exchange,提问作者平田智剛

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.14 12:13:04