5 Rust patterns that replaced my Python scripts
<p>I used to reach for Python every time I needed a quick script.<br> File renaming, log parsing, API polling, directory cleanup --<br> Python was the default because it was fast to write and good enough to run.</p> <p>That changed gradually.<br> Not because I decided to rewrite everything in Rust,<br> but because I kept running into the same friction points:<br> shipping the script to another machine, handling errors properly,<br> or running it somewhere Python wasn't available.</p> <p>Here are five patterns where Rust has genuinely replaced Python for me.</p> <h2> 1. Error handling that forces you to think </h2> <p>In Python, the path of least resistance is letting exceptions propagate and hoping for the best.<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight pytho
I used to reach for Python every time I needed a quick script. File renaming, log parsing, API polling, directory cleanup -- Python was the default because it was fast to write and good enough to run.
That changed gradually. Not because I decided to rewrite everything in Rust, but because I kept running into the same friction points: shipping the script to another machine, handling errors properly, or running it somewhere Python wasn't available.
Here are five patterns where Rust has genuinely replaced Python for me.
1. Error handling that forces you to think
In Python, the path of least resistance is letting exceptions propagate and hoping for the best.
import json
def load_config(path): with open(path) as f: return json.load(f)
config = load_config("config.json") print(config["database"]["host"])`
Enter fullscreen mode
Exit fullscreen mode
This crashes at runtime with a different error depending on which thing goes wrong: FileNotFoundError, JSONDecodeError, KeyError -- each one needs a different handler, and you usually find out the hard way.
In Rust, the type system prevents this from being an afterthought.
#[derive(Deserialize)] struct DatabaseConfig { host: String, }
#[derive(Deserialize)] struct Config { database: DatabaseConfig, }
fn load_config(path: &str) -> Result { let contents = fs::read_to_string(path) .with_context(|| format!("failed to read config from {path}"))?; let config: Config = serde_json::from_str(&contents) .context("failed to parse config JSON")?; Ok(config) }
fn main() -> Result<()> { let config = load_config("config.json")?; println!("{}", config.database.host); Ok(()) }`
Enter fullscreen mode
Exit fullscreen mode
The ? operator propagates errors without hiding them. anyhow::Context adds human-readable messages to each failure point. When something breaks, you get a proper error chain, not a bare traceback pointing at line 4.
The upfront cost is real -- you have to define your types and think about what can fail. The payoff is that production surprises mostly stop happening.
2. Type safety for configuration and data
Python's dynamic typing is convenient until you're debugging a config key that was renamed three months ago.
Enter fullscreen mode
Exit fullscreen mode
Rust forces you to be explicit about what your data looks like.
use serde::Deserialize;
#[derive(Deserialize)] struct NotificationConfig { webhook_url: String, #[serde(default = "default_timeout")] timeout_secs: u64, }
fn default_timeout() -> u64 { 30 }
fn send_notification(config: &NotificationConfig) { // webhook_url is guaranteed to exist // timeout_secs has a known default // these facts are checked at deserialization, not at call time }`
Enter fullscreen mode
Exit fullscreen mode
If the config schema changes, the compiler tells you everywhere it breaks. You don't find out at 2am when the script runs in production.
3. Single binary distribution
This is the practical one. Shipping a Python script means: "Install Python 3.11, pip install these dependencies, create a venv..." Then debug why it works on your machine and not theirs.
With Rust you cargo build --release and copy one file.
ship it
scp target/x86_64-unknown-linux-musl/release/my-tool user@server:/usr/local/bin/`
Enter fullscreen mode
Exit fullscreen mode
The musl target produces a fully statically linked binary. No shared libraries, no runtime, no dependency installation. It runs on any Linux machine with the same architecture.
This matters most for small operational tools that run in Docker containers or on servers where you don't control the environment. The binary is typically 3-8 MB and starts in under 10ms.
4. Cross-compilation that actually works
Related to the previous point, but worth calling out separately.
[target.x86_64-pc-windows-gnu] linker = "x86_64-w64-mingw32-gcc"`
Enter fullscreen mode
Exit fullscreen mode
build
cargo build --release --target aarch64-unknown-linux-gnu cargo build --release --target x86_64-pc-windows-gnu`
Enter fullscreen mode
Exit fullscreen mode
A GitHub Actions workflow that cross-compiles and uploads binaries on each release means you can install any tool with a single curl command, regardless of what machine you're on.
Python can cross-compile too (via PyInstaller and friends), but it's fragile and the output is bloated. Rust cross-compilation is boring and reliable. Boring and reliable is what you want in tooling.
5. Performance where it actually matters
Rust isn't always faster than Python in ways you'll notice. For most scripts, performance doesn't matter.
But there's a category where it does: anything processing large files, running in a tight loop, or doing work on every commit in a git hook. Here's a real example: a git hook that scans for accidentally committed secrets.
PATTERNS = [ r'AKIA[0-9A-Z]{16}', # AWS key r'sk-[a-zA-Z0-9]{48}', # OpenAI key ]
result = subprocess.run(['git', 'diff', '--staged'], capture_output=True, text=True) for pattern in PATTERNS: if re.search(pattern, result.stdout): print(f"Potential secret found: {pattern}") exit(1)`
Enter fullscreen mode
Exit fullscreen mode
fn main() { let patterns = [ Regex::new(r"AKIA[0-9A-Z]{16}").unwrap(), Regex::new(r"sk-[a-zA-Z0-9]{48}").unwrap(), ];
let output = Command::new("git") .args(["diff", "--staged"]) .output() .expect("failed to run git diff");
let diff = String::from_utf8_lossy(&output.stdout);
for pattern in &patterns { if pattern.is_match(&diff) { eprintln!("Potential secret found matching: {}", pattern.as_str()); std::process::exit(1); } } }`
Enter fullscreen mode
Exit fullscreen mode
The Rust version is roughly 10-20x faster on large diffs. On a repo with many files staged at once, that's the difference between a hook that feels instant and one that adds a noticeable pause to every commit.
When Python is still the right call
These are the cases where I still reach for Python without hesitation:
-
Data analysis and anything NumPy/Pandas -- the ecosystem has no real equivalent in Rust yet
-
One-off scripts I'll run once and delete -- setup cost doesn't pay off
-
Anything that needs heavy library support Rust doesn't have
-
Prototyping something where the shape isn't clear yet
The switch to Rust isn't ideological. It's about identifying the specific friction ahead of time -- distribution, correctness, performance, long-term maintenance -- and picking the tool that removes it.
Once you have a few patterns memorized, the startup cost drops fast. The first tool takes a weekend. The fifth takes an afternoon.
The code examples above are copy-paste ready. Starter templates for each pattern are at github.com/noxcraftdev.
Sign in to highlight and annotate this article

Conversation starters
Daily AI Digest
Get the top 5 AI stories delivered to your inbox every morning.
Knowledge Map
Connected Articles — Knowledge Graph
This article is connected to other articles through shared AI topics and tags.


Discussion
Sign in to join the discussion
No comments yet — be the first to share your thoughts!