Configuring Python test environments shouldn't require fighting with sys.path or brittle shell scripts. When you combine modern pytest settings with a centralized pyproject.toml file, you solve import errors once and for all.
You'll set up clean package discovery without polluting your global environment or relying on obsolete configuration files like setup.cfg or tox.ini. Let's look at how modern Python projects configure root paths and test runners cleanly.
Why Traditional Python Testing Paths Fail
In older Python projects, test discovery often relied on how you invoked the interpreter from your terminal. If you ran pytest from the root directory without proper configuration, you'd frequently hit ModuleNotFoundError because your source code directory wasn't on the Python path.
Developers used to fix this by exporting environment variables in their shell profiles:
export PYTHONPATH=$PYTHONPATH:src
pytest
This approach breaks down across different developer machines and CI/CD pipelines. If a teammate forgets to export the variable, their test suite fails immediately. Modern testing workflows require configuration that lives inside the repository, ensuring consistent behavior regardless of where you execute the command from.
Configuring Pytest with Pyproject Toml
The pyproject.toml file is the standard configuration hub for Python projects as defined by PEP 518. Instead of scattering configuration across multiple dotfiles, you can manage your test parameters directly inside your project metadata file.
Here is how you define your test paths and options cleanly:
[tool.pytest.ini_options]
minversion = "7.0"
addopts = "-ra -q"
testpaths = [
"tests",
]
pythonpath = ["src"]
By specifying pythonpath = ["src"], you explicitly tell pytest to add the src directory to sys.path during test execution. You don't need manual exports or wrapper scripts.
Understanding Test Paths vs Pythonpath
It's common to confuse where pytest looks for tests with what Python makes importable.
testpaths: Tells pytest where to search for test files if no specific directories are passed in the command line.pythonpath: Tells pytest what directories to add tosys.pathso your test files can import your source modules successfully.
Separating these concerns keeps your project structure modular. Your source code lives in src/, your tests live in tests/, and pytest bridges the gap automatically via your configuration file.
Handling Import Layouts: Src Layout vs Flat Layout
Your directory layout dictates how you configure your testing paths. Modern Python packaging heavily favors the src layout because it prevents accidental imports of local code during development.
Here is a standard project tree:
my_project/
├── pyproject.toml
├── README.md
├── src/
│ └── my_package/
│ ├── __init__.py
│ └── core.py
└── tests/
├── __init__.py
└── test_core.py
In this layout, my_package is not installed in editable mode by default in older Python versions. Without setting the pythonpath in pyproject.toml, running pytest will raise an import error when test_core.py tries to import my_package.
If you use a flat layout where your package sits directly in the root directory, you run the risk of importing untested local code instead of installed packages. The src layout paired with explicit path configuration eliminates this ambiguity.
Common Configuration Mistakes to Avoid
Even with modern tools, a few recurring pitfalls can break your test runner.
- Hardcoding absolute paths: Never put machine-specific paths like
/home/user/project/srcinto your configuration. Always use relative paths from the project root. - Duplicating options: Don't mix old
pytest.inifiles withpyproject.tomlsettings. Pytest readspyproject.tomlnatively, but conflicting files lead to unpredictable behavior. - Forgetting test markers: If you define custom markers in
pyproject.toml, register them to avoid warnings or execution errors.
Here is how you register custom markers safely inside your configuration file:
[tool.pytest.ini_options]
markers = [
"slow: marks tests as slow to run",
"integration: marks tests as integration tests",
]
Integrating with Coverage and Linting Tools
A major advantage of centralizing configuration in pyproject.toml is that other tools can share similar path contexts. For instance, code coverage tools like Coverage.py and linters read from the same configuration ecosystem.
You can configure coverage settings right alongside your test runner parameters:
[tool.coverage.run]
source = ["src"]
branch = true
[tool.coverage.report]
exclude_lines = [
"pragma: no cover",
"if __name__ == '__main__':",
]
This ensures that when you run tests with coverage plugins enabled, it inspects the exact source directories you defined for pytest.
Frequently Asked Questions
Why is my pytest configuration in pyproject.toml being ignored?
Ensure your pytest version is 6.0 or higher, as earlier versions do not support parsing pyproject.toml. Also, check that your table header is spelled correctly as [tool.pytest.ini_options], not [tool.pytest].
Can I override the pyproject.toml path settings from the command line?
Yes. You can pass command-line flags or use the -c switch to point to a specific configuration file if you need to run tests under alternate conditions.
Is pythonpath supported natively by pytest without plugins?
Yes, starting from pytest 7.0, the pythonpath configuration option is built directly into the core framework without requiring third-party plugins.
Should I use a src layout for small scripts or simple utilities?
For tiny scripts or single-file projects, a flat layout is fine. However, as soon as your project grows beyond a single module, adopting a src layout prevents import pollution and test bugs.