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

📅 2026/8/2 23:00:06 👁️ 阅读次数 📝 编程学习
【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.tomlmypy自检:

[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纳入打包——这是类型生态的入场券,不是可选项。