<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Bob Taylor</title>
    <description>The latest articles on DEV Community by Bob Taylor (@spongeb0b).</description>
    <link>https://dev.to/spongeb0b</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4080234%2F6ac3a13e-c3ff-4b09-92a6-01bc2aa8ad64.jpg</url>
      <title>DEV Community: Bob Taylor</title>
      <link>https://dev.to/spongeb0b</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/spongeb0b"/>
    <language>en</language>
    <item>
      <title>A Fast Alternative to Pylint R0801</title>
      <dc:creator>Bob Taylor</dc:creator>
      <pubDate>Tue, 25 Aug 2026 04:53:27 +0000</pubDate>
      <link>https://dev.to/spongeb0b/a-fast-alternative-to-pylint-r0801-4dno</link>
      <guid>https://dev.to/spongeb0b/a-fast-alternative-to-pylint-r0801-4dno</guid>
      <description>&lt;p&gt;&lt;em&gt;If you only need Pylint for duplicate-code detection, you don't necessarily need to run Pylint to get it.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Python tooling has changed quite a bit over the last few years.&lt;/p&gt;

&lt;p&gt;Ruff can replace a large part of the traditional Python linting stack, and it does that work extremely quickly. For a lot of projects, adopting Ruff means there is much less reason to run several different linters over the same source tree.&lt;/p&gt;

&lt;p&gt;There is one check I wasn't able to move out of Pylint, though: duplicate-code detection.&lt;/p&gt;

&lt;p&gt;Pylint reports duplicated code as &lt;code&gt;R0801&lt;/code&gt;, or &lt;code&gt;duplicate-code&lt;/code&gt;. It comes from Pylint's similarities checker and looks for similar lines across Python source.&lt;/p&gt;

&lt;p&gt;It's a useful check. The problem for me wasn't what R0801 did. It was having to run Pylint to get it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I Still Wanted R0801
&lt;/h2&gt;

&lt;p&gt;Duplicate code is easy to dismiss as a style problem until you've maintained enough of it.&lt;/p&gt;

&lt;p&gt;Two copies of the same implementation mean two places that may need to change when the behavior changes. Three copies mean three. Eventually one gets fixed while another doesn't, and code that looked identical gradually stops behaving identically.&lt;/p&gt;

&lt;p&gt;Pylint's own documentation makes essentially the same point: duplicated logic increases the number of places that have to be found, changed, and tested, and can make code harder to understand during review.&lt;/p&gt;

&lt;p&gt;So turning off R0801 wasn't the solution I wanted.&lt;/p&gt;

&lt;p&gt;I wanted to keep the check without paying for a much broader analysis pass just to get that one result.&lt;/p&gt;

&lt;h2&gt;
  
  
  Running Only Pylint's Duplicate-Code Check
&lt;/h2&gt;

&lt;p&gt;If you already have Pylint installed, the first thing to try is simply running less Pylint.&lt;/p&gt;

&lt;p&gt;You can enable only duplicate-code detection:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pylint &lt;span class="nt"&gt;--disable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;all &lt;span class="nt"&gt;--enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;duplicate-code &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is a perfectly reasonable solution, particularly if Pylint is already part of your project and its performance is acceptable.&lt;/p&gt;

&lt;p&gt;There are also useful controls around what contributes to similarity. Pylint supports ignoring comments, docstrings, imports, and function signatures when constructing the source representation used for duplicate detection.&lt;/p&gt;

&lt;p&gt;So before replacing anything, I'd start there.&lt;/p&gt;

&lt;p&gt;If a dedicated R0801 pass is fast enough for your repository, you may already have the solution you need.&lt;/p&gt;

&lt;p&gt;For me, it wasn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Duplicate Detection Can Become Noticeable
&lt;/h2&gt;

&lt;p&gt;Duplicate-code detection is different from many ordinary lint rules.&lt;/p&gt;

&lt;p&gt;A rule such as "this import is unused" can largely reason about a particular file or syntax tree. Duplicate detection has to compare source across a corpus because the code in one file may duplicate code somewhere else entirely.&lt;/p&gt;

&lt;p&gt;That distinction becomes more important as a repository grows.&lt;/p&gt;

&lt;p&gt;It also complicates the obvious answer to performance problems: just split the work across files.&lt;/p&gt;

&lt;p&gt;Cross-file analysis needs a global view of the source being compared. There are real-world projects that have ended up separating Pylint's duplicate-code checker into its own single-process CI job because partitioning files among workers can change the detected clusters.&lt;/p&gt;

&lt;p&gt;That doesn't mean Pylint is a bad tool. Pylint does far more than duplicate detection.&lt;/p&gt;

&lt;p&gt;But it does raise a reasonable question if R0801 is the reason you're still running it:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do you need a general-purpose linter for this particular job?&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  A Focused Alternative
&lt;/h2&gt;

&lt;p&gt;That question is why I built &lt;a href="https://github.com/sponge-b0b/arid" rel="noopener noreferrer"&gt;Arid&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Arid is a Python duplicate-code checker written in Rust. It isn't intended to replace Pylint as a whole, and it isn't trying to replace Ruff.&lt;/p&gt;

&lt;p&gt;It replaces one job:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Pylint R0801
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;with a tool dedicated to duplicate-code detection.&lt;/p&gt;

&lt;p&gt;The workflow I use is simply:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ruff check &lt;span class="nb"&gt;.&lt;/span&gt;
arid &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ruff handles the broad linting work. Arid handles duplication.&lt;/p&gt;

&lt;p&gt;That separation is useful beyond performance. A focused tool can make its configuration, reporting, CI behavior, and machine interfaces specifically about the problem it's solving rather than accommodating an entire linting framework.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Much Faster?
&lt;/h2&gt;

&lt;p&gt;I don't think "written in Rust" is a benchmark, so I maintain a pinned performance campaign for Arid.&lt;/p&gt;

&lt;p&gt;For Arid 2.0, I compared it with Pylint 4.0.6 while isolating Pylint's duplicate-code functionality rather than comparing Arid with an entire Pylint lint run.&lt;/p&gt;

&lt;p&gt;Running serially, the results were:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Requests — 191.19x faster&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pydantic — 219.06x faster&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Polaris — 249.68x faster&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Those numbers aren't meant to establish that Pylint itself is "200x slower." That would be an unfair comparison because Pylint performs many checks Arid doesn't even attempt.&lt;/p&gt;

&lt;p&gt;They answer a much narrower question:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If the job is duplicate-code detection, what does the focused implementation cost compared with Pylint's implementation of that job?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That's the comparison that mattered to me.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Arid Actually Detects
&lt;/h2&gt;

&lt;p&gt;There is another important qualification.&lt;/p&gt;

&lt;p&gt;"Duplicate code" can mean several different things.&lt;/p&gt;

&lt;p&gt;At one extreme is exact textual duplication. At the other are sophisticated clone detectors trying to recognize code that is structurally or semantically similar despite substantial differences in its source.&lt;/p&gt;

&lt;p&gt;Arid isn't trying to solve the entire code-clone research problem.&lt;/p&gt;

&lt;p&gt;It detects exact duplicated Python source after configurable Python-aware normalization. Depending on the configuration, comments, docstrings, imports, and function signatures can be excluded from duplicate identity.&lt;/p&gt;

&lt;p&gt;That makes it much closer in purpose to the R0801 workflow I wanted to replace.&lt;/p&gt;

&lt;p&gt;If you're looking for semantic clone detection—two differently written implementations that happen to do the same thing—Arid isn't the tool for that.&lt;/p&gt;

&lt;p&gt;I think being explicit about that boundary is important. A focused tool is only useful if its focus matches the problem you actually have.&lt;/p&gt;

&lt;h2&gt;
  
  
  What About an Existing Codebase Full of Duplication?
&lt;/h2&gt;

&lt;p&gt;This is where replacing a checker and actually adopting one become different problems.&lt;/p&gt;

&lt;p&gt;Suppose you run duplicate detection on a mature project for the first time and discover 300 existing findings.&lt;/p&gt;

&lt;p&gt;Technically, the tool worked.&lt;/p&gt;

&lt;p&gt;Practically, you've just created 300 reasons for your team not to enable it in CI.&lt;/p&gt;

&lt;p&gt;You could fix everything before enforcing the check, but that's often unrealistic. You could ignore duplicate detection entirely, but then new duplication continues accumulating.&lt;/p&gt;

&lt;p&gt;The more useful approach is a baseline.&lt;/p&gt;

&lt;p&gt;With Arid, existing findings can be recorded as accepted debt. CI can then reject new duplication without requiring you to eliminate everything that existed before the check was introduced.&lt;/p&gt;

&lt;p&gt;As existing duplication gets refactored, stale baseline entries can be identified and pruned.&lt;/p&gt;

&lt;p&gt;That changes adoption from:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;fix all existing duplication
        ↓
enable the check
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;into:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;record existing duplication
        ↓
prevent new duplication
        ↓
improve old duplication over time
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For mature codebases, I think the second model is considerably more realistic.&lt;/p&gt;

&lt;p&gt;I'll cover that workflow separately because it turns out to be a more general engineering problem than just configuring a duplicate-code checker.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using It in CI
&lt;/h2&gt;

&lt;p&gt;For a basic local check, Arid doesn't require much:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv tool &lt;span class="nb"&gt;install &lt;/span&gt;arid
arid &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Arid 2.0 also has an official GitHub Action for CI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sponge-b0b/arid@v2.0.0&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;V2 can produce text, JSON, Markdown, and SARIF reports, including multiple representations from a single analysis.&lt;/p&gt;

&lt;p&gt;That matters if, for example, you want readable output for developers, structured JSON for another tool, and SARIF for code scanning. The source doesn't need to be analyzed independently for every consumer.&lt;/p&gt;

&lt;p&gt;Again, none of that makes duplicate detection inherently better. It makes the detector easier to incorporate into the systems surrounding it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Should You Replace Pylint R0801?
&lt;/h2&gt;

&lt;p&gt;Not necessarily.&lt;/p&gt;

&lt;p&gt;If you're already using Pylint extensively, R0801 performs well enough for your repository, and you like its behavior, replacing it solely because another implementation is faster may accomplish very little.&lt;/p&gt;

&lt;p&gt;I'd consider a dedicated alternative when the situation looks more like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;you've moved most linting to Ruff;&lt;/li&gt;
&lt;li&gt;duplicate-code detection is one of the remaining reasons you're running Pylint;&lt;/li&gt;
&lt;li&gt;R0801 has become noticeable on your repository;&lt;/li&gt;
&lt;li&gt;you want duplicate detection in a fast local feedback loop;&lt;/li&gt;
&lt;li&gt;you need baseline-based adoption for an existing codebase; or&lt;/li&gt;
&lt;li&gt;you want structured duplicate-code output for CI or other tooling.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's the situation Arid was built for.&lt;/p&gt;

&lt;p&gt;I didn't start the project because I thought Python needed another general-purpose linter. Quite the opposite. I started it because I wanted one useful capability without carrying a general-purpose linter along just to get it.&lt;/p&gt;

&lt;p&gt;If that's your problem too, Arid may be useful.&lt;/p&gt;

&lt;p&gt;If it isn't, keep using R0801.&lt;/p&gt;

&lt;p&gt;The goal isn't to replace a tool that already works for you. It's to avoid running more tooling than the problem actually requires.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/sponge-b0b/arid" rel="noopener noreferrer"&gt;Arid&lt;/a&gt;&lt;/strong&gt; is an open-source Python duplicate-code checker written in Rust and designed to complement Ruff.&lt;/p&gt;

&lt;h3&gt;
  
  
  About the Author
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Bob Taylor&lt;/strong&gt; is a software engineer and architect who builds developer tools and AI systems. He is currently developing &lt;a href="https://github.com/sponge-b0b/arid" rel="noopener noreferrer"&gt;Arid&lt;/a&gt; and &lt;a href="https://github.com/sponge-b0b/Polaris" rel="noopener noreferrer"&gt;Polaris&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/sponge-b0b" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>productivity</category>
      <category>opensource</category>
      <category>devtools</category>
    </item>
    <item>
      <title>Arid 2.0: From Fast Python Duplicate Detection to CI-Ready Tooling</title>
      <dc:creator>Bob Taylor</dc:creator>
      <pubDate>Sun, 23 Aug 2026 08:43:32 +0000</pubDate>
      <link>https://dev.to/spongeb0b/arid-20-from-fast-python-duplicate-detection-to-ci-ready-tooling-4h1o</link>
      <guid>https://dev.to/spongeb0b/arid-20-from-fast-python-duplicate-detection-to-ci-ready-tooling-4h1o</guid>
      <description>&lt;p&gt;&lt;em&gt;Arid started because Pylint R0801 was too slow. Version 2.0 keeps the fast detector and builds the workflows around it.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A few months ago, I had a fairly simple problem: Pylint was too slow.&lt;/p&gt;

