Pytest

Pytest is the most widely used testing framework in the Python ecosystem. It's popular because it's trivial to start (plain assert statements, no boilerplate classes), scales smoothly to large suites, and has a uniquely powerful fixture system for building clean, reusable, composable test setup. A rich plugin ecosystem (coverage, async, mocking, parallelism) extends it further.

Use it for both unit and integration testing. The principles from Unit Testing apply directly; pytest just makes them ergonomic in Python.

TL;DR

Quick Example

No classes, no self.assertEqual — just assert:

Pytest auto-discovers files named test_*.py and functions/classes named test_*.

Fixtures: The Superpower

Fixtures define reusable setup, injected by parameter name:

Use yield for teardown, and compose with built-in fixtures like tmp_path:

Fixtures can depend on other fixtures, and scope= (function/module/session) controls how often they run.

Markers & Selecting Tests

Register custom markers in pyproject.toml/pytest.ini to avoid warnings.

Parameterization

One test, many cases — each runs (and reports) independently:

Coverage

Focus on covering critical logic paths, not on hitting 100% for its own sake. See Test Coverage.

Best Practices

Common Mistakes

Overgrown fixtures

Hidden network calls in "unit" tests

FAQ

Why pytest instead of unittest?

unittest (in the standard library) requires test classes and verbose self.assertEqual methods. Pytest lets you write plain functions with bare assert statements (with rich failure introspection), and adds fixtures, parameterization, markers, and a large plugin ecosystem. Pytest can also run existing unittest tests, so adoption is incremental. Most Python projects choose pytest for the ergonomics.

How do fixtures differ from setup/teardown methods?

Classic setup/teardown runs the same code around every test in a class. Fixtures are modular, named, and composable — a test requests exactly the fixtures it needs by parameter, fixtures can depend on other fixtures, and scope controls reuse (per function, module, or session). This eliminates duplication and makes dependencies explicit rather than implicit in a base class.

How do I make tests that use time or randomness deterministic?

Remove the non-determinism: freeze the clock with freezegun's @freeze_time, seed or inject the random source, and isolate filesystem work with the built-in tmp_path fixture. Anything touching the network should be mocked for unit tests (or treated as an integration test). Deterministic tests are the cure for flakiness.

What's a good coverage target?

There's no universal number — chase signal, not a percentage. Cover critical logic, branching, and error paths thoroughly; don't contort tests to hit trivial getters just to reach 100%. Many teams set a threshold (e.g. 80%) as a floor in CI while reviewing what's covered. Coverage shows what code ran during tests, not whether the assertions were meaningful.

Related Topics

References