Code Compression in Rust: Same Logic, Far Fewer Lines

The Pull Request That Made Me Stop and Stare

I am partway through porting the bot from Python to Rust, and I just opened a pull request that deletes more lines than it adds — by a wide margin. A whole module that had been roughly two hundred lines in Python lands at well under a hundred in Rust. My first instinct is suspicion. Did I forget to translate something? Did I cut a code path?

I walk through it line by line. Nothing is missing. The Rust file does everything the Python file did. It just does it without all the scaffolding I used to need.

This episode is about that scaffolding. About what disappears when you move from a language that trusts you at runtime to one that interrogates you at compile time. The honest answer — and I want to put this up front, because I have been burned by Rust evangelism that overstates its case — is that Rust is not always shorter than Python. Sometimes it is meaningfully longer. But for the kind of code I write inside a trading bot — error chains, validation, type-tagged identifiers, state machines around RPC calls — Rust compresses in ways that surprised me.

Why This Matters for the Bot Specifically

My bot has a particular shape. It pulls account data over RPC, parses bytes off-chain into typed structures, runs arbitrage math, builds transactions, signs, and submits. At every boundary I am converting from "some bytes I got off the wire" to "a struct I can trust." And every one of those conversions can fail.

In Python, that meant defensive code everywhere. Is this account the right owner? Is this number positive? Is this token mint the one we expected? Each function that touched the data re-asked those questions, because Python's type system is, as Henkel Data & Analytics put it, a set of "hints that developers can ignore" — the language "delegates the responsibility for type-safety to the development team". Even with type annotations and Pydantic, validation has to happen at runtime, and you have to remember to run it everywhere.

Rust does not work that way. The compiler runs that validation for me. And once a value has a type, every downstream function gets to assume the invariant has already been checked. That single shift — "the compiler becomes your validation engine," as entropicdrift.com phrases it — is where most of the line reduction comes from.

The Quote That Names the Problem

Before I show what compresses, I want to surface the line that crystallized the problem for me. From the corrode.dev essay on making illegal states unrepresentable:

"Enterprise code is full of defensive programming which can quickly lead to the program becoming like 50% error handling by volume, many of the errors being impossible to trigger because the app logic is validating a condition already checked in the validation layer."

That is exactly what my Python codebase looked like. I had not measured it, but eyeballing my files, I would guess somewhere around a third of every function was checking conditions that had already been checked one or two stack frames up. The check was cheap. The cognitive cost was not. Every time I read a function, I had to scroll past the guards to find the actual logic. Like wading through TSA twice to get from the gate to your seat.

The goal of the rewrite, then, is not to make the code shorter for its own sake. It is to make the business logic visible without a magnifying glass.

Mechanism One — The Question Mark Operator

The single biggest source of line reduction is the ? operator. It does three things in one character: unwrap a successful value, return early on failure, and convert the error type via the From trait if needed. In a function that calls four fallible operations in sequence, that means four explicit match blocks collapse into four trailing question marks.

The Blackwell Systems blog walks through three concrete cases I can verify by counting lines in their examples. A file-reading function written with explicit match arms runs about sixteen lines; rewritten with ?, it lands at five. A user-email-lookup function with three fallible calls runs about twenty-four lines with match; with ?, five again. An HTTP handler with three async fallible calls runs over thirty lines verbose; with ?, seven.

I am not going to recreate those code blocks here — go read the post if you want to see them — but the pattern is consistent. Anywhere a function chains together "do X, if it fails return the error; do Y, if it fails return the error; do Z, if it fails return the error," Rust gets to delete most of the words.

In my own code, the place this hits hardest is RPC chains. I fetch an account. I deserialize the account. I check the discriminator. I extract a sub-field. Each of those can fail in its own way. The Python equivalent has a try that wraps everything plus type checks inside, or it has a sequence of if x is None: return ... lines. The Rust equivalent has the same operations with a ? glued to the end of each.

This is not Rust being clever for cleverness's sake. It is the language saying: error propagation is a syntactically dominant pattern, so we'll give it a sigil instead of a paragraph.

Mechanism Two — The Newtype Pattern

In Python, every account address is a str. Every lamport amount is an int. Every token mint is a str. The language gives me no help distinguishing them. I can pass a wallet address into a function that wants a token mint and Python will happily run until something downstream complains, often with a message that does not point back to where I crossed the wires.

Rust gives me the newtype pattern. I wrap a primitive in a single-field struct, and now WalletAddress(Pubkey) and TokenMint(Pubkey) are different types. Passing one where the other is expected becomes a compile error. The ruggero.io guide calls this a "zero-cost abstraction" — at runtime there is no overhead, because the compiler erases the wrapper. At read-time, though, the wrapper is enormous: I no longer need a comment that says # id is a wallet address, not a mint, because the type says it for me.

In Python, you could simulate this with classes, but most people don't, because the ergonomic cost is real and the runtime check is just isinstance, which crashes late. So you end up with comments and naming conventions that are roughly as reliable as those signs at the gym that say "please re-rack your weights."

