# Pytest Testing Framework: Architecture, Hooks, and Plugin System Explained

> Explore the pytest testing framework architecture, discover how its hooks and plugin system enhance test discovery, fixtures, and reporting. Learn to leverage pytest effectively for Python projects.

- Repository: [pytest-dev/pytest](https://github.com/pytest-dev/pytest)
- Tags: deep-dive
- Published: 2026-02-20

---

**Pytest is a feature-rich, extensible Python testing framework built on a hook-based plugin architecture that enables automatic test discovery, dependency injection via fixtures, and flexible reporting through the `pluggy` library.**

The `pytest-dev/pytest` repository provides a mature testing infrastructure that goes beyond basic assertion checking. Its architecture is designed around three core concepts—collection and discovery, hooks and plugins, and execution and reporting—that together enable everything from simple unit tests to complex integration scenarios.

## Collection and Discovery

Pytest automatically locates and collects test modules, classes, and functions using the `pytest_collect` protocol implemented in [`src/_pytest/pathlib.py`](https://github.com/pytest-dev/pytest/blob/main/src/_pytest/pathlib.py) and [`src/_pytest/python.py`](https://github.com/pytest-dev/pytest/blob/main/src/_pytest/python.py). The framework walks the filesystem to identify files matching the patterns `test_*.py` or `*_test.py`.

Once identified, test objects are wrapped in `Item` subclasses defined in [`src/_pytest/python.py`](https://github.com/pytest-dev/pytest/blob/main/src/_pytest/python.py), including `Function`, `Instance`, and `Class` types. This collection tree forms the structure that the execution engine traverses during test runs.

## Hooks and Plugins

The heart of pytest's extensibility lies in its hook system, defined using the `pluggy` library. Pytest registers a comprehensive set of hooks in [`src/_pytest/hookspec.py`](https://github.com/pytest-dev/pytest/blob/main/src/_pytest/hookspec.py) that plugins can implement to modify behavior.

Key hooks include:

- `pytest_runtest_setup` – Executed before each test runs
- `pytest_collection_modifyitems` – Allows modification of collected items before execution
- `pytest_configure` – Called once after command-line options are parsed

Core functionality for fixtures, markers, and command-line options is provided by built-in plugins located under `src/_pytest`. Third-party plugins such as `pytest-cov` and `pytest-mock` are discovered via entry points defined in [`pyproject.toml`](https://github.com/pytest-dev/pytest/blob/main/pyproject.toml) or [`setup.cfg`](https://github.com/pytest-dev/pytest/blob/main/setup.cfg).

### Creating a Custom Plugin

To extend pytest, create a plugin module implementing hook specifications:

```python

# my_plugin.py

import pytest

def pytest_configure(config):
    # Called once after command line options are parsed

    config.addinivalue_line(
        "markers", "slow: mark test as slow-running"
    )

@pytest.hookimpl
def pytest_collection_modifyitems(session, config, items):
    for item in items:
        if "slow" in item.keywords and config.getoption("--run-slow"):
            item.add_marker(pytest.mark.run_slow)

```

Activate the plugin via entry point:

```toml

# pyproject.toml

[project.entry-points.pytest11]
my_plugin = "my_plugin"

```

## Execution and Reporting

Test execution is orchestrated by the `Node` API. A `Session` object creates a `Collector` tree, then iterates over `Item` objects. Each `Item` runs its `runtest` method, invoking fixtures defined in [`src/_pytest/fixtures.py`](https://github.com/pytest-dev/pytest/blob/main/src/_pytest/fixtures.py) and yielding a `CallInfo` result.

The **assertion rewrite** mechanism in [`src/_pytest/assertion/rewrite.py`](https://github.com/pytest-dev/pytest/blob/main/src/_pytest/assertion/rewrite.py) intercepts import statements to rewrite `assert` statements, providing detailed failure messages without requiring special assertion methods.

Reporting is handled by reporter classes:

- **Terminal reporting**: [`src/_pytest/terminal.py`](https://github.com/pytest-dev/pytest/blob/main/src/_pytest/terminal.py) formats and outputs results to the console
- **JUnit XML**: [`src/_pytest/junitxml.py`](https://github.com/pytest-dev/pytest/blob/main/src/_pytest/junitxml.py) generates JUnit-compatible XML reports for CI integration
- **Custom formats**: Additional reporters can be implemented via hooks

## Key Architectural Components

| Component | Purpose | Important Files |
|-----------|---------|-----------------|
| **Path & Collection** | Resolves paths, builds collection tree | [`src/_pytest/pathlib.py`](https://github.com/pytest-dev/pytest/blob/main/src/_pytest/pathlib.py), [`src/_pytest/python.py`](https://github.com/pytest-dev/pytest/blob/main/src/_pytest/python.py) |
| **Hook System** | Enables plugins to extend behavior | [`src/_pytest/hookspec.py`](https://github.com/pytest-dev/pytest/blob/main/src/_pytest/hookspec.py), [`src/_pytest/hooks.py`](https://github.com/pytest-dev/pytest/blob/main/src/_pytest/hooks.py) |
| **Fixtures** | Manages dependency injection for tests | [`src/_pytest/fixtures.py`](https://github.com/pytest-dev/pytest/blob/main/src/_pytest/fixtures.py) |
| **Assertion Rewrite** | Improves failure messages by rewriting assert statements | [`src/_pytest/assertion/rewrite.py`](https://github.com/pytest-dev/pytest/blob/main/src/_pytest/assertion/rewrite.py) |
| **CLI & Config** | Parses command-line options, reads [`pytest.ini`](https://github.com/pytest-dev/pytest/blob/main/pytest.ini), [`pyproject.toml`](https://github.com/pytest-dev/pytest/blob/main/pyproject.toml) | [`src/_pytest/config/__init__.py`](https://github.com/pytest-dev/pytest/blob/main/src/_pytest/config/__init__.py), [`src/_pytest/main.py`](https://github.com/pytest-dev/pytest/blob/main/src/_pytest/main.py) |
| **Reporting** | Generates terminal, JUnit XML, HTML reports | [`src/_pytest/terminal.py`](https://github.com/pytest-dev/pytest/blob/main/src/_pytest/terminal.py), [`src/_pytest/junitxml.py`](https://github.com/pytest-dev/pytest/blob/main/src/_pytest/junitxml.py) |

## Practical Code Examples

### Simple Test with Fixtures

```python

# tests/test_math.py

import pytest

@pytest.fixture
def numbers():
    return (1, 2, 3)

def test_sum(numbers):
    a, b, c = numbers
    assert a + b + c == 6

```

### Using Markers and Command-Line Options

```python

# tests/test_api.py

import pytest

@pytest.mark.slow
def test_long_running():
    # simulate a long operation

    assert True

```

Run only fast tests:

```bash
pytest -m "not slow"

```

Generate JUnit XML reports:

```bash
pytest --junitxml=results.xml

```

### Custom Timing Plugin

```python

# plugin/timer.py

import time
import pytest

@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_call(item):
    start = time.time()
    yield
    duration = time.time() - start
    print(f"{item.name} took {duration:.3f}s")

```

Enable via:

```bash
pytest -p timer

```

## Summary

- **Pytest** uses a sophisticated collection mechanism in [`src/_pytest/python.py`](https://github.com/pytest-dev/pytest/blob/main/src/_pytest/python.py) to discover tests matching `test_*.py` patterns automatically.
- The **hook system** powered by `pluggy` allows extending every phase of the testing lifecycle through well-defined specifications in [`src/_pytest/hookspec.py`](https://github.com/pytest-dev/pytest/blob/main/src/_pytest/hookspec.py).
- **Fixtures** provide powerful dependency injection capabilities managed by [`src/_pytest/fixtures.py`](https://github.com/pytest-dev/pytest/blob/main/src/_pytest/fixtures.py), enabling modular and reusable test setups.
- **Assertion rewriting** in [`src/_pytest/assertion/rewrite.py`](https://github.com/pytest-dev/pytest/blob/main/src/_pytest/assertion/rewrite.py) delivers detailed failure diagnostics without requiring custom assertion methods.
- The framework supports multiple **reporting formats** including terminal output and JUnit XML via [`src/_pytest/terminal.py`](https://github.com/pytest-dev/pytest/blob/main/src/_pytest/terminal.py) and [`src/_pytest/junitxml.py`](https://github.com/pytest-dev/pytest/blob/main/src/_pytest/junitxml.py).

## Frequently Asked Questions

### How does pytest discover test files automatically?

Pytest implements a filesystem walker in [`src/_pytest/pathlib.py`](https://github.com/pytest-dev/pytest/blob/main/src/_pytest/pathlib.py) that identifies modules matching the `test_*.py` and `*_test.py` naming conventions. Once identified, [`src/_pytest/python.py`](https://github.com/pytest-dev/pytest/blob/main/src/_pytest/python.py) imports these modules and wraps test functions and classes into `Item` subclasses like `Function` and `Class`, building a collection tree that the execution engine traverses.

### What is the role of fixtures in pytest's architecture?

Fixtures serve as the dependency injection mechanism managed by [`src/_pytest/fixtures.py`](https://github.com/pytest-dev/pytest/blob/main/src/_pytest/fixtures.py). They allow tests to request resources—such as database connections, temporary directories, or mock objects—through function arguments. The fixture system handles setup, caching, and teardown automatically, enabling modular test design and resource sharing across multiple test cases without explicit boilerplate code.

### How can I extend pytest with custom plugins?

You can extend pytest by implementing hook specifications defined in [`src/_pytest/hookspec.py`](https://github.com/pytest-dev/pytest/blob/main/src/_pytest/hookspec.py) using the `pluggy` library. Create a Python module with functions decorated with `@pytest.hookimpl`, such as `pytest_collection_modifyitems` to alter test selection or `pytest_configure` to add command-line options. Register your plugin via entry points in [`pyproject.toml`](https://github.com/pytest-dev/pytest/blob/main/pyproject.toml) under `[project.entry-points.pytest11]` or activate it temporarily using the `-p` command-line flag.