Skip to content

pytest — Modern Python Testing

pytest is the de-facto standard for testing Python code. Unlike the built-in unittest module, pytest requires no class inheritance, no special assertion methods, and no boilerplate — you write plain functions and plain assert statements. pytest’s test discovery, rich failure output, fixture system, and plugin ecosystem make it the first tool to reach for in any Python project.

pytest discovers test files whose names start with test_ or end with _test.py, and within them, functions whose names start with test_. No class needed — a module-level function is enough.

math_utils.py
def add(a: int, b: int) -> int:
return a + b
def divide(a: float, b: float) -> float:
if b == 0:
raise ValueError("Cannot divide by zero")
return a / b
test_math_utils.py
from math_utils import add, divide
import pytest
def test_add_positive():
assert add(2, 3) == 5
def test_add_negative():
assert add(-1, -2) == -3
def test_divide_normal():
assert divide(10, 2) == 5.0
def test_divide_by_zero():
with pytest.raises(ValueError, match="Cannot divide by zero"):
divide(1, 0)
Terminal window
# run all tests
python -m pytest
# verbose output
python -m pytest -v
# run only tests matching a keyword
python -m pytest -k "divide"

Fixtures are reusable setup (and teardown) functions. pytest injects them into test functions by parameter name — no base class, no setUp method required. Use yield to separate setup from teardown.

import pytest
@pytest.fixture
def sample_data():
"""Provide a list of numbers for testing."""
return [1, 2, 3, 4, 5]
def test_sum(sample_data):
assert sum(sample_data) == 15
def test_length(sample_data):
assert len(sample_data) == 5
@pytest.fixture
def db_connection():
"""Setup and teardown a fake DB connection."""
conn = {"connected": True} # imagine a real DB here
yield conn
conn["connected"] = False # teardown runs after test
def test_db(db_connection):
assert db_connection["connected"] is True

@pytest.mark.parametrize runs the same test function with multiple sets of inputs, keeping your test suite DRY and making it easy to add edge cases.

import pytest
from math_utils import add
@pytest.mark.parametrize("a, b, expected", [
(1, 2, 3),
(0, 0, 0),
(-1, 1, 0),
(100, -50, 50),
])
def test_add_parametrized(a, b, expected):
assert add(a, b) == expected

Placing fixtures in a conftest.py file makes them available to all test files in the same directory and its subdirectories, without any import.

# conftest.py (place in project root or test dir)
import pytest
@pytest.fixture(scope="session")
def app_config():
return {"debug": True, "db_url": "sqlite:///:memory:"}
What naming convention must test files and test functions follow for pytest to discover them?
What does `@pytest.fixture` do?
What does `@pytest.mark.parametrize` achieve?