Chapter 11 · Configuration Files In Depth
Chapter goals: nail down the discovery order and precedence of
pytest.toml/pytest.ini/pyproject.toml/tox.ini/setup.cfg, how rootdir is determined, and the engineering use of core options likeaddopts/testpaths/python_files.
11.1 Five Config Files and Their Discovery Order
pytest accepts configuration in several files, searching in this order and taking the first file that hits (the rest are all ignored):
pytest.toml → pytest.ini → tox.ini (with a [pytest] section) → setup.cfg (with a [tool:pytest] section) → pyproject.toml (with a [tool.pytest.ini_options] table)Two details to note:
tox.ini/setup.cfgonly count when they actually contain a pytest config section;pyproject.tomlis the most popular choice — it centralizes all project tooling config (build, lint, pytest) in one place.
Official Warning About setup.cfg
The official docs explicitly discourage setup.cfg: .cfg files use a different parser than ini/toml, leading to hard-to-debug issues. Use pyproject.toml directly for new projects. From pytest 9 onward the pure-TOML pytest.toml is also supported.
A typical pyproject.toml:
# content of pyproject.toml
[tool.pytest.ini_options]
addopts = "-ra --strict-markers"
testpaths = ["tests"]
python_files = ["test_*.py"]
markers = [
"slow: marks tests as slow",
"smoke: smoke-test subset",
]
filterwarnings = [
"error", # escalate warnings to errors
'ignore::DeprecationWarning:pkg_resources', # then exempt specific sources
]11.2 rootdir Determination: The Root of Path Confusion
rootdir is not "the directory you ran pytest from" — it's the project root pytest derives algorithmically, used to locate config files and resolve relative paths. Simplified rules:
- Start from the common ancestor of all collected test arguments and walk upwards;
- The first directory containing a config file (in
pytest.toml→pytest.ini→… order) becomes rootdir; - With no config file, look for
setup.py/setup.cfg/tox.ini/pyproject.tomlamong ancestors; failing that, fall back to the common ancestor itself.
The run-output header shows the actual result:
========== test session starts ==========
rootdir: /home/user/myproj
configfile: pyproject.tomlYou can also self-check by reading these directly in a test:
# content of test_config_sanity.py — environment sanity test
def test_rootdir_is_project_root(request):
# request.config provides rootpath and configfile info
assert request.config.rootpath.name == "myproj"
assert str(request.config.rootpath / "pyproject.toml").endswith("pyproject.toml")Debugging Heuristic
When "pytest behaves differently from directory A vs B", first check the rootdir and configfile lines in the header — nine times out of ten different config files got hit. You can also force it with --rootdir=.
11.3 Core Options Explained
addopts: command-line arguments appended to every run — team conventions baked in:
[tool.pytest.ini_options]
# -ra shows a summary of every outcome except passed; --strict-markers errors on unregistered marks
addopts = ["-ra", "--strict-markers", "--tb=short"]addopts vs CI
Options inside addopts cannot be "negated back". To skip a global option locally, override the whole thing with -o addopts="", or put heavyweight options into the PYTEST_ADDOPTS environment variable for per-environment control.
testpaths: when running bare pytest, search only these directories — avoiding accidental descents into virtualenvs and third-party libraries:
testpaths = ["tests"] # equivalent to defaulting to pytest tests/python_files / python_classes / python_functions: customize collection naming conventions. Defaults are test_*.py *_test.py, Test*, and test* respectively (note classes have no underscore wildcard, and Test-prefixed classes with __init__ are not collected). Example overrides:
python_files = ["check_*.py", "*_test.py"]
python_functions = ["should_*"]After which even non-standard names get collected:
# content of check_cart.py — discovered thanks to python_files
class TestCart: # collected via the default Test* prefix
def should_merge_duplicate_items(self): # discovered thanks to python_functions
cart = Cart()
cart.add("apple")
cart.add("apple")
assert len(cart.items()) == 1 # merged duplicate items
def helper_not_a_test(): # prefix doesn't match; not collected
...norecursedirs: directory patterns skipped during recursion (defaults .git .tox venv dist build ...), e.g. frontend asset dirs:
norecursedirs = [".git", "node_modules", "*.egg-info"]minversion / required_plugins: environment guardrails — insufficient versions or missing plugins error out instead of failing mysteriously:
minversion = "9.0"
required_plugins = ["pytest-cov>=7.0", "pytest-xdist"]11.4 Temporary Overrides with -o and Environment Layering
Any option can be overridden on the CLI with -o/--override-ini, repeatable:
pytest -o console_output_style=count -o cache_dir=/tmp/mycache
pytest -o addopts="" # empty global addopts for a "pristine" pytest runCombined with the PYTEST_ADDOPTS environment variable you get environment layering: keep pyproject.toml minimal in-repo, and inject a coverage gate in your CI image via PYTEST_ADDOPTS="--cov=src --cov-fail-under=90" — Chapter 13 practices this combo. Code under test can be layered on environments too:
# content of test_env_layering.py
import os
def resolve_strictness() -> bool:
"""CI defaults to strict mode; local defaults off."""
return os.environ.get("CI", "") == "true"
def test_ci_mode(monkeypatch):
monkeypatch.setenv("CI", "true")
assert resolve_strictness() is True
def test_local_mode(monkeypatch):
monkeypatch.delenv("CI", raising=False)
assert resolve_strictness() is False11.5 Chapter Summary
- Config hit order:
pytest.toml→pytest.ini→tox.ini→setup.cfg→pyproject.toml; only the first file with a valid section counts; toml formats are recommended andsetup.cfgcautioned against; - rootdir is derived algorithmically, not the cwd; for inconsistent behavior check the header's
rootdir/configfilefirst; - Six options —
addopts/testpaths/python_*/norecursedirs/minversion— cover 90% of engineering needs; -o name=valueandPYTEST_ADDOPTSprovide command-line and environment-level override channels.
🧪 随堂测验
点击你认为正确的选项。答错时会展示正确答案与原因解析。
1. A project has both pytest.ini (valid content) and pyproject.toml (with [tool.pytest.ini_options]). Which does pytest use?
2. Which table holds pytest config in pyproject.toml?
3. The run header shows configfile: tox.ini but you want the root pyproject.toml. Most likely cause?
4. Which statement about testpaths is correct?
🛠️ Hands-On Exercises
- Complete your project's
[tool.pytest.ini_options]inpyproject.toml: at leastaddopts,testpaths, andmarkers; runpytestand verify the header'sconfigfilepoints at it. - Create a "dual-config conflict": place both a valid
pytest.iniandpyproject.toml, observe the header, explain why, then deletepytest.inito restore. - Use
-oto override two options in one run over your suite (e.g.-o testpaths=... -o python_files=...), noting the output differences.
Next chapter surveys pytest's plugin ecosystem: the loading mechanism, popular plugins, and the principles behind "install and it just works": Chapter 12.