Skip to main content

A static, AST-based test-smell linter for pytest suites.

Project description

pytest-tidy

CI PyPI Python versions License: MIT

A static, AST-based test-smell linter for pytest suites. Cross-platform, zero runtime execution — it reads your tests, it never runs them.

pytest-tidy walks the abstract syntax tree of your test files and flags the semantic smells that let test suites rot: tests that assert nothing, assert True, except: pass swallowing failures, pytest.raises(Exception) catching everything, time.sleep(5) making CI slow and flaky, fixtures with the wrong scope, and more. flake8-pytest-style catches formatting; pytest-tidy catches meaning.

It ships three ways: a standalone CLI, a pre-commit hook, and a pytest plugin that warns during collection.

$ pytest-tidy tests/
tests/test_orders.py:14:12: PTD002 assertion on a 2-tuple is always true; did you mean `assert <cond>, <message>`? [*]
tests/test_orders.py:28:5: PTD101 `except Exception:` with no handling body swallows failures
tests/test_orders.py:41:5: PTD003 test 'test_refund' contains no assertions
tests/test_billing.py:9:23: PTD203 scope="function" is the default and can be removed [*]

Found 4 problems  2 fixable with --fix

Why

A test that silently passes is worse than no test: it gives false confidence and hides regressions forever. These bugs are invisible in review because the code looks like it tests something. pytest-tidy finds them statically:

# All of these pass no matter what your code does:
assert (order.total == 99, "wrong total")   # PTD002 - a 2-tuple is always truthy
mock.assert_called_once                       # PTD501 - never called; missing ()
try:
    charge(order)
except Exception:
    pass                                      # PTD101 - swallows AssertionError too

Highlights

  • 32 rules across 7 categories (assertions, exceptions, fixtures, timing, structure, mocking, markers) - see the full catalog.
  • Zero required dependencies. The core is pure-stdlib ast. Works on Python 3.9+, every OS.
  • Autofix. --fix rewrites the unambiguous ones (byte-precise range edits that preserve your formatting and comments); --diff previews them.
  • Near-zero false positives by design. Noisier rules are opt-in, not on by default.
  • Three delivery modes: CLI, pre-commit, and a pytest plugin.
  • CI-friendly output: --format github emits GitHub Actions annotations; --format json is machine-readable.
  • Configurable via pyproject.toml or setup.cfg, with # noqa support.

Install

pip install pytest-tidy

On Python 3.11+ TOML config works out of the box. On 3.9 / 3.10, add the toml extra if you want to configure via pyproject.toml (or just use setup.cfg, which needs nothing extra):

pip install "pytest-tidy[toml]"

Usage

pytest-tidy                     # lint test files under the current directory
pytest-tidy tests/ pkg/tests    # lint specific paths
pytest-tidy tests/ --fix        # apply autofixes in place
pytest-tidy tests/ --diff       # show what --fix would change, write nothing
pytest-tidy tests/ --statistics # per-rule counts

When given a directory, pytest-tidy lints only files matching your test-file patterns (test_*.py, *_test.py, conftest.py). When given an explicit file, it always lints it.

Useful flags

Flag Purpose
--select CODES Enable only these rules/prefixes (replaces defaults). --select ALL turns on everything.
--extend-select CODES Enable extra rules on top of the defaults (handy for opt-in rules).
--ignore CODES Disable rules/prefixes.
--fix / --diff Apply / preview autofixes.
--format {text,json,github} Output format.
--show-source Print the offending line with a caret.
--statistics Per-rule problem counts.
--list-rules Print the whole catalog (respects your config).
--explain PTD002 Show a single rule's rationale.
--no-config Ignore any discovered config file.

Codes are matched by prefix, so --select PTD2 selects the whole fixtures category and --ignore PTD3 silences all timing rules.

Exit codes: 0 clean, 1 problems found, 2 usage/internal error.

pre-commit

Add to .pre-commit-config.yaml:

repos:
  - repo: https://github.com/arizzubair/pytest-tidy
    rev: v0.1.0
    hooks:
      - id: pytest-tidy
      # ...or auto-fix on commit instead:
      # - id: pytest-tidy-fix

Both hooks only run on test_*.py, *_test.py, and conftest.py files.

pytest plugin

The plugin is opt-in so it never surprises an existing suite. Enable it per run:

pytest --tidy

...or persistently in pyproject.toml:

[tool.pytest.ini_options]
tidy = true
# optional, mirror the CLI selection flags:
tidy_extend_select = ["PTD404"]
tidy_ignore = ["PTD003"]

Each finding surfaces as a PytestTidyWarning in the warnings summary during collection - visible, but it never fails the run.

Configuration

pytest-tidy reads [tool.pytest-tidy] from the nearest pyproject.toml, or [pytest-tidy] from setup.cfg / tox.ini (closest file wins):

[tool.pytest-tidy]
# Turn opt-in rules on alongside the defaults:
extend-select = ["PTD008", "PTD302"]

# Silence a rule or a whole category:
ignore = ["PTD404"]

