Testing with pytest: first tests, fixtures and parametrize
pytest runs plain functions whose names start with test_ and reports every failing assert in detail. This guide covers setup, assertions, exceptions, parametrize and fixtures.
A test is a small function that calls your code and checks the result. pytest is the most widely used test runner for Python: tests are ordinary functions, checks are ordinary assert statements, and when something fails it shows you exactly which values differed.
Install and lay out the project
python -m pip install pytest # inside your virtual environment
myproject/
pyproject.toml
textutils.py
tests/
test_textutils.py
pytest collects files named test_*.py or *_test.py, and inside them functions whose names start with test. Here is the code under test:
def slugify(title):
words = title.lower().split()
return "-".join("".join(ch for ch in word if ch.isalnum()) for word in words)
def parse_price(text):
if not text.startswith("$"):
raise ValueError(f"not a price: {text!r}")
return float(text[1:])
Your first tests
import pytest
from textutils import parse_price, slugify
def test_slugify_simple():
assert slugify("Hello World") == "hello-world"
def test_parse_price():
assert parse_price("$4.50") == 4.5
def test_parse_price_rejects_garbage():
with pytest.raises(ValueError, match="not a price"):
parse_price("4.50")
pytest.raises passes only if the block raises that exception; match is a regular expression searched in the error message.
Running tests
python -m pytest # run everything
python -m pytest -q # quieter output
python -m pytest -k slugify # only tests whose name matches
python -m pytest -x # stop at the first failure
python -m pytest tests/test_textutils.py::test_parse_price
python -m pytest adds the current folder to the import path, so from textutils import ... works. The plain pytest command does not, and fails with ModuleNotFoundError in this layout. To make both work, add this to pyproject.toml:
[tool.pytest.ini_options]
pythonpath = ["."]
Reading a failure
If slugify returned hello_world, pytest would print the failing line plus a comparison along the lines of AssertionError: assert 'hello_world' == 'hello-world', with a marker under the differing character. You do not need special assertEqual helpers; pytest rewrites plain assert to show the values.
Many cases, one test: parametrize
@pytest.mark.parametrize(
("title", "expected"),
[
("Hello World", "hello-world"),
(" Extra spaces ", "extra-spaces"),
("Python 3.12 is out!", "python-312-is-out"),
],
)
def test_slugify_cases(title, expected):
assert slugify(title) == expected
Each tuple becomes its own test, reported separately, so one bad case does not hide the others.
Fixtures: shared setup
A fixture is a function that prepares something a test needs. A test asks for it by naming it as a parameter. pytest also ships built-in fixtures; tmp_path gives each test a fresh temporary folder as a pathlib.Path, so tests never touch your real files.
@pytest.fixture
def notes_file(tmp_path):
path = tmp_path / "notes.txt"
path.write_text("first\nsecond\n")
return path
def test_count_lines(notes_file):
assert len(notes_file.read_text().splitlines()) == 2
Put fixtures used by several test files in tests/conftest.py; pytest discovers them without imports. If a fixture must clean up afterwards, use yield instead of return and put the cleanup code after the yield.
Good habits
- Test behavior, not implementation: call public functions and check results.
- Keep each test small and named after what it proves.
- Run the suite before every commit, and in CI on every pull request.
- Other useful built-ins:
monkeypatch(temporarily replace attributes or environment variables) andcapsys(capture printed output).
See also
- 0261 Build a Python command-line to-do app: from empty folder to installable command Create a small to-do tool in Python with argparse and JSON storage, test it with pytest and package it with pyproject.toml so… Beginner · Tutorial · Any system Article Environments & packages · Files & automation · Testing 15 Oct 2026
Repos in the wild
- pytest pytest-dev/pytest pytest is the de facto standard test runner for Python: plain functions with assert, detailed failure reports, fixtures, parametrization and a large plugin ecosystem. GitHub
- Ruff: Python linter and formatter astral-sh/ruff Ruff is a fast Python linter and code formatter written in Rust. It catches bugs such as undefined names and unused imports before you run your code. GitHub
History
Every saved edit is kept. The plot on the sleeve draws one line per edit. Open any edit to see exactly what changed.
- commitpedia-admin +3,939 chars