mrkeyoor.com_
Sat 19 Sept 15:48 UTC
PyPITestingupdated 19 Sept 2026

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.

Verdict

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

Lab card: what happened when we installed pytestScreenshot of pytest documentation
Install✓ · 0.3s5 packages on disk · 8 MB
Importimport _pytest in 0.01s · pure Python · py.typed · requires Python >=3.10
Known vulns0(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.

API stability4/5Test functions, assert rewriting, fixture injection, `conftest.py`, marks, parametrization, and the main command options remain recognizable across pytest majors. Major releases still remove deprecated hooks and raise Python floors. Patch 9.1.1 had to correct indirect fixture parametrization and initial config discovery regressions, so suites using collection edge cases should test runner upgrades before merging them.
Docs5/5The stable manual separates tutorials, task guides, explanations, and references for fixtures, parametrization, assertion rewriting, capture, logs, temporary paths, monkeypatch, configuration, hooks, plugins, and unittest integration. Fixture pages show teardown and scope behavior rather than only a passing example. Readers still need to confirm the version selector because old hook signatures remain searchable.
Maintenance5/5GitHub shows an unarchived repository pushed on 2026-08-26 with 14,445 stars and 807 open issues and pull requests. Release 9.1.1 shipped on 2026-06-19 with four focused fixes for exception-group messages, indirect parametrization, typing, and initial `conftest.py` discovery. The project maintains a changelog, security reporting route, and public issue tracker.
Ecosystem5/5PyPI Stats recorded 237,782,854 downloads in the latest week. The README points to more than 1,300 external plugins, and pytest can run many unittest suites while framework packages contribute their own fixtures and marks. That reach makes integrations easy to find, but automatic plugin loading also means an installed package can alter collection or command behavior without an import in the test file.

Discussed on

  1. hnRunning C unit tests with Pytest132 points
  2. hnPytest 3.2.0 released124 points
  3. hnPytest Tips and Tricks116 points
  4. hnGetting started with property-based testing in Python with hypothesis and Pytest95 points
  5. 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.
Skip it if

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]) == 10

Pytest 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) == expected

Readable 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.text

Logger 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

PackageRegistryPick it when
unittest2PyPIUse it only when an old Python environment needs backported standard-library unittest behavior.
nose2PyPIUse it when an established unittest-style suite already depends on nose2 discovery and plugins.
wardPyPIEvaluate it for a smaller function-based runner after accepting its prerelease status.
toxPyPIUse 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.