Pytest doesn’t actually care where you put its configuration; it’s happy to read it from pyproject.toml, pytest.ini, or setup.cfg.
Let’s see it in action. Imagine you have a simple test file:
# test_example.py
def test_addition():
assert 1 + 1 == 2
And you want to control pytest’s verbosity. Without any configuration, running pytest gives you this:
============================= test session starts ==============================
platform linux -- Python 3.10.12, pytest-7.4.0, pluggy-1.3.0
rootdir: /home/user/project
collected 1 item
test_example.py . [100%]
============================== 1 passed in 0.01s ===============================
Now, let’s create a pytest.ini file in the same directory:
# pytest.ini
[pytest]
verbose = 2
Running pytest again yields:
============================= test session starts ==============================
platform linux -- Python 3.10.12, pytest-7.4.0, pluggy-1.3.0
rootdir: /home/user/project
collected 1 item
test_example.py::test_addition PASSED [100%]
============================== 1 passed in 0.01s ===============================
The ::test_addition PASSED line shows the increased verbosity.
You could achieve the exact same thing using pyproject.toml:
# pyproject.toml
[tool.pytest.ini_options]
verbose = 2
Or setup.cfg:
# setup.cfg
[tool:pytest]
verbose = 2
Pytest searches for these files in the following order: pyproject.toml, pytest.ini, and setup.cfg. It reads the first one it finds. If pyproject.toml exists, it won’t even look for pytest.ini or setup.cfg. This precedence is key: pyproject.toml overrides pytest.ini, which overrides setup.cfg.
The primary problem these configuration files solve is centralizing and standardizing pytest’s behavior across your project. Instead of passing command-line arguments like pytest -v every time, you set it once. This is crucial for team collaboration and ensuring consistent testing environments.
Internally, pytest uses a configuration discovery mechanism. It walks up the directory tree from where you run pytest until it finds one of these files. Once found, it parses the relevant section ([pytest] for .ini/.cfg, [tool.pytest.ini_options] for pyproject.toml) and applies those settings to the current test session. This means configuration can be project-wide or even scoped to subdirectories if you place a config file there.
The most common settings you’ll find here are:
markers: Defining custom markers for tests.addopts: Adding default command-line options.testpaths: Specifying directories to discover tests in.python_files,python_classes,python_functions: Controlling how pytest names files, classes, and functions it considers tests.filterwarnings: Suppressing or controlling warnings.
For example, to add a default -v and -s (show stdout/stderr) to every run, you’d use:
# pytest.ini
[pytest]
addopts = -v -s
Or in pyproject.toml:
# pyproject.toml
[tool.pytest.ini_options]
addopts = ["-v", "-s"]
You can also define custom markers to categorize your tests, which is super useful for selective test runs. For instance, marking tests as slow or integration:
# pytest.ini
[pytest]
markers =
slow: marks tests as slow to run
integration: marks tests as integration tests
Then, in your test files, you’d use @pytest.mark.slow. Running pytest -m slow would only execute those marked tests.
If you have multiple config files present, pytest will only read the first one it finds based on the precedence: pyproject.toml > pytest.ini > setup.cfg. This behavior is deterministic and prevents confusion. For example, if you have both pytest.ini and pyproject.toml in your root directory, pytest will only read pyproject.toml and ignore pytest.ini entirely.
The actual mechanism involves pytest’s config object, which is populated during the initialization phase. When pytest starts, it looks for these files in the current directory and parent directories. The first one encountered dictates the initial configuration. Any settings found are then merged with command-line options, with command-line options taking precedence. This layered approach allows for both global project configuration and fine-grained overrides via the command line.
The fact that pytest reads configuration files from the current directory upwards means that a configuration file in a subdirectory can override or extend settings from a higher-level one, without needing to explicitly specify which file to use. Pytest automatically applies the closest configuration it finds.
The next thing you’ll likely want to configure is how pytest discovers your tests, especially if they aren’t in a test_ or _test naming convention.