The newtype pattern by itself does not always shrink line counts. What it does is delete a category of bug that Python addresses with runtime checks and review discipline. The lines that disappear are the assertions, the comments, and the post-mortem fixes after the wires got crossed.

Mechanism Three — Parse, Don't Validate

This is the principle that turned the lights on for me. The phrase comes from a 2019 essay by Alexis King, building on Yaron Minsky's earlier "make illegal states unrepresentable" idea from a 2010 talk and Richard Feldman's elm-conf 2016 popularization. The version I keep in my head is the one from entropicdrift.com:

"Validation returns a boolean, parsing returns a value."

The email-validation example in that post shows the shape clearly. Imagine three functions — send a regular email, send a welcome email, send a password reset — each of which takes a String and starts with if !is_valid_email(&to) { return Err(...); }. Same check, three times, identical bodies. Forget to add it in a fourth function and you have a bug.

Now define an Email(String) type whose constructor performs the validation and returns a Result<Email, EmailError>. Change every function signature to take Email instead of String. Every guard line at the top of those functions disappears. Permanently. The compiler refuses to call those functions with anything that has not first passed through the constructor.

The same source frames the broader pattern as a boundary discipline: validate aggressively at the API boundary where untrusted bytes enter, convert into refined types, and from that point on every internal function trusts the type. The phrase from that post is

"Service layer trusts validated types — no re-validation needed."

For my bot, the boundaries are obvious. RPC responses come in as bytes. Off-chain instruction parsing produces typed structs. Once those parsers succeed, every downstream function — the math, the route construction, the transaction builder — works on types whose validity is already proven. None of them need to re-check that a Pubkey is well-formed, that a u64 is non-zero, or that an account discriminator matches.

In Python, the equivalent would be classes with __init__ validation, but with no compile-time enforcement that downstream functions actually require those classes. Someone could pass in a raw dict and the function would accept it until it tried to call a method that didn't exist. So the defensive code stays — just in case.

Mechanism Four — The Typestate Pattern

This one I am still learning to use, and I want to flag it as something I am exploring rather than something I have mastered. The idea is to encode state-machine states in the type itself. The ruggero.io guide gives the canonical example: a Connection<Closed> only has an open() method, and a Connection<Open> has send(), receive(), and close(). Trying to call send() on a closed connection is not a runtime error — it is a compile error.

The author's framing of why this matters is the part I keep coming back to:

"Calling send() on a Connection is a compile error. Ensures protocol adherence at compile time."

In my bot, the natural application is around transaction state. A transaction in the "unsigned" state should not be submittable. A transaction in the "submitted but unconfirmed" state should not be re-signable. In Python, these would be runtime checks, possibly buried in if-branches inside methods, possibly accompanied by exceptions with names like WrongStateError that exist purely because the language gives me no other way to express the constraint.

In Rust, the wrong call site never compiles in the first place. The lines that would have implemented the runtime check, raised the error, tested the error path, and documented why the error exists — all of those lines do not need to be written.

Mechanism Five — Option Replacing the None Dance

Python's None is everywhere and nowhere. Any reference can become None. Any function can return None whether or not its signature suggests it. The convention is to check before use, but as the JetBrains RustRover blog summarizes the issue, you have to remember to do that for every value that may become absent — and that does not scale to large codebases.

Rust's Option<T> is the same idea, but with teeth. A value is either Some(T) or None, and the compiler refuses to let you use it as T until you have handled the None case. Forgetting to check is not a runtime crash waiting to happen — it is a compile error, surfaced before the code ever runs.

The line-count effect is mixed. A match with Some and None arms is sometimes longer than a Python if x is not None. But the convenience methods on Optionunwrap_or, map, and_then, ok_or — let me express common patterns in a single line. The bigger win is not the count; it is that I no longer write a defensive if obj.field is None: return None at the top of helper functions, because the type already says whether the field is optional.

The Counter-Example I Owe You

If I only show wins, I am not being honest. So here is a case where Rust is straightforwardly more verbose than Python, drawn from rustinpieces.dev.

In Python, filtering and mapping a list is a one-liner: a list comprehension that filters and projects in a single expression. In Rust, the equivalent is into_iter().filter(...).map(...).collect::<Vec<_>>() — four lines if formatted readably, with explicit closure syntax and a turbofish for the collected type.

The author of that post does not pretend otherwise:

"Python's list comprehensions are more concise than Rust's map and filter functions in most cases."

For simple transformations, Python wins. For one-off scripts, glue code, and exploratory analysis, Python wins. The Rust ergonomic tax on a single map-filter-collect chain is real, and pretending it isn't would be cargo-culting Rust evangelism.

Where Rust wins, then, is not at the line of code level. It is at the codebase-shape level — the cumulative effect of the type system removing categories of code I would otherwise have to write and maintain.

