When you run pytest, it doesn’t just magically know which files contain your tests. It has a whole system for discovering them, and understanding that system is key to not wasting time or running tests you didn’t intend to.

Let’s see pytest in action, discovering some tests. Imagine this directory structure:

.
├── tests/
│   ├── __init__.py
│   ├── test_basic.py
│   ├── test_complex.py
│   └── data/
│       ├── __init__.py
│       └── test_helpers.py
└── src/
    └── my_module.py

And here are the files:

tests/test_basic.py:

def test_addition():
    assert 1 + 1 == 2

tests/test_complex.py:

class TestMyFeature:
    def test_feature_works(self):
        assert True

tests/data/test_helpers.py:

# This file has "test" in the name, but is it a test file?
def helper_function():
    return "I'm a helper"

When you run pytest in the root directory, it finds tests/test_basic.py and tests/test_complex.py because they follow the default naming conventions. It also finds tests/data/test_helpers.py. Why? Because pytest’s default discovery rules are quite broad.

By default, pytest looks for files named test_*.py or *_test.py. It then looks for functions named test_* or methods named test_* within classes named Test*. This is why tests/data/test_helpers.py is collected, even though it doesn’t contain any actual test functions. pytest collects the file, but since there are no test functions inside, it reports 0 tests collected from that file.

The problem arises when you have files that match the naming convention but aren’t intended to be test files, or when you have tests in non-standard locations. You might have a test_data.py file containing test data, or helper functions for your tests. pytest will dutifully collect these files, and if they happen to contain something that looks like a test (even if it’s not meant to be run), you’ll get unexpected test runs or errors.

The primary way to control what pytest collects is through the python_files option in your pytest.ini or pyproject.toml file. This option lets you specify glob patterns for which Python files should even be considered for test collection.

For example, to only collect files named test_*.py and *_test.py that are directly in the tests directory (and not subdirectories), you could add this to your pytest.ini:

[pytest]
python_files = test_*.py *_test.py

If you want to be more specific and only collect files named test_*.py in the tests directory and its subdirectories, you might use:

[pytest]
python_files = tests/test_*.py

This would prevent tests/data/test_helpers.py from being collected if it doesn’t match the pattern.

Alternatively, you can use the --collect-only flag to see exactly what pytest would collect without actually running the tests. This is invaluable for debugging your collection configuration.

pytest --collect-only

This command will output a tree-like structure showing all discovered tests. If you see files or tests you don’t expect, you know your python_files configuration might be too broad, or you need to adjust your file naming.

You can also control which functions and classes are collected within the discovered files using python_functions and python_classes. These work similarly, using glob patterns. For instance, if your test functions were named spec_* instead of test_*, you’d add:

[pytest]
python_functions = spec_*

The most surprising truth about pytest’s collection is that it’s not just about naming conventions; it’s a hierarchical process. pytest first identifies candidate files based on python_files, then it looks for candidate classes within those files based on python_classes, and finally, it searches for candidate functions within those files or classes based on python_functions. If any of these stages don’t match, the item is excluded from collection. This layered approach allows for very fine-grained control.

For instance, if you want to collect tests only from files named test_*.py, but specifically exclude any files that contain the word _integration_ in their name, you can use negation within your glob patterns (though this is less common and can get complex). A more practical approach is to ensure your python_files pattern is precise enough to avoid matching unwanted files in the first place.

You can also use markers to selectively run or skip tests. While not directly controlling discovery, markers allow you to filter the collected tests at runtime. For example, to run only tests marked as "slow":

pytest -m slow

This doesn’t change what is discovered, but it changes what is run from the discovered set.

The next common hurdle after mastering test discovery is understanding how to manage test dependencies and fixtures effectively.

Want structured learning?

Take the full Pytest course →