pytest review
pytest 9.1.1 discovers Python tests, rewrites plain `assert` statements for useful failure output, injects fixtures, expands parametrized cases, and exposes hooks to plugins. It can execute many `unittest` suites while letting new tests stay function-based. Version 9.1.1 repairs four specific 9.1 defects: misleading `RaisesGroup` match text, duplicate parametrization when an indirect parameter overrides a fixture, mypy failures for parametrized values, and skipped initial `conftest.py` files below invocation-directory test paths. The current release requires Python 3.10 or newer.
pytest 9.1.1 installed in 0.3 seconds and left 5 packages using 8 MB in our sandbox, with 0 pip-audit findings and a Python 3.10 floor. It remains the sensible default for new Python suites that need fixtures and parametrization, provided fixture graphs stay readable and plugins are treated as executable dependencies.
We installed it
| Install | ✓ · 0.3s | 5 packages on disk · 8 MB |
| Import | ✓ | import _pytest in 0.01s · pure Python · py.typed · requires Python >=3.10 |
| Known vulns | 0 | (pip-audit) |
Answers from our run
Does pytest install cleanly?
Yes. In a fresh container with an empty cache, pip install pytest finished in 0.3s, leaving 5 packages and 8 MB on disk. pip-audit reported no known vulnerabilities.
What does pytest need to run?
Python >=3.10, and nothing compiled: it is pure Python. In our run import _pytest succeeded in 0.01s, and the package ships py.typed for type checkers.
pytest or unittest2: which should you use?
unittest2: Use it only when an old Python environment needs backported standard-library unittest behavior. pytest 9.1.1 installed in 0.3 seconds and left 5 packages using 8 MB in our sandbox, with 0 pip-audit findings and a Python 3.10 floor.
When should you not use pytest?
A small standard-library project already has a dependable unittest suite and adding another runner offers little return.
Discussed on
- hnRunning C unit tests with Pytest132 points
- hnPytest 3.2.0 released124 points
- hnPytest Tips and Tricks116 points
- hnGetting started with property-based testing in Python with hypothesis and Pytest95 points
- hnSolving Algorithmic Problems in Python with Pytest (2019)77 points
Use it if
- Failure output should explain compared values and expressions without `self.assert*` method families.
- Tests share clients, temporary stores, database connections, or setup layers that map cleanly to fixture dependencies.
- One behavior needs named combinations of input, expected result, marks, or fixture parameters.
- The project needs plugins for async frameworks, coverage, process workers, web frameworks, or property-based tests.
- A small standard-library project already has a dependable unittest suite and adding another runner offers little return.
- The team expects setup order to follow file position. Pytest resolves fixtures through scope and dependency, often across distant `conftest.py` files.
- Plugins cannot be pinned and reviewed. They execute inside collection and test processes, auto-register globally, and can conflict with new pytest majors.
- Every test must start in a clean process. Default pytest shares one interpreter, imported modules, environment, and global state.
- Production or CI still supports Python 3.9. pytest 9.1.1 sets Python 3.10 as its minimum.
Setup reality
We installed pytest 9.1.1 in a clean Python 3.12 Bookworm sandbox in 0.3 seconds. The environment ended with 5 packages and 8 MB on disk. pip-audit found 0 known vulnerabilities. Our package measurement reports 14 direct dependencies, pure Python code, a py.typed marker, and a Python 3.10 minimum; its license field was unknown. import _pytest completed in 0.01 seconds, though _pytest is private and normal projects should invoke the pytest command or import the public pytest package.
There are no credentials. Configuration may live in pyproject.toml, pytest.ini, tox.ini, or setup.cfg, each with documented discovery and precedence. Set testpaths, strict marker handling, and default options deliberately. A conftest.py file registers fixtures and hooks for its directory tree without test-module imports. Version 9.1.1 fixes an invocation case where a starting conftest.py below a test* directory was missed when no path argument was supplied.
Fixture instances are cached for their declared scope. Session scope shares one resource for the run; function scope creates one per test invocation. Code after a yield is teardown, so cleanup should survive a test that already closed or partly changed the resource. Parametrization expands during collection. A large Cartesian product can make collection slow before a single test body executes; readable IDs and targeted cases make CI failures easier to isolate.
The default runner uses one process, which lets environment variables, current directories, module globals, sockets, and logging configuration leak into later tests. monkeypatch, tmp_path, yield fixtures, and finalizers restore what each test changes. Parallelism comes from plugins such as pytest-xdist and changes port allocation, database ownership, ordering, and temp-resource design. A suite that depends on collection order is fragile unless a named ordering plugin and explicit policy make that dependency visible.
Patterns
Assert a calculation directly write-basic-test
def test_total():
assert sum([2, 3, 5]) == 10Pytest rewrites this assertion during collection so a failure shows the actual expression values.
Match an expected exception message check-exception
import pytest
def test_rejects_blank_name():
with pytest.raises(ValueError, match='name is required'):
create_user(name='')`match` is a regular expression. Escape metacharacters when the intended check is literal text.
Yield and close a per-test client define-yield-fixture
import pytest
@pytest.fixture
def client():
value = ApiClient('http://127.0.0.1:9000')
yield value
value.close()Everything after `yield` is teardown. Make it tolerate a test that closed the client early.
Share one expensive resource per run share-session-fixture
@pytest.fixture(scope='session')
def database():
db = start_test_database()
yield db
db.stop()Session scope shares mutable state. Reset data through a narrower fixture so tests do not depend on execution order.
Name every parametrized case parametrize-cases
@pytest.mark.parametrize(
('text', 'expected'),
[('1', 1), ('-2', -2), ('0', 0)],
ids=['positive', 'negative', 'zero'],
)
def test_parse_int(text, expected):
assert parse_int(text) == expectedReadable IDs identify the failing generated case in CI output and in `-k` selections.
Route a parameter through a fixture use-indirect-fixture
@pytest.fixture
def account(request):
return make_account(role=request.param)
@pytest.mark.parametrize('account', ['admin', 'viewer'], indirect=True)
def test_dashboard(account):
assert can_open_dashboard(account)Version 9.1.1 fixes the duplicate-parametrization error triggered when an indirect parameter overrides a parametrized fixture.
Write a file under a test-owned path write-temp-file
def test_load_config(tmp_path):
path = tmp_path / 'app.toml'
path.write_text('debug = true', encoding='utf-8')
assert load_config(path)['debug'] is True`tmp_path` is a pathlib Path unique to that test invocation and is managed by pytest.
Change an environment variable temporarily patch-environment
def test_region_from_environment(monkeypatch):
monkeypatch.setenv('APP_REGION', 'eu-west')
assert read_region() == 'eu-west'`monkeypatch` restores the previous environment after the requesting fixture or test finishes.
Capture a warning without changing later tests capture-logs
import logging
def test_retry_warning(caplog):
with caplog.at_level(logging.WARNING):
retry_once()
assert 'retrying request' in caplog.textLogger levels and propagation still control what reaches `caplog`. Keep the level override inside the context block.
Choose a domain-specific float tolerance compare-floats
import pytest
def test_ratio():
assert calculate_ratio(1, 3) == pytest.approx(0.333333, rel=1e-5)Set relative or absolute tolerance from the calculation's error budget rather than copying an arbitrary default.
Register and select a custom marker mark-slow-test
# pyproject.toml
[tool.pytest.ini_options]
markers = ['slow: requires external test services']
# test_report.py
import pytest
@pytest.mark.slow
def test_full_report():
assert build_full_report()Registering the marker plus strict marker mode catches spelling errors; `-m 'not slow'` excludes it.
Make an unexpected pass fail CI mark-known-failure
import pytest
@pytest.mark.xfail(reason='parser does not accept leap seconds', strict=True)
def test_leap_second():
assert parse_time('23:59:60')`strict=True` turns XPASS into failure so a fixed bug cannot leave stale expected-failure coverage behind.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| unittest2 | PyPI | Use it only when an old Python environment needs backported standard-library unittest behavior. |
| nose2 | PyPI | Use it when an established unittest-style suite already depends on nose2 discovery and plugins. |
| ward | PyPI | Evaluate it for a smaller function-based runner after accepting its prerelease status. |
| tox | PyPI | Use it to orchestrate tests across environments; it can still run pytest as the test command. |
More testing guides
chai · vitest · jsdom · playwright · coverage · axe-core · the whole shelf →
How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.