The blog post by Justin Ellis at jellis18.github.io drives this home with another concrete number. To implement a Rust-style Result type in Python — Ok and Err classes with the methods that make the pattern actually usable — takes around fifty lines of boilerplate. In Rust it is built into the standard library. That is not a comparison about which language is shorter for a single function; it is a comparison about what you get for free versus what you have to construct.

What Compresses, Concretely, in My Bot

Let me ground this in the actual rewrite. I am not going to share specific files, parameters, or internal architecture, because that would hand competitors a roadmap for free. But the categories of code that shrink are repeatable across any project of this shape:

  • Account parsing. Each account type becomes a struct with a constructor that takes raw bytes and returns a Result. The constructor checks the discriminator, the size, the owner. After that, every downstream function takes the typed struct directly. The discriminator-check, size-check, owner-check lines that used to live at the top of every consumer function are gone.
  • Numeric guards. Quantities that must be non-zero, indices that must be in-range, percentages that must be between zero and one — each becomes a newtype with a fallible constructor. Downstream math gets to assume the invariant.
  • Address discrimination. Wallet addresses, mint addresses, vault addresses, program addresses — all distinct types over the same underlying primitive. Mismatches become compile errors instead of runtime puzzles.
  • State transitions. Anywhere a flow has phases — unsigned to signed, prepared to submitted, observed to confirmed — typestate transitions enforce ordering. The runtime guards that previously enforced ordering are deleted.
  • Error propagation. Every fallible function returns Result, and the ? operator linearizes the happy path. Nested try/except chains, accumulator-of-errors patterns, explicit early-return guards — all gone.

The file that prompted me to write this post had three of those categories firing at once. That is why the line count fell so dramatically. It is also why the file is easier to read now: when I open it, I see what the function does, not what the function defends against.

What Does Not Compress — and Why That's Fine

For balance, here is what is roughly the same length or longer in Rust:

  • Simple single-step transformations. A function that takes a number and returns a number. The Python and Rust versions are nearly identical, modulo the Result wrapper if the function can fail. The basic-divide example from the jellis18.github.io post is four lines in Python, five in Rust.
  • Throwaway scripts. Anything I would write once, run once, and delete. Python wins because the type tax has to be paid up front and the script never lives long enough to amortize it.
  • Exploratory data manipulation. REPL-driven shape-finding, where the code is the thinking. Python's iterator ergonomics are better tuned for this.
  • Glue code. Calling four libraries in sequence to produce a one-shot artifact. Python's looser conventions let you write less.

This is why I am not rewriting everything. The off-chain analysis scripts, the one-off backtests, the chart-makers — those stay in Python. The hot path, where invariants matter and bugs are expensive, moves to Rust. Different tools for different shifts, like cast iron for the steakhouse and a non-stick for Sunday morning eggs.

The Real Compression Isn't Vertical, It's Cognitive

The line counts are real, but they are not the most important number. The number that actually matters is how much of each function I have to read before I find the part that does the work. In the Python version, the answer was "more than I'd like." In the Rust version, the validation lives at the type boundary, the error propagation is a single character at the end of each call, and the function body is mostly business logic.

That is the compression I care about. Not lines per file but signal per line. When I open a Rust function, the first thing I see is what it does. When I opened the Python equivalent, the first thing I saw was what it was protecting itself from.

There is a cost to this. The type-design phase is harder in Rust. Getting the boundary types right is a real exercise — what should be a newtype, what should stay primitive, where to draw the parse-don't-validate line. I have spent hours in design that I would not have spent in Python. But that is exercise that pays back across every consumer of the type.

Which brings me back to the pull request that started this. Two hundred lines down to under a hundred is a real number, but the reason I am writing about it is not the number. It is that when I read the new file, I can see what the bot does. The defensive infrastructure that used to crowd out the logic has moved into the type system, where the compiler maintains it for me. I no longer have to.

Key Takeaways

  • Rust's compression versus Python is not uniform. Simple transformations and one-line iterator chains favor Python; long error chains, repeated validation, and state machines favor Rust dramatically.
  • The ? operator collapses fallible call chains by roughly seventy to eighty percent in the examples documented at Blackwell Systems — sixteen lines down to five, twenty-four down to five, thirty-plus down to seven.
  • The deeper compression mechanism is "parse, don't validate": move runtime checks into type constructors, and downstream code no longer has to repeat them.
  • Newtype and typestate patterns shrink code by deleting the assertions, comments, and runtime guards that would otherwise enforce the same invariants.
  • The honest framing is that Rust does not produce shorter functions — it produces a codebase shape with less defensive infrastructure, because the compiler is doing the defending.

Disclaimer

This article is for informational and educational purposes only and does not constitute financial, investment, legal, or professional advice. Content is produced independently and supported by advertising revenue. While we strive for accuracy, this article may contain unintentional errors or outdated information. Readers should independently verify all facts and data before making decisions. Company names and trademarks are referenced for analysis purposes under fair use principles. Always consult qualified professionals before making financial or legal decisions.