如何阻止存根文件中定义的类型别名被其他模块调用?
我现在在做一个基于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还是会让其他模块识别到这个别名。
尝试的修复手段
我试了几种方法,但都没能达到预期:
- 重命名类型别名:把
T改成_T,想靠Python的私有命名约定隐藏它,但没用; - 使用
if TYPE_CHECKING:包裹:把别名定义放在这个代码块里,结果其他模块还是能访问到; - 添加
__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,提问作者平田智剛

