Testing with pytest
Write expressive tests using plain asserts, fixtures, and parametrization
Overview
pytest is the de facto Python testing framework, favored for its plain assert statements, rich failure introspection, and powerful fixture system. Test files named test_*.py and functions named test_* are collected automatically. Fixtures inject reusable setup, and parametrization runs one test across many inputs.
Syntax / Usage
Write a function starting with test_ and assert expected behavior. Run pytest from the project root.
# test_math.py
def add(a: int, b: int) -> int:
return a + b
def test_add_positive():
assert add(2, 3) == 5
def test_add_negative():
assert add(-1, -1) == -2
# Assert that an exception is raised
import pytest
def test_divide_by_zero():
with pytest.raises(ZeroDivisionError):
_ = 1 / 0
pytest rewrites assert so a failure shows the actual and expected values without extra helpers.
Examples
Parametrize a test to cover many cases without duplication:
import pytest
def is_palindrome(s: str) -> bool:
return s == s[::-1]
@pytest.mark.parametrize("value,expected", [
("racecar", True),
("hello", False),
("", True),
])
def test_is_palindrome(value, expected):
assert is_palindrome(value) == expected
Share setup with a fixture; pytest passes it in by name:
import pytest
@pytest.fixture
def sample_users():
return [{"name": "ada"}, {"name": "linus"}]
def test_user_count(sample_users):
assert len(sample_users) == 2
def test_first_user(sample_users):
assert sample_users[0]["name"] == "ada"
Common Mistakes
- Naming files or functions without the
test_prefix, so pytest never collects them - Asserting
pytest.raiseswithout awithblock, which never actually checks anything - Sharing mutable state between tests instead of isolating with fixtures
- Overusing broad fixtures with
scope="session"that leak state across tests - Catching exceptions inside the test and asserting nothing, hiding real failures
See Also
python-type-hints python-exceptions python-functions