Skip to content
Commitpedia
Log in Join & edit

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.

Not checked by a trusted editor yet Last edited Outdated?

Edit history of Testing with pytest: first tests, fixtures and parametrize: 1 recorded edit
Edit history · 1 edit
Type
Article
Edits
1
Editors
1
First issued
15 Oct 2026
Last edit
15 Oct 2026
Reading
3 min

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) and capsys (capture printed output).

Did this work for you?

No votes yet. Be the first.

If it did not work, the report form below tells the editors what went wrong.

See also

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.

  1. Commitpedia +3,939 chars

Report a problem

Something wrong, unsafe or copied? Tell the editors.
What is wrong?