&lt;p&gt;I use Pylint's &lt;code&gt;R0801&lt;/code&gt; duplicate-code detection on &lt;a href="https://github.com/sponge-b0b/Polaris" rel="noopener noreferrer"&gt;Polaris&lt;/a&gt;, a fairly large Python project I've been building. I also use Ruff, which handles most of the Python linting I care about and handles it very quickly.&lt;/p&gt;

&lt;p&gt;Unfortunately, Ruff doesn't detect duplicate code. So every time I wanted that one check, I was back to waiting for Pylint.&lt;/p&gt;

&lt;p&gt;Eventually I got tired of waiting and built &lt;a href="https://github.com/sponge-b0b/arid" rel="noopener noreferrer"&gt;Arid&lt;/a&gt;, a focused Python duplicate-code checker written in Rust. The idea was deliberately simple: do the job I was using Pylint &lt;code&gt;R0801&lt;/code&gt; for, do it accurately, and do it fast enough that I wouldn't mind running it all the time.&lt;/p&gt;

&lt;p&gt;The first versions of Arid proved that idea worked. Arid 2.0 is about something different: what does a fast detector need around it before it becomes a tool you can comfortably build into real development workflows? That question ended up defining the release.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Detector Didn't Need Reinventing
&lt;/h2&gt;

&lt;p&gt;Major versions have a way of encouraging major rewrites: new architecture, new algorithm, new semantics. Everything is better because everything is new.&lt;/p&gt;

&lt;p&gt;I didn't do that.&lt;/p&gt;

&lt;p&gt;Arid 2.0 uses the same basic detection model as 1.2. It still detects exact duplicated Python source after configurable Python-aware normalization. It still reports &lt;code&gt;DUP001&lt;/code&gt;. Comments, docstrings, imports, and function signatures can be excluded from duplicate identity. The detector isn't suddenly trying to find semantically equivalent code or fuzzy AST clones.&lt;/p&gt;

&lt;p&gt;That's intentional. The detector was already solving the problem I wanted it to solve, so instead of redesigning the part that worked, I concentrated v2 on the things surrounding it: stable machine contracts, CI integration, baseline management, focused workflows, incomplete-analysis handling, project control, and better support for external tooling.&lt;/p&gt;

&lt;p&gt;Arid 2.0 isn't really a new detector. It's the same detector with a much more useful development workflow around it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fast Still Matters
&lt;/h2&gt;

&lt;p&gt;None of that would matter much if Arid stopped being fast.&lt;/p&gt;

&lt;p&gt;The v2 performance campaign used the same pinned benchmark corpora and Hyperfine methodology used to qualify Arid 1.2. Against Pylint 4.0.6, running serially, Arid 2.0 measured:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Project&lt;/th&gt;
&lt;th&gt;Arid v2 vs. Pylint&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Requests&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;191.19x faster&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pydantic&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;219.06x faster&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Polaris&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;249.68x faster&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Those aren't comparisons against an entire Pylint run. The benchmark isolates Pylint's duplicate-code functionality so the comparison is actually about the job Arid replaces.&lt;/p&gt;

&lt;p&gt;I also compared v2 directly against the qualified Arid 1.2 implementation. The additional v2 functionality introduced only low-single-digit overhead across the canonical corpora.&lt;/p&gt;

&lt;p&gt;Could I have spent more time trying to recover that few percent? Sure. Would anybody using Arid notice the difference between roughly 219x faster than Pylint and slightly more than 219x faster than Pylint? Probably not. At some point optimization becomes an excellent way to avoid working on things users actually need.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stable Identity for a Finding
&lt;/h2&gt;

&lt;p&gt;One of those things is identity.&lt;/p&gt;

&lt;p&gt;Suppose Arid finds the same duplicated code today and tomorrow, but somebody inserts 20 lines near the top of the file. The physical line numbers changed.&lt;/p&gt;

&lt;p&gt;The duplicate didn't.&lt;/p&gt;

&lt;p&gt;Or perhaps a file moves to another directory. Maybe the order of occurrences changes, or the same duplicate appears in another file. If external tooling identifies findings using locations, those findings become surprisingly unstable.&lt;/p&gt;

&lt;p&gt;Arid 2.0 gives every finding a versioned fingerprint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;arid-finding-v1:sha256:...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That fingerprint identifies the normalized duplicate content independently of path, physical line number, occurrence ordering and multiplicity, structural metadata, output format, and worker mode. The same identity is exposed in SARIF through a versioned partial fingerprint.&lt;/p&gt;

&lt;p&gt;This isn't particularly exciting when you're looking at a CLI report. It becomes considerably more useful when a CI system, reporting service, coding agent, or other tool needs to reason about the same finding across multiple runs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Focus the Report, Not the Analysis
&lt;/h2&gt;

&lt;p&gt;Large projects create another problem. Sometimes I don't care about every duplicate in the repository. I'm working on one package, directory, or file and want to know what's relevant to the thing I'm changing.&lt;/p&gt;

&lt;p&gt;The obvious implementation is to scan only that path, but that's wrong for duplicate detection.&lt;/p&gt;

&lt;p&gt;Suppose I'm working in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;src/payments/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and some code there duplicates code in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;src/customers/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If I analyze only &lt;code&gt;src/payments/&lt;/code&gt;, I've removed half of the evidence.&lt;/p&gt;

&lt;p&gt;Arid 2.0 therefore separates what gets analyzed from what gets reported:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;arid &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;--focus&lt;/span&gt; src/payments
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Arid still performs whole-corpus duplicate detection. Baseline enforcement still happens against the complete result, and only afterward does focus filtering determine which groups are reported. If a focused finding also occurs outside the focused path, those occurrences remain part of the finding.&lt;/p&gt;

&lt;p&gt;In other words, focus changes what you ask Arid to show you without changing the corpus Arid uses to determine whether the code is duplicated. That's an important distinction for CI jobs and coding agents operating on a specific part of a larger repository.&lt;/p&gt;

&lt;h2&gt;
  
  
  Existing Duplicate Debt Is a Lifecycle
&lt;/h2&gt;

&lt;p&gt;Baselines were already part of Arid before v2. The idea is straightforward: perhaps you're introducing duplicate-code enforcement into a mature project that already has 300 duplicate groups.&lt;/p&gt;

&lt;p&gt;You could fix all 300 before adopting the tool.&lt;/p&gt;

&lt;p&gt;Or don't adopt the tool.&lt;/p&gt;

&lt;p&gt;Neither is especially compelling.&lt;/p&gt;

&lt;p&gt;A baseline gives you a third option: accept the existing debt temporarily while preventing new duplicate debt from being introduced. Arid 2.0 extends that into an actual lifecycle.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;arid &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;--baseline-status&lt;/span&gt; arid-baseline.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;can distinguish accepted duplicate debt, active/new findings, and stale baseline entries. Then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;arid &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;--prune-baseline&lt;/span&gt; arid-baseline.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;removes stale acceptance when the corresponding duplication no longer exists. It never silently accepts new debt.&lt;/p&gt;

&lt;p&gt;That gives a team a useful progression:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;existing duplication
        ↓
baseline it
        ↓
prevent new duplication
        ↓
refactor existing duplication over time
        ↓
prune stale baseline entries
        ↓
smaller baseline
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You don't have to make an old codebase perfect before you're allowed to stop making it worse. I suspect that principle applies to considerably more than duplicate code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure Doesn't Have to Mean "Tell Me Nothing"
&lt;/h2&gt;

&lt;p&gt;Source analysis has another annoying edge case. Imagine scanning 3,000 Python files and one cannot be read, parsed, or normalized. Should the entire analysis disappear?&lt;/p&gt;

&lt;p&gt;Sometimes yes. If you're enforcing a complete quality gate, an incomplete analysis cannot be treated as success. But that doesn't mean the useful results from the other 2,999 files need to vanish.&lt;/p&gt;

&lt;p&gt;Arid 2.0 adds:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;arid &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;--keep-going&lt;/span&gt; &lt;span class="nt"&gt;--json&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Independent source failures are collected while valid files continue through detection. The important part is that Arid doesn't pretend partial analysis is complete analysis.&lt;/p&gt;

&lt;p&gt;A report-v4 result explicitly says:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"complete"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and includes structured source errors. The process still exits with operational status &lt;code&gt;2&lt;/code&gt;, and incomplete reports cannot be emitted as SARIF.&lt;/p&gt;

&lt;p&gt;The intent is simple: produce as much useful information as you safely can, but be explicit about how complete that information is. A human can inspect the partial result, and an automated consumer can make its own decision. Neither has to guess whether the analysis silently skipped something.&lt;/p&gt;

&lt;h2&gt;
  
  
  One Scan, Several Consumers
&lt;/h2&gt;

&lt;p&gt;A CI pipeline often wants more than one representation of the same result. Maybe developers want readable console output, the build system wants JSON, GitHub code scanning wants SARIF, and the job summary wants Markdown.&lt;/p&gt;

&lt;p&gt;The inefficient answer is to run the analyzer four times.&lt;/p&gt;

&lt;p&gt;Arid 2.0 can instead produce multiple outputs from one in-memory report:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;arid &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--format&lt;/span&gt; text &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--report&lt;/span&gt; &lt;span class="nv"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;artifacts/arid.json &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--report&lt;/span&gt; &lt;span class="nv"&gt;markdown&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;artifacts/arid.md &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--report&lt;/span&gt; &lt;span class="nv"&gt;sarif&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;artifacts/arid.sarif
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The source isn't reparsed four times and duplicate detection isn't repeated four times. The analysis happens once, and the result is rendered for the consumers that need it.&lt;/p&gt;

&lt;p&gt;Obvious in retrospect? Probably. Still worth doing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Machine-Readable Means Having a Contract
&lt;/h2&gt;

&lt;p&gt;Once other software starts consuming CLI output, "it happens to be JSON" isn't enough.&lt;/p&gt;

&lt;p&gt;Arid 2.0 introduces report schema v4 with explicit fields for things like the schema version, tool version, analysis metadata, completion state, structured errors, and finding fingerprints. The schema itself is published:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;schemas/report-v4.schema.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Arid also publishes contracts for capabilities and fatal JSON-mode operational errors. And:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;arid &lt;span class="nt"&gt;--capabilities&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;allows tooling to discover deterministic build capabilities without first discovering or analyzing a project.&lt;/p&gt;

&lt;p&gt;This is partly about ordinary CI integration, but there's another consumer I care about more now than I would have a few years ago: coding agents.&lt;/p&gt;

&lt;p&gt;I use AI heavily in my own development workflow. An agent interacting with a tool shouldn't have to scrape human-readable console output and hope a sentence doesn't change in the next release. If we're increasingly going to have software using software on our behalf, the interfaces between those tools need to become more explicit, not less.&lt;/p&gt;

&lt;p&gt;JSON gives us a machine-readable format. Publishing the schema tells the consumer what that format actually promises.&lt;/p&gt;

&lt;h2&gt;
  
  
  Arid Now Has an Official GitHub Action
&lt;/h2&gt;

&lt;p&gt;Of course, the easiest integration is the one you don't have to assemble yourself.&lt;/p&gt;

&lt;p&gt;Arid 2.0 ships an official composite GitHub Action:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sponge-b0b/arid@v2.0.0&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Action installs the exact Arid release associated with its tag and performs one scan. It can expose core metrics as outputs, write a job summary, and produce SARIF when configured.&lt;/p&gt;

&lt;p&gt;Before v2, Arid could certainly be used in CI. Now there's a supported integration that makes doing it considerably simpler.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sometimes You Want to Analyze Code That Isn't on Disk
&lt;/h2&gt;

