Testing with pytest
Python ships with a built-in testing framework, unittest, modeled after Java's JUnit. It works, but it is verbose: tests live in classes that inherit from unittest.TestCase, assertions go through special methods like assertEqual and assertTrue, and setup/teardown is wired through setUp/tearDown overrides. pytest is a third-party framework that has become the de facto standard for testing Python code because it removes almost all of that ceremony while producing much more useful failure output.
Why pytest won
Plain
assertstatements — no need to memorizeassertEqual,assertIn,assertIsInstance, and dozens of other methods. Writeassert x == yand pytest figures out what failed.No boilerplate classes — a test is just a function named
test_something. You don't need to inherit from anything.Rich failure introspection — when an assertion fails, pytest rewrites the assert expression to show you the actual values on both sides of the comparison, not just "AssertionError".
Fixtures — a dependency-injection style mechanism for setup/teardown that is far more flexible and reusable than
setUp/tearDown.A huge plugin ecosystem — coverage reporting (
pytest-cov), mocking helpers (pytest-mock), parallel test runs (pytest-xdist), Django/Flask integrations, and hundreds more.pytest can also run existing
unittest-style test suites unchanged, which made migration painless — teams could adopt it without rewriting anything.
Installing pytest
pip install pytest
A plain test function
pytest discovers tests automatically: any file named test_*.py or *_test.py, and inside it any function named test_*, is treated as a test. There is no class, no base class, and no special import beyond what you're actually testing.
# calculator.py
def add(a, b):
return a + b
def divide(a, b):
if b == 0:
raise ValueError("cannot divide by zero")
return a / b# test_calculator.py
import pytest
from calculator import add, divide
def test_add():
assert add(2, 3) == 5
def test_divide():
assert divide(10, 2) == 5
def test_divide_by_zero_raises():
with pytest.raises(ValueError):
divide(10, 0)Notice pytest.raises is used as a context manager to assert that a specific exception is thrown — no assertRaises method call needed.
Fixtures: dependency-injection style setup
A fixture is a function decorated with @pytest.fixture that prepares something a test needs — a database connection, a temp file, a sample object — and optionally cleans it up afterward. Any test function that declares a parameter with the same name as the fixture automatically receives its return value. pytest resolves this wiring for you; there is no manual setUp/tearDown lifecycle to manage.
import pytest
@pytest.fixture
def sample_cart():
cart = {"apples": 3, "bananas": 2}
yield cart # code after yield runs as teardown
cart.clear()
def test_cart_has_apples(sample_cart):
assert sample_cart["apples"] == 3
def test_cart_total_items(sample_cart):
assert sum(sample_cart.values()) == 5Parametrized tests
@pytest.mark.parametrize runs the same test function multiple times with different inputs, so you avoid copy-pasting near-identical test cases.
import pytest
from calculator import add
@pytest.mark.parametrize(
"a, b, expected",
[
(2, 3, 5),
(-1, 1, 0),
(0, 0, 0),
(100, 200, 300),
],
)
def test_add_parametrized(a, b, expected):
assert add(a, b) == expectedpytest reports each parameter combination as its own test result, so a failure tells you exactly which inputs broke.
Running your tests
pytest # discover and run every test in the project pytest test_calculator.py # run one file pytest -k "divide" # run tests whose name matches "divide" pytest -v # verbose output, one line per test pytest --maxfail=1 # stop after the first failure
unittest vs pytest
Aspect | unittest | pytest |
|---|---|---|
Assertion style |
| plain |
Boilerplate | Test classes must subclass | Plain functions, no class required |
Setup/teardown |
| Reusable, composable fixtures via |
Failure output | Reports which assert method failed | Shows the actual values in the failing expression |
Plugin ecosystem | Standard library, limited extensibility | Hundreds of plugins (coverage, mocking, parallelism, and more) |
Adoption | Built-in, used in legacy and stdlib-only projects | De facto standard for new Python projects |