pytest replaced Python’s built-in unittest as the de facto standard for a reason: plain assert statements instead of a wall of self.assertEqual() calls, powerful fixtures, and genuinely readable failure output.
A Test Is Just a Function
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():
import pytest
with pytest.raises(ZeroDivisionError):
divide(10, 0)
pip install pytest pytest # runs every test_*.py file it finds pytest -v # verbose output, one line per test pytest test_calculator.py::test_add # run a single specific test
Fixtures: Reusable Setup
import pytest
@pytest.fixture
def sample_cart():
cart = ShoppingCart()
cart.add_item("Widget", 9.99, 2)
return cart
def test_cart_total(sample_cart):
assert sample_cart.total() == 19.98
def test_cart_item_count(sample_cart):
assert len(sample_cart.items) == 1
Each test that requests sample_cart as a parameter gets a fresh instance — no shared state leaking between tests, and no repeated setup code copy-pasted across every function.
Parametrizing to Cover Multiple Cases
import pytest
@pytest.mark.parametrize("a, b, expected", [
(2, 3, 5),
(-1, 1, 0),
(0, 0, 0),
])
def test_add_various(a, b, expected):
assert add(a, b) == expected
Measuring Coverage
pip install pytest-cov pytest --cov=calculator --cov-report=term-missing
Coverage percentage is a useful signal, not a goal to game — 100% coverage with weak assertions catches far fewer bugs than 70% coverage of the code paths that actually matter.