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
- Use
pytestfor unit and integration testing in Python. - Use fixtures for clean setup/teardown.
- Keep tests deterministic — control time, randomness, and I/O.
- Chase high-signal coverage, not 100% for its own sake.
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
- Keep fixtures small and composable — many focused fixtures beat one giant one.
- Mock at boundaries — network/DB calls belong to integration tests or get faked for units.
- Assert behavior and outputs, not implementation details.
- Control non-determinism — freeze time (
freezegun), seed randomness, isolate I/O. - Use
tmp_pathand other built-ins instead of touching real shared state.
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
- Testing — The hub
- Unit Testing — Principles
- Mocking & Stubbing —
unittest.mockpatterns - Python — The language
- Test Coverage — Measuring tests