# Or lock to an explicit set (replaces the defaults):
# select = ["PTD001", "PTD002", "PTD1"]

# Don't lint these paths:
exclude = ["tests/data", "tests/legacy"]

# Extra helper calls that count as "an assertion" for PTD003:
assert-calls = ["assert_matches", "verify"]

# Treat functions with this prefix as tests (default: "test"):
test-function-prefix = "test"

# Which files count as test files when a directory is scanned:
test-file-patterns = ["test_*.py", "*_test.py", "conftest.py"]

The equivalent setup.cfg (no extra dependency needed):

[pytest-tidy]
extend-select = PTD008, PTD302
ignore = PTD404

CLI flags always override file config.

Inline suppression

Standard # noqa comments are honored:

assert True            # noqa            - suppress every rule on this line
assert value == None   # noqa: PTD004    - suppress just PTD004 (comma-separate more)

Rules

32 rules, grouped by category. Full rationale, examples, and autofix notes live in docs/rules.md; run pytest-tidy --list-rules to see the set enabled by your config.

Category Codes Examples
Assertions PTD001-PTD009 assert-on-constant, assert-on-tuple, test-without-assertion, assert-eq-none
Exceptions PTD101-PTD104 except-pass, raises-broad-exception, raises-multiple-statements
Fixtures PTD201-PTD206 fixture-scope-mismatch, redundant-fixture-scope, yield-fixture-deprecated
Timing & determinism PTD301-PTD304 sleep-in-test, datetime-now, random-without-seed, network-call
Structure & collection PTD401-PTD404 test-class-init, duplicate-test-name, return-in-test
Mocking PTD501-PTD502 mock-uncalled-assert, mock-nonexistent-attr
Markers & parametrize PTD601-PTD603 skip-without-reason, unconditional-skip, parametrize-values-mismatch

Opt-in (disabled by default) rules: PTD008, PTD009, PTD103, PTD206, PTD302, PTD303, PTD304, PTD404, PTD602. Enable them with --extend-select or config.

Philosophy

  • Read, never run. Everything is a pure ast walk. No imports of your code, no side effects, fully deterministic and cross-platform.
  • Near-zero false positives. A linter you have to argue with gets turned off. Rules that can't be sure stay opt-in, and default rules bias hard toward "only flag it if it's almost certainly a bug."
  • Fix what's safe. Autofixes only fire on unambiguous rewrites, and re-lint to convergence so chained fixes settle.

Contributing / development

python -m venv .venv
.venv/Scripts/pip install -e ".[dev]"     # POSIX: .venv/bin/pip
python -m pytest                          # run the suite
python -m pytest_tidy tests               # dogfood the linter on itself
python scripts/gen_rules_doc.py           # regenerate docs/rules.md

Each rule is a small class in src/pytest_tidy/rules/ declaring the AST node types it cares about and a check() method; the engine walks each file once and dispatches nodes to interested rules.

Releasing

Continuous integration runs the suite across Python 3.9-3.13 on Linux, macOS, and Windows (.github/workflows/ci.yml). Publishing is automated via .github/workflows/publish.yml using PyPI Trusted Publishing (OIDC), so no API tokens are stored in the repo. To cut a release:

  1. Bump version in pyproject.toml and update CHANGELOG.md.
  2. Tag and push: git tag v0.1.0 && git push --tags.
  3. Publish a GitHub Release for the tag - the workflow builds the sdist/wheel and uploads them to PyPI.

Run the publish workflow manually (workflow_dispatch) to push to TestPyPI first as a dry run.

License

MIT - see LICENSE.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pytest_tidy-0.1.0.tar.gz (51.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pytest_tidy-0.1.0-py3-none-any.whl (46.7 kB view details)

Uploaded Python 3

File details

Details for the file pytest_tidy-0.1.0.tar.gz.

File metadata

  • Download URL: pytest_tidy-0.1.0.tar.gz
  • Upload date:
  • Size: 51.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for pytest_tidy-0.1.0.tar.gz
Algorithm Hash digest
SHA256 d8dd30bb9e035cd4df91c00eb1f2a1a0e3504074133121fb48452970f9dc5d8c
MD5 1e3b4fc5d505265a53631ba3a5a4f212
BLAKE2b-256 99c2300b145681c237ad7bf601afe6b0ef206a56089de7ec8f6f995001860064

See more details on using hashes here.

Provenance

The following attestation bundles were made for pytest_tidy-0.1.0.tar.gz:

Publisher: publish.yml on az-pz/pytest-tidy

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file pytest_tidy-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: pytest_tidy-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 46.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for pytest_tidy-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 2b56b5147c094e4cdc8101f7ee5e412e673b8da203ecbec9314f1bf94e0e69a2
MD5 6eb313a21a701e25e51286eaf910b758
BLAKE2b-256 7d6cdf5f66d11ab9d53ce6d8173ce326b97d9ce11baa6b44c1f976e2ccddb323

See more details on using hashes here.

Provenance

The following attestation bundles were made for pytest_tidy-0.1.0-py3-none-any.whl:

Publisher: publish.yml on az-pz/pytest-tidy

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page