---
# === IDENTITY ===
id: software/devops/pytest-configuration/2026
canonical_question: "pytest testing configuration reference"
aliases:
  - "How to configure pytest in pyproject.toml"
  - "pytest fixtures conftest.py best practices"
  - "pytest markers parametrize and coverage setup"
  - "Python testing configuration with pytest"
entity_type: software_reference
domain: software > devops > pytest Testing Configuration
region: global
jurisdiction: global
temporal_scope: 2024-2026

# === VERIFICATION ===
last_verified: 2026-02-28
confidence: 0.94
version: 1.0
first_published: 2026-02-28

# === TEMPORAL VALIDITY ===
temporal_validity:
  status: stable
  last_breaking_change: "pytest 8.0 (January 2024) — changed collection behavior, deprecated pytest.warns(None)"
  next_review: 2026-08-27
  change_sensitivity: medium

# === CONSTRAINTS ===
constraints:
  - "pytest requires Python 3.8+ (pytest 8.x) — older Python versions need pytest 7.x"
  - "Custom markers must be registered in pyproject.toml or conftest.py — unregistered markers trigger warnings"
  - "conftest.py files are auto-discovered by directory — never import them directly in test files"
  - "Fixture scope hierarchy: session > package > module > class > function — higher-scoped fixtures cannot depend on lower-scoped ones"
  - "parametrize decorator is spelled without the 'e' — @pytest.mark.parametrize, not parameterize"

# === SKIP CONDITIONS ===
skip_this_unit_if:
  - condition: "Using unittest instead of pytest"
    use_instead: "Python unittest docs at docs.python.org/3/library/unittest.html"
  - condition: "Testing JavaScript/TypeScript, not Python"
    use_instead: "software/devops/jest-configuration/2026"

# === AGENT HINTS ===
inputs_needed:
  - key: "async_code"
    question: "Does your code use async/await?"
    type: boolean
  - key: "project_layout"
    question: "What is your project layout?"
    type: choice
    options: ["src layout (src/mypackage/)", "flat layout (mypackage/)", "single module"]
  - key: "needs_coverage"
    question: "Do you need coverage reporting?"
    type: boolean

# === DISTRIBUTION ===
canonical_source: "https://knowledgelib.io/software/devops/pytest-configuration/2026"
suggested_citation: "Source: knowledgelib.io — AI Knowledge Library (verified 2026-02-28)"

# === RELATED UNITS ===
related_kos:
  depends_on: []
  related_to:
    - id: "software/devops/pre-commit-hooks/2026"
      label: "Pre-commit Hooks Setup Reference"
  solves: []
  alternative_to:
    - id: "software/devops/jest-configuration/2026"
      label: "Jest Testing Configuration Reference"
  often_confused_with: []

# === SOURCES ===
sources:
  - id: src1
    title: "Configuration — pytest documentation"
    author: pytest
    url: https://docs.pytest.org/en/stable/reference/customize.html
    type: official_docs
    published: 2024-01-01
    reliability: authoritative
  - id: src2
    title: "Good Integration Practices — pytest"
    author: pytest
    url: https://docs.pytest.org/en/stable/explanation/goodpractices.html
    type: official_docs
    published: 2024-01-01
    reliability: authoritative
  - id: src3
    title: "How to parametrize fixtures and test functions"
    author: pytest
    url: https://docs.pytest.org/en/stable/how-to/parametrize.html
    type: official_docs
    published: 2024-01-01
    reliability: authoritative
  - id: src4
    title: "Working with custom markers"
    author: pytest
    url: https://docs.pytest.org/en/stable/example/markers.html
    type: official_docs
    published: 2024-01-01
    reliability: authoritative
  - id: src5
    title: "Writing plugins — pytest"
    author: pytest
    url: https://docs.pytest.org/en/stable/how-to/writing_plugins.html
    type: official_docs
    published: 2024-01-01
    reliability: authoritative
  - id: src6
    title: "API Reference — pytest"
    author: pytest
    url: https://docs.pytest.org/en/stable/reference/reference.html
    type: official_docs
    published: 2024-01-01
    reliability: authoritative
  - id: src7
    title: "pytest Config Files — Pytest with Eric"
    author: Eric Sales De Andrade
    url: https://pytest-with-eric.com/configuration/pytest-config-file/
    type: technical_blog
    published: 2024-09-01
    reliability: moderate_high
---

# pytest Testing Configuration Reference

## TL;DR

