三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

【Bug已解决】Missing library stubs or py.typed marker 解决方案

【Bug已解决】Missing library stubs or py.typed marker 解决方案

【Bug已解决】Missing library stubs or py.typed marker 解决方案

一、现象长什么样

你维护一个 Python 库,下游用户用mypy/pyright做类型检查时,关于你这个库的调用全部报"缺少类型信息":

error: Library stubs for "your_lib" are missing (install with: pip install ...) note: (or use # type: ignore 来抑制) # 或 mypy --strict 下 error: Skipping analyzing "your_lib": module is installed but missing py.typed marker

或者 IDE(PyCharm / VSCode)里你这个库的函数没有参数提示、没有返回类型推断。

最小判据:

触发:下游对使用了你的库的项目做类型检查 / IDE 分析 现象:报 missing stubs / missing py.typed,类型推断失效 根因:你的包没有声明自己是"带类型的"(缺 py.typed),或没提供类型存根 影响:下游类型安全与开发体验受损,CI 的 mypy 严格模式失败

最迷惑的是:你的代码明明写了类型注解,下游却说"没有类型信息"——因为类型信息有没有写,和包有没有"声明自己是带类型的",是两回事。

二、背景

PEP 561 规定:一个发行到 PyPI 的包,若想让下游的类型检查器(mypy/pyright)使用它的类型注解,必须在包里包含一个名为py.typed的空标记文件,并在打包时把它作为package_data包含进 wheel。

原理:

  • Python 类型检查器默认不信任第三方包的类型注解(历史上很多包类型不准,全信会误报);
  • py.typed是包的"我声明我的类型注解是可靠的,请使用它们"的显式信号;
  • 没有py.typed,类型检查器要么忽略该包的类型(报 "skipping analyzing"),要么要求单独的types-xxxstub 包。

另外两种情况:

  1. 包是纯 Python 且有内联注解:只需加py.typed即可,类型检查器直接读源码注解;
  2. 包含 C 扩展 / 动态生成模块:源码注解读不到,需要提供.pyi存根文件(stub),并同样配py.typed指向这些 stub。

bug 的根因是"打包配置遗漏了py.typed标记和package_data",导致 wheel 里没有这个文件,下游类型检查器拒绝使用该包类型。

三、根因

抽象成代码(示意):

# pyproject.toml(问题所在) [build-system] requires = ["setuptools"] [project] name = "your_lib" # BUG:没有声明 py.typed 为 package_data,wheel 里不含该标记

根因链条:

  1. 库代码有类型注解,但 wheel 里没有py.typed标记;
  2. 类型检查器按 PEP 561 规则,发现无py.typed-> 拒绝使用该包类型;
  3. 下游mypy报 missing stubs / skipping analyzing;
  4. IDE 因类型检查器给不到信息,参数提示、返回类型推断失效;
  5. 根因是"打包遗漏py.typed+package_data"。

一句话:wheel 缺py.typed标记(且未纳入 package_data),下游类型检查器拒绝使用该包类型。

四、最小可运行复现

用纯 Python 模拟"无 py.typed 时类型检查器拒绝":

# repro_py_typed.py def typechecker_accepts(package_has_py_typed): if not package_has_py_typed: raise RuntimeError("missing py.typed: 拒绝使用包的类型") return "use package types" def main(): try: typechecker_accepts(package_has_py_typed=False) except RuntimeError as e: print("复现成功 ->", e) print(typechecker_accepts(package_has_py_typed=True)) if __name__ == "__main__": main()

运行输出:

复现成功 -> missing py.typed: 拒绝使用包的类型 use package types

无py.typed时类型检查器拒绝,正是真实 bug 的抽象。

五、解决方案(第一层:最小直接修复)

最小且必须的一步:在包目录里放一个空的py.typed文件,并在打包配置里把它作为package_data包含进 wheel:

your_lib/ __init__.py core.py py.typed # 空文件,PEP 561 标记

pyproject.toml(setuptools):

[build-system] requires = ["setuptools>=61"] build-backend = "setuptools.build_meta" [project] name = "your_lib" version = "0.1.0" [tool.setuptools.packages.find] where = ["."] include = ["your_lib*"] [tool.setuptools.package-data] your_lib = ["py.typed"] # 关键:把 py.typed 打进 wheel

构建后验证 wheel 内含py.typed:

python -m build unzip -l dist/your_lib-0.1.0-py3-none-any.whl | grep py.typed

要点:

  • py.typed是空文件,仅作标记;
  • package-data确保它被纳入 wheel;
  • 下游mypy立刻能用该包内联注解。

六、解决方案(第二层:结构性改进)

把"类型声明完整性"做成发布前的自动校验:CI 在构建后检查 wheel 是否含py.typed,并对源码做mypy --strict自检,保证发布的类型可靠:

# fix_layer2.py from pathlib import Path import zipfile def wheel_has_py_typed(wheel_path: str, package: str) -> bool: with zipfile.ZipFile(wheel_path) as z: names = z.namelist() marker = f"{package}/py.typed" return marker in names def assert_typed_release(wheel_path, package): assert wheel_has_py_typed(wheel_path, package), \ f"wheel 缺少 {package}/py.typed,下游无法使用类型" # CI 用法 assert_typed_release("dist/your_lib-0.1.0-py3-none-any.whl", "your_lib")

并在pyproject.toml配mypy自检:

[tool.mypy] strict = true files = ["your_lib"]

要点:

  • wheel_has_py_typed在 CI 验证标记存在,缺则发布失败;
  • mypy --strict对源码自检,保证发布的类型本身可靠(否则下游即使有 py.typed 也会误报);
  • 类型质量与"是否声明"双管齐下。

七、解决方案(第三层:断言 / CI 守护)

写 pytest 验证"py.typed 存在且被打包":

# test_py_typed.py import pytest import zipfile, pathlib def test_py_typed_in_wheel(): wheel = pathlib.Path("dist/your_lib-0.1.0-py3-none-any.whl") if not wheel.exists(): pytest.skip("wheel 未构建") with zipfile.ZipFile(wheel) as z: assert "your_lib/py.typed" in z.namelist() def test_py_typed_marker_present_in_source(): marker = pathlib.Path("your_lib/py.typed") assert marker.exists(), "源码树必须有 py.typed 标记文件" def test_stub_or_inline_types(): # 至少有内联注解或 .pyi 存根之一 has_pyi = any(pathlib.Path("your_lib").rglob("*.pyi")) has_annotations = True # 实际应扫描源码是否有注解 assert has_pyi or has_annotations

CI 一旦有人把py.typed从打包配置删掉,test_py_typed_in_wheel立即变红。

八、排查清单

下游报 missing stubs / missing py.typed 时:

  1. 确认 wheel 里是否含your_lib/py.typed(解压看);
  2. 若没有,在源码树放空py.typed,并在package-data声明;
  3. 重新python -m build,验证 marker 进 wheel;
  4. 若包含 C 扩展 / 动态模块,额外提供.pyi存根;
  5. 用mypy --strict对源码自检,保证类型本身可靠;
  6. 把第七节的 pytest 接进 CI,守护 py.typed 被打包;
  7. 下游重新pip install你的新 wheel 后类型恢复。

九、小结

库的类型信息下游用不上,根因是 wheel 缺py.typed标记(且未纳入package-data)。PEP 561 规定第三方包必须显式带py.typed才能被类型检查器信任,缺它则下游mypy报 missing stubs / skipping analyzing,IDE 推断失效。

三层层级:

  • 第一层:源码树放空py.typed,并在package-data声明打进 wheel;
  • 第二层:CI 构建后校验 wheel 含py.typed,并对源码mypy --strict自检;
  • 第三层:pytest 验证 marker 存在且被打包,锁进 CI。

核心教训:写了类型注解 ≠ 下游能用类型。是否"声明为带类型包"由py.typed这个 PEP 561 标记决定。任何发布到 PyPI 的库,只要希望下游享受类型安全,都必须把py.typed纳入打包——这是类型生态的入场券,不是可选项。

← 返回列表