Porting Python code to Rust is one of the most common performance optimization plays in modern software engineering.
Whether you are rebuilding a bottlenecked web service (moving from FastAPI to Actix), accelerating a data pipeline (moving from Pandas to Polars), or rewriting a CLI utility, the performance gains are massive. Rust services routinely run 10x to 100x faster while consuming a fraction of the RAM.
But once you install Rust and set up your Cargo.toml, you hit the first roadblock: dependency mapping.
Python's PyPI ecosystem and Rust's Crates.io ecosystem look completely different. Python code relies on dynamic runtime patterns and heavy frameworks, whereas Rust prioritizes compiled type-safety, explicit memory management, and modular crates.
To save you hours of browsing crates.io, here is the ultimate cheat sheet for mapping common Python packages to their Rust equivalents, followed by a way to automate this directly inside your editor.
📊 Python ➡️ Rust Crate Mapping Cheat Sheet
| Python Package | Rust Crate Equivalent | Why & How to Use It |
|---|---|---|
| requests | reqwest |
The undisputed standard for making HTTP requests in Rust. Supports both async and blocking calls. |
| pandas | polars |
Written natively in Rust, Polars is a lightning-fast DataFrame library. It’s so fast that Python developers actually import the Polars Python wrapper to speed up their Python code! |
| numpy | ndarray |
Provides n-dimensional arrays, matrix operations, and numerical computation helpers. |
| FastAPI / Flask |
axum or actix-web
|
Use Axum if you want a clean router backed by the Tokio team. Use Actix-web if you want one of the most mature and fastest web frameworks in the entire tech sector. |
| pydantic | serde |
In Rust, you don't need a heavy library for validation and serialization. You declare standard Rust structs and derive Serde (serde_json) for ultra-fast JSON serialization/deserialization. |
| pytest |
cargo test (built-in) |
Rust has testing built directly into the language and compiler. For property-based testing (like Pytest's hypothesis), use the proptest crate. |
| sqlite3 | rusqlite |
High-quality, ergonomic bindings to the SQLite database. |
| celery |
apalis or background-jobs
|
For running background task workers. Or, for simple concurrency, you can often just spawn background asynchronous tasks using tokio::spawn. |
🔍 In-Depth Mappings & Code Examples
1. HTTP Requests: requests ➡️ reqwest
In Python, fetching data from an API is famously simple:
import requests
response = requests.get('https://api.github.com/users/octocat')
data = response.json()
print(data['name'])
In Rust, Reqwest handles this asynchronously (using Tokio as the runtime). We pair it with Serde to safely parse the JSON into a strongly-typed struct:
use serde::Deserialize;
#[derive(Deserialize, Debug)]
struct GithubUser {
name: String,
}
#[tokio::main]
async fn main() -> Result<(), reqwest::Error> {
let user: GithubUser = reqwest::Client::new()
.get("https://api.github.com/users/octocat")
.header("User-Agent", "rust-app")
.send()
.await?
.json()
.await?;
println!("User Name: {}", user.name);
Ok(())
}
2. DataFrames: pandas ➡️ polars
If you are processing millions of rows of data, Rust's Polars will feel like moving from a bicycle to a rocket ship:
use polars::prelude::*;
fn main() -> Result<()> {
// Read a CSV and filter rows where age > 30
let df = CsvReader::from_path("users.csv")?
.has_header(true)
.finish()?
.lazy()
.filter(col("age").gt(lit(30)))
.collect()?;
println!("{}", df);
Ok(())
}
3. Web Frameworks: FastAPI ➡️ Axum
FastAPI is loved for its automatic type validation and clean path routing. In Rust, Axum uses a declarative handler system that feels very familiar to FastAPI developers, but runs with near-zero latency overhead:
use axum::{routing::get, Json, Router};
use serde::Serialize;
#[derive(Serialize)]
struct Status {
status: String,
}
#[tokio::main]
async fn main() {
let app = Router::new().route("/status", get(handler));
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
async fn handler() -> Json<Status> {
Json(Status {
status: "ok".to_string(),
})
}
🤖 How to automate this in VS Code
Instead of context-switching to browser tabs to find crate names and boilerplate code, you can use PackagePal.
It is a free VS Code extension that does the lookup for you directly inside your editor:
- Open any Python file.
- Set your target language to Rust in the status bar.
- Hover over any import statement (like
import requestsorimport pandas). - Instantly see the best crate equivalents, usage code snippets, and direct links to official documentation.
It supports 13 languages (including Rust, Go, Python, Node.js, and Java) and is completely private—it uses your own free Gemini API key stored securely in VS Code’s native secret storage.
Over to you!
If you've migrated Python services to Rust, what was the most difficult package mapping you had to write? Let me know in the comments below! 👇
If you found this cheat sheet helpful, check out *PackagePal on the VS Code Marketplace** and give the project a star on GitHub!*
Top comments (8)
One small thing worth flagging on the pydantic row: serde covers the serialize and deserialize half, but not the validation half that pydantic is really known for. If someone's leaning on pydantic for range checks and custom validators, they'll usually want something like the validator crate or garde on top of serde, since a plain derive rejects a badly shaped value but not a bad one. The rest of the table lines up with what I'd expect, reqwest and polars especially.
This is an excellent point and a major oversight on my part. You are 100% correct—Serde handles the structure parsing, but relying on standard derives will let bad data slip through. I’ll be updating the article to recommend pairing serde with validator or garde to match Pydantic's validation checks.
In fact, this feedback is so valuable that I'm currently designing an update for the PackagePal VS Code extension to suggest these companion validator crates automatically when a developer hovers over Pydantic. Thanks for calling this out!
We've seen a lot of teams tackling this kind of migration for performance-critical components in SaaS, especially backend services and data processing pipelines. While mapping Python packages to Rust crates is a crucial first step and a great way to start thinking about the transition, it's often where the real architectural decisions start to diverge from a simple 1:1 port. The why behind the migration usually dictates a much deeper re-evaluation of the solution design.
What often happens is you find yourself not just substituting a library, but fundamentally redesigning how data is handled, how state is managed, or how concurrency is achieved. A Python dictionary that was perfectly fine for quick lookups might need to become a
HashMapwith explicit error handling and careful lifecycle management in Rust due to its strong type system and ownership model. Similarly, moving fromasyncioto something liketokioisn't just a syntax change; it often requires a deeper understanding of futures, executors, and how your I/O operations are structured, especially in high-load services where subtle performance characteristics matter.The key takeaway from our experience has been to really scrutinize the why behind each port. Is it raw CPU performance, memory footprint, or better concurrency guarantees? Sometimes, the most pragmatic solution isn't a full port, but leveraging FFI to call Rust from Python for specific hot paths, creating a hybrid service. This allows you to gain the performance benefits where they're most needed without rewriting an entire, potentially stable, Python application, letting the Python part handle orchestration and less critical logic while Rust crunches numbers or manages high-throughput network connections.
Spot on. The 'async contagion' is a real thing in Rust and can catch developers off guard mid-migration. Highlighting the lock-in of tokio early in the decision-making process is crucial.
Also, your point about hybrid migration is very pragmatic. Most production teams choose a modular port via PyO3/FFI rather than a risky codebase rewrite. I'm going to update the extension's database to suggest PyO3/Maturin paths when developers are looking at performance-sensitive modules, rather than just suggesting standard Rust crates. Thank you for the architectural context!
Solid cheat sheet. The row I would expand is pydantic. serde gives you the parse layer, but for the validation half you want
gardeorvalidatorderiving on the deserialized struct. Worth splitting those two in people's heads so nobody expects serde to reject a negative age.The other thing that bites mid-migration is not the crate map, it is async coupling. Once one dependency pulls in tokio, half your other choices get made for you. And most teams never rewrite wholesale anyway. You carve the hot path out as a PyO3 extension and leave the rest in Python. The map still helps, you just apply it one module at a time.
Ah, great catch! Serde is awesome for parsing, but yeah, it won't stop a negative age or invalid email from slipping through. You definitely need validator or garde on top of it to match what Pydantic does. I'll update the table so it doesn't trip anyone up.
I'm actually adding this validation pairing logic directly into the next update of the PackagePal extension. We're also working on a migration doc generator (to scan workspaces and export markdown checklists) and adding PyO3/FFI hints for hybrid rewrites.
Appreciate the solid feedback! 🙌
Cool 👍🏼
Thanks