- **Bottom line**: Configure pytest in `pyproject.toml` under `[tool.pytest.ini_options]`; use `conftest.py` for shared fixtures; register custom markers to avoid warnings; use `src` layout with `pythonpath = ["src"]` for clean imports.
- **Key tool/command**: `pytest -v --tb=short`
- **Watch out for**: Misspelling `parametrize` as `parameterize` — pytest silently ignores the decorator and your test runs only once without parameterized data.
- **Works with**: pytest 7.x–8.x, Python 3.8+, all major plugins (pytest-cov, pytest-asyncio, pytest-mock, pytest-xdist).

## Constraints
<!-- Agents: read this section before recommending any action from this unit.
     These are hard constraints that must not be violated. -->

- pytest requires Python 3.8+ (pytest 8.x) — older Python versions need pytest 7.x
- Custom markers must be registered in `pyproject.toml` or `conftest.py` — unregistered markers trigger warnings
- `conftest.py` files are auto-discovered by directory — never import them directly in test files
- Fixture scope hierarchy: `session` > `package` > `module` > `class` > `function` — higher-scoped fixtures cannot depend on lower-scoped ones
- `parametrize` decorator is spelled without the second 'e' — `@pytest.mark.parametrize`, not `parameterize`

## Quick Reference

| Setting | Location | Purpose |
|---|---|---|
| `[tool.pytest.ini_options]` | `pyproject.toml` | Main pytest configuration section |
| `testpaths` | pyproject.toml | Directories to search for tests |
| `pythonpath` | pyproject.toml | Paths added to sys.path (use `["src"]` for src layout) |
| `addopts` | pyproject.toml | Default command-line options |
| `markers` | pyproject.toml | Register custom markers |
| `minversion` | pyproject.toml | Minimum pytest version required |
| `filterwarnings` | pyproject.toml | Warning filters for tests |
| `conftest.py` | any test directory | Auto-discovered shared fixtures and hooks |
| `@pytest.fixture` | conftest.py or test file | Define reusable test setup |
| `@pytest.mark.parametrize` | test function | Run test with multiple inputs |
| `@pytest.mark.skip` | test function | Skip a test unconditionally |
| `@pytest.mark.skipif` | test function | Skip a test conditionally |
| `@pytest.mark.xfail` | test function | Mark test as expected failure |
| `--cov` | command line | Enable coverage (pytest-cov) |
| `-x` | command line | Stop on first failure |
| `-n auto` | command line | Parallel execution (pytest-xdist) |

## Decision Tree

```
START
├── New Python project?
│   ├── YES → Use src layout + pyproject.toml config
│   └── NO → Check existing config format ↓
├── Using pyproject.toml already?
│   ├── YES → Add [tool.pytest.ini_options] section
│   └── NO → Migrate from pytest.ini/setup.cfg to pyproject.toml
├── Testing async code?
│   ├── YES → Install pytest-asyncio, set asyncio_mode = "auto"
│   └── NO ↓
├── Need parallel execution?
│   ├── YES → Install pytest-xdist, use -n auto
│   └── NO ↓
├── Need coverage reporting?
│   ├── YES → Install pytest-cov, add --cov to addopts
│   └── NO ↓
└── DEFAULT → pyproject.toml + conftest.py + pytest-cov
```

## Step-by-Step Guide

### 1. Install pytest and common plugins

Set up the testing stack. [src1]

```bash
pip install pytest pytest-cov pytest-mock pytest-xdist
# For async code:
pip install pytest-asyncio
# For timeout protection:
pip install pytest-timeout
```

**Verify**: `pytest --version` → `pytest 8.x.x`

### 2. Configure pyproject.toml

Add the pytest configuration section with sensible defaults. [src1]

```toml
# pyproject.toml
[tool.pytest.ini_options]
minversion = "8.0"
testpaths = ["tests"]
pythonpath = ["src"]
addopts = [
    "-ra",
    "--strict-markers",
    "--strict-config",
    "-v",
    "--tb=short",
]
markers = [
    "slow: marks tests as slow (deselect with '-m \"not slow\"')",
    "integration: marks integration tests (require external services)",
    "e2e: marks end-to-end tests",
]
filterwarnings = [
    "error",
    "ignore::DeprecationWarning:third_party_lib.*",
]
```

**Verify**: `pytest --co` → lists discovered tests

### 3. Set up project layout with conftest.py

Create the directory structure and shared fixtures. [src2]

```
project/
├── pyproject.toml
├── src/
│   └── mypackage/
│       ├── __init__.py
│       └── core.py
└── tests/
    ├── conftest.py          # Shared fixtures
    ├── test_core.py
    ├── integration/
    │   ├── conftest.py      # Integration-specific fixtures
    │   └── test_api.py
    └── e2e/
        └── test_workflow.py
```

