This release is a pre-release and may not be stable for production use.
Arid
Fast Python duplicate-code checker written in Rust. A focused replacement for Pylint R0801 that complements Ruff.
What is Arid? · Project status · Goals · Usage · Output · Configuration · Pre-commit · Architecture · License
Project status
[!IMPORTANT] Arid is currently a release candidate. The release feature set and core interfaces are frozen, and the current build is believed ready for stable release without product-code changes.
Arid is a small, focused CLI for one job:
Detect duplicated Python source code quickly and accurately.
What is Arid?
Arid is a Python-specific CLI for duplicate-code detection.
It is designed to replace the duplicate-code functionality of Pylint R0801 / symilar without turning into another general-purpose linter. Arid is intentionally narrow in scope and is meant to run alongside Ruff, not compete with it.
Ruff
├── linting
├── formatting
├── imports
├── modernization
└── general code quality
Arid
└── duplicate-code detection
Why Arid? Because duplicated code isn't DRY.
Why not just use Pylint?
Pylint's R0801 checker provides useful Python-aware duplicate-code detection, but duplicate analysis can become very slow on larger codebases.
Arid aims to preserve the useful behavior of R0801 while using a Rust-native architecture designed specifically for duplicate detection.
The goal is not bug-for-bug compatibility. Where Pylint relies on textual heuristics, Arid prefers correct Python syntax interpretation.
Why not just use jscpd?
jscpd is a capable multi-language copy/paste detector, and its current implementation is also written in Rust.
Arid occupies a narrower niche:
- Python only
- focused on Pylint-style duplicate-code semantics
- Python-aware filtering for comments, docstrings, imports, and signatures
- designed to fit naturally into modern Python workflows
- intentionally minimal in scope
Arid is not intended to replace jscpd for multi-language repositories.
Goals
Arid v1 is designed to:
- detect duplicated Python source blocks across files
- detect duplicated blocks within the same file
- ignore comments, docstrings, imports, and function signatures when configured
- preserve accurate original source locations
- report concise
DUP001diagnostics - describe duplicate findings using Python structural context
- provide deterministic duplication metrics
- support
pyproject.tomlconfiguration via[tool.arid] - provide deterministic text, JSON, Markdown, and SARIF output
- support baseline-based incremental adoption
- integrate with pre-commit while preserving whole-project detection
- run substantially faster than Pylint's duplicate-code checker
- require no Python runtime to analyze Python source
Non-goals
Arid is intentionally not a general-purpose linter.
It does not aim to provide:
- formatting
- import sorting
- type checking
- dead-code detection
- complexity analysis
- security scanning
- semantic clone detection
- structural clone matching
- fuzzy AST similarity
- multi-language duplicate detection
If a feature belongs naturally in Ruff, it does not belong in Arid.
Usage
Arid is intended to fit naturally into a Python quality workflow:
ruff check .
arid .
Scan specific files or directories:
arid src tests
Require a larger duplicate before reporting it:
arid . --min-lines 8
Override normalization behavior for a single scan:
arid . --no-ignore-docstrings
Configurable boolean options support both positive and negative forms:
--ignore-comments --no-ignore-comments
--ignore-docstrings --no-ignore-docstrings
--ignore-imports --no-ignore-imports
--ignore-signatures --no-ignore-signatures
--same-file --no-same-file
--hidden --no-hidden
This allows command-line arguments to explicitly override either value from pyproject.toml.
Hidden files and directories are skipped by default during directory discovery. Include them when needed with:
arid . --hidden
This allows Arid to scan Python files under hidden directories such as .github/ while still honoring .gitignore and configured exclude patterns.
Exclude matching paths:
arid . --exclude 'generated/**'
--exclude may be repeated:
arid . \
--exclude 'generated/**' \
--exclude 'vendor/**'
Include the original source in each finding:
arid . --show-source
Choose an output format:
arid . --format text
arid . --format json
arid . --format markdown
arid . --format sarif
text is the default. The existing JSON shorthand remains supported:
arid . --json
Control text color explicitly when needed:
arid . --color auto
arid . --color always
arid . --color never
Create a baseline for existing duplicate debt:
arid . --write-baseline arid-baseline.json
Then enforce it explicitly:
arid . --baseline arid-baseline.json
or configure it in [tool.arid] so normal arid . scans enforce the baseline automatically.
Example diagnostic:
DUP001 4 duplicated lines
Context: declarative
Scope: class
Occurrences: 2 across 2 files (cross-file)
src/models/user.py:12-15
src/models/account.py:20-23
Found 1 duplicate group.
4 duplicate lines (2.31%).
Understanding Arid's output
Arid separates two questions:
Detection answers: "Is this code duplicated?"
Context helps answer: "What kind of code is duplicated?"
Arid deliberately does not assign a severity or decide whether duplication should be removed. Duplicate code can be intentional, harmless, framework-driven, or worth refactoring.
The structural metadata exists to help you make that decision.
Consider:
DUP001 4 duplicated lines
Context: declarative
Scope: class
Occurrences: 2 across 2 files (cross-file)
src/models/user.py:12-15
src/models/account.py:20-23
Found 1 duplicate group.
4 duplicate lines (2.31%).
DUP001 4 duplicated lines
DUP001 is Arid's duplicate-code diagnostic.
4 duplicated lines means the matching region contains four effective normalized lines that satisfy the configured duplicate threshold.
Arid compares source after its configured Python-aware normalization. Depending on configuration, this can remove constructs such as:
- comments
- docstrings
- imports
- function signatures
Blank lines do not count toward min-lines, and lines containing only non-substantive punctuation do not increase the effective-line count.
Because of that, a finding reported as four duplicated lines may span more than four physical source lines.
Context
Context describes the structural kind of Python code involved in the duplicate.
Possible values are:
| Context | Meaning |
|---|---|
declarative |
The duplicate consists of declarations or definitions, such as direct module/class assignments or definitions. |
executable |
The duplicate consists of executable statements, control flow, or function-body logic. |
mixed |
The duplicate contains or occurs across more than one structural context. |
For example:
Context: declarative
often appears for repeated class or module definitions.
Context: executable
often appears for repeated application logic inside functions.
[!NOTE] Context is descriptive, not a severity.
declarativedoes not mean "safe to ignore," andexecutabledoes not mean "must refactor."
Arid describes Python structure without attempting to infer framework semantics or developer intent.
It therefore does not label findings as "ORM boilerplate," "configuration noise," "safe duplication," or similar framework-specific categories.
Scope
Scope describes where the duplicated code occurs structurally.
Possible values are:
| Scope | Meaning |
|---|---|
module |
Module-level code. |
class |
Code structurally associated with a class. |
function |
Code structurally associated with a function or method. |
mixed |
The duplicate spans or occurs across more than one scope. |
For example:
Context: executable
Scope: function
indicates repeated executable logic within functions or methods.
By contrast:
Context: declarative
Scope: class
indicates repeated declarative code associated with classes.
Again, scope describes where the duplicate exists, not whether it is a problem.
Occurrences
The occurrence line tells you how widely the duplicate appears.
Occurrences: 2 across 2 files (cross-file)
contains three pieces of information:
- the number of duplicate occurrences
- the number of distinct files containing them
- how those occurrences are distributed
Distribution values are:
| Distribution | Meaning |
|---|---|
same-file |
All occurrences are contained in one file. |
cross-file |
Occurrences are spread across multiple files, with one occurrence in each involved file. |
mixed |
Multiple files are involved and at least one file contains multiple occurrences. |
Examples:
Occurrences: 2 across 1 file (same-file)
means the same block appears twice in one file.
Occurrences: 3 across 3 files (cross-file)
means one occurrence appears in each of three files.
Occurrences: 4 across 3 files (mixed)
means the duplicate spans multiple files and at least one of those files contains more than one occurrence.
Source locations
Locations such as:
src/models/user.py:12-15
always refer to the original physical Python source, not Arid's internal normalized representation.
This remains true even when ignored comments, imports, signatures, docstrings, or blank lines appear within the physical range.
Use:
arid . --show-source
to include the original source text alongside each location.
Duplicate groups
When the same block appears more than twice, Arid reports it as one duplicate group rather than generating every possible pair.
For example, a block appearing in:
a.py
b.py
c.py
is one finding with three occurrences, not three separate pairwise findings.
Duplicate lines and duplication percentage
The final summary:
4 duplicate lines (2.31%).
measures redundant effective lines, not every line participating in a duplicate.
One occurrence of each duplicate group is treated as canonical. Only redundant copies beyond that canonical occurrence contribute duplicate lines.
For example:
10-line block × 2 occurrences
contributes:
10 duplicate lines
not 20.
A 10-line block appearing three times contributes:
20 duplicate lines
because two of the three copies are redundant.
Overlapping redundant regions are not counted repeatedly.
The duplication percentage is:
duplicate effective lines
───────────────────────── × 100
analyzed effective lines
This makes the metric represent how much analyzed code is redundant rather than how much code merely participates in a duplicated region.
How to interpret findings
There is no universal rule for which duplicate should be refactored first, but Arid's metadata can help you triage a large report.
A practical review order is often:
- Look at larger duplicate regions before very short ones.
- Review
executable/functionfindings for repeated application logic. - Look at findings with many occurrences to identify patterns repeated broadly through the codebase.
- Use
same-file,cross-file, andmixedto distinguish localized repetition from code repeated across modules. - Review
declarativefindings in context. Repeated declarations may be intentional, generated by a common coding pattern, or candidates for consolidation depending on the project.
Arid intentionally stops short of saying:
high severity
low value
safe to ignore
must refactor
Those are project-specific judgments.
Its job is to provide accurate duplicate detection and enough objective structural information for the developer to make them.
Configuration
Arid uses [tool.arid] in pyproject.toml:
[tool.arid]
min-lines = 4
ignore-comments = true
ignore-docstrings = true
ignore-imports = true
ignore-signatures = true
same-file = true
hidden = false
exclude = [
"generated/**",
"vendor/**",
]
Current defaults are:
| Option | Default | Meaning |
|---|---|---|
min-lines |
4 |
Minimum effective normalized lines required for a duplicate. |
ignore-comments |
true |
Ignore Python comments during matching. |
ignore-docstrings |
true |
Ignore structural Python docstrings. |
ignore-imports |
true |
Ignore import statements. |
ignore-signatures |
true |
Ignore function and method declaration signatures. |
same-file |
true |
Detect non-overlapping duplicate regions within the same file. |
hidden |
false |
Include hidden files and directories during directory discovery. |
exclude |
[] |
Path patterns excluded from discovery. |
baseline |
none | Optional baseline file used to accept existing duplicate debt while reporting new debt. |
Configuration precedence is:
CLI arguments
↓
pyproject.toml
↓
built-in defaults
For example:
[tool.arid]
min-lines = 6
ignore-docstrings = true
same-file = true
hidden = false
can be overridden for one scan with:
arid . \
--min-lines 10 \
--no-ignore-docstrings \
--no-same-file \
--hidden
Each configurable boolean has both an enabling and disabling CLI form. This matters when the project configuration differs from the built-in default. For example, if the project contains:
[tool.arid]
ignore-comments = false
then:
arid . --ignore-comments
explicitly enables comment filtering for that scan.
Likewise:
arid . --no-ignore-comments
explicitly disables it.
Supplying one or more --exclude options on the command line overrides the configured exclude list for that scan:
arid . \
--exclude 'build/**' \
--exclude 'generated/**'
To enforce an existing baseline on every normal scan:
[tool.arid]
baseline = "arid-baseline.json"
An explicit --baseline path overrides the configured baseline for that scan. --format, --color, --json, --write-baseline, and --show-source are CLI-only presentation or administrative options.
Pre-commit
Arid provides an official pre-commit hook that runs a whole-project arid . scan rather than limiting duplicate detection to staged Python files.
Arid must already be installed and available as arid on PATH, and the official hook requires pre-commit 4.4.0 or newer.
repos:
- repo: https://github.com/sponge-b0b/arid
rev: v1.1.0
hooks:
- id: arid
The hook honors normal [tool.arid] configuration, including baseline = "arid-baseline.json".
See Arid pre-commit integration for installation details and behavior.
Detection model
Arid is focused on exact duplicate source blocks after configurable Python-aware normalization.
For example, with comments and function signatures ignored:
def first():
# explanation
value = calculate_value()
save_value(value)
and:
def second():
# different explanation
value = calculate_value()
save_value(value)
can be considered duplicates.
Arid v1 does not normalize identifiers, so these are intentionally different:
value = calculate_value()
save_value(value)
result = calculate_value()
save_value(result)
Arid can attach structural context such as declarative, executable, class, or function to a duplicate that it has already detected.
That does not make Arid a structural clone detector. Two pieces of code that are merely structurally similar but do not become identical after normalization are not considered duplicates.
Semantic clone detection, identifier-renaming clone detection, and fuzzy AST similarity remain outside the v1 scope.
Suppressing intentional duplication
Arid supports source-level suppression regions:
# arid: disable
# intentionally duplicated code
# arid: enable
Code inside a disabled region does not participate in duplicate detection.
Suppression regions also create matching boundaries, so Arid does not construct a duplicate across disabled source.
Use suppression for duplication that is intentionally accepted by the project rather than expecting Arid to infer whether a particular framework pattern or coding convention should be ignored.
Architecture
The v1 architecture is intentionally small:
discover
↓
parse
↓
normalize
↓
intern lines
↓
suffix array
↓
LCP
↓
maximal repeats
↓
DUP001
Arid analyzes Python source entirely in Rust and never imports or executes the project being scanned.
Duplicate detection operates on Arid's normalized source representation. Structural context is derived from Python syntax and attached as reporting metadata; it does not alter whether two normalized regions match.
Installation
[!NOTE] See Project status for the current release stage and stability expectations.
uv
Install Arid as an isolated command-line tool:
uv tool install arid
pip
Install Arid from PyPI:
python -m pip install --pre arid
Verify the installation:
arid --version
Scan the current project:
arid .
Exit codes
Arid uses predictable exit codes for CLI and CI usage:
| Exit code | Meaning |
|---|---|
0 |
Scan completed successfully and no duplicate findings failed the scan. |
1 |
Duplicate-code findings were reported. |
2 |
Invocation, configuration, parsing, or internal error. |
A finding exit status is therefore distinct from an Arid execution failure.
License
Licensed under either of:
- Apache License, Version 2.0
- MIT License
at your option.
Development
Arid includes dedicated tooling and documentation for release qualification, performance benchmarking, and real-world validation:
- Release qualification — automated acceptance of published release candidates and stable releases, including artifact validation, equivalence checks, benchmarks, and stable-promotion enforcement.
- Benchmarks — reproducible performance comparisons against Pylint
R0801and jscpd, including corpus provisioning and benchmark execution. - Validation — real-world correctness and robustness validation across Black, Django, mypy, Rich, determinism checks, malformed-source handling, and filesystem edge cases.
- Release roadmap — release stages, qualification gates, and release metadata preparation with
./release.sh.
Contributing
Contributions should preserve Arid's focused scope and existing product contract. Bug fixes, compatibility improvements, tests, documentation, and performance work are welcome; scope-expanding features should be discussed before implementation.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distributions
Built Distributions
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file arid-1.1.0rc2-py3-none-win_amd64.whl.
File metadata
- Download URL: arid-1.1.0rc2-py3-none-win_amd64.whl
- Upload date:
- Size: 2.1 MB
- Tags: Python 3, Windows x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6568491cf6a44067c2265a6e3ee0f1f8fd95c67f54ff5e2adb521dbeeaa1f907
|
|
| MD5 |
ab124d043bb220adcc11f04a05bd989e
|
|
| BLAKE2b-256 |
63c455f87f3fc21c2150d32031d65b1de8dc64e63c4e291f2f8326044b62a587
|
Provenance
The following attestation bundles were made for arid-1.1.0rc2-py3-none-win_amd64.whl:
Publisher:
release.yml on sponge-b0b/arid
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
arid-1.1.0rc2-py3-none-win_amd64.whl -
Subject digest:
6568491cf6a44067c2265a6e3ee0f1f8fd95c67f54ff5e2adb521dbeeaa1f907 - Sigstore transparency entry: 2492173246
- Sigstore integration time:
-
Permalink:
sponge-b0b/arid@2c3704b5f541711bd83e78abba66a36270c3d860 -
Branch / Tag:
refs/tags/v1.1.0-rc.2 - Owner: https://github.com/sponge-b0b
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@2c3704b5f541711bd83e78abba66a36270c3d860 -
Trigger Event:
push
-
Statement type:
File details
Details for the file arid-1.1.0rc2-py3-none-manylinux_2_34_x86_64.whl.
File metadata
- Download URL: arid-1.1.0rc2-py3-none-manylinux_2_34_x86_64.whl
- Upload date:
- Size: 2.4 MB
- Tags: Python 3, manylinux: glibc 2.34+ x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3ca87d3790907d62ed535211df571ce30f519a5508113977136873117a6b4beb
|
|
| MD5 |
a21f3a7a90b7a27b0abcd41431c580c5
|
|
| BLAKE2b-256 |
6111acc0a5ec9829d042201113666a7799328dfca0eb8e75a815b68833917150
|
Provenance
The following attestation bundles were made for arid-1.1.0rc2-py3-none-manylinux_2_34_x86_64.whl:
Publisher:
release.yml on sponge-b0b/arid
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
arid-1.1.0rc2-py3-none-manylinux_2_34_x86_64.whl -
Subject digest:
3ca87d3790907d62ed535211df571ce30f519a5508113977136873117a6b4beb - Sigstore transparency entry: 2492172699
- Sigstore integration time:
-
Permalink:
sponge-b0b/arid@2c3704b5f541711bd83e78abba66a36270c3d860 -
Branch / Tag:
refs/tags/v1.1.0-rc.2 - Owner: https://github.com/sponge-b0b
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@2c3704b5f541711bd83e78abba66a36270c3d860 -
Trigger Event:
push
-
Statement type:
File details
Details for the file arid-1.1.0rc2-py3-none-macosx_11_0_arm64.whl.
File metadata
- Download URL: arid-1.1.0rc2-py3-none-macosx_11_0_arm64.whl
- Upload date:
- Size: 2.2 MB
- Tags: Python 3, macOS 11.0+ ARM64
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0863ecb8100ce86809aaaa56fa282440860b148b6c4028b4d24b0012a8bb4311
|
|
| MD5 |
5fd5d0d0b37678ead6a0b22e63a2756c
|
|
| BLAKE2b-256 |
d7a1436002689d51971f33875d78b130679a235ad2431875bf761d37e9b3a102
|
Provenance
The following attestation bundles were made for arid-1.1.0rc2-py3-none-macosx_11_0_arm64.whl:
Publisher:
release.yml on sponge-b0b/arid
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
arid-1.1.0rc2-py3-none-macosx_11_0_arm64.whl -
Subject digest:
0863ecb8100ce86809aaaa56fa282440860b148b6c4028b4d24b0012a8bb4311 - Sigstore transparency entry: 2492174124
- Sigstore integration time:
-
Permalink:
sponge-b0b/arid@2c3704b5f541711bd83e78abba66a36270c3d860 -
Branch / Tag:
refs/tags/v1.1.0-rc.2 - Owner: https://github.com/sponge-b0b
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@2c3704b5f541711bd83e78abba66a36270c3d860 -
Trigger Event:
push
-
Statement type:
File details
Details for the file arid-1.1.0rc2-py3-none-macosx_10_12_x86_64.whl.
File metadata
- Download URL: arid-1.1.0rc2-py3-none-macosx_10_12_x86_64.whl
- Upload date:
- Size: 2.3 MB
- Tags: Python 3, macOS 10.12+ x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7ec670905ae3cd21cf99e55f6da9d85b7b1ea259fdc3c5a2bb5f97a9f9ccabb8
|
|
| MD5 |
c7117769df8873e8af21fb9f2a8bbe07
|
|
| BLAKE2b-256 |
b4c7d3eb1febb22f67be81afa6ccf164685e7a9f06cffa8907c87604dbb464ae
|
Provenance
The following attestation bundles were made for arid-1.1.0rc2-py3-none-macosx_10_12_x86_64.whl:
Publisher:
release.yml on sponge-b0b/arid
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
arid-1.1.0rc2-py3-none-macosx_10_12_x86_64.whl -
Subject digest:
7ec670905ae3cd21cf99e55f6da9d85b7b1ea259fdc3c5a2bb5f97a9f9ccabb8 - Sigstore transparency entry: 2492173757
- Sigstore integration time:
-
Permalink:
sponge-b0b/arid@2c3704b5f541711bd83e78abba66a36270c3d860 -
Branch / Tag:
refs/tags/v1.1.0-rc.2 - Owner: https://github.com/sponge-b0b
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@2c3704b5f541711bd83e78abba66a36270c3d860 -
Trigger Event:
push
-
Statement type: