There’s a pattern that shows up when people build their first production CLI tool in Python and then try to give it to someone else: the moment they realize their tool assumes a working Python environment, all the ergonomics they built around click or typer suddenly matter less. One developer’s recent writeup documents exactly this progression, landing on Rust as the better fit. That conclusion is sound, but the reasons run deeper than startup speed or benchmark numbers.
The Startup Tax
Python’s startup overhead is a known limitation and one that’s rarely discussed honestly. The CPython interpreter itself takes around 50ms before your first line of code runs. That’s a floor, not a number you can optimize away from the application side. Import-heavy CLIs push further. A tool using click, rich, and a few utility libraries commonly clocks 200-400ms at startup on a reasonably fast machine, measured with a tool like hyperfine:
$ hyperfine 'python mytool.py --help'
Time (mean ± σ): 312.4 ms ± 8.7 ms
For a tool you run dozens of times a day, that overhead accumulates. For a tool called inside a shell loop or a CI pipeline, it compounds. An equivalent Rust binary typically measures in the 2-10ms range for the same operation, because Rust compiles to native code with no runtime initialization step.
This matters more than it sounds for developer tooling specifically. Tools like ripgrep and fd outperform their Unix counterparts in benchmarks, and part of that margin is startup time. When a tool runs in a tight loop over thousands of directories, 300ms per invocation is the difference between responsive and painful.
The Distribution Problem Python Never Solved
Startup time is real but discussable. The distribution problem is what actually forces the decision for many projects.
Shipping a Python CLI tool to end users who may or may not have Python installed is genuinely unresolved. The main options each carry their own category of failure:
-
Tell users to
pip install: This assumes Python is installed at the right version and that the user is comfortable with pip. For developer-facing tools inside a company, it works. For tools aimed at a broader audience, it’s a significant barrier. -
PyInstaller or Nuitka: These bundle the Python runtime and dependencies into a single executable. PyInstaller routinely produces bundles of 20-50MB for modest tools, triggers antivirus false positives because of how it extracts files at runtime, and can fail silently when native extensions are involved. Nuitka produces better-performing output but requires a C compiler and a significantly more complex build pipeline.
-
Shiv or PEX: Both package Python code into a zip-style archive that still requires a Python interpreter to execute. You’ve simplified distribution slightly without removing the runtime dependency.
None of these feel like a solved problem. Each one has a category of failure you encounter only when a user reports a bug that doesn’t reproduce in your environment.
Rust sidesteps this entirely. cargo build --release produces a statically linked binary. The user downloads it, makes it executable, puts it in their PATH, and it runs. No runtime to install, no shared libraries beyond the C standard library. With strip applied and opt-level = "z" set in Cargo.toml, a feature-rich CLI tool can weigh under 2MB:
[profile.release]
opt-level = "z"
strip = true
lto = true
codegen-units = 1
This is not theoretical. Tools like bat, delta, and starship are distributed this way. Their installation instructions amount to a single curl command or a GitHub release download, with no prerequisite software required on the user’s machine.
What Rust’s CLI Ecosystem Actually Looks Like
The practical ergonomics of building CLIs in Rust are better than the language’s reputation for complexity suggests. clap v4 introduced a derive macro API that defines argument structures through plain Rust structs, letting the library generate help text, short flags, value validation, and shell completion from the struct definition alone:
use clap::Parser;
#[derive(Parser, Debug)]
#[command(author, version, about)]
struct Args {
/// Path to process
#[arg(short, long)]
path: std::path::PathBuf,
/// Verbosity level
#[arg(short, long, action = clap::ArgAction::Count)]
verbose: u8,
/// Output format
#[arg(value_enum, default_value_t = Format::Text)]
format: Format,
}
#[derive(clap::ValueEnum, Clone, Debug)]
enum Format {
Text,
Json,
Csv,
}
The builder API is also available when you need programmatic construction. The gap between this and click or typer is narrower than people expect, and the compiler enforces correctness at every layer: a missing required argument is a compile error, not a runtime panic.
Surrounding clap, the ecosystem has matured into a coherent toolkit. indicatif handles progress bars, dialoguer covers interactive prompts, colored manages terminal color, crossterm provides cross-platform raw terminal access, and ratatui serves as the foundation for full TUI applications. The Python equivalents are more established but not meaningfully better in practice for most CLI use cases.
Cross-Compilation and Release Automation
One remaining practical advantage Python has is that pip install handles cross-platform distribution transparently. Python bytecode runs on any platform with an interpreter. With Rust, you need to build a separate binary for each target platform.
The cross tool addresses this by running Rust cross-compilation inside Docker containers with pre-configured toolchains:
cross build --target aarch64-unknown-linux-musl --release
cross build --target x86_64-pc-windows-gnu --release
cross build --target x86_64-apple-darwin --release
cargo-dist, from the team at Axo, automates the full release pipeline: it generates GitHub Actions workflows that build binaries for multiple targets, creates platform-appropriate installers (shell scripts, PowerShell, Homebrew formulae, MSI packages), and publishes everything to a GitHub release. The configuration is a handful of lines in Cargo.toml. For CLI tools specifically, this is a more complete and composable distribution story than anything the Python ecosystem offers end users.
Where Python Is Still the Right Answer
Reaching for Rust for every CLI tool would be wrong. Python is the correct choice when the tool is internal, when the team already maintains a Python codebase, or when the functionality is primarily about assembling library calls from Python’s rich ecosystem. A script that ingests CSV files, runs transformations with pandas, and writes reports belongs in Python. The distribution problem doesn’t apply if deployment is pip install inside a company Docker image that already has Python pre-installed.
The argument for Rust sharpens specifically when the tool crosses from internal scripting to external distribution. When you want to publish something to GitHub Releases and have someone download it without prerequisites, when you need the tool to appear in Homebrew or a Linux package manager, or when startup latency directly affects the user experience in a tight feedback loop, that’s where the calculus shifts decisively.
The original article frames this as Rust “shining,” which undersells what’s actually happening. Rust doesn’t just perform better here; its deployment model is structurally better suited to the CLI use case. Python’s distribution story has been improving for over a decade and remains genuinely unresolved for end-user tooling. Rust’s is, for practical purposes, solved.
The proliferating catalog of Rust tools replacing Unix classics, ripgrep over grep, fd over find, bat over cat, zoxide over plain cd, reflects developer communities arriving at this same conclusion independently. These tools exist in Rust not primarily because their authors preferred the language for its own sake, but because Rust is where the deployment story works out cleanly, without a 50MB bundle or a prerequisite runtime that half your users may not have.