<?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>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>
