<?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: Pal</title>
    <description>The latest articles on DEV Community by Pal (@pal11103).</description>
    <link>https://dev.to/pal11103</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%2F4064494%2F6f4f7f5b-f3c7-4a7a-9f1c-09664c16f659.png</url>
      <title>DEV Community: Pal</title>
      <link>https://dev.to/pal11103</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/pal11103"/>
    <language>en</language>
    <item>
      <title>How We Ported Python’s natsort to Rust — And the 133,000 Fuzz Inputs That Proved Us Wrong</title>
      <dc:creator>Pal</dc:creator>
      <pubDate>Wed, 05 Aug 2026 15:26:51 +0000</pubDate>
      <link>https://dev.to/pal11103/how-we-ported-pythons-natsort-to-rust-and-the-133000-fuzz-inputs-that-proved-us-wrong-5</link>
      <guid>https://dev.to/pal11103/how-we-ported-pythons-natsort-to-rust-and-the-133000-fuzz-inputs-that-proved-us-wrong-5</guid>
      <description>&lt;p&gt;&lt;strong&gt;A post-mortem on building natsort-rs, fixing a 45x performance bug, and catching subtle edge cases with a custom Pytest monkeypatch bridge.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Why natsort?&lt;/strong&gt;&lt;br&gt;
Porting Python code to Rust using the "Astral playbook" sounds straightforward on paper: rewrite the hot path, compile to native binary, profit. We chose Seth M. Morton’s popular natsort library—a ubiquitous utility for natural sorting (item2 before item10).&lt;br&gt;
In Python, natsort relies heavily on dynamic typing, tuple-based comparisons, and fallback numeric parsing (try_int, try_float). Translating Python’s dynamic Union[str, int, float] return types into Rust required introducing explicit tagged enums (ParsedComponent) and a hand-crafted Ord trait implementation in key.rs.&lt;br&gt;
Zero unsafe blocks. Pure memory safety. But getting it to build was only 10% of the battle.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;The Benchmark That Humbling Us (And the Fix)&lt;/strong&gt;&lt;br&gt;
When we ran our first full 10,000-item workload benchmark against Python's natsort, the result was embarrassing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Python natsort&lt;/strong&gt;: ~0.24s mean&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Our Compiled Rust Port&lt;/strong&gt;: ~0.90s mean&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;An native binary running nearly 4x slower than interpreted Python was unacceptable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Root Cause&lt;/strong&gt;: In our initial implementation, natsorted() recompiled the number-matching regular expression from scratch for every single item in the input array. For 10,000 items, we were recompiling regex 10,000 times.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Fix&lt;/strong&gt;: We refactored split.rs to compile the regex once per natsorted() invocation and pass a reference down through the key generation pipeline.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Real Result&lt;/strong&gt;: Latency dropped from ~900ms to ~20ms—shifting our port from 4x slower than Python to 11.8x faster on identical workloads. Disclosing this mistake is important: benchmarking without profiling is just guessing.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;The Edge Case That Ate Hours: Bare "nan" Tokens&lt;/strong&gt;&lt;br&gt;
To prove behavioral equivalence, we didn't just write unit tests—we built a differential fuzz harness (fuzz/differential_fuzz.py) that generated randomized strings, floats, and signs, feeding them simultaneously into real Python natsort and our Rust CLI.&lt;br&gt;
At iteration 125,000 (REAL mode, Seed 2), the fuzzer flagged a mismatch (1 out of 500 batches / 0.2% failure rate).&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The Input&lt;/strong&gt;: A bare text token "nAn" without any accompanying digits.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Python's Behavior&lt;/strong&gt;: Python's parse_string_factory applied try_float() to every split component—even ones that didn't match the numeric regex. Because Python’s float("nan") parses as NaN, Python treated "nAn" as a float and sorted it first under default NaN ordering.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Our Rust Port&lt;/strong&gt;: Our regex splitter only attempted float parsing on substrings containing digits or adjacent signs. It treated "nAn" as plain text, placing it alphabetically amidst text strings.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The Fix&lt;/strong&gt;: We rewrote split_into_components_with_regex to split into all pieces (matched and unmatched), uniformly apply float parsing, and insert NUMAFTER-aware separators between adjacent numeric components. We re-ran 133,000+ randomized inputs through the fuzzer across DEFAULT and REAL modes: 0 mismatches.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;The Pytest Bridge: What Fuzzing Missed&lt;/strong&gt;&lt;br&gt;
Fuzzing is great for random input distribution, but structured logic bugs require deterministic tests.&lt;br&gt;
Instead of re-writing natsort’s test suite in Rust (which risks recreating our own biases), we built a Pytest monkeypatch bridge in tests/original/conftest.py. It intercept calls to natsort.natsorted at module load time and shells out directly to our compiled Rust binary using raw bitmask CLI flags (--alg=).&lt;br&gt;
This bridge caught two major bugs that 133,000 fuzz items completely missed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The NUMAFTER Leading Placeholder Bug&lt;/strong&gt;: When an input string started with a number (e.g., "100_apples"), our sequence-alternation logic inserted a hardcoded empty string as a leading placeholder rather than a NUMAFTER-aware sentinel value. This masked NUMAFTER's effect entirely whenever the first component was numeric.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Missing PRESORT&lt;/strong&gt;: ns.PRESORT had zero implementation in our Rust port—an outright missing feature.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once both were fixed, running pytest test_natsorted.py directly against the unmodified upstream Python test file yielded 16 passed, 35 honestly skipped (unsupported locale/path flags), 0 failed, and 0 errors.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;What We Walked Back &amp;amp; Honest Disclosures&lt;/strong&gt;&lt;br&gt;
If you want honest numbers over confident claims, here is what natsort-rs does not do:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The Unicode Claim Correction&lt;/strong&gt;: We initially documented that Rust's char::to_digit(10) supported non-ASCII Unicode decimal digits (like Devanagari or Arabic-Indic numerals). A clippy review revealed char::to_digit is strictly ASCII-only at any radix. We updated our documentation to explicitly state ASCII-only numeral support rather than leaving an overclaim standing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Numeric Bounds&lt;/strong&gt;: We bounded integers to i64 and floats to f64. Numbers exceeding 9.2 × 10¹⁸ fall back gracefully to text comparison rather than arbitrary-precision integer comparisons.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Legacy OS Locales&lt;/strong&gt;: We explicitly skipped ns.LOCALE and os_sorted. Replicating 30-entry legacy OSX/BSD lookup tables or linking against heavy ICU C-libraries added build complexity for non-deterministic OS features.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Summary&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Codebase&lt;/strong&gt;: 0 unsafe blocks, clean clippy &amp;amp; fmt.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Testing&lt;/strong&gt;: 42 Rust-native tests (including 7 proptest property-based tests verifying Ord transitivity and reflexivity).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Parity&lt;/strong&gt;: 133,000+ fuzz items (0 mismatches) + Upstream Pytest suite bridge (16 passed, 0 failed).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Performance&lt;/strong&gt;: 9–12x speedup over Python.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Check out the code, benchmark methodology, and full 12-section decision log on GitHub:&lt;br&gt;
🔗 &lt;br&gt;
&lt;a href="//github.com/pal-123456789/port_mortem-natsort-rust"&gt;GitHub Link&lt;/a&gt;&lt;br&gt;
&lt;a href="https://x.com/i/status/2085012198171226326" rel="noopener noreferrer"&gt;X Link&lt;/a&gt;&lt;br&gt;
&lt;a href="https://youtu.be/pX7gl6dFbbY?si=ekWIXhqtSTd94e0h" rel="noopener noreferrer"&gt;YouTube Link&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Submitted for Hackathon Raptors: Port Mortem 2026 (Track D: Python -&amp;gt; Rust) 🦖&lt;/p&gt;

</description>
    </item>
  </channel>
</rss>
