
在 Python 包开发中,将单元测试与源码共置(如 Rust 的 #[cfg(test)] 风格)可提升可维护性,但需确保测试函数不被意外导出或安装到最终包中;本文介绍一种稳定、符合 Python 惯例的解决方案。
在 python 包开发中,将单元测试与源码共置(如 rust 的 `#[cfg(test)]` 风格)可提升可维护性,但需确保测试函数不被意外导出或安装到最终包中;本文介绍一种稳定、符合 python 惯例的解决方案。
Python 默认会将模块中所有顶层可调用对象(包括 test_* 函数)作为公共接口的一部分导入和分发,这与 Rust 中 #[cfg(test)] 的编译期条件排除机制有本质区别。直接使用 if "pytest" in sys.modules 等运行时检测方式虽可行,但存在环境判断脆弱、IDE 误提示、类型检查干扰等问题,不推荐用于生产级项目。
✅ 推荐方案:伪私有测试类 + pytest 自定义发现规则
核心思路是利用 Python 的命名约定(单下划线前缀 _ 或双下划线 __)暗示私有性,并结合 pytest 的配置能力,仅让测试发现器识别特定结构,同时阻止测试函数被直接导入:
# add.py
def add(a, b):
"""Return the sum of a and b."""
return a + b
class __Test:
"""Private test container — not intended for import or runtime use."""
def test_add(self):
assert add(1, 2) == 3
def test_add_negative(self):
assert add(-1, 1) == 0接着,在项目根目录创建 pytest.ini(或 pyproject.toml 中的 [tool.pytest.ini_options]),显式声明测试类匹配模式:
立即学习“Python免费学习笔记(深入)”;
# pytest.ini [pytest] python_classes = __Test
✅ 效果说明:
uv build或pip install .构建/安装后,__Test类仍存在于模块中(符合 Python 运行时语义),但其方法 无法通过from foo.add import test_add导入(因方法嵌套在类内,非模块级符号);uv run pytest或pytest能正常发现并执行__Test.test_add(得益于python_classes配置);- IDE 和类型检查器(如 mypy、pyright)默认忽略双下划线前缀类的自动补全,降低误用概率;
- 符合 PEP 8 关于
__name命名表示“强私有”的约定,语义清晰、无副作用。
⚠️ 注意事项:
- 不要将
__Test类命名为_Test(单下划线),因为 pytest 默认python_classes = Test*,可能意外匹配其他测试类; - 若使用
pyproject.toml管理 pytest 配置,请写为:[tool.pytest.ini_options] python_classes = ["__Test"]
- 此方案不依赖运行时条件判断,避免了
sys.modules检查的可靠性问题(例如某些测试 runner 可能未注入"pytest"模块名); - 如需彻底排除测试代码(如发布精简版包),可配合
MANIFEST.in排除含test_的函数定义(不推荐),或采用源码预处理工具(复杂度高)——而本方案在简洁性、兼容性与工程实践间取得了最佳平衡。
总结:通过 __Test 类封装 + pytest.ini 显式配置,你既能享受测试紧贴实现的开发体验,又能保证发布的包干净、专业、零污染。


