&lt;p&gt;Arid normally discovers and analyzes Python files in a project, but editors, coding agents, and other tools frequently have source that doesn't exist on disk yet—or source that differs from what's currently there.&lt;/p&gt;

&lt;p&gt;V2 adds virtual Python source through standard input:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat &lt;/span&gt;src/example.py | arid &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;--stdin-path&lt;/span&gt; src/example.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The virtual source goes through the same Python parser and normalizer as disk-backed source. If an equivalent disk path exists, the virtual version replaces it for that scan. Otherwise it can be added to the corpus when the resolved project context permits it. Arid never writes that source to disk.&lt;/p&gt;

&lt;p&gt;That means another tool can effectively ask, "If this were the contents of &lt;code&gt;src/example.py&lt;/code&gt;, what duplicates would exist?" without first modifying the working tree. That's useful for editors and automation, and yes, it's particularly useful for coding agents.&lt;/p&gt;

&lt;h2&gt;
  
  
  Explicit When You Need It
&lt;/h2&gt;

&lt;p&gt;Convention is great until automation needs certainty.&lt;/p&gt;

&lt;p&gt;Arid still supports its existing nearest-config behavior, but v2 adds explicit control over project and configuration context:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;arid &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;--config&lt;/span&gt; path/to/pyproject.toml
arid &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;--no-config&lt;/span&gt;
arid &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;--project-root&lt;/span&gt; path/to/project
arid &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;--show-config&lt;/span&gt;
arid &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;--list-files&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For somebody running Arid manually in a normal repository, most of this can stay invisible. For CI, monorepos, editor integrations, and agents, being able to ask exactly which project and configuration are being used becomes considerably more important.&lt;/p&gt;

&lt;p&gt;The common case can still rely on convenient defaults. The less common cases now have a way to be explicit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Not Everything Became Public
&lt;/h2&gt;

&lt;p&gt;There was one place where v2 deliberately became less extensible.&lt;/p&gt;

&lt;p&gt;Arid is primarily a CLI application, but its Rust crate naturally exposes Rust code too. In v2, I narrowed the semver-supported Rust surface to a small crate-root application API. Implementation modules, detector internals, and reporting internals are no longer promises to downstream Rust consumers.&lt;/p&gt;

&lt;p&gt;That decision fits something I've been thinking about a lot lately: every public interface creates an obligation. Once implementation details become supported API, changing your own internals becomes somebody else's breaking change.&lt;/p&gt;

&lt;p&gt;If Arid were intended to be a general duplicate-detection framework, that would be a different conversation. It isn't, and not everything another developer &lt;em&gt;could&lt;/em&gt; call needs to become something they &lt;em&gt;should&lt;/em&gt; depend on.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Didn't Change May Matter More
&lt;/h2&gt;

&lt;p&gt;For a major release, the compatibility list is almost as important as the feature list.&lt;/p&gt;

&lt;p&gt;Arid 2.0 preserves the things ordinary users depend on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;exact normalized duplicate semantics;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;DUP001&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;normal CLI invocation;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;[tool.arid]&lt;/code&gt; configuration;&lt;/li&gt;
&lt;li&gt;normalization behavior;&lt;/li&gt;
&lt;li&gt;source suppression;&lt;/li&gt;
&lt;li&gt;existing baseline-v1 files;&lt;/li&gt;
&lt;li&gt;serial execution by default;&lt;/li&gt;
&lt;li&gt;worker controls;&lt;/li&gt;
&lt;li&gt;the &lt;code&gt;0&lt;/code&gt; / &lt;code&gt;1&lt;/code&gt; / &lt;code&gt;2&lt;/code&gt; exit meanings;&lt;/li&gt;
&lt;li&gt;pre-commit integration;&lt;/li&gt;
&lt;li&gt;supported release platforms.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For CLI-only users who don't consume Arid's machine contracts or Rust internals, upgrading from 1.2 may require no migration work at all.&lt;/p&gt;

&lt;p&gt;The intentional breaking changes are concentrated where a major version gives us room to make contracts cleaner: report JSON, SARIF finding identity, and the supported Rust API. That's the kind of major version I prefer: break what you have a good reason to break and leave everything else alone.&lt;/p&gt;

&lt;h2&gt;
  
  
  I Tried It on Real Projects
&lt;/h2&gt;

&lt;p&gt;I don't want Arid's correctness story to be based entirely on unit tests and a repository containing six carefully selected Python files.&lt;/p&gt;

&lt;p&gt;The v2 validation campaign exercised Arid against Black, Django, mypy, Rich, Unicode and space-containing paths, and combinations of the new workflow features.&lt;/p&gt;

&lt;p&gt;For equivalent settings, canonical duplicate groups from Arid 2.0 were compared directly with qualified Arid 1.2 results across Black, Django, mypy, and Rich. No detector-semantic regression was found.&lt;/p&gt;

&lt;p&gt;Validation also covered focus behavior, baseline-before-focus ordering, virtual-source replacement without disk mutation, controlled malformed source with &lt;code&gt;--keep-going&lt;/code&gt;, multi-output on Django, worker determinism, and the published GitHub Action.&lt;/p&gt;

&lt;p&gt;Performance matters, but so does knowing that the fast answer is still the right answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Arid Fits
&lt;/h2&gt;

&lt;p&gt;Arid isn't trying to replace Ruff. Quite the opposite.&lt;/p&gt;

&lt;p&gt;My normal mental model is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ruff check &lt;span class="nb"&gt;.&lt;/span&gt;
arid &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ruff handles the broad Python linting problem extraordinarily well. Arid handles one problem Ruff currently doesn't: duplicate code.&lt;/p&gt;

&lt;p&gt;Because Arid is deliberately focused on that problem, I can make decisions around its detection model, reporting, baselines, and automation without turning it into another general-purpose linter. That's still the philosophy behind v2:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cleaner contracts. Better automation. Same focused detector.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Try It
&lt;/h2&gt;

&lt;p&gt;With &lt;code&gt;uv&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv tool &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="s2"&gt;"arid==2.0.0"&lt;/span&gt;
arid &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or with &lt;code&gt;pip&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python &lt;span class="nt"&gt;-m&lt;/span&gt; pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="s2"&gt;"arid==2.0.0"&lt;/span&gt;
arid &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you're already using Ruff and want duplicate-code detection, try Arid on a real project. If you're still running Pylint primarily because you need &lt;code&gt;R0801&lt;/code&gt;, I'm particularly interested in what you think.&lt;/p&gt;

&lt;p&gt;I'm also interested in people integrating static-analysis tools into CI, editors, or coding-agent workflows. A large part of Arid 2.0 exists because once a command-line tool starts participating in larger development systems, speed isn't the only thing that matters anymore.&lt;/p&gt;

&lt;p&gt;Arid started because I didn't want to wait for Pylint.&lt;/p&gt;

&lt;p&gt;Version 2.0 is what happened after the detector became fast enough that speed stopped being the most interesting problem.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/sponge-b0b/arid" rel="noopener noreferrer"&gt;Arid&lt;/a&gt;&lt;/strong&gt; is an open-source Python duplicate-code checker written in Rust. Version 2.0 is available now.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Documentation:&lt;/strong&gt; &lt;a href="https://github.com/sponge-b0b/arid/blob/main/docs/releases/v2.0.0.md" rel="noopener noreferrer"&gt;Arid 2.0 release notes&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  About the Author
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Bob Taylor&lt;/strong&gt; is a software engineer and architect who builds developer tools and AI systems. He is currently developing &lt;a href="https://github.com/sponge-b0b/arid" rel="noopener noreferrer"&gt;Arid&lt;/a&gt; and &lt;a href="https://github.com/sponge-b0b/Polaris" rel="noopener noreferrer"&gt;Polaris&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/sponge-b0b" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>opensource</category>
      <category>devtools</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Every Abstraction Is a Bet on the Future</title>
      <dc:creator>Bob Taylor</dc:creator>
      <pubDate>Wed, 19 Aug 2026 06:30:22 +0000</pubDate>
      <link>https://dev.to/spongeb0b/every-abstraction-is-a-bet-on-the-future-507i</link>
      <guid>https://dev.to/spongeb0b/every-abstraction-is-a-bet-on-the-future-507i</guid>
      <description>&lt;p&gt;&lt;em&gt;Your architecture shouldn't solve requirements that don't exist yet.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Software developers love abstractions.&lt;/p&gt;

&lt;p&gt;I know. I'm one of them.&lt;/p&gt;

&lt;p&gt;Give us a small enough problem and eventually somebody will propose an interface, a factory, a plugin architecture, dependency injection, and perhaps a message bus just in case the three functions need to communicate asynchronously someday.&lt;/p&gt;

&lt;p&gt;You know. For flexibility.&lt;/p&gt;

&lt;p&gt;I've been thinking about this while building &lt;a href="https://github.com/sponge-b0b/arid" rel="noopener noreferrer"&gt;Arid&lt;/a&gt;, a Python duplicate-code checker written in Rust.&lt;/p&gt;

&lt;p&gt;Arid has a deliberately narrow job:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Detect duplicated Python source code quickly and accurately.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's it.&lt;/p&gt;

&lt;p&gt;It doesn't format Python.&lt;/p&gt;

&lt;p&gt;It doesn't sort imports.&lt;/p&gt;

&lt;p&gt;It doesn't type-check anything.&lt;/p&gt;

&lt;p&gt;It doesn't detect dead code.&lt;/p&gt;

&lt;p&gt;It doesn't scan JavaScript.&lt;/p&gt;

&lt;p&gt;And unless something changes dramatically, it isn't going to make coffee either.&lt;/p&gt;

&lt;p&gt;That narrow scope created an interesting architectural question:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How much architecture does a small tool actually need?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;My answer has increasingly become:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Enough to make the current problem clean. No more.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That sounds like YAGNI.&lt;/p&gt;

&lt;p&gt;It is.&lt;/p&gt;

&lt;p&gt;But I think there's a more useful way to look at it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Every Abstraction Is a Prediction
&lt;/h2&gt;

&lt;p&gt;Suppose Arid had started with this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Language
    ├── Python
    ├── JavaScript
    ├── TypeScript
    └── ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Looks reasonable.&lt;/p&gt;

&lt;p&gt;Maybe even responsible.&lt;/p&gt;

&lt;p&gt;After all, duplicate-code detection isn't inherently a Python problem. Why couple the architecture to Python?&lt;/p&gt;

&lt;p&gt;So we introduce something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;LanguageFrontend
    parse()
    normalize()
    classify()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Python implements it today.&lt;/p&gt;

&lt;p&gt;JavaScript can implement it tomorrow.&lt;/p&gt;

&lt;p&gt;Look at us. Future-proof already.&lt;/p&gt;

&lt;p&gt;Except there is currently no JavaScript version of Arid.&lt;/p&gt;

&lt;p&gt;There is no TypeScript version.&lt;/p&gt;

&lt;p&gt;There is no requirement for either one.&lt;/p&gt;

&lt;p&gt;There isn't even a roadmap item for another language.&lt;/p&gt;

&lt;p&gt;What exactly did the abstraction buy us?&lt;/p&gt;

&lt;p&gt;It bought us an interface.&lt;/p&gt;

&lt;p&gt;It bought us indirection.&lt;/p&gt;

&lt;p&gt;It bought us a contract that future implementations now have to fit.&lt;/p&gt;

&lt;p&gt;It bought us tests for a generic concept that currently has exactly one implementation.&lt;/p&gt;

&lt;p&gt;And perhaps most importantly, it quietly made a prediction:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Arid will need multiple language frontends.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Maybe that's true someday.&lt;/p&gt;

&lt;p&gt;Maybe it isn't.&lt;/p&gt;

&lt;p&gt;Either way, we've paid for part of that future before we know whether it's coming.&lt;/p&gt;

&lt;p&gt;That's the thing about abstractions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;They're not free flexibility. They're bets about where the software is going.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture Has Carrying Costs
&lt;/h2&gt;

&lt;p&gt;The cost of an abstraction isn't just the code required to create it.&lt;/p&gt;

&lt;p&gt;Code is usually the cheap part.&lt;/p&gt;

&lt;p&gt;The real cost is that somebody has to understand it.&lt;/p&gt;

&lt;p&gt;Consider a hypothetical detector architecture:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DuplicateDetector
    ├── ExactDetector
    ├── StructuralDetector
    ├── SemanticDetector
    └── FuzzyDetector
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nice.&lt;/p&gt;

