DEV Community

Cover image for Migrating from Python to Rust? Here's how to map your packages
Sagar Kashyap
Sagar Kashyap

Posted on

Migrating from Python to Rust? Here's how to map your packages

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'])
Enter fullscreen mode Exit fullscreen mode

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(())
}
Enter fullscreen mode Exit fullscreen mode

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(())
}
Enter fullscreen mode Exit fullscreen mode

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(),
    })
}
Enter fullscreen mode Exit fullscreen mode

🤖 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:

  1. Open any Python file.
  2. Set your target language to Rust in the status bar.
  3. Hover over any import statement (like import requests or import pandas).
  4. 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)

Collapse
 
nazar-boyko profile image
Nazar Boyko

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.

Collapse
 
sagar_kashyap_d20f1d5fa65 profile image
Sagar Kashyap

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!

Collapse
 
aldo_cve profile image
Aldo

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 HashMap with explicit error handling and careful lifecycle management in Rust due to its strong type system and ownership model. Similarly, moving from asyncio to something like tokio isn'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.

Collapse
 
sagar_kashyap_d20f1d5fa65 profile image
Sagar Kashyap

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!

Collapse
 
dipankar_sarkar profile image
Dipankar Sarkar

Solid cheat sheet. The row I would expand is pydantic. serde gives you the parse layer, but for the validation half you want garde or validator deriving 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.

Collapse
 
sagar_kashyap_d20f1d5fa65 profile image
Sagar Kashyap

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! 🙌

Collapse
 
nube_colectiva_nc profile image
Nube Colectiva

Cool 👍🏼

Collapse
 
sagar_kashyap_d20f1d5fa65 profile image
Sagar Kashyap

Thanks