Skip to content

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 like addopts/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):

text
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.cfg only count when they actually contain a pytest config section;
  • pyproject.toml is 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:

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:

  1. Start from the common ancestor of all collected test arguments and walk upwards;
  2. The first directory containing a config file (in pytest.toml→pytest.ini→… order) becomes rootdir;
  3. With no config file, look for setup.py/setup.cfg/tox.ini/pyproject.toml among ancestors; failing that, fall back to the common ancestor itself.

The run-output header shows the actual result:

text
========== test session starts ==========
rootdir: /home/user/myproj
configfile: pyproject.toml

You can also self-check by reading these directly in a test:

python
# 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:

toml
[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:

toml
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:

toml
python_files = ["check_*.py", "*_test.py"]
python_functions = ["should_*"]

After which even non-standard names get collected:

python
# 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:

toml
norecursedirs = [".git", "node_modules", "*.egg-info"]

minversion / required_plugins: environment guardrails — insufficient versions or missing plugins error out instead of failing mysteriously:

toml
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:

bash
pytest -o console_output_style=count -o cache_dir=/tmp/mycache
pytest -o addopts=""            # empty global addopts for a "pristine" pytest run

Combined 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:

python
# 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 False

11.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 and setup.cfg cautioned against;
  • rootdir is derived algorithmically, not the cwd; for inconsistent behavior check the header's rootdir/configfile first;
  • Six options — addopts/testpaths/python_*/norecursedirs/minversion — cover 90% of engineering needs;
  • -o name=value and PYTEST_ADDOPTS provide 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 ​

  1. Complete your project's [tool.pytest.ini_options] in pyproject.toml: at least addopts, testpaths, and markers; run pytest and verify the header's configfile points at it.
  2. Create a "dual-config conflict": place both a valid pytest.ini and pyproject.toml, observe the header, explain why, then delete pytest.ini to restore.
  3. Use -o to 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.