&lt;p&gt;Except Arid has one detector.&lt;/p&gt;

&lt;p&gt;It performs exact duplicate detection after configurable Python-aware normalization.&lt;/p&gt;

&lt;p&gt;There is no structural detector.&lt;/p&gt;

&lt;p&gt;There is no semantic detector.&lt;/p&gt;

&lt;p&gt;There is no fuzzy detector.&lt;/p&gt;

&lt;p&gt;In fact, those are explicitly outside Arid's current scope.&lt;/p&gt;

&lt;p&gt;So if I introduce &lt;code&gt;DuplicateDetector&lt;/code&gt;, what have I modeled?&lt;/p&gt;

&lt;p&gt;Not the software that exists.&lt;/p&gt;

&lt;p&gt;I've modeled software I can imagine.&lt;/p&gt;

&lt;p&gt;That's a subtle but important difference.&lt;/p&gt;

&lt;p&gt;Now every developer reading the code has additional questions:&lt;/p&gt;

&lt;p&gt;Why is this an interface?&lt;/p&gt;

&lt;p&gt;Are there other implementations?&lt;/p&gt;

&lt;p&gt;Can the implementation change at runtime?&lt;/p&gt;

&lt;p&gt;Am I expected to add new detectors this way?&lt;/p&gt;

&lt;p&gt;What guarantees does the abstraction make?&lt;/p&gt;

&lt;p&gt;Which behavior belongs to the interface and which belongs to the implementation?&lt;/p&gt;

&lt;p&gt;The abstraction has increased the number of concepts required to understand the system without increasing what the system can do.&lt;/p&gt;

&lt;p&gt;That's architectural debt too.&lt;/p&gt;

&lt;p&gt;We just don't usually call it that because the code looks clean.&lt;/p&gt;

&lt;h2&gt;
  
  
  But Don't Just Put Everything in &lt;code&gt;main()&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;There is an obvious bad interpretation of this argument:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Small software doesn't need architecture.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I don't believe that.&lt;/p&gt;

&lt;p&gt;Arid 1.1.0 has an architecture.&lt;/p&gt;

&lt;p&gt;The main application pipeline is roughly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;discover
   ↓
read
   ↓
normalize
   ↓
build corpus
   ↓
detect duplicates
   ↓
apply baseline
   ↓
build report
   ↓
render output
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those are real boundaries because they represent different responsibilities in the problem.&lt;/p&gt;

&lt;p&gt;File discovery shouldn't know how suffix arrays work.&lt;/p&gt;

&lt;p&gt;Duplicate detection shouldn't know how Python comments are parsed.&lt;/p&gt;

&lt;p&gt;Normalization shouldn't know how Markdown reports are rendered.&lt;/p&gt;

&lt;p&gt;Reporting shouldn't decide what counts as a duplicate.&lt;/p&gt;

&lt;p&gt;Those separations aren't predictions about hypothetical future products.&lt;/p&gt;

&lt;p&gt;They're properties of the problem Arid solves today.&lt;/p&gt;

&lt;p&gt;That's the distinction I care about.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Good architecture separates things that are actually different. Overarchitecture separates things because they might become different someday.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Those are not the same thing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Concrete Is Not a Dirty Word
&lt;/h2&gt;

&lt;p&gt;At some point, "concrete" became suspicious in software design.&lt;/p&gt;

&lt;p&gt;If one module directly calls another module, perhaps we're too tightly coupled.&lt;/p&gt;

&lt;p&gt;Better introduce an interface.&lt;/p&gt;

&lt;p&gt;But coupling isn't automatically bad.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Incorrect coupling is bad.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Arid's duplicate detector depends on Arid's corpus representation.&lt;/p&gt;

&lt;p&gt;Of course it does.&lt;/p&gt;

&lt;p&gt;That's the data it detects duplicates in.&lt;/p&gt;

&lt;p&gt;The normalization layer produces Arid's normalized representation.&lt;/p&gt;

&lt;p&gt;Again: yes.&lt;/p&gt;

&lt;p&gt;That's its job.&lt;/p&gt;

&lt;p&gt;Those relationships aren't architectural mistakes waiting to be abstracted away.&lt;/p&gt;

&lt;p&gt;They're the architecture.&lt;/p&gt;

&lt;p&gt;The question shouldn't be:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;How do I eliminate coupling?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It should be:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Are these things coupled for a reason that belongs to the domain?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If the answer is yes, hiding that relationship behind another interface doesn't necessarily improve anything.&lt;/p&gt;

&lt;p&gt;Sometimes it just makes the coupling harder to see.&lt;/p&gt;

&lt;h2&gt;
  
  
  One Implementation Is a Clue
&lt;/h2&gt;

&lt;p&gt;I don't subscribe to a hard rule that an interface must always have multiple implementations.&lt;/p&gt;

&lt;p&gt;There are legitimate reasons to put a boundary in front of a single implementation.&lt;/p&gt;

&lt;p&gt;External systems are an obvious example.&lt;/p&gt;

&lt;p&gt;Testing can be another.&lt;/p&gt;

&lt;p&gt;A meaningful architectural seam can exist before the second implementation arrives.&lt;/p&gt;

&lt;p&gt;But one implementation should at least make you ask a question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What variation am I modeling?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If the answer is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Well, someday we might...&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I become suspicious.&lt;/p&gt;

&lt;p&gt;Someday is responsible for a lot of software.&lt;/p&gt;

&lt;p&gt;Someday we'll support another database.&lt;/p&gt;

&lt;p&gt;Someday we'll have multiple cloud providers.&lt;/p&gt;

&lt;p&gt;Someday this will become a distributed system.&lt;/p&gt;

&lt;p&gt;Someday users will write plugins.&lt;/p&gt;

&lt;p&gt;Someday we'll support seventeen programming languages.&lt;/p&gt;

&lt;p&gt;Maybe.&lt;/p&gt;

&lt;p&gt;When someday becomes a requirement, we can design for someday with considerably more information than we have now.&lt;/p&gt;

&lt;p&gt;And if the current design makes that future literally impossible without rewriting the entire system, that's worth considering.&lt;/p&gt;

&lt;p&gt;But there is an enormous amount of territory between:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Don't make the future impossible.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;and:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Implement the future now.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;We seem to confuse those surprisingly often.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Plugin System Nobody Asked For
&lt;/h2&gt;

&lt;p&gt;Plugin systems are one of my favorite examples.&lt;/p&gt;

&lt;p&gt;Imagine adding plugins to Arid.&lt;/p&gt;

&lt;p&gt;What can a plugin do?&lt;/p&gt;

&lt;p&gt;Add a language?&lt;/p&gt;

&lt;p&gt;Change normalization?&lt;/p&gt;

&lt;p&gt;Replace duplicate detection?&lt;/p&gt;

&lt;p&gt;Add output formats?&lt;/p&gt;

&lt;p&gt;Filter findings?&lt;/p&gt;

&lt;p&gt;Modify configuration?&lt;/p&gt;

&lt;p&gt;Now we need a plugin API.&lt;/p&gt;

&lt;p&gt;Then we need to decide which internal concepts are public.&lt;/p&gt;

&lt;p&gt;Then those concepts need stability guarantees.&lt;/p&gt;

&lt;p&gt;Then plugins need version compatibility.&lt;/p&gt;

&lt;p&gt;Then failures need isolation.&lt;/p&gt;

&lt;p&gt;Then documentation.&lt;/p&gt;

&lt;p&gt;Then testing.&lt;/p&gt;

&lt;p&gt;Then somebody writes a plugin that depends on behavior we thought was an implementation detail.&lt;/p&gt;

&lt;p&gt;Congratulations.&lt;/p&gt;

&lt;p&gt;Our tiny duplicate-code checker now has an ecosystem to govern.&lt;/p&gt;

&lt;p&gt;For what requirement?&lt;/p&gt;

&lt;p&gt;There isn't one.&lt;/p&gt;

&lt;p&gt;A plugin architecture would not make Arid more flexible today.&lt;/p&gt;

&lt;p&gt;It would create an obligation to remain flexible tomorrow.&lt;/p&gt;

&lt;p&gt;That's different.&lt;/p&gt;

&lt;h2&gt;
  
  
  Generalization Can Make the Current Problem Worse
&lt;/h2&gt;

&lt;p&gt;There's another cost to premature abstraction that bothers me more than the extra code.&lt;/p&gt;

&lt;p&gt;It can make the abstraction &lt;strong&gt;less correct&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Arid's frontend is Python-aware for a reason.&lt;/p&gt;

&lt;p&gt;A comment isn't simply "text following a comment delimiter."&lt;/p&gt;

&lt;p&gt;A docstring isn't simply "a string."&lt;/p&gt;

&lt;p&gt;A function signature has Python-specific syntax.&lt;/p&gt;

&lt;p&gt;Structural context depends on Python syntax.&lt;/p&gt;

&lt;p&gt;If I had started by demanding a language-neutral abstraction, I would have needed to decide what all programming languages have in common before I had completely solved the Python problem.&lt;/p&gt;

&lt;p&gt;What is a &lt;code&gt;Function&lt;/code&gt; in the generic model?&lt;/p&gt;

&lt;p&gt;What is a &lt;code&gt;Comment&lt;/code&gt;?&lt;/p&gt;

&lt;p&gt;What is a &lt;code&gt;Docstring&lt;/code&gt; in a language that doesn't have docstrings?&lt;/p&gt;

&lt;p&gt;What is &lt;code&gt;StructuralScope&lt;/code&gt; across Python, Rust, JavaScript, SQL, and whatever somebody asks for next?&lt;/p&gt;

&lt;p&gt;Now we're not merely implementing duplicate detection.&lt;/p&gt;

&lt;p&gt;We're designing a theory of programming languages.&lt;/p&gt;

&lt;p&gt;All because somebody might want TypeScript someday.&lt;/p&gt;

&lt;p&gt;No thanks.&lt;/p&gt;

&lt;p&gt;Arid can understand Python correctly.&lt;/p&gt;

&lt;p&gt;If another language becomes a real requirement later, &lt;em&gt;then&lt;/em&gt; we can compare two concrete implementations and discover which concepts are genuinely shared.&lt;/p&gt;

&lt;p&gt;That's usually a much better time to generalize.&lt;/p&gt;

&lt;p&gt;The second implementation teaches you things the first one can't.&lt;/p&gt;

&lt;h2&gt;
  
  
  Duplication Isn't Always Worse Than the Wrong Abstraction
&lt;/h2&gt;

&lt;p&gt;This is where DRY can get us into trouble.&lt;/p&gt;

&lt;p&gt;We're trained to see duplication and eliminate it.&lt;/p&gt;

&lt;p&gt;I'm literally building a tool that finds duplicated code, so I'm probably supposed to be careful here.&lt;/p&gt;

&lt;p&gt;But eliminating duplication by creating the wrong abstraction can be worse than the duplication itself.&lt;/p&gt;

&lt;p&gt;Two pieces of code can look similar today and evolve for completely different reasons tomorrow.&lt;/p&gt;

&lt;p&gt;Combine them too early and you've coupled their futures.&lt;/p&gt;

&lt;p&gt;The same thing happens architecturally.&lt;/p&gt;

&lt;p&gt;We see two concepts that &lt;em&gt;might&lt;/em&gt; eventually share behavior, so we manufacture a common parent before we understand either one.&lt;/p&gt;

&lt;p&gt;Then reality arrives.&lt;/p&gt;

&lt;p&gt;One implementation needs a special case.&lt;/p&gt;

&lt;p&gt;Then another.&lt;/p&gt;

&lt;p&gt;The abstraction starts accumulating flags.&lt;/p&gt;

&lt;p&gt;Then optional methods.&lt;/p&gt;

&lt;p&gt;Then configuration.&lt;/p&gt;

&lt;p&gt;Eventually the "generic" abstraction is mostly a complicated description of the differences it was supposed to hide.&lt;/p&gt;

&lt;p&gt;Sometimes duplication is information.&lt;/p&gt;

&lt;p&gt;It tells you:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;These things look similar.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It does &lt;strong&gt;not&lt;/strong&gt; necessarily tell you:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;These things are the same concept.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's a decision we still have to make.&lt;/p&gt;

