
本文详解在 src/ 结构化布局下,因模块搜索路径差异导致 from bar import baz 在运行时正常但 pytest 报 ModuleNotFoundError 的根本原因,并提供基于 pyproject.toml 的标准化、零侵入式解决方案。
本文详解在 src/ 结构化布局下,因模块搜索路径差异导致 from bar import baz 在运行时正常但 pytest 报 modulenotfounderror 的根本原因,并提供基于 pyproject.toml 的标准化、零侵入式解决方案。
在采用现代 Python 项目结构(如 src/basic_package/)时,一个典型痛点是:应用代码可正常运行,但 pytest 测试却因导入失败而中断。其本质并非语法错误,而是 Python 解释器与 pytest 在执行时对 sys.path 的初始化策略不同——这直接决定了相对导入与绝对导入能否成功解析。
以你提供的项目结构为例:
.
├── pyproject.toml
├── src
│ └── basic_package
│ ├── bar.py
│ ├── __init__.py
│ └── main.py # from bar import baz
└── tests
└── test_app.py # from basic_package.main import main- ✅ 直接运行应用(如
python -m basic_package.main)时,Python 将src/basic_package/加入sys.path,因此from bar import baz能定位到同目录下的bar.py; - ❌ 运行 pytest(如
pytest tests/)时,默认仅将src/加入sys.path,导致bar不在任何已知包路径中,报错ModuleNotFoundError: No module named 'bar'。
⚠️ 常见误区:试图用 from .bar import baz(显式相对导入)修复测试——这虽让 pytest 通过,却破坏了作为独立模块运行的能力(python src/basic_package/main.py 会报 ImportError: attempted relative import with no known parent package),违背“一次编写、多场景运行”的工程原则。
✅ 正确解法:统一模块解析上下文,而非修改源码逻辑。通过 pyproject.toml 精准配置 pytest 的模块搜索路径,使其与应用运行时环境对齐:
立即学习“Python免费学习笔记(深入)”;
# pyproject.toml [tool.pytest.ini_options] pythonpath = ["src/basic_package"]
? 原理说明:
pythonpath选项会 prepend 到sys.path前端,使bar成为顶层可导入模块(即import bar或from bar import baz可直接工作),且完全不影响basic_package作为包的层级语义。
? 补充建议(增强健壮性):
确保
src/basic_package/__init__.py存在(即使为空),标识其为合法 Python 包;-
在
pyproject.toml中声明包根路径,避免隐式依赖当前工作目录:[build-system] requires = ["setuptools>=45", "wheel"] build-backend = "setuptools.build_meta" [project] name = "basic-package" version = "0.1.0" # 显式声明源码位置(兼容 PEP 621) [project.options] packages = [{include = "basic_package", from = "src"}] -
运行验证命令:
# 测试导入是否就绪 python -c "from bar import baz; print('OK')" # 运行测试(无需修改任何 .py 文件) pytest tests/ -v
? 总结:该问题的本质是开发流程中“执行上下文不一致”,而非代码缺陷。通过 pythonpath = ["src/basic_package"] 统一路径配置,既保持源码简洁性与可运行性,又满足 pytest 的可发现性要求,是符合 pytest 官方最佳实践 的推荐方案。无需 patch、无需 __main__.py、无需 PYTHONPATH 环境变量——一行配置,全局生效。


