```python
# tests/conftest.py
import pytest

@pytest.fixture
def sample_data():
    """Provide sample test data."""
    return {"name": "test", "value": 42}

@pytest.fixture(scope="session")
def db_connection():
    """Create a database connection for the entire test session."""
    conn = create_connection()
    yield conn
    conn.close()

@pytest.fixture(autouse=True)
def reset_environment(monkeypatch):
    """Auto-applied: reset env vars for every test."""
    monkeypatch.delenv("API_KEY", raising=False)
```

**Verify**: `pytest --fixtures` → shows your custom fixtures with docstrings

### 4. Write parametrized tests

Use `@pytest.mark.parametrize` to test multiple inputs efficiently. [src3]

```python
# tests/test_core.py
import pytest
from mypackage.core import calculate_discount

@pytest.mark.parametrize("price,expected", [
    (100, 90),    # 10% discount
    (200, 180),
    (0, 0),       # Edge case: zero price
    (50, 45),
])
def test_calculate_discount(price, expected):
    assert calculate_discount(price, discount=0.10) == expected

@pytest.mark.parametrize("invalid_input", [-1, -100, None])
def test_calculate_discount_invalid(invalid_input):
    with pytest.raises((ValueError, TypeError)):
        calculate_discount(invalid_input, discount=0.10)
```

**Verify**: `pytest -v tests/test_core.py` → shows parameterized variants

### 5. Configure coverage reporting

Add coverage to default options and set thresholds. [src1]

```toml
# pyproject.toml
[tool.pytest.ini_options]
addopts = ["-ra", "--strict-markers", "--cov=src", "--cov-report=term-missing"]

[tool.coverage.run]
source = ["src"]
branch = true
omit = ["*/tests/*", "*/__pycache__/*"]

[tool.coverage.report]
fail_under = 80
show_missing = true
exclude_lines = [
    "pragma: no cover",
    "if __name__ == .__main__.",
    "if TYPE_CHECKING:",
    "raise NotImplementedError",
]
```

**Verify**: `pytest` → coverage report printed with threshold enforcement

### 6. Configure async testing

Set up pytest-asyncio for testing async code. [src6]

```toml
# pyproject.toml
[tool.pytest.ini_options]
asyncio_mode = "auto"
```

```python
# tests/test_async.py
import pytest

async def test_async_fetch():
    result = await fetch_data("https://api.example.com")
    assert result.status == 200

@pytest.fixture
async def async_client():
    async with AsyncClient() as client:
        yield client

async def test_with_async_fixture(async_client):
    response = await async_client.get("/health")
    assert response.status_code == 200
```

**Verify**: `pytest tests/test_async.py -v` → async tests pass

## Code Examples

### Complete pyproject.toml with all plugins

> Full script: [complete-pyproject-toml-with-all-plugins.toml](scripts/complete-pyproject-toml-with-all-plugins.toml) (42 lines)

```toml
# pyproject.toml — comprehensive pytest config
[tool.pytest.ini_options]
minversion = "8.0"
testpaths = ["tests"]
pythonpath = ["src"]
# ... (see full script)
```

### Advanced conftest.py with fixture factories

> Full script: [advanced-conftest-py-with-fixture-factories.py](scripts/advanced-conftest-py-with-fixture-factories.py) (37 lines)

```python
# tests/conftest.py — advanced fixture patterns
import pytest
from unittest.mock import AsyncMock, MagicMock
# Fixture factory — returns a function that creates users
@pytest.fixture
# ... (see full script)
```

### Parametrize with fixtures and IDs

```python
# tests/test_advanced.py — advanced parametrize patterns
import pytest

# Parametrize with custom IDs for readable output
@pytest.mark.parametrize(
    "input_data,expected",
    [
        pytest.param({"key": "value"}, True, id="valid-dict"),
        pytest.param({}, False, id="empty-dict"),
        pytest.param(None, False, id="none-input"),
        pytest.param({"key": ""}, False, id="empty-value"),
    ],
)
def test_validate_data(input_data, expected):
    assert validate(input_data) == expected

# Stack parametrize decorators for combinatorial testing
@pytest.mark.parametrize("method", ["GET", "POST", "PUT"])
@pytest.mark.parametrize("auth", [True, False])
def test_api_endpoints(method, auth, api_client):
    response = api_client.request(method, "/resource", auth=auth)
    if not auth:
        assert response.status_code == 401
    else:
        assert response.status_code in (200, 201)
```

## Anti-Patterns

### Wrong: Importing conftest.py directly

```python
# ❌ BAD — conftest.py is auto-discovered; importing breaks pytest
from tests.conftest import sample_data  # Never do this!

def test_something():
    data = sample_data()
    assert data["name"] == "test"
```