&lt;h2&gt;
  
  
  Extensibility Is a Feature
&lt;/h2&gt;

&lt;p&gt;We often talk about extensibility as though every system should have as much of it as possible.&lt;/p&gt;

&lt;p&gt;I don't think that's true.&lt;/p&gt;

&lt;p&gt;Extensibility is a product capability.&lt;/p&gt;

&lt;p&gt;Like any other capability, it has users, requirements, costs, and tradeoffs.&lt;/p&gt;

&lt;p&gt;If third-party developers need to extend your system without modifying it, extensibility may be essential.&lt;/p&gt;

&lt;p&gt;If your organization has five implementations behind a stable contract, abstraction may be essential.&lt;/p&gt;

&lt;p&gt;If you're publishing a framework whose entire purpose is to support unknown use cases, flexibility may be the product.&lt;/p&gt;

&lt;p&gt;But Arid is a CLI that finds duplicate Python code.&lt;/p&gt;

&lt;p&gt;Its value isn't proportional to the number of ways it can be extended.&lt;/p&gt;

&lt;p&gt;Its value comes from doing its one job correctly, quickly, and predictably.&lt;/p&gt;

&lt;p&gt;That changes the architecture I want.&lt;/p&gt;

&lt;h2&gt;
  
  
  Small Doesn't Mean Crude
&lt;/h2&gt;

&lt;p&gt;Architectural restraint doesn't mean throwing everything into one file until it becomes unbearable.&lt;/p&gt;

&lt;p&gt;Look at Arid's internal model.&lt;/p&gt;

&lt;p&gt;A prepared file owns its original source, normalized source, normalized lines, and segments.&lt;/p&gt;

&lt;p&gt;A normalized line carries things the detector and reporting pipeline genuinely need: its range in normalized text, original source line, whether it's effective, and its structural context and scope.&lt;/p&gt;

&lt;p&gt;A duplicate occurrence identifies a file and a normalized range.&lt;/p&gt;

&lt;p&gt;A duplicate group contains its effective size and occurrences.&lt;/p&gt;

&lt;p&gt;These are small types.&lt;/p&gt;

&lt;p&gt;They exist because the domain has those concepts.&lt;/p&gt;

&lt;p&gt;That's very different from introducing types whose primary purpose is to make the architecture look sophisticated.&lt;/p&gt;

&lt;p&gt;The test I increasingly like is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Can I explain why this abstraction exists without talking about a hypothetical future?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If I can say:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;We need this boundary because parsing Python and detecting repeated normalized sequences are different responsibilities.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Good.&lt;/p&gt;

&lt;p&gt;If I say:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;We need this because eventually we may support arbitrary parser backends selected dynamically from third-party plugins...&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I'm going to need considerably more evidence.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Happens When Requirements Change?
&lt;/h2&gt;

&lt;p&gt;The obvious objection is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Isn't this shortsighted? What happens when Arid needs another language?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Then I change the architecture.&lt;/p&gt;

&lt;p&gt;Seriously.&lt;/p&gt;

&lt;p&gt;Architecture is not a one-time ceremony performed before implementation begins.&lt;/p&gt;

&lt;p&gt;It's the structure of a living system.&lt;/p&gt;

&lt;p&gt;If Arid someday has a legitimate requirement to support Rust source, I'll have something incredibly valuable that I don't have today:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;a second real language implementation.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Then I can look at Python and Rust and ask:&lt;/p&gt;

&lt;p&gt;What is actually common?&lt;/p&gt;

&lt;p&gt;What must vary?&lt;/p&gt;

&lt;p&gt;Where does the boundary belong?&lt;/p&gt;

&lt;p&gt;Which assumptions in the Python implementation were language-specific?&lt;/p&gt;

&lt;p&gt;Which concepts really are universal?&lt;/p&gt;

&lt;p&gt;The abstraction designed from those answers is likely to be better than the one I invent today while staring at a single Python implementation.&lt;/p&gt;

&lt;p&gt;Will refactoring cost something?&lt;/p&gt;

&lt;p&gt;Of course.&lt;/p&gt;

&lt;p&gt;So does maintaining an unnecessary abstraction for three years waiting for a requirement that never arrives.&lt;/p&gt;

&lt;p&gt;Architecture is tradeoffs.&lt;/p&gt;

&lt;p&gt;There is no option where we pay nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  You Can Always Add Code Later
&lt;/h2&gt;

&lt;p&gt;This sounds ridiculously obvious, but software development sometimes behaves as if there's a code shortage.&lt;/p&gt;

&lt;p&gt;There isn't.&lt;/p&gt;

&lt;p&gt;If a requirement appears later, we're allowed to write more code.&lt;/p&gt;

&lt;p&gt;If a second implementation appears, we're allowed to extract an interface.&lt;/p&gt;

&lt;p&gt;If users need plugins, we're allowed to design a plugin model.&lt;/p&gt;

&lt;p&gt;If the pipeline needs asynchronous execution, we're allowed to introduce it.&lt;/p&gt;

&lt;p&gt;If Arid needs another language, we're allowed to refactor the frontend.&lt;/p&gt;

&lt;p&gt;We don't receive bonus points for having predicted every requirement five years early.&lt;/p&gt;

&lt;p&gt;In fact, predictions made too early can make the real requirement harder to implement because now it has to fit the imaginary one.&lt;/p&gt;

&lt;p&gt;The goal isn't to avoid changing the architecture.&lt;/p&gt;

&lt;p&gt;The goal is to make the architecture &lt;strong&gt;easy to change when reality gives us a reason to change it&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Those are very different objectives.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build the Architecture You Can Defend
&lt;/h2&gt;

&lt;p&gt;Arid 1.1.0 isn't architecturally small because I don't care about architecture.&lt;/p&gt;

&lt;p&gt;It's small &lt;strong&gt;because I do&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Its pipeline has boundaries.&lt;/p&gt;

&lt;p&gt;Its Python-specific parsing is isolated from duplicate detection.&lt;/p&gt;

&lt;p&gt;Its internal representation carries the information downstream stages actually need.&lt;/p&gt;

&lt;p&gt;Detection doesn't decide presentation.&lt;/p&gt;

&lt;p&gt;Structural metadata describes findings without changing duplicate identity.&lt;/p&gt;

&lt;p&gt;Those are architectural decisions.&lt;/p&gt;

&lt;p&gt;But there is no generic language framework.&lt;/p&gt;

&lt;p&gt;There is no detector hierarchy.&lt;/p&gt;

&lt;p&gt;There is no plugin system.&lt;/p&gt;

&lt;p&gt;There is no dependency-injection framework.&lt;/p&gt;

&lt;p&gt;There is no async runtime.&lt;/p&gt;

&lt;p&gt;Not because those things are bad.&lt;/p&gt;

&lt;p&gt;Because Arid doesn't currently have problems that they solve.&lt;/p&gt;

&lt;p&gt;That's the standard I want to apply more often:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Don't ask whether an abstraction could be useful. Ask what requirement makes it necessary.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you can't name one, maybe don't build it yet.&lt;/p&gt;

&lt;p&gt;Your small tool doesn't need a framework.&lt;/p&gt;

&lt;p&gt;It needs an architecture that makes the problem it actually solves obvious.&lt;/p&gt;

&lt;p&gt;Build that.&lt;/p&gt;

&lt;p&gt;When the problem changes, change the architecture.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/sponge-b0b/arid" rel="noopener noreferrer"&gt;Arid&lt;/a&gt;&lt;/strong&gt; is an open-source Python duplicate-code checker written in Rust. This article discusses the architecture of &lt;strong&gt;Arid 1.1.0&lt;/strong&gt;. Its intentionally small application pipeline can be seen in &lt;a href="https://github.com/sponge-b0b/arid/blob/v1.1.0/src/lib.rs" rel="noopener noreferrer"&gt;&lt;code&gt;lib.rs&lt;/code&gt;&lt;/a&gt;, and its core domain representation is in &lt;a href="https://github.com/sponge-b0b/arid/blob/v1.1.0/src/model.rs" rel="noopener noreferrer"&gt;&lt;code&gt;model.rs&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  About the Author
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Bob Taylor&lt;/strong&gt; is a software engineer and architect who builds developer tools and AI systems. He is currently developing &lt;a href="https://github.com/sponge-b0b/arid" rel="noopener noreferrer"&gt;Arid&lt;/a&gt; and &lt;a href="https://github.com/sponge-b0b/Polaris" rel="noopener noreferrer"&gt;Polaris&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/sponge-b0b" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>softwareengineering</category>
      <category>cleancode</category>
      <category>developertools</category>
    </item>
    <item>
      <title>Detecting Duplicate Python Code Is Harder Than Comparing Text</title>
      <dc:creator>Bob Taylor</dc:creator>
      <pubDate>Mon, 17 Aug 2026 16:59:54 +0000</pubDate>
      <link>https://dev.to/spongeb0b/detecting-duplicate-python-code-is-harder-than-comparing-text-3hdo</link>
      <guid>https://dev.to/spongeb0b/detecting-duplicate-python-code-is-harder-than-comparing-text-3hdo</guid>
      <description>&lt;h1&gt;
  
  
  Detecting Duplicate Python Code Is Harder Than Comparing Text
&lt;/h1&gt;

&lt;p&gt;&lt;em&gt;The hard part isn't finding repeated lines. It's deciding what "the same code" actually means.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Duplicate-code detection sounds easy.&lt;/p&gt;

&lt;p&gt;You have some source files. Find sequences of lines that occur more than once.&lt;/p&gt;

&lt;p&gt;How hard could it be?&lt;/p&gt;

&lt;p&gt;I asked essentially that question when I started building &lt;a href="https://github.com/sponge-b0b/arid" rel="noopener noreferrer"&gt;Arid&lt;/a&gt;, a duplicate-code checker for Python.&lt;/p&gt;

&lt;p&gt;The answer, as it often is in software, was: it depends on what you mean.&lt;/p&gt;

&lt;p&gt;What exactly is a duplicate?&lt;/p&gt;

&lt;p&gt;Consider these two functions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;save_customer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# Persist the customer
&lt;/span&gt;    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;serialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;database&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;save_account&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# Store the account
&lt;/span&gt;    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;serialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;database&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As text, they're different.&lt;/p&gt;

&lt;p&gt;As executable logic, they're the same.&lt;/p&gt;

&lt;p&gt;If I change the comment again, is it suddenly different code?&lt;/p&gt;

&lt;p&gt;What about the function name?&lt;/p&gt;

&lt;p&gt;What about a docstring?&lt;/p&gt;

&lt;p&gt;Imports?&lt;/p&gt;

&lt;p&gt;Blank lines?&lt;/p&gt;

&lt;p&gt;Decorators?&lt;/p&gt;

&lt;p&gt;Formatting?&lt;/p&gt;

&lt;p&gt;At some point duplicate-code detection stops being a string-comparison problem and becomes a language problem.&lt;/p&gt;

&lt;p&gt;That was one of the first lessons I learned building Arid.&lt;/p&gt;

&lt;h2&gt;
  
  
  First, Define "Duplicate"
&lt;/h2&gt;

&lt;p&gt;There are a lot of ways two pieces of code can be similar.&lt;/p&gt;

&lt;p&gt;These are obviously identical:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;calculate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;calculate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now change the variable name:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;calculate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;calculate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Are those duplicates?&lt;/p&gt;

&lt;p&gt;A human can look at them and reasonably say yes.&lt;/p&gt;

&lt;p&gt;Arid says no.&lt;/p&gt;

&lt;p&gt;That's deliberate.&lt;/p&gt;

&lt;p&gt;Arid 1.1.0 detects &lt;strong&gt;exact duplicate source after configured normalization&lt;/strong&gt;. It is not trying to determine whether two pieces of code are semantically equivalent, structurally similar, or suspiciously alike.&lt;/p&gt;

&lt;p&gt;Changing &lt;code&gt;value&lt;/code&gt; to &lt;code&gt;result&lt;/code&gt; changes the code being compared.&lt;/p&gt;

&lt;p&gt;Changing a literal from &lt;code&gt;10&lt;/code&gt; to &lt;code&gt;20&lt;/code&gt; changes it.&lt;/p&gt;

&lt;p&gt;Changing an expression changes it.&lt;/p&gt;

