20.03.2026
Python has long been the go-to language for scripting — from system automation to build pipelines and developer tooling. It’s expressive, widely available, and “good enough.” But as projects grow and performance or correctness starts to matter, Python’s limitations become friction. Rust offers a compelling alternative, and increasingly, a practical one.
This article explores two modes of using Rust for scripting: native Rust binaries and rust-script, and why you might reach for one or both in your day-to-day development work.
Python scripts are often temporary in spirit but permanent in practice. A quick automation script becomes load-bearing infrastructure. Problems that surface along the way include:
Rust solves all of these, with a tradeoff: it requires more upfront ceremony. Whether that tradeoff is worth it depends on the use case — and that’s exactly where the two Rust scripting modes shine in different situations.
A native Rust binary is simply a compiled Rust project used as a
script replacement. You write a main.rs, compile it, and
run it. There’s nothing exotic here — it’s standard Rust.
Startup time is near-zero. A native Rust binary starts in milliseconds. Compare this to Python, which must spin up an interpreter, import modules, and resolve dependencies before executing a single line of logic. For scripts that run hundreds of times a day — in CI pipelines, Git hooks, or watch processes — this adds up.
The compiler is your co-pilot. Rust’s type system
and borrow checker catch entire classes of bugs at compile time. Your
“script” won’t crash halfway through deleting files because you forgot
to handle a None case.
Cross-compilation is straightforward. You can
compile a script once and distribute a single static binary that runs
anywhere — no Python installation required, no pip install,
no virtualenv. This is invaluable for developer tooling shared across a
team.
Performance is unconditional. Native Rust is among the fastest execution environments available. If your script processes large files, spawns many subprocesses, or does any non-trivial computation, Rust will outperform Python by orders of magnitude.
A script that watches a directory, processes incoming files, and
dispatches build jobs — written in Python with watchdog and
subprocess — is a common pattern. In Rust, the same tool
using notify and std::process::Command
compiles to a single binary, handles errors explicitly, and runs with
near-zero overhead.
use std::process::Command;
fn main() {
let output = Command::new("git")
.args(["log", "--oneline", "-10"])
.output()
.expect("Failed to execute git");
println!("{}", String::from_utf8_lossy(&output.stdout));
}Even this trivial example illustrates the point: error handling is mandatory, the types are clear, and the result is a binary you can ship.
rust-script is a tool that lets
you run .rs files directly, like you would a Python script,
without a Cargo.toml or a project directory. It handles
compilation transparently and caches the result.
rust-script my_tool.rsYou can also declare dependencies inline, directly in the source file:
#!/usr/bin/env rust-script
//! ```cargo
//! [dependencies]
//! reqwest = { version = "0.11", features = ["blocking"] }
//! serde_json = "1"
//! ```
fn main() {
let body = reqwest::blocking::get("https://httpbin.org/get")
.unwrap()
.text()
.unwrap();
println!("{}", body);
}This is the Python scripting experience — a single file, run directly — with Rust underneath.
rust-script is technically still in alpha, and its API can change.
That said, it is remarkably stable for day-to-day use. The caching
mechanism means that subsequent runs of an unchanged script are nearly
instant. The inline dependency declaration is expressive and
unambiguous. And the full power of the Rust ecosystem —
serde, tokio, clap,
reqwest, and thousands of other crates — is available from
a single file.
The alpha status matters mainly for: automated tooling that depends on specific CLI flags, scripts used in production systems where you need guaranteed stability, and environments where you can’t control the version of rust-script installed. For everything else — local development, personal tooling, team scripts — it works exceptionally well.
rust-script supports Unix shebangs, which means you can make a
.rs file directly executable:
#!/usr/bin/env rust-script
fn main() {
println!("Hello from a Rust script");
}chmod +x my_script.rs
./my_script.rsThis brings the ergonomics as close to Python as possible. The file extension is the only visible difference.
One of the most natural patterns when working with rust-script day-to-day is letting the tool’s lifecycle determine its form. The workflow looks like this:
A new need arises — parsing some output, automating a repetitive
task, gluing two tools together. You reach for rust-script and write a
single .rs file. It’s quick, self-contained, and gets the
job done. You run it a few times, tweak it, maybe add a dependency
inline.
Over time, some of these scripts prove themselves. They stop being “that thing I wrote once” and become tools you rely on — canonical parts of your project or system. At that point, the script graduates: you compile it to a native binary and install it on your system.
# Compile and install directly from a rust-script file
rustc my_tool.rs -o ~/.local/bin/my_tool
# Or promote it to a proper Cargo project first
cargo new my_tool
# move the logic across, then:
cargo install --path .The promotion requires almost no rewriting. The Rust code you wrote
for rust-script is valid Rust — you’re mostly just adding the project
scaffolding around it. The inline [dependencies] block maps
directly to Cargo.toml. The logic moves over unchanged.
What this gives you is a natural filter: rust-script handles the
exploratory, volatile phase where scripts might be rewritten or
discarded. Native binaries handle the stable, load-bearing tools that
earn their place in $PATH. You don’t have to decide upfront
which category a script belongs to — you let usage tell you.
| Concern | Native Rust | rust-script |
|---|---|---|
| Startup time | Near-zero | First run: seconds. Cached: fast |
| Distribution | Single binary, no runtime needed | Requires rust-script installed |
| Dependencies | Cargo.toml |
Inline in the source file |
| Project overhead | Full Cargo project | Single .rs file |
| Stability | Fully stable | Alpha — but usable |
| Best for | Long-lived tools, CI, performance | Quick scripts, exploration, sharing |
The practical workflow is straightforward: start with rust-script when you need something fast, and graduate to a native Cargo project when the script grows up. The code itself rarely needs to change — you’re mostly just adding the project scaffolding around it.
One reason Python dominated scripting is its library ecosystem. Rust’s ecosystem, particularly for systems and developer tooling, is now extremely strong. Libraries like:
clap — argument parsing, better than
argparseserde +
serde_json/serde_yaml — serialization
that makes data wrangling trivialtokio — async runtime for concurrent
scripting tasksreqwest — HTTP clientwalkdir,
glob — filesystem traversalindicatif — progress bars and
spinnerscolored — terminal colors…mean that the “Python has a library for everything” advantage has largely closed for the use cases where scripting lives.
Python isn’t going away, and there are contexts — data science, rapid prototyping with ML frameworks, environments where Rust isn’t installed — where it remains the right choice. But for system scripting, developer tooling, and automation that lives inside software projects, Rust is no longer a stretch. It’s a practical, increasingly ergonomic choice.
Native Rust gives you maximum performance and correctness with zero runtime dependencies. rust-script gives you Python-like immediacy with Rust’s type safety and ecosystem underneath. Together, they cover the full scripting spectrum.
The next time you reach for Python to write a build tool or system
script, consider whether a .rs file might serve you better.
The compiler’s feedback loop is faster than you remember, and the
resulting binary will outlast the script it replaced.