### Correct: Request fixtures as function parameters

```python
# ✅ GOOD — pytest injects fixtures automatically
def test_something(sample_data):  # Fixture injected by name
    assert sample_data["name"] == "test"
```

### Wrong: Higher-scoped fixture depending on lower-scoped

```python
# ❌ BAD — session fixture cannot use function fixture
@pytest.fixture(scope="session")
def database(tmp_path):  # tmp_path is function-scoped!
    return create_db(tmp_path)
# ScopeMismatch error
```

### Correct: Match or use higher scope

```python
# ✅ GOOD — use tmp_path_factory (session-compatible)
@pytest.fixture(scope="session")
def database(tmp_path_factory):  # Session-scoped equivalent
    path = tmp_path_factory.mktemp("data")
    return create_db(path)
```

### Wrong: Spelling parametrize with an 'e'

```python
# ❌ BAD — silently ignored, test runs once without parameters
@pytest.mark.parameterize("x", [1, 2, 3])  # Wrong spelling!
def test_values(x):
    assert x > 0
# Result: test_values runs once with no 'x' parameter → NameError
```

### Correct: Use the correct spelling

```python
# ✅ GOOD — correct British spelling without second 'e'
@pytest.mark.parametrize("x", [1, 2, 3])
def test_values(x):
    assert x > 0
# Result: test_values[1], test_values[2], test_values[3]
```

## Common Pitfalls

- **ModuleNotFoundError in tests**: Python cannot find your package. Fix: add `pythonpath = ["src"]` to pyproject.toml and use src layout. [src2]
- **Fixture not found**: Fixture defined in wrong conftest.py. Fix: place conftest.py in the directory tree above the test file. [src1]
- **Unregistered marker warnings**: Custom markers not declared. Fix: add to `markers` list in pyproject.toml with `--strict-markers` in addopts. [src4]
- **Slow tests blocking CI**: No test timeout. Fix: install `pytest-timeout` and add `timeout = 30` to config. [src1]
- **Coverage too low but tests pass**: Only tested files counted. Fix: set `source = ["src"]` in `[tool.coverage.run]` to include all source files. [src7]
- **Async tests silently skipped**: Missing `pytest-asyncio` or wrong mode. Fix: install `pytest-asyncio` and set `asyncio_mode = "auto"` in config. [src6]

## Diagnostic Commands

```bash
# Show all discovered tests without running
pytest --collect-only

# Show available fixtures and their locations
pytest --fixtures

# Run tests matching a keyword
pytest -k "test_auth or test_login"

# Run tests with a specific marker
pytest -m "not slow and not integration"

# Run with parallel execution
pytest -n auto

# Show test durations (find slow tests)
pytest --durations=10

# Generate HTML coverage report
pytest --cov=src --cov-report=html

# Verbose output with full tracebacks
pytest -v --tb=long
```

## Version History & Compatibility

| Version | Status | Breaking Changes | Migration Notes |
|---|---|---|---|
| pytest 8.x | Current | Collection changes, `pytest.warns(None)` deprecated | Use `recwarn` fixture instead of `warns(None)` |
| pytest 7.x | Maintenance | `pythonpath` added, `--import-mode=importlib` default | Best upgrade path from 6.x |
| pytest 6.x | EOL | pyproject.toml support added | Migrate config from `pytest.ini` to `pyproject.toml` |
| pytest 5.x | EOL | Python 2 dropped | Upgrade to 7+ for security patches |

## When to Use / When Not to Use

| Use When | Don't Use When | Use Instead |
|---|---|---|
| Any Python project | JavaScript/TypeScript project | Jest or Vitest |
| Need rich plugin ecosystem | Minimal testing with no dependencies | unittest (stdlib) |
| Complex fixture dependency graphs | Running tests inside notebooks | nbmake or ipytest |
| Parametrized and property-based testing | Load/performance testing | locust or k6 |

## Important Caveats

- `conftest.py` at project root applies to ALL tests — be careful with auto-use fixtures at this level
- `pytest-asyncio` `auto` mode marks all async tests automatically — remove manual `@pytest.mark.asyncio` decorators to avoid warnings
- `--cov` in `addopts` runs coverage on every `pytest` invocation including debugging — consider moving to CI-only scripts
- `pytest-xdist` (`-n auto`) does not support `--pdb` — remove `-n` when debugging

## Related Units

- [Pre-commit Hooks Setup Reference](/software/devops/pre-commit-hooks/2026)
- [Jest Testing Configuration Reference](/software/devops/jest-configuration/2026)