&lt;p&gt;That's an important boundary because "find code that means roughly the same thing" is a very different problem from "find source that has actually been duplicated."&lt;/p&gt;

&lt;p&gt;The first problem starts taking you toward AST similarity, clone classification, semantic analysis, and eventually a fairly interesting discussion about what "equivalent" even means.&lt;/p&gt;

&lt;p&gt;I wasn't trying to solve that problem.&lt;/p&gt;

&lt;p&gt;I wanted a fast replacement for the duplicate-code functionality I was using in Pylint.&lt;/p&gt;

&lt;p&gt;Pylint's similarity checker already has useful concepts for this. By default, it can exclude comments, docstrings, imports, and function signatures from similarity calculation. &lt;a href="https://pylint.readthedocs.io/en/stable/user_guide/configuration/all-options.html#similarities-checker" rel="noopener noreferrer"&gt;Those options are part of Pylint's similarity checker&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Arid keeps that general idea.&lt;/p&gt;

&lt;p&gt;The interesting part is figuring out how to do it correctly.&lt;/p&gt;

&lt;h2&gt;
  
  
  A &lt;code&gt;#&lt;/code&gt; Is Not Necessarily a Comment
&lt;/h2&gt;

&lt;p&gt;Let's start with comments.&lt;/p&gt;

&lt;p&gt;This looks easy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt;  &lt;span class="c1"&gt;# this is a comment
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Remove everything after &lt;code&gt;#&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Done.&lt;/p&gt;

&lt;p&gt;Until this shows up:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;# this is not a comment&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now our sophisticated duplicate-code checker has helpfully converted valid Python source into:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Excellent.&lt;/p&gt;

&lt;p&gt;You can keep adding increasingly clever text-processing rules, or you can ask Python what the thing actually is.&lt;/p&gt;

&lt;p&gt;Arid takes the second approach.&lt;/p&gt;

&lt;p&gt;Its Python frontend tokenizes the source and identifies tokens whose kind is actually &lt;code&gt;Comment&lt;/code&gt;. The comment's source range can then be removed from the representation used for duplicate matching.&lt;/p&gt;

&lt;p&gt;So:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;# this is not a comment&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;other&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt;  &lt;span class="c1"&gt;# actual comment
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;normalizes to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;# this is not a comment&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;other&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;when comments are ignored.&lt;/p&gt;

&lt;p&gt;The distinction seems obvious when you see the example.&lt;/p&gt;

&lt;p&gt;But that's the point.&lt;/p&gt;

&lt;p&gt;The distinction is obvious because &lt;strong&gt;you understand Python syntax&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A text processor doesn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  A String Is Not Necessarily a Docstring
&lt;/h2&gt;

&lt;p&gt;Docstrings get more interesting.&lt;/p&gt;

&lt;p&gt;Consider:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;calculate&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Calculate the current value.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If docstrings are configured to be ignored, we don't want that string to participate in duplicate identity.&lt;/p&gt;

&lt;p&gt;Now consider:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;calculate&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;This is an ordinary string expression.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Should that string disappear too?&lt;/p&gt;

&lt;p&gt;No.&lt;/p&gt;

&lt;p&gt;They're both string literals.&lt;/p&gt;

&lt;p&gt;Only one is a docstring.&lt;/p&gt;

&lt;p&gt;The difference isn't the quotes. The difference is &lt;strong&gt;where that expression exists in the Python program&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Arid identifies structural docstrings as string-expression statements in the docstring position of a module, class, or function body.&lt;/p&gt;

&lt;p&gt;So this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;module documentation&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Customer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;class documentation&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;method documentation&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
        &lt;span class="nf"&gt;persist&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;can have all three docstrings excluded from matching.&lt;/p&gt;

&lt;p&gt;But this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="nf"&gt;persist&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;ordinary string expression&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="nf"&gt;finish&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;keeps the string expression.&lt;/p&gt;

&lt;p&gt;You can't make that distinction reliably by looking for triple quotes.&lt;/p&gt;

&lt;p&gt;You have to understand the syntax tree.&lt;/p&gt;

&lt;p&gt;And now our simple "compare some lines" project has a parser.&lt;/p&gt;

&lt;p&gt;That escalated quickly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Function Signatures Are Worse Than They Look
&lt;/h2&gt;

&lt;p&gt;Ignoring function signatures sounds simple too.&lt;/p&gt;

&lt;p&gt;Remove the &lt;code&gt;def&lt;/code&gt; line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;calculate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Except Python doesn't require a function declaration to fit on one line.&lt;/p&gt;

&lt;p&gt;It can look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;calculate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;]],&lt;/span&gt;
    &lt;span class="n"&gt;multiplier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;multiplier&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;calculate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;]],&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There can be nested brackets, type annotations, default values, and plenty of colons inside those expressions before you reach the colon that actually terminates the function signature.&lt;/p&gt;

&lt;p&gt;So "remove everything through the first colon" isn't going to last very long.&lt;/p&gt;

&lt;p&gt;Arid walks the parser tokens beginning at &lt;code&gt;def&lt;/code&gt; or &lt;code&gt;async&lt;/code&gt;, tracks bracket nesting, and finds the colon that terminates the declaration at nesting level zero.&lt;/p&gt;

&lt;p&gt;Then it masks that source range.&lt;/p&gt;

&lt;p&gt;The body remains.&lt;/p&gt;

&lt;p&gt;There's another detail I like here.&lt;/p&gt;

&lt;p&gt;Decorators remain significant.&lt;/p&gt;

&lt;p&gt;Given:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@transactional&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nf"&gt;persist&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@cached&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nf"&gt;persist&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;ignoring function signatures doesn't silently erase the decorators.&lt;/p&gt;

&lt;p&gt;Arid removes the function declaration.&lt;/p&gt;

&lt;p&gt;It doesn't remove everything vaguely associated with the function.&lt;/p&gt;

&lt;p&gt;That distinction is easier to maintain when syntax tells you where the boundaries actually are.&lt;/p&gt;

&lt;h2&gt;
  
  
  Then There Are Imports
&lt;/h2&gt;

&lt;p&gt;Imports have their own collection of small traps.&lt;/p&gt;

&lt;p&gt;This:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;is easy.&lt;/p&gt;

&lt;p&gt;This:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;package&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;first&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;second&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;third&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;takes several physical lines.&lt;/p&gt;

&lt;p&gt;Imports can also occur inside control flow or functions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;optional_backend&lt;/span&gt;
    &lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If imports are ignored, Arid removes the import statement while retaining:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then somebody writes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you remove only the AST range belonging to &lt;code&gt;import os&lt;/code&gt;, you're left with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wonderful.&lt;/p&gt;

&lt;p&gt;Or:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So Arid's frontend deliberately consumes the appropriate adjacent semicolon when an ignored statement shares a logical line with retained code.&lt;/p&gt;

&lt;p&gt;The result in both cases is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's a tiny implementation detail.&lt;/p&gt;

&lt;p&gt;It's also exactly the kind of tiny implementation detail that separates "works on my example" from "works on Python source."&lt;/p&gt;

&lt;h2&gt;
  
  
  Normalization Is Not Parsing
&lt;/h2&gt;

&lt;p&gt;At this point it would be easy to let the parser take over the entire design.&lt;/p&gt;

&lt;p&gt;I didn't want that either.&lt;/p&gt;

&lt;p&gt;Arid uses Python syntax to answer questions that require knowledge of Python:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Is this actually a comment?&lt;/li&gt;
&lt;li&gt;Is this string expression actually a docstring?&lt;/li&gt;
&lt;li&gt;What source range belongs to this import?&lt;/li&gt;
&lt;li&gt;Where does this function signature end?&lt;/li&gt;
&lt;li&gt;Is this code associated with a module, class, or function?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then that knowledge crosses a boundary.&lt;/p&gt;

&lt;p&gt;The Python frontend converts parser-specific information into ordinary source ranges and structural regions owned by Arid.&lt;/p&gt;

&lt;p&gt;The normalization layer doesn't operate on AST nodes.&lt;/p&gt;

&lt;p&gt;The duplicate detector certainly doesn't.&lt;/p&gt;

&lt;p&gt;The basic relationship is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Python source
    ↓
parser + tokenizer
    ↓
source masks + structural regions
    ↓
normalized lines
    ↓
duplicate detector
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I think that distinction matters.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Python syntax determines what participates in comparison. It does not perform the comparison.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The parser is there because text alone can't reliably tell me which text should be ignored.&lt;/p&gt;

&lt;p&gt;Once that question has been answered, the rest of Arid doesn't need to know which parser answered it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Source You Compare Is Not the Source You Report
&lt;/h2&gt;

&lt;p&gt;Normalization creates another problem.&lt;/p&gt;

&lt;p&gt;Suppose the original code is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;calculate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;first&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;second&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Add the values
&lt;/span&gt;    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;first&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;second&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With comments and signatures ignored, the meaningful normalized representation might effectively be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;first&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;second&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's useful for matching.&lt;/p&gt;

&lt;p&gt;It's terrible for reporting if you lose the relationship to the original file.&lt;/p&gt;

&lt;p&gt;A developer doesn't want a diagnostic that says:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Duplicate found at normalized line 2.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Normalized line 2 does not exist in the file they're editing.&lt;/p&gt;

&lt;p&gt;Arid therefore keeps the original physical source-line location on every normalized line.&lt;/p&gt;

&lt;p&gt;The detector works with the normalized representation.&lt;/p&gt;

&lt;p&gt;The report maps the result back to the original Python source.&lt;/p&gt;

&lt;p&gt;That means ignored comments, docstrings, signatures, imports, and blank lines can disappear from duplicate identity without making the diagnostic point somewhere imaginary.&lt;/p&gt;

&lt;p&gt;This sounds like bookkeeping.&lt;/p&gt;

&lt;p&gt;It is bookkeeping.&lt;/p&gt;

&lt;p&gt;Bookkeeping is architecture when getting it wrong makes the product useless.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four Lines Isn't Always Four Lines
&lt;/h2&gt;

&lt;p&gt;There's another deceptively small question:&lt;/p&gt;

&lt;p&gt;What does &lt;code&gt;--min-lines 4&lt;/code&gt; mean?&lt;/p&gt;

&lt;p&gt;Four physical lines?&lt;/p&gt;

&lt;p&gt;Four normalized lines?&lt;/p&gt;

&lt;p&gt;Four lines containing actual code?&lt;/p&gt;

&lt;p&gt;Consider:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;values&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="n"&gt;first&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;second&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The closing bracket is a normalized physical line.&lt;/p&gt;

&lt;p&gt;But should &lt;code&gt;]&lt;/code&gt; count as one of the four meaningful lines required to declare a duplicate?&lt;/p&gt;

&lt;p&gt;Arid distinguishes a &lt;strong&gt;normalized line&lt;/strong&gt; from an &lt;strong&gt;effective line&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A line is effective when it contains at least one alphanumeric character or underscore.&lt;/p&gt;

&lt;p&gt;So punctuation-only lines can remain part of a repeated source sequence without artificially helping that sequence satisfy the configured minimum duplicate size.&lt;/p&gt;

&lt;p&gt;Blank lines don't count either.&lt;/p&gt;

&lt;p&gt;That means a reported four-line duplicate means four effective normalized lines satisfied the threshold, even if the corresponding physical source range spans more lines.&lt;/p&gt;

&lt;p&gt;Again, this isn't difficult because the algorithm is exotic.&lt;/p&gt;

&lt;p&gt;It's difficult because apparently simple words like &lt;strong&gt;line&lt;/strong&gt; turn out to require definitions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Python-Aware Does Not Mean Semantic
&lt;/h2&gt;

&lt;p&gt;This is probably the most important boundary in Arid's normalization model.&lt;/p&gt;

&lt;p&gt;Using a Python parser does &lt;strong&gt;not&lt;/strong&gt; mean Arid performs semantic duplicate detection.&lt;/p&gt;

&lt;p&gt;These two blocks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;calculate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;calculate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;are not duplicates to Arid.&lt;/p&gt;

&lt;p&gt;Nor are:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;ready&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;is_ready&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;They may represent the same pattern.&lt;/p&gt;

&lt;p&gt;They may deserve refactoring.&lt;/p&gt;

&lt;p&gt;They may even have been created by copy and paste followed by renaming a variable.&lt;/p&gt;

