Pytest is the dominant testing framework for Python. It is loved for its no-boilerplate syntax (assert x == y), powerful fixture system, and rich plugin ecosystem.
When to Use
- Python Standard Testing Framework: The most widely adopted, powerful testing framework for Python web backends, data science, and ML.
- Clean Fixture Dependency Injection: Managing test setup, database connections, and teardown cleanly with
@pytest.fixture. - Parameterized Testing: Running test functions across multi-dimensional input sets with
@pytest.mark.parametrize. - Rich Plugin Ecosystem: Extending capabilities via
pytest-asyncio,pytest-cov,pytest-xdist, andpytest-mock.
Quick Start
# content of test_sample.py
def inc(x):
return x + 1
def test_answer(): assert inc(3) == 5
Running it:
```bash
pytest
Core Concepts
Dependency Injection with Fixtures
Fixtures provide modular, reusable dependencies with explicit lifecycle scopes:
# conftest.py
import pytest
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
@pytest.fixture(scope="session")
def db_engine():
engine = create_engine("sqlite:///:memory:")
yield engine
engine.dispose()
@pytest.fixture
def db_session(db_engine):
Session = sessionmaker(bind=db_engine)
session = Session()
yield session
session.rollback()
session.close()
Parameterized Tests (@pytest.mark.parametrize)
Tests multiple input/output scenarios cleanly without loop boilerplate:
import pytest
@pytest.mark.parametrize("email,expected_valid", [
("user@example.com", True),
("invalid-email", False),
("@missinguser.com", False),
("user.name+tag@sub.domain.co", True),
])
def test_email_validation(email, expected_valid):
assert validate_email(email) == expected_valid
Async Testing with pytest-asyncio
Tests async coroutines seamlessly:
import pytest
import httpx
@pytest.mark.asyncio
async def test_async_fetch():
async with httpx.AsyncClient() as client:
response = await client.get("https://httpbin.org/get")
assert response.status_code == 200
Common Patterns
Scoped Fixtures with Teardown / Cleanup
Problem: Database or mock setup code repeated across dozens of test functions creates duplication and leftover state.
Solution:
Use @pytest.fixture with the yield statement:
import pytest
@pytest.fixture(scope="function")
def db_session():
# Setup test database session
session = create_test_session()
yield session
# Cleanup and rollback after test completes
session.rollback()
session.close()
def test_user_creation(db_session):
user = db_session.create_user("alice@example.com")
assert user.id is not None
Best Practices
Do:
- Use Plain
assertStatements: Pytest provides detailed assertion introspection without requiring special assertion methods. - Use
conftest.pyfor Shared Fixtures: Place global fixtures and configuration hooks inconftest.pyfiles. - Run Parallel Tests with
pytest-xdist: Accelerate test runs across all CPU cores withpytest -n auto. - Mark Tests by Category: Use custom marks (
@pytest.mark.slow,@pytest.mark.integration) and filter runs with-m "not slow".
Don't:
- Use
assertwith parentheses: Writingassert(a == b, "msg")evaluates a non-empty tuple which is always truthy. - Mutate shared session-scoped fixtures: Keep session fixtures read-only; use function-scoped fixtures for test-isolated mutations.
- Create deep nested class hierarchies: Pytest favors simple standalone test functions over unittest-style classes.
Troubleshooting
| Error | Cause | Solution |
|---|---|---|
ModuleNotFoundError: No module named 'src' |
Current directory not in sys.path when running pytest. |
Run pytest as a module: python -m pytest, or configure pythonpath = ["src"] in pyproject.toml. |
FixtureNotFound: fixture '...' not found |
Typo in fixture argument name or fixture defined outside conftest.py. |
Move shared fixtures to root conftest.py file. |
Failed: DID NOT RAISE <class '...'> |
Code did not raise the exception expected by pytest.raises. |
Inspect inputs to verify failure condition is actually triggered. |