Skip to main content

pydry

pydry finds duplicated Python code and ranks it by how much you would save by consolidating it. It works on the syntax tree, so renamed variables, changed literals, and reformatted code do not hide a copy.

It reports three kinds of finding:

  • Exact duplicates: whole functions that are equivalent after normalization, labeled with the tightest equivalence that holds (identical, renamed, or constants).
  • Repeated blocks: runs of consecutive statements copied into two or more places, including the middle of larger functions.
  • Near matches: pairs of functions that share most of their statements, with a diff-derived explanation of what differs (inserted statements, changed constants, a parameter hard-coded on one side) and a suggested refactor.

Findings are ranked by estimated savings in statements, weighted by a confidence score, so the first result is the one most worth acting on. Stubs, accessors, and other call-free boilerplate are skipped by default.

Installation

python -m pip install pydry-cli

For local development from a checkout:

make venv
source venv/bin/activate
make install
make check

Quick start

pydry showcase ./src
Summary: exact_groups=6 repeated_blocks=1 near_pairs=3 abstract_candidates=3

[1/3] Exact duplicates (whole functions)
  1. 3x renamed, 8 statements, saves ~16: process_csv_row, process_json_entry, clean_record
  2. 2x constants, 5 statements, saves ~5: build_user_query, build_admin_query

[2/3] Repeated blocks (inside larger functions)
  1. 11 statements x2, saves ~11: parse_records:12-22, summarize_file:27-37

[3/3] Near matches (ranked by priority)
  1. priority 7.6, 8 shared statements, saves ~8 (delegate_to_general_form): retry, retry_quickly
  2. priority 4.8, 5 shared statements, saves ~5 (delegate_to_general_form): clamp_value, clamp_to_unit

Each command also has a --format json mode for tooling.

Commands

pydry exact

Whole functions that are duplicates under normalization. Local variable names and literal values are normalized by default; docstrings, annotations, decorators, and parameter names are always ignored.

pydry exact ./src
pydry exact ./src --no-normalize-constants     # only report renamed or identical copies
pydry exact ./src --min-count 3 --format json

Each group reports its tier, its statement count, and savings, the number of statements that would disappear if all copies but one were removed.

pydry blocks

Runs of statements repeated in two or more places. This is what catches a parsing loop pasted into the middle of a larger function, which whole-function comparison cannot see.

pydry blocks ./src
pydry blocks ./src --block-min-statements 8    # only longer runs

Blocks are matched on the normalized statement form, so renamed locals and changed constants still match. Runs that cover most of both functions are left to the exact and near-match reports.

pydry near

Pairs of functions that share at least --threshold of their statements, measured as 2 * shared / (len_a + len_b) over an alignment of the two statement sequences.

pydry near ./src
pydry near ./src --threshold 0.7 --top-k 20

Results are grouped into clusters of transitively connected functions. Each pair reports:

  • shared_statements and the length of each side, so you can see the diff at a glance.
  • pattern_labels derived from the alignment: renamed_locals, literal_specialization, parameter_specialization, structural_variant, mixed_variation, different_dependencies.
  • risk_flags that discourage a merge: async_boundary_diff, return_shape_diff, exception_behavior_diff, possible_side_effects, ambient_dependency_diff.
  • suggested_refactor_kind: remove_duplicate, parameterize_constant, delegate_to_general_form, extract_common_helper, extract_shared_steps, inject_dependency, or leave_separate.
  • refactorability_score, a confidence estimate, and priority, which is shared statements multiplied by that confidence.

Functions that are exact duplicates of one another are represented by a single member, so exact groups are never repeated here.

pydry abstract

The near matches whose suggestion is not leave_separate.

pydry report

One JSON document with exact, blocks, near, and abstract sections plus a summary.

pydry report ./src --output reports/pydry-report.json

pydry check

Evaluate findings against a policy and return a CI-friendly status: 0 for pass, 1 for a policy violation, or 2 for a configuration or execution failure.

pydry check
pydry check ./src --profile strict
pydry check --baseline .pydry-baseline.json
pydry check --update-baseline

Policy lives in a pydry.toml next to your code:

root = "src"
profile = "balanced"        # strict | balanced | lenient
exclude = ["tests", "**/generated_*.py"]
baseline = ".pydry-baseline.json"

max_exact_groups = 0        # fail on any whole-function duplicate
max_block_clones = 0        # fail on any repeated block
max_abstract_candidates = "none"   # report near matches, do not enforce

With a baseline, check only counts findings that are not already recorded, so an existing codebase can adopt pydry without first paying down every duplicate. Run pydry check --update-baseline to accept the current state, commit the file, and the check will fail only on new duplication. Baselines are keyed on the normalized content of the duplicated code and record how many copies were accepted. An accepted finding resurfaces when another copy appears or when a copy changes structurally; renaming locals or changing literal values in an accepted copy keeps it accepted, because those differences are normalized away.

Profiles set defaults for the sensitivity knobs; explicit keys override them:

Profile Threshold Min statements Block size Enforced by default
strict 0.8 2 5 exact, blocks, abstract
balanced 0.8 2 6 exact, blocks
lenient 0.85 4 8 exact

Not every project weights DRYness the same way. Test suites in particular are repetitive by design, which is what exclude and lenient are for.

Shared options

Every analysis command accepts:

  • --min-statements N: ignore functions with fewer statements in every analysis, including block detection (default 2).
  • --no-ignore-trivial: also analyze stubs, accessors, and call-free boilerplate.
  • --exclude GLOB: skip paths matching a glob relative to the root; repeatable.
  • --top-level-only: ignore nested functions and methods.
  • --strict: fail on files that cannot be read or parsed.

GitHub Actions

The repository ships as a composite action that runs pydry check, writes the JSON report, and adds findings as annotations:

name: pydry
on:
  pull_request:
  merge_group:
permissions:
  contents: read
jobs:
  pydry:
    name: pydry
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - uses: hesreallyhim/pydry@v0

Action inputs mirror the pydry.toml keys and override them individually. See the integration guide.

How it works

Every function is flattened into a sequence of statement tokens. Each token is the statement's syntax tree with local names replaced by positional placeholders and constants replaced by typed placeholders, so total += item.price and acc += row.price produce the same token while acc += row.cost does not. Attribute names, module-level names, and imports are kept, because they are what the code depends on.

  • Exact groups hash the whole normalized function at three tiers and report the tightest one that holds.
  • Near matches align two token sequences with a longest common subsequence. The aligned statements are compared again at the stricter tiers to classify what differs, and a looser tier that also equates parameters with literals catches a function that hard-codes an argument of its sibling.
  • Repeated blocks hash every window of consecutive tokens, extend matching windows to maximal runs, and group runs by content.

The scores are heuristics: a high similarity means two functions look alike structurally, not that they are semantically interchangeable. The risk flags and the leave_separate suggestion exist because structurally similar code is sometimes best left apart.

Noise on real code

Defaults are tuned against the standard library, which is well maintained and where most whole-function similarity is idiomatic rather than a copy. The benchmark script and its current numbers are in docs/benchmarks.md; run make benchmark to reproduce.

Project Structure

.
├── .github/ (7 files, 2 folders)
├── benchmarks/
│   └── stdlib_noise.py
├── demo/
│   ├── async_handlers.py
│   ├── data_utils.py
│   ├── formatters.py
│   ├── pipelines.py
│   └── validators.py
├── docs/
│   ├── README.action.md # GitHub Action README
│   ├── benchmarks.md
│   └── releasing.md
├── src/
│   └── pydry/
│       ├── __init__.py
│       ├── __main__.py
│       ├── _version.py
│       ├── analyze.py
│       ├── baseline.py
│       ├── blocks.py
│       ├── builtin_plugins.py
│       ├── canonical.py
│       ├── check.py
│       ├── cli.py
│       ├── config.py
│       ├── engine.py
│       ├── github.py
│       ├── models.py
│       ├── normalize.py
│       ├── plugins.py
│       ├── render.py
│       └── scan.py
├── tests/ (11 files)
├── README.md
├── action.yml # GitHub Action metadata
└── pydry.toml

Python API

The core functions are importable:

from pathlib import Path

from pydry.engine import block_clones, exact_groups, near_matches, scan_functions

profiles = scan_functions(Path("src"))
groups = exact_groups(Path("src"), profiles=profiles)
pairs = near_matches(Path("src"), threshold=0.8, profiles=profiles)
blocks = block_clones(Path("src"), profiles=profiles)

Passing profiles lets several analyses share one scan.

License

MIT. See LICENSE.

Metadata

Release files for pydry-cli 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for pydry-cli 0.1.0
File Size Uploaded
pydry_cli-0.1.0.tar.gz 80.1 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for pydry-cli 0.1.0
File Interpreter ABI Platform
pydry_cli-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 129.3 kB

Release files / pydry_cli-0.1.0.tar.gz

Download URL pydry_cli-0.1.0.tar.gz
Size 80.1 kB
Tags Source
SHA-256 checksum
How to use checksums
1c6b235c6c16bb20626ea5c7462d24345e788bd9b397a157686621a065aa76f8
BLAKE2b-256 checksum
How to use checksums
58868971e2d3a43d661e1adc863819b2c66627a4fc1da70aedb58eb46a7b1748
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.13

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 23, 2026.

Transparency log

Release files / pydry_cli-0.1.0-py3-none-any.whl

Download URL pydry_cli-0.1.0-py3-none-any.whl
Size 49.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
3765870c0539497edc8c9936318d4cee280dd7f15d20713b1ed7f3c144f0191e
BLAKE2b-256 checksum
How to use checksums
c2075556dc9afdf5cbaecc921938513dd840750cc9c961d6e541eef2f46272f8
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.13

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 23, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files

0.0.3

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page