&lt;p&gt;Arid still says they're different.&lt;/p&gt;

&lt;p&gt;Why?&lt;/p&gt;

&lt;p&gt;Because the parser is being used to interpret Python syntax accurately, not to erase meaningful Python source until everything vaguely similar starts matching everything else.&lt;/p&gt;

&lt;p&gt;There's a line somewhere between useful normalization and inventing equivalence.&lt;/p&gt;

&lt;p&gt;As of Arid 1.1.0, that line sits in a fairly conservative place.&lt;/p&gt;

&lt;p&gt;Comments, structural docstrings, imports, and function signatures can be configured out.&lt;/p&gt;

&lt;p&gt;What remains must match exactly.&lt;/p&gt;

&lt;p&gt;Could Arid eventually detect renamed-variable clones, structural clones, or fuzzy AST similarity?&lt;/p&gt;

&lt;p&gt;Sure.&lt;/p&gt;

&lt;p&gt;It could also become an IDE, package manager, database, and small accounting system.&lt;/p&gt;

&lt;p&gt;The question isn't whether those things can be built.&lt;/p&gt;

&lt;p&gt;The question is whether they're the problem Arid is supposed to solve.&lt;/p&gt;

&lt;p&gt;For now, they're not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Description Is Not Identity
&lt;/h2&gt;

&lt;p&gt;Arid does use the Python structure for one other purpose: describing what it found.&lt;/p&gt;

&lt;p&gt;A duplicate can be reported as declarative or executable and associated with module, class, or function scope.&lt;/p&gt;

&lt;p&gt;For example, repeated class-level assignments might be described differently from repeated executable logic inside functions.&lt;/p&gt;

&lt;p&gt;But that metadata doesn't change duplicate identity.&lt;/p&gt;

&lt;p&gt;Two blocks don't become equal because they're both executable.&lt;/p&gt;

&lt;p&gt;They don't stop being equal because one occurs in a different structural context.&lt;/p&gt;

&lt;p&gt;First Arid answers:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Is this source duplicated?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Then it can help answer:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;What kind of source did I find?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I deliberately keep those questions separate.&lt;/p&gt;

&lt;p&gt;The moment classification starts deciding whether something "really counts" as duplication, the tool starts making domain judgments it doesn't have enough information to make.&lt;/p&gt;

&lt;p&gt;Repeated declarative code might be framework boilerplate.&lt;/p&gt;

&lt;p&gt;It might be accidental duplication.&lt;/p&gt;

&lt;p&gt;It might be exactly what the project wants.&lt;/p&gt;

&lt;p&gt;Arid doesn't know.&lt;/p&gt;

&lt;p&gt;Neither does the AST.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Algorithm Wasn't the First Hard Part
&lt;/h2&gt;

&lt;p&gt;Arid ultimately uses a generalized suffix array and longest-common-prefix analysis to find repeated normalized sequences.&lt;/p&gt;

&lt;p&gt;That's the algorithmically interesting part of the project, and I'll get into it separately.&lt;/p&gt;

&lt;p&gt;But something surprised me while building the tool.&lt;/p&gt;

&lt;p&gt;Before you can efficiently find repeated sequences, you have to decide &lt;strong&gt;what the sequence is&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That decision contains a surprising amount of the product's behavior.&lt;/p&gt;

&lt;p&gt;Do comments matter?&lt;/p&gt;

&lt;p&gt;Which strings are docstrings?&lt;/p&gt;

&lt;p&gt;Do imports matter?&lt;/p&gt;

&lt;p&gt;What is a function signature?&lt;/p&gt;

&lt;p&gt;What happens to decorators?&lt;/p&gt;

&lt;p&gt;What does a line mean?&lt;/p&gt;

&lt;p&gt;How do normalized lines map back to physical source?&lt;/p&gt;

&lt;p&gt;Does structural context affect equality?&lt;/p&gt;

&lt;p&gt;How much difference is allowed before code stops being "the same"?&lt;/p&gt;

&lt;p&gt;Those aren't suffix-array questions.&lt;/p&gt;

&lt;p&gt;They're product-definition questions.&lt;/p&gt;

&lt;p&gt;And they're language questions.&lt;/p&gt;

&lt;p&gt;The duplicate detector can only be as correct as the representation you give it.&lt;/p&gt;

&lt;p&gt;Feed it bad normalization quickly and all you've built is a very fast way to produce bad answers.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Parser Isn't the Point
&lt;/h2&gt;

&lt;p&gt;I started Arid because Pylint's duplicate-code checker was too slow for my workflow.&lt;/p&gt;

&lt;p&gt;I expected performance to be the interesting problem.&lt;/p&gt;

&lt;p&gt;And performance certainly mattered.&lt;/p&gt;

&lt;p&gt;But building the tool reinforced something I keep running into in software architecture:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Before optimizing the solution, define the problem precisely.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;"Find duplicate code" isn't precise enough.&lt;/p&gt;

&lt;p&gt;"Find exact repeated Python source after removing a configurable set of syntactically understood constructs" is considerably closer.&lt;/p&gt;

&lt;p&gt;Once that definition existed, a lot of architectural decisions became easier.&lt;/p&gt;

&lt;p&gt;Use Python syntax where Python syntax matters.&lt;/p&gt;

&lt;p&gt;Turn that knowledge into a small internal representation.&lt;/p&gt;

&lt;p&gt;Keep parser details out of the detector.&lt;/p&gt;

&lt;p&gt;Preserve the mapping back to the source developers actually edit.&lt;/p&gt;

&lt;p&gt;And don't quietly turn exact duplicate detection into semantic similarity because the parser happens to make that possible.&lt;/p&gt;

&lt;p&gt;It turns out duplicate-code detection is harder than comparing text.&lt;/p&gt;

&lt;p&gt;Not because comparing text is hard.&lt;/p&gt;

&lt;p&gt;Because deciding &lt;strong&gt;which text means what&lt;/strong&gt; is.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/sponge-b0b/arid" rel="noopener noreferrer"&gt;Arid&lt;/a&gt;&lt;/strong&gt; is an open-source Python duplicate-code checker written in Rust. This article describes the normalization model in &lt;strong&gt;Arid 1.1.0&lt;/strong&gt;. The implementation is available in &lt;a href="https://github.com/sponge-b0b/arid/blob/v1.1.0/src/normalize.rs" rel="noopener noreferrer"&gt;&lt;code&gt;normalize.rs&lt;/code&gt;&lt;/a&gt;, and the Python syntax frontend is in &lt;a href="https://github.com/sponge-b0b/arid/blob/v1.1.0/src/python.rs" rel="noopener noreferrer"&gt;&lt;code&gt;python.rs&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  About the Author
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Bob Taylor&lt;/strong&gt; is a software engineer and architect who builds developer tools and AI systems. He is currently developing &lt;a href="https://github.com/sponge-b0b/arid" rel="noopener noreferrer"&gt;Arid&lt;/a&gt; and &lt;a href="https://github.com/sponge-b0b/Polaris" rel="noopener noreferrer"&gt;Polaris&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/sponge-b0b" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>rust</category>
      <category>softwareengineering</category>
      <category>developertools</category>
    </item>
    <item>
      <title>Pylint Was Too Slow, So I Built Arid</title>
      <dc:creator>Bob Taylor</dc:creator>
      <pubDate>Sun, 16 Aug 2026 15:11:47 +0000</pubDate>
      <link>https://dev.to/spongeb0b/pylint-was-too-slow-so-i-built-arid-2h60</link>
      <guid>https://dev.to/spongeb0b/pylint-was-too-slow-so-i-built-arid-2h60</guid>
      <description>&lt;p&gt;I didn't set out to write a duplicate-code checker.&lt;/p&gt;

&lt;p&gt;I was working on &lt;a href="https://github.com/sponge-b0b/Polaris" rel="noopener noreferrer"&gt;Polaris&lt;/a&gt;, a fairly large Python project, and doing what I normally do: running code-quality tools against it. Ruff handles most of what I want from a Python linter these days, and it handles it very quickly.&lt;/p&gt;

&lt;p&gt;There was one problem.&lt;/p&gt;

&lt;p&gt;Ruff doesn't detect duplicated blocks of code.&lt;/p&gt;

&lt;p&gt;That isn't an oversight in my configuration. &lt;a href="https://github.com/astral-sh/ruff/issues/18432" rel="noopener noreferrer"&gt;Ruff simply doesn't support project-wide duplicate-code detection today&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Pylint does.&lt;/p&gt;

&lt;p&gt;Its &lt;code&gt;R0801&lt;/code&gt; checker has been finding similar code for years, and Pylint also exposes the same capability through its standalone &lt;a href="https://pylint.readthedocs.io/en/latest/additional_tools/symilar/index.html" rel="noopener noreferrer"&gt;&lt;code&gt;symilar&lt;/code&gt;&lt;/a&gt; tool. It can ignore comments, docstrings, imports, and function signatures while looking for repeated blocks.&lt;/p&gt;

&lt;p&gt;There was just one small problem.&lt;/p&gt;

&lt;p&gt;It was slow.&lt;/p&gt;

&lt;p&gt;On Polaris, it was &lt;em&gt;really&lt;/em&gt; slow.&lt;/p&gt;

&lt;p&gt;Eventually I got tired of waiting for it.&lt;/p&gt;

&lt;p&gt;So I built &lt;a href="https://github.com/sponge-b0b/arid" rel="noopener noreferrer"&gt;Arid&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Hard Could It Be?
&lt;/h2&gt;

&lt;p&gt;Those may be some of the most dangerous words in software development.&lt;/p&gt;

&lt;p&gt;At first glance, duplicate-code detection doesn't sound particularly complicated. Read some files, compare some lines, find the parts that repeat, print them out.&lt;/p&gt;

&lt;p&gt;Done.&lt;/p&gt;

&lt;p&gt;Except we aren't comparing text files. We're analyzing Python source code.&lt;/p&gt;

&lt;p&gt;Suppose these two functions contain the same executable logic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;save_customer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# Persist the current customer
&lt;/span&gt;    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;serialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;database&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;save_account&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# Store the account
&lt;/span&gt;    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;serialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;database&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Are they duplicates?&lt;/p&gt;

&lt;p&gt;If comments are ignored and function signatures are ignored, they probably should be.&lt;/p&gt;

&lt;p&gt;Now add docstrings. Imports. Blank lines. Decorators. Multiline function signatures. Parenthesized expressions. Different physical source ranges. Same-file duplicates that overlap one another.&lt;/p&gt;

&lt;p&gt;Suddenly "compare some lines" needs a little more definition.&lt;/p&gt;

&lt;p&gt;Pylint already has an answer to many of these questions, and I wasn't trying to invent a completely different meaning for duplicate code. Arid started with the intent of Pylint's &lt;code&gt;R0801&lt;/code&gt;: find repeated Python source while allowing things such as comments, docstrings, imports, and signatures to be excluded from the comparison.&lt;/p&gt;

&lt;p&gt;But I didn't need to reproduce Pylint's implementation.&lt;/p&gt;

&lt;p&gt;That distinction turned out to matter.&lt;/p&gt;

&lt;h2&gt;
  
  
  One Job
&lt;/h2&gt;

&lt;p&gt;Once I decided to build the tool, I had one significant advantage over Pylint.&lt;/p&gt;

&lt;p&gt;Arid only needed to do one thing.&lt;/p&gt;

&lt;p&gt;Pylint is a general-purpose static-analysis system. Duplicate-code detection is one capability among many.&lt;/p&gt;

&lt;p&gt;Arid has exactly one production responsibility:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Find duplicated Python source code.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's it.&lt;/p&gt;

&lt;p&gt;No formatting. No import sorting. No type checking. No complexity analysis. No security scanner. No dead-code detector.&lt;/p&gt;

&lt;p&gt;Ruff already does a lot of those things exceptionally well. I have no interest in building a slower, less capable Ruff just so Arid can have a longer feature list.&lt;/p&gt;

&lt;p&gt;In fact, one of the rules I settled on for Arid is fairly simple:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If a feature naturally belongs in Ruff, it probably doesn't belong in Arid.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That decision had architectural consequences.&lt;/p&gt;

&lt;p&gt;Arid supports one programming language, so it doesn't have a generic language abstraction.&lt;/p&gt;

&lt;p&gt;It has one detector, so it doesn't have a detector hierarchy.&lt;/p&gt;

&lt;p&gt;It doesn't have a plugin system.&lt;/p&gt;

&lt;p&gt;It doesn't have a reporter registry.&lt;/p&gt;

&lt;p&gt;It doesn't have a dependency-injection framework.&lt;/p&gt;

&lt;p&gt;It doesn't have an async runtime.&lt;/p&gt;

&lt;p&gt;And I did not create a parser abstraction just in case I wake up one morning three years from now and decide to replace the parser.&lt;/p&gt;

&lt;p&gt;I realize this may be shocking.&lt;/p&gt;

&lt;p&gt;The duplicate-code checker somehow manages to function anyway.&lt;/p&gt;

&lt;p&gt;Those choices are explicit in &lt;a href="https://github.com/sponge-b0b/arid/blob/main/docs/arid-v1-technical-architecture-and-design.md" rel="noopener noreferrer"&gt;Arid's technical architecture&lt;/a&gt;. Parser-specific knowledge stays in the Python frontend, while everything downstream operates on Arid-owned data structures rather than parser AST or token types.&lt;/p&gt;

&lt;p&gt;That isn't an argument against abstraction.&lt;/p&gt;

&lt;p&gt;It's an argument for paying for abstraction when you actually have a requirement for it.&lt;/p&gt;

&lt;p&gt;Every abstraction has a cost. More types. More indirection. More concepts somebody has to understand. More extension points that have to remain stable. More opportunities to design for a future that never arrives.&lt;/p&gt;

&lt;p&gt;If Arid eventually develops requirements that justify one of those abstractions, then I'll have a reason to build it.&lt;/p&gt;

&lt;p&gt;Until then, I have a duplicate-code checker to write.&lt;/p&gt;

&lt;h2&gt;
  
  
  Small Scope Does Not Mean a Trivial Problem
&lt;/h2&gt;

&lt;p&gt;Keeping the product small didn't make the underlying problem simple.&lt;/p&gt;

&lt;p&gt;Arid still has to understand enough Python syntax to distinguish source that participates in duplicate identity from source that may be ignored.&lt;/p&gt;

&lt;p&gt;The basic pipeline became:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Python source
    ↓
file discovery
    ↓
Python parse + tokenize
    ↓
Python-aware filtering + structural classification
    ↓
normalized source lines
    ↓
global exact-duplicate index
    ↓
maximal repeated blocks
    ↓
duplicate findings + metrics
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Python frontend uses syntax to identify comments, docstrings, imports, function declarations, structural scope, and their source ranges. It converts that parser-specific information into Arid's internal representation.&lt;/p&gt;

&lt;p&gt;After that boundary, the duplicate-detection engine doesn't know what a Python AST node is.&lt;/p&gt;

&lt;p&gt;That was deliberate.&lt;/p&gt;

&lt;p&gt;The parser is a dependency.&lt;/p&gt;

&lt;p&gt;Python is the domain.&lt;/p&gt;

&lt;p&gt;Those aren't the same thing.&lt;/p&gt;

&lt;p&gt;I want Arid to understand Python, but I don't want the entire application architecture to understand the implementation details of whichever parser happens to provide that information.&lt;/p&gt;

&lt;p&gt;There's another important distinction in the design: structural information describes a duplicate, but it does not determine whether two blocks are duplicates.&lt;/p&gt;

&lt;p&gt;Arid can tell you that repeated code is executable logic inside a function or declarative code associated with a class. That can be useful when deciding what deserves attention.&lt;/p&gt;

&lt;p&gt;But "this looks like framework boilerplate" is a judgment.&lt;/p&gt;

&lt;p&gt;So is "this duplicate is harmless."&lt;/p&gt;

&lt;p&gt;So is "you need to refactor this."&lt;/p&gt;

&lt;p&gt;Arid doesn't pretend to know your application's intent. Its job is to detect the duplication accurately and give you enough objective information to make your own decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  Exact Means Exact
&lt;/h2&gt;

&lt;p&gt;I also didn't want duplicate findings based solely on the assumption that two hashes being equal means two pieces of source code are equal.&lt;/p&gt;

&lt;p&gt;Hashes can be useful internally.&lt;/p&gt;

&lt;p&gt;They are not proof.&lt;/p&gt;

&lt;p&gt;Arid v1 defines duplication as exact equality after the configured Python-aware normalization. Hashing can be an implementation technique, but equality eventually has to resolve to actual equality.&lt;/p&gt;

&lt;p&gt;The global detection engine uses a generalized suffix array with longest-common-prefix analysis to identify repeated normalized source sequences. Arid then turns those candidates into maximal duplicate groups while handling things like overlapping occurrences and deterministic canonicalization.&lt;/p&gt;

&lt;p&gt;That probably deserves an article of its own.&lt;/p&gt;

&lt;p&gt;The important point here is that once duplicate detection became the entire problem instead of one feature inside a larger tool, I could choose an architecture specifically for that problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fast Was Still the Point
&lt;/h2&gt;

&lt;p&gt;All of this architectural discussion can make the project sound much more philosophical than it actually was.&lt;/p&gt;

&lt;p&gt;Let's not rewrite history.&lt;/p&gt;

&lt;p&gt;I built Arid because I was tired of waiting for Pylint.&lt;/p&gt;

&lt;p&gt;So eventually I had to answer the obvious question:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is it actually faster?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For Arid 1.0, I built a &lt;a href="https://github.com/sponge-b0b/arid/blob/main/benchmarks/README.md" rel="noopener noreferrer"&gt;reproducible benchmark suite&lt;/a&gt; around three real Python repositories at fixed revisions: Requests, Pydantic, and Polaris.&lt;/p&gt;

&lt;p&gt;The benchmark isolates Pylint's duplicate-code checker, pins tool versions and repository revisions, records environment metadata, uses repeated Hyperfine measurements, and distinguishes comparisons with approximately equivalent semantics from comparisons against tools whose clone-detection semantics differ.&lt;/p&gt;

&lt;p&gt;Against Pylint 4.0.6, the published Arid 1.0 measurements were:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Project&lt;/th&gt;
&lt;th&gt;Python files&lt;/th&gt;
&lt;th&gt;Arid vs. Pylint&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Requests&lt;/td&gt;
&lt;td&gt;37&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;196.79× faster&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pydantic&lt;/td&gt;
&lt;td&gt;404&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;192.88× faster&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Polaris&lt;/td&gt;
&lt;td&gt;1,452&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;264.45× faster&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;On the pinned Polaris benchmark, Arid completed the duplicate scan in about 442 milliseconds.&lt;/p&gt;

&lt;p&gt;Pylint took about 117 seconds.&lt;/p&gt;

&lt;p&gt;That's the difference between a check I don't mind running and one that interrupts my workflow.&lt;/p&gt;

&lt;p&gt;But benchmark numbers need context.&lt;/p&gt;

&lt;p&gt;Pylint is not standing still.&lt;/p&gt;

&lt;p&gt;Its upcoming &lt;a href="https://pylint.readthedocs.io/en/latest/whatsnew/4/4.1/index.html" rel="noopener noreferrer"&gt;4.1 release&lt;/a&gt; includes significant work on the duplicate-code checker: reuse of an already-parsed AST when running inside Pylint, rolling hashes with caching, and changes intended to avoid quadratic behavior in problematic inputs. The Pylint project reports improvements ranging from roughly 1.5× on small projects to 20× on large ones, along with lower memory usage.&lt;/p&gt;

&lt;p&gt;Good.&lt;/p&gt;

&lt;p&gt;I hope it gets even faster.&lt;/p&gt;

&lt;p&gt;Arid doesn't need Pylint to be bad in order for Arid to be useful.&lt;/p&gt;

&lt;p&gt;And those benchmark numbers are measurements of particular versions of two pieces of software on particular corpora.&lt;/p&gt;

&lt;p&gt;They are not a law of physics.&lt;/p&gt;

&lt;p&gt;If Arid's entire identity were "Pylint 4.0.6 is slow," the project would have a fairly short shelf life.&lt;/p&gt;

&lt;p&gt;The original &lt;em&gt;motivation&lt;/em&gt; was performance.&lt;/p&gt;

&lt;p&gt;The resulting tool has a broader reason to exist: focused, Python-aware duplicate-code detection that can sit next to Ruff without requiring a general-purpose linter for that one remaining job.&lt;/p&gt;

&lt;p&gt;Performance is still the reason I started.&lt;/p&gt;

&lt;p&gt;It just isn't the only engineering property I care about now.&lt;/p&gt;

&lt;h2&gt;
  
  
  Yes, I Built It With AI
&lt;/h2&gt;

&lt;p&gt;There's another part of the story worth being explicit about.&lt;/p&gt;

&lt;p&gt;I built Arid with ChatGPT as an AI coding partner.&lt;/p&gt;

&lt;p&gt;The same is true of Polaris.&lt;/p&gt;

&lt;p&gt;I use AI extensively in my development workflow. It helps generate code, examine designs, write tests, review changes, reason about problems, and accelerate implementation.&lt;/p&gt;

&lt;p&gt;I'm not particularly interested in pretending otherwise.&lt;/p&gt;

&lt;p&gt;What I have found, though, is that generating code and engineering software are not the same activity.&lt;/p&gt;

&lt;p&gt;The difficult parts don't disappear because an LLM can produce Rust.&lt;/p&gt;

&lt;p&gt;You still have to decide what the product is supposed to do.&lt;/p&gt;

&lt;p&gt;You have to define the invariants.&lt;/p&gt;

&lt;p&gt;You have to recognize a bad abstraction when one appears.&lt;/p&gt;

&lt;p&gt;You have to decide whether a result is actually correct.&lt;/p&gt;

&lt;p&gt;You have to build validation that doesn't merely prove the implementation agrees with itself.&lt;/p&gt;

&lt;p&gt;You have to benchmark honestly.&lt;/p&gt;

&lt;p&gt;You have to reject unnecessary code.&lt;/p&gt;

&lt;p&gt;And sooner or later, you have to decide that the thing is stable enough to put &lt;code&gt;1.0.0&lt;/code&gt; on it.&lt;/p&gt;

&lt;p&gt;AI changes how I write software.&lt;/p&gt;

&lt;p&gt;It does not remove the need to engineer it.&lt;/p&gt;

&lt;p&gt;That experience probably deserves an article too.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Tool I Wanted to Use
&lt;/h2&gt;

&lt;p&gt;Arid began with a very unremarkable engineering problem.&lt;/p&gt;

&lt;p&gt;I had a useful tool.&lt;/p&gt;

&lt;p&gt;One part of it was too slow for the way I wanted to work.&lt;/p&gt;

&lt;p&gt;The tool I preferred to use for the rest of my Python linting didn't provide that capability.&lt;/p&gt;

&lt;p&gt;So I wrote the missing piece.&lt;/p&gt;

&lt;p&gt;I didn't need another Python linter.&lt;/p&gt;

&lt;p&gt;I needed duplicate-code detection that was fast enough that I wouldn't think twice about running it.&lt;/p&gt;

&lt;p&gt;That constraint eventually led to a Rust implementation, Python-aware normalization, exact matching, deterministic output, a suffix-array-based detector, a reproducible benchmark suite, and a deliberately small architecture.&lt;/p&gt;

&lt;p&gt;But none of those things were the original idea.&lt;/p&gt;

&lt;p&gt;The original idea was much simpler:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I was tired of waiting.&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/sponge-b0b/arid" rel="noopener noreferrer"&gt;Arid&lt;/a&gt;&lt;/strong&gt; is open source and available on GitHub. If duplicate-code detection is part of your Python workflow, give it a try. Feedback, bug reports, and contributions are welcome.&lt;/p&gt;

&lt;h3&gt;
  
  
  About the Author
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Bob Taylor&lt;/strong&gt; is a software engineer and architect who builds developer tools and AI systems. He is currently developing &lt;a href="https://github.com/sponge-b0b/arid" rel="noopener noreferrer"&gt;Arid&lt;/a&gt;, a fast Python duplicate-code checker written in Rust, and &lt;a href="https://github.com/sponge-b0b/Polaris" rel="noopener noreferrer"&gt;Polaris&lt;/a&gt;, an AI-assisted portfolio intelligence platform.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/sponge-b0b" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>rust</category>
      <category>softwareengineering</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
