Convert Python to Rust
Convert Python code to idiomatic Rust. This skill extends meta-convert-dev with Python-to-Rust specific type mappings, idiom translations, and tooling for transforming dynamic, garbage-collected Python code into static, ownership-based Rust.
This Skill Extends
meta-convert-dev- Foundational conversion patterns (APTV workflow, testing strategies)
For general concepts like the Analyze → Plan → Transform → Validate workflow, testing strategies, and common pitfalls, see the meta-skill first.
This Skill Adds
- Type mappings: Python types → Rust types (dynamic → static)
- Idiom translations: Python patterns → idiomatic Rust
- Error handling: Exceptions → Result<T, E>
- Async patterns: asyncio → tokio/async-std
- Memory/Ownership: GC + dynamic typing → ownership + borrowing + static types
- Type system: Duck typing → generics + traits
This Skill Does NOT Cover
- General conversion methodology - see
meta-convert-dev - Python language fundamentals - see
lang-python-dev - Rust language fundamentals - see
lang-rust-dev - Reverse conversion (Rust → Python) - see
convert-rust-python
Quick Reference
| Python | Rust | Notes |
|---|---|---|
int |
i32, i64, i128, num_bigint::BigInt |
Python has arbitrary precision |
float |
f64 |
Default float |
bool |
bool |
Direct mapping |
str |
String, &str |
Owned vs borrowed |
bytes |
Vec<u8>, &[u8] |
Owned vs borrowed |
list[T] |
Vec<T> |
Growable array |
tuple |
(T, U, ...) |
Fixed-size tuple |
dict[K, V] |
HashMap<K, V>, BTreeMap<K, V> |
Hash vs ordered |
set[T] |
HashSet<T>, BTreeSet<T> |
Hash vs ordered |
None |
Option<T> |
Explicit nullable |
Union[T, U] |
enum { A(T), B(U) } |
Tagged union |
Callable[[Args], Ret] |
Fn(Args) -> Ret |
Function trait |
async def |
async fn |
Async function |
@dataclass |
#[derive(Debug, Clone)] struct |
Data classes |
Exception |
Result<T, E> |
Error handling |
When Converting Code
- Analyze source thoroughly before writing target
- Map types first - create type equivalence table
- Handle arbitrary-precision integers - decide if
i64is enough or if you needBigInt - Preserve semantics over syntax similarity
- Adopt Rust idioms - don't write "Python code in Rust syntax"
- Handle edge cases - None, exceptions, dynamic typing assumptions
- Test equivalence - same inputs → same outputs
Type System Mapping
Primitive Types
| Python | Rust | Notes |
|---|---|---|
int |
i32 |
Default for small integers |
int |
i64 |
Large integers (64-bit) |
int |
i128 |
Very large integers (128-bit) |
int |
num_bigint::BigInt |
Python default - arbitrary precision |
float |
f64 |
IEEE 754 double precision |
bool |
bool |
Direct mapping |
str |
String |
Owned, heap-allocated UTF-8 |
str |
&str |
Borrowed string slice |
bytes |
Vec<u8> |
Owned byte vector |
bytes |
&[u8] |
Borrowed byte slice |
bytearray |
Vec<u8> |
Mutable byte vector |
None |
Option<T> |
Use None variant |
... (Ellipsis) |
- | No direct equivalent |
Critical Note on Integers: Python's int type has arbitrary precision and never overflows. Rust integers are fixed-size and can overflow (panic in debug, wrap in release). Always validate range or use BigInt for Python-like behavior.
Collection Types
| Python | Rust | Notes |
|---|---|---|
list[T] |
Vec<T> |
Owned, growable, ordered |
tuple |
(T, U, ...) |
Fixed-size, immutable |
tuple[T, ...] |
Vec<T> |
Variable-length tuple → Vec |
dict[K, V] |
HashMap<K, V> |
Hash-based, unordered |
dict[K, V] |
BTreeMap<K, V> |
Tree-based, ordered |
set[T] |
HashSet<T> |
Hash-based, unique values |
set[T] |
BTreeSet<T> |
Tree-based, ordered unique |
frozenset[T] |
HashSet<T> |
Immutable by default in Rust |
collections.deque |
VecDeque<T> |
Double-ended queue |
collections.OrderedDict |
indexmap::IndexMap<K, V> |
Insertion-order map |
collections.defaultdict |
HashMap + entry() API |
Use or_insert() pattern |
collections.Counter |
HashMap<T, usize> |
Count occurrences |
Composite Types
| Python | Rust | Notes |
|---|---|---|
class (data) |
struct |
Data containers |
class (behavior) |
trait + impl |
Behavior contracts |
@dataclass |
#[derive(Debug, Clone)] struct |
Auto-derive common traits |
typing.Protocol |
trait |
Structural types → nominal traits |
typing.TypedDict |
struct |
Named fields |
typing.NamedTuple |
struct or tuple |
Prefer struct for clarity |
enum.Enum |
enum |
Algebraic data types |
typing.Literal["a", "b"] |
enum { A, B } |
Literal types → enums |
typing.Union[T, U] |
enum { A(T), B(U) } |
Tagged union |
typing.Optional[T] |
Option<T> |
Nullable types |
typing.Callable[[Args], Ret] |
Fn(Args) -> Ret |
Function types |
typing.Generic[T] |
<T> |
Generic types |
Type Annotations → Generics + Traits
| Python | Rust | Notes |
|---|---|---|
def f(x: T) -> T |
fn f<T>(x: T) -> T |
Unconstrained generic |
def f(x: Iterable[T]) |
fn f<T, I: IntoIterator<Item=T>> |
Trait bound |
def f(x: Sequence[T]) |
fn f<T>(x: &[T]) |
Slice for sequences |
x: Any |
Avoid - use generics | Any is a code smell |
x: object |
Avoid - use generics | No Object root in Rust |
Idiom Translation
Pattern 1: None Handling (Optional Chaining)
Python:
# Optional chaining with walrus operator
if user := get_user(user_id):
name = user.name
else:
name = "Anonymous"
# Or simpler
name = user.name if user else "Anonymous"
Rust:
// Option combinators
let name = get_user(user_id)
.map(|u| u.name.clone())
.unwrap_or_else(|| "Anonymous".to_string());
// Or with as_ref() to avoid moving
let name = get_user(user_id)
.as_ref()
.map(|u| u.name.as_str())
.unwrap_or("Anonymous");
Why this translation:
- Python uses truthiness (
if user) while Rust uses explicitOption<T> - Rust's combinator methods (
map,unwrap_or) are more explicit about handling theNonecase as_ref()convertsOption<T>toOption<&T>to avoid consuming the value
Pattern 2: List Comprehensions → Iterator Chains
Python:
# List comprehension
squared_evens = [x * x for x in numbers if x % 2 == 0]
# Generator expression
total = sum(x * x for x in numbers if x % 2 == 0)
Rust:
// Iterator chain (collect for Vec)
let squared_evens: Vec<i32> = numbers
.iter()
.filter(|x| *x % 2 == 0)
.map(|x| x * x)
.collect();
// Iterator chain (sum for aggregation)
let total: i32 = numbers
.iter()
.filter(|x| *x % 2 == 0)
.map(|x| x * x)
.sum();
Why this translation:
- Python list comprehensions are eager; Rust iterators are lazy (more efficient)
- Rust requires explicit
collect()to materialize into a collection - Terminal operations like
sum()consume the iterator automatically
Pattern 3: Dictionary Operations
Python:
# Get with default
value = config.get("timeout", 30)
# Setdefault pattern
cache.setdefault(key, expensive_compute())
# Dictionary comprehension
squared = {k: v * v for k, v in items.items()}
Rust:
// Get with default
let value = config.get("timeout").copied().unwrap_or(30);
// Entry API (doesn't compute if present)
let value = cache.entry(key).or_insert_with(|| expensive_compute());
// Collect from iterator
let squared: HashMap<K, i32> = items
.into_iter()
.map(|(k, v)| (k, v * v))
.collect();
Why this translation:
- Rust's
entry()API is more efficient than Python'ssetdefault()for expensive defaults or_insert_with()takes a closure, only calling it if the key is missing- Rust's iterator
collect()can build many collection types, includingHashMap
Pattern 4: String Formatting
Python:
# f-strings (Python 3.6+)
message = f"User {user.name} has {count} items"
# format method
message = "User {} has {} items".format(user.name, count)
# % formatting (old style)
message = "User %s has %d items" % (user.name, count)
Rust:
// format! macro (heap-allocated)
let message = format!("User {} has {} items", user.name, count);
// print! / println! macros (direct output)
println!("User {} has {} items", user.name, count);
// write! macro (into a buffer)
use std::fmt::Write;
let mut buf = String::new();
write!(&mut buf, "User {} has {} items", user.name, count).unwrap();
Why this translation:
- Rust's
format!macro is compile-time checked for type safety {}is the default placeholder; use{:?}for debug output,{:#?}for pretty-print- Rust doesn't have string interpolation; use macros instead
Pattern 5: Duck Typing → Traits
Python:
# Duck typing - if it has a .read() method, it's file-like
def process_data(file_like):
data = file_like.read()
return parse(data)
# Works with files, StringIO, BytesIO, etc.
Rust:
// Trait bounds - explicit interface
use std::io::Read;
fn process_data<R: Read>(mut reader: R) -> Result<Data, Error> {
let mut data = String::new();
reader.read_to_string(&mut data)?;
parse(&data)
}
// Works with File, Cursor, TcpStream, etc. (anything implementing Read)
Why this translation:
- Python relies on runtime checks (duck typing); Rust checks at compile time
- Rust traits are explicit contracts, catching errors early
- Generic functions in Rust are monomorphized (one compiled version per concrete type)
Pattern 6: Context Managers → RAII
Python:
# with statement for resource management
with open("data.txt") as f:
data = f.read()
# File automatically closed
# Custom context manager
with lock_held(mutex):
# Critical section
pass
# Lock automatically released
Rust:
// RAII - Drop trait handles cleanup
{
let f = File::open("data.txt")?;
let mut data = String::new();
f.read_to_string(&mut data)?;
// File automatically closed when f goes out of scope
}
// Mutex guard - RAII
{
let guard = mutex.lock().unwrap();
// Critical section - guard holds the lock
// Lock automatically released when guard is dropped
}
Why this translation:
- Python uses
__enter__/__exit__protocols; Rust usesDroptrait - Rust's ownership system guarantees cleanup at scope exit (compile-time enforced)
- No need for explicit
withstatement - scope-based cleanup is automatic
Pattern 7: Dynamic Attribute Access
Python:
# Dynamic attribute access
value = getattr(obj, "field", default)
setattr(obj, "field", value)
hasattr(obj, "field")
# Dynamic method calls
method = getattr(obj, method_name)
result = method(*args)
Rust:
// Static access only - use enums for dynamic behavior
enum Field {
Name(String),
Age(u32),
Email(String),
}
impl Object {
fn get_field(&self, field: &str) -> Option<Field> {
match field {
"name" => Some(Field::Name(self.name.clone())),
"age" => Some(Field::Age(self.age)),
"email" => Some(Field::Email(self.email.clone())),
_ => None,
}
}
}
// For true dynamic behavior, use HashMap
struct DynamicObject {
fields: HashMap<String, Value>,
}
Why this translation:
- Rust has no runtime reflection for dynamic attribute access
- Use enums for known variants,
HashMapfor truly dynamic data - Trade runtime flexibility for compile-time safety and performance
Pattern 8: Exception Chaining
Python:
# Exception chaining
try:
data = fetch_data(url)
except NetworkError as e:
raise ProcessingError(f"Failed to fetch {url}") from e
# Catching and re-raising
try:
risky_operation()
except Exception:
logger.error("Operation failed")
raise
Rust:
// Error conversion with context
fn fetch_data(url: &str) -> Result<Data, ProcessingError> {
let data = fetch(url)
.map_err(|e| ProcessingError::FetchFailed {
url: url.to_string(),
source: e,
})?;
Ok(data)
}
// Using anyhow for error context
use anyhow::Context;
fn fetch_data(url: &str) -> anyhow::Result<Data> {
fetch(url)
.context(format!("Failed to fetch {}", url))?;
Ok(data)
}
Why this translation:
- Rust doesn't have exception chaining; use nested error types or libraries like
anyhow map_err()transforms errors explicitly?operator propagates errors up the call stack (like re-raising)
Pattern 9: Multiple Return Values
Python:
# Tuple unpacking
def parse_coord(s):
parts = s.split(",")
return int(parts[0]), int(parts[1])
x, y = parse_coord("10,20")
Rust:
// Tuple return
fn parse_coord(s: &str) -> Result<(i32, i32), ParseError> {
let parts: Vec<&str> = s.split(',').collect();
let x = parts[0].parse()?;
let y = parts[1].parse()?;
Ok((x, y))
}
let (x, y) = parse_coord("10,20")?;
// Named struct (preferred for clarity)
#[derive(Debug)]
struct Coord { x: i32, y: i32 }
fn parse_coord(s: &str) -> Result<Coord, ParseError> {
let parts: Vec<&str> = s.split(',').collect();
Ok(Coord {
x: parts[0].parse()?,
y: parts[1].parse()?,
})
}
Why this translation:
- Both languages support tuple returns
- Rust prefers named structs for complex returns (better documentation, field names)
- Rust requires explicit error handling (hence
Result)
Pattern 10: Decorators → Macros or Trait Implementations
Python:
# Function decorator
@cache
def expensive_func(x):
return compute(x)
# Class decorator
@dataclass
class Point:
x: int
y: int
# Property decorator
@property
def full_name(self):
return f"{self.first} {self.last}"
Rust:
// Procedural macro (like class decorator)
#[derive(Debug, Clone, PartialEq)]
struct Point {
x: i32,
y: i32,
}
// Manual memoization (no decorator syntax)
use std::collections::HashMap;
use std::cell::RefCell;
thread_local! {
static CACHE: RefCell<HashMap<i32, i32>> = RefCell::new(HashMap::new());
}
fn expensive_func(x: i32) -> i32 {
CACHE.with(|cache| {
cache.borrow_mut().entry(x).or_insert_with(|| compute(x)).clone()
})
}
// Computed properties (no @property syntax)
impl Person {
fn full_name(&self) -> String {
format!("{} {}", self.first, self.last)
}
}
Why this translation:
- Rust has no decorator syntax; use
#[derive(...)]for common patterns - Function decorators require manual implementation or crates like
cached - Properties are just methods in Rust (no special syntax)
Error Handling
Python Exception Model → Rust Result Model
| Python | Rust | Notes |
|---|---|---|
raise Exception("error") |
return Err(Error::Message) |
Exceptions → Result |
try: ... except E: ... |
match result { Ok(v) => ..., Err(e) => ... } |
Pattern matching |
try: ... except: ... |
Anti-pattern - always specify error type | No catch-all |
try: ... finally: ... |
RAII / Drop trait | Automatic cleanup |
raise ... from ... |
Nested error types or anyhow::Context |
Error chains |
assert x, "msg" |
assert!(x, "msg") |
Panic for invariants |
Exception Hierarchy Translation
Python:
# Exception hierarchy
class AppError(Exception):
pass
class NetworkError(AppError):
def __init__(self, url, status):
self.url = url
self.status = status
super().__init__(f"Network error for {url}: {status}")
class ParseError(AppError):
def __init__(self, message):
self.message = message
super().__init__(message)
# Raising exceptions
if response.status_code != 200:
raise NetworkError(url, response.status_code)
# Catching exceptions
try:
data = fetch_and_parse(url)
except NetworkError as e:
log.error(f"Network error: {e.url} returned {e.status}")
retry()
except ParseError as e:
log.error(f"Parse error: {e.message}")
return None
Rust:
// Error enum with thiserror
use thiserror::Error;
#[derive(Debug, Error)]
enum AppError {
#[error("Network error for {url}: {status}")]
Network { url: String, status: u16 },
#[error("Parse error: {message}")]
Parse { message: String },
#[error(transparent)]
Io(#[from] std::io::Error),
}
// Returning errors
fn fetch(url: &str) -> Result<Data, AppError> {
let response = http_get(url)?;
if response.status() != 200 {
return Err(AppError::Network {
url: url.to_string(),
status: response.status(),
});
}
Ok(response.data())
}
// Handling errors
match fetch_and_parse(url) {
Ok(data) => process(data),
Err(AppError::Network { url, status }) => {
log::error!("Network error: {} returned {}", url, status);
retry()?;
}
Err(AppError::Parse { message }) => {
log::error!("Parse error: {}", message);
return None;
}
Err(e) => return Err(e),
}
Why this translation:
- Python uses exception inheritance; Rust uses enum variants
- Rust's
thiserrorcrate providesDisplayandErrortrait implementations ?operator propagates errors (like Python's exception unwinding)- Pattern matching is more explicit than try-except blocks
Error Propagation Patterns
Python:
# Implicit propagation (exception bubbles up)
def outer():
return inner() # Exceptions propagate automatically
def inner():
raise ValueError("error")
Rust:
// Explicit propagation with ?
fn outer() -> Result<Data, Error> {
let data = inner()?; // ? propagates Err variants
Ok(data)
}
fn inner() -> Result<Data, Error> {
Err(Error::Message("error".to_string()))
}
Why this translation:
- Python exceptions propagate implicitly; Rust requires explicit
?or pattern matching - Rust's approach forces you to think about error handling at each call site
- Type system ensures errors are handled or explicitly propagated
Async Patterns
Python asyncio → Rust tokio/async-std
| Python | Rust (tokio) | Notes |
|---|---|---|
async def f(): ... |
async fn f() { ... } |
Async function |
await coro |
coro.await |
Await syntax |
asyncio.run(coro) |
tokio::runtime::Runtime::new()?.block_on(coro) |
Run async code |
asyncio.gather(*coros) |
tokio::join!(coros) or futures::join_all |
Concurrent execution |
asyncio.create_task(coro) |
tokio::spawn(coro) |
Background task |
asyncio.sleep(secs) |
tokio::time::sleep(Duration::from_secs(secs)) |
Async sleep |
asyncio.wait_for(coro, timeout) |
tokio::time::timeout(duration, coro) |
Timeout |
asyncio.Queue |
tokio::sync::mpsc::channel |
Async channel |
asyncio.Lock |
tokio::sync::Mutex |
Async mutex |
Basic Async Function Translation
Python:
import asyncio
async def fetch_user(user_id: int) -> User:
async with aiohttp.ClientSession() as session:
async with session.get(f"/users/{user_id}") as response:
data = await response.json()
return User(**data)
# Running async code
async def main():
user = await fetch_user(123)
print(user)
asyncio.run(main())
Rust:
use tokio;
use reqwest;
async fn fetch_user(user_id: u32) -> Result<User, reqwest::Error> {
let url = format!("/users/{}", user_id);
let user = reqwest::get(&url)
.await?
.json::<User>()
.await?;
Ok(user)
}
// Running async code
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let user = fetch_user(123).await?;
println!("{:?}", user);
Ok(())
}
Why this translation:
- Both use
async/awaitsyntax - Python's context managers become RAII in Rust (automatic cleanup)
- Rust requires explicit error handling (
Result+?) #[tokio::main]macro sets up the async runtime automatically
Concurrent Execution
Python:
# asyncio.gather for concurrent execution
users, orders = await asyncio.gather(
fetch_users(),
fetch_orders()
)
# asyncio.create_task for background tasks
task1 = asyncio.create_task(fetch_users())
task2 = asyncio.create_task(fetch_orders())
users = await task1
orders = await task2
Rust:
// tokio::join! for concurrent execution (fixed number)
let (users, orders) = tokio::join!(
fetch_users(),
fetch_orders()
);
// tokio::spawn for background tasks
let task1 = tokio::spawn(fetch_users());
let task2 = tokio::spawn(fetch_orders());
let users = task1.await??; // First ? for JoinError, second for task error
let orders = task2.await??;
// futures::join_all for dynamic list
use futures::future::join_all;
let tasks: Vec<_> = ids.into_iter().map(fetch_user).collect();
let users = join_all(tasks).await;
Why this translation:
tokio::join!is macro-based (compile-time), similar toasyncio.gathertokio::spawncreates a separate task (likecreate_task)- Spawned tasks return
JoinHandle, requiring double??to unwrap both join and task errors
Async Streams/Generators
Python:
# Async generator
async def fetch_pages(url: str):
page = 1
while True:
response = await fetch(f"{url}?page={page}")
if not response.ok:
break
yield await response.json()
page += 1
# Consuming async generator
async for page in fetch_pages(url):
process(page)
Rust:
// Async stream (using async-stream crate)
use async_stream::stream;
use futures::stream::Stream;
fn fetch_pages(url: String) -> impl Stream<Item = Result<Page, Error>> {
stream! {
let mut page = 1;
loop {
let response = fetch(&format!("{}?page={}", url, page)).await?;
if !response.status().is_success() {
break;
}
yield response.json::<Page>().await?;
page += 1;
}
}
}
// Consuming async stream
use futures::stream::StreamExt;
let mut pages = fetch_pages(url);
while let Some(result) = pages.next().await {
match result {
Ok(page) => process(page),
Err(e) => eprintln!("Error: {}", e),
}
}
Why this translation:
- Python's
async for→ Rust'sStreamExt::next()in a loop - Rust requires
async-streamcrate for generator-like syntax - Streams yield
Resultfor error handling (Python would raise exceptions)
Cancellation and Timeouts
Python:
# Timeout with asyncio.wait_for
try:
result = await asyncio.wait_for(fetch_data(url), timeout=5.0)
except asyncio.TimeoutError:
print("Request timed out")
# Manual cancellation
task = asyncio.create_task(long_operation())
# ... later
task.cancel()
try:
await task
except asyncio.CancelledError:
print("Task was cancelled")
Rust:
// Timeout with tokio::time::timeout
use tokio::time::{timeout, Duration};
match timeout(Duration::from_secs(5), fetch_data(url)).await {
Ok(Ok(result)) => println!("Success: {:?}", result),
Ok(Err(e)) => println!("Request failed: {}", e),
Err(_) => println!("Request timed out"),
}
// Manual cancellation via drop
let handle = tokio::spawn(long_operation());
// ... later
handle.abort(); // Cancel the task
match handle.await {
Ok(result) => println!("Completed: {:?}", result),
Err(e) if e.is_cancelled() => println!("Task was cancelled"),
Err(e) => println!("Task failed: {}", e),
}
Why this translation:
- Python uses
asyncio.wait_for; Rust usestokio::time::timeout - Rust's
timeoutreturnsResult<Result<T, E>, Elapsed>(nested Results) - Cancellation in Rust happens via
abort()onJoinHandle
Memory & Ownership
Python GC → Rust Ownership
| Python Model | Rust Model | Translation |
|---|---|---|
| Reference counting + cycle detection | Ownership + borrowing | Explicit ownership transfer |
| Shared references everywhere | &T (immutable) or &mut T (mutable) |
Borrow checker enforces aliasing rules |
| Mutable by default | Immutable by default (let vs let mut) |
Explicit mutability |
| No lifetime tracking | Explicit lifetimes ('a) |
Compiler ensures references are valid |
del or rely on GC |
Drop trait (RAII) |
Automatic, deterministic cleanup |
Ownership Decision Patterns
Python (shared references):
# Python allows multiple mutable references
class Cache:
def __init__(self):
self.data = {}
def get(self, key):
return self.data.get(key)
def set(self, key, value):
self.data[key] = value
# Multiple references to cache
cache = Cache()
ref1 = cache
ref2 = cache
ref1.set("key", "value")
print(ref2.get("key")) # Works fine
Rust (explicit ownership):
use std::collections::HashMap;
struct Cache {
data: HashMap<String, String>,
}
impl Cache {
fn new() -> Self {
Self { data: HashMap::new() }
}
// Borrow immutably (read-only)
fn get(&self, key: &str) -> Option<&String> {
self.data.get(key)
}
// Borrow mutably (write access)
fn set(&mut self, key: String, value: String) {
self.data.insert(key, value);
}
}
// Single owner, multiple borrows
let mut cache = Cache::new();
cache.set("key".to_string(), "value".to_string());
println!("{:?}", cache.get("key"));
// For shared ownership, use Rc/Arc
use std::rc::Rc;
use std::cell::RefCell;
let cache = Rc::new(RefCell::new(Cache::new()));
let ref1 = Rc::clone(&cache);
let ref2 = Rc::clone(&cache);
ref1.borrow_mut().set("key".to_string(), "value".to_string());
println!("{:?}", ref2.borrow().get("key"));
Why this translation:
- Python's GC allows unrestricted shared mutable state
- Rust enforces "either one mutable reference OR many immutable references"
- For Python-like shared mutability, use
Rc<RefCell<T>>(single-threaded) orArc<Mutex<T>>(multi-threaded)
Avoiding Clone Overhead
Python (cloning is implicit and cheap):
def process_items(items):
# Items can be passed around freely
for item in items:
handle(item)
transform(item)
Rust (explicit borrowing to avoid clones):
// BAD: Unnecessary cloning
fn process_items(items: Vec<Item>) {
for item in items.clone() { // Clones entire vector!
handle(&item);
transform(&item);
}
}
// GOOD: Borrow instead
fn process_items(items: &[Item]) {
for item in items {
handle(item); // item is &Item
transform(item);
}
}
// If mutation needed, use &mut
fn process_items_mut(items: &mut [Item]) {
for item in items {
transform_in_place(item); // item is &mut Item
}
}
Why this translation:
- Python's reference counting makes passing references cheap
- Rust's ownership requires explicit choices: move, borrow, or clone
- Prefer borrowing (
&T,&mut T) over cloning for performance
Lifetime Elision and Annotations
Python (no lifetime concept):
class Parser:
def __init__(self, source):
self.source = source
def parse(self):
# Can reference self.source freely
return self.source.split()
Rust (explicit lifetimes):
// Lifetime elision - compiler infers lifetimes
struct Parser<'a> {
source: &'a str,
}
impl<'a> Parser<'a> {
fn new(source: &'a str) -> Self {
Self { source }
}
fn parse(&self) -> Vec<&'a str> {
self.source.split_whitespace().collect()
}
}
// The 'a lifetime ties the parser to the source string
// Parser cannot outlive the source
Why this translation:
- Python's GC allows references to outlive their source
- Rust's borrow checker prevents dangling references at compile time
- Explicit lifetimes document reference validity constraints
Type System Translation
Duck Typing → Generics + Traits
Python (duck typing):
# Any object with .read() method works
def process_file(file_like):
data = file_like.read()
return parse(data)
# Works with files, StringIO, BytesIO, etc.
with open("data.txt") as f:
process_file(f)
Rust (trait bounds):
use std::io::Read;
fn process_file<R: Read>(mut reader: R) -> Result<Data, Error> {
let mut data = String::new();
reader.read_to_string(&mut data)?;
parse(&data)
}
// Works with File, Cursor, TcpStream, etc.
let f = File::open("data.txt")?;
process_file(f)?;
Why this translation:
- Python checks method existence at runtime (duck typing)
- Rust checks trait implementation at compile time
- Generics with trait bounds provide type safety without runtime overhead
TypedDict / NamedTuple → Struct
Python:
from typing import TypedDict, NamedTuple
# TypedDict (Python 3.8+)
class User(TypedDict):
id: int
name: str
email: str
# NamedTuple
class Point(NamedTuple):
x: int
y: int
user: User = {"id": 1, "name": "Alice", "email": "alice@example.com"}
point = Point(x=10, y=20)
Rust:
// Struct with derive macros
#[derive(Debug, Clone, PartialEq)]
struct User {
id: u32,
name: String,
email: String,
}
#[derive(Debug, Clone, Copy, PartialEq)]
struct Point {
x: i32,
y: i32,
}
let user = User {
id: 1,
name: "Alice".to_string(),
email: "alice@example.com".to_string(),
};
let point = Point { x: 10, y: 20 };
Why this translation:
- Python's
TypedDictis for type hints; Rust's structs are enforced at compile time - Rust's
#[derive]macros auto-generate common trait implementations - Rust structs require owned data (
Stringnot&strfor struct fields)
Union Types → Enums
Python:
from typing import Union
# Union type
def process(value: Union[int, str]) -> str:
if isinstance(value, int):
return f"Number: {value}"
else:
return f"String: {value}"
result = process(42)
result = process("hello")
Rust:
// Tagged union (enum)
enum Value {
Number(i32),
Text(String),
}
fn process(value: Value) -> String {
match value {
Value::Number(n) => format!("Number: {}", n),
Value::Text(s) => format!("String: {}", s),
}
}
let result = process(Value::Number(42));
let result = process(Value::Text("hello".to_string()));
Why this translation:
- Python's
Unionis a type hint checked by mypy/pyright - Rust's enums are tagged unions, enforcing exhaustive pattern matching
- Rust catches missing match cases at compile time
Protocol (Structural) → Trait (Nominal)
Python:
from typing import Protocol
# Structural typing
class Drawable(Protocol):
def draw(self) -> None:
...
# Any class with a draw() method satisfies Drawable
class Circle:
def draw(self) -> None:
print("Drawing circle")
def render(obj: Drawable) -> None:
obj.draw()
render(Circle()) # Works due to structural typing
Rust:
// Nominal typing - must explicitly implement trait
trait Drawable {
fn draw(&self);
}
struct Circle;
impl Drawable for Circle {
fn draw(&self) {
println!("Drawing circle");
}
}
fn render<T: Drawable>(obj: &T) {
obj.draw();
}
render(&Circle); // Only works if Circle explicitly implements Drawable
Why this translation:
- Python's
Protocoluses structural typing (method signature match) - Rust's traits require explicit
impl Trait for Typedeclarations - Rust's approach enables better error messages and clearer intent
Common Pitfalls
1. Arbitrary Precision Integer Overflow
Problem:
// Python: unlimited integer size
# x = 10 ** 100 # Works fine
// Rust: fixed-size integers
let x: i32 = 10_i32.pow(100); // PANIC! Overflow in debug mode
Solution:
// Use appropriate size or BigInt
use num_bigint::BigInt;
use num_traits::pow::Pow;
let x = BigInt::from(10).pow(100_u32); // No overflow
Why this matters: Python integers never overflow; Rust integers panic (debug) or wrap (release).
2. Mutable Aliasing
Problem:
// Python: multiple mutable references allowed
# items = [1, 2, 3]
# ref1 = items
# ref2 = items
# ref1.append(4)
# ref2.append(5)
// Rust: borrow checker prevents this
let mut items = vec![1, 2, 3];
let ref1 = &mut items;
let ref2 = &mut items; // ERROR: cannot borrow as mutable more than once
Solution:
// Use scopes to separate borrows
{
let ref1 = &mut items;
ref1.push(4);
}
{
let ref2 = &mut items;
ref2.push(5);
}
// Or use interior mutability (Rc<RefCell<T>> or Arc<Mutex<T>>)
Why this matters: Rust prevents data races at compile time; Python allows them.
3. String Ownership
Problem:
// Python: strings are immutable but freely aliased
# name = user.get("name")
# print(name)
// Rust: String vs &str confusion
fn get_name(user: &HashMap<String, String>) -> &str {
user.get("name").unwrap() // Returns &String, not &str
}
Solution:
// Use .as_str() or accept &str
fn get_name(user: &HashMap<String, String>) -> &str {
user.get("name").unwrap().as_str()
}
// Or return Option<&str>
fn get_name(user: &HashMap<String, String>) -> Option<&str> {
user.get("name").map(|s| s.as_str())
}
Why this matters: Rust distinguishes owned (String) and borrowed (&str) strings.
4. Truthiness vs Explicit Boolean
Problem:
// Python: truthy/falsy values
# if items: # Empty list is falsy
# process(items)
// Rust: explicit boolean checks required
if items { // ERROR: expected `bool`, found `Vec<T>`
process(&items);
}
Solution:
// Explicitly check for emptiness
if !items.is_empty() {
process(&items);
}
// Or check for None
if let Some(value) = option {
process(value);
}
Why this matters: Rust has no implicit truthiness; always use explicit boolean expressions.
5. Default Arguments vs Builder Pattern
Problem:
// Python: default arguments
# def connect(host, port=80, timeout=30):
# ...
// Rust: no default arguments
fn connect(host: &str, port: u16, timeout: u64) -> Connection {
// All arguments required!
}
Solution:
// Use Option for optional parameters
fn connect(host: &str, port: Option<u16>, timeout: Option<u64>) -> Connection {
let port = port.unwrap_or(80);
let timeout = timeout.unwrap_or(30);
// ...
}
// Or use builder pattern
struct ConnectionBuilder {
host: String,
port: u16,
timeout: u64,
}
impl ConnectionBuilder {
fn new(host: String) -> Self {
Self { host, port: 80, timeout: 30 }
}
fn port(mut self, port: u16) -> Self {
self.port = port;
self
}
fn timeout(mut self, timeout: u64) -> Self {
self.timeout = timeout;
self
}
fn connect(self) -> Connection {
// ...
}
}
// Usage
let conn = ConnectionBuilder::new("localhost")
.port(8080)
.timeout(60)
.connect();
Why this matters: Rust has no default arguments; use Option or builder pattern for ergonomics.
6. List Modification During Iteration
Problem:
// Python: modifying list during iteration (undefined behavior)
# for item in items:
# if condition(item):
# items.remove(item) # Dangerous!
// Rust: borrow checker prevents this
for item in &items {
if condition(item) {
items.remove(item); // ERROR: cannot borrow as mutable while borrowed
}
}
Solution:
// Collect indices to remove, then remove in reverse
let to_remove: Vec<usize> = items.iter()
.enumerate()
.filter(|(_, item)| condition(item))
.map(|(i, _)| i)
.collect();
for &i in to_remove.iter().rev() {
items.remove(i);
}
// Or use retain
items.retain(|item| !condition(item));
Why this matters: Rust prevents iterator invalidation at compile time.
7. Global Mutable State
Problem:
// Python: global mutable state is easy
# counter = 0
# def increment():
# global counter
# counter += 1
// Rust: global mutable state requires unsafe or synchronization
static mut COUNTER: i32 = 0; // Unsafe!
fn increment() {
unsafe {
COUNTER += 1; // Requires unsafe block
}
}
Solution:
// Use static with Mutex or Atomic
use std::sync::Mutex;
static COUNTER: Mutex<i32> = Mutex::new(0);
fn increment() {
let mut counter = COUNTER.lock().unwrap();
*counter += 1;
}
// Or use atomic types for simple counters
use std::sync::atomic::{AtomicI32, Ordering};
static COUNTER: AtomicI32 = AtomicI32::new(0);
fn increment() {
COUNTER.fetch_add(1, Ordering::SeqCst);
}
Why this matters: Rust makes global mutable state explicit and safe via synchronization primitives.
8. Exception vs Result Propagation
Problem:
// Python: exceptions propagate automatically
# def outer():
# return inner() # Exceptions bubble up
# def inner():
# raise ValueError("error")
// Rust: forgetting ? operator
fn outer() -> Result<Data, Error> {
let data = inner(); // ERROR: expected `Data`, found `Result<Data, Error>`
Ok(data)
}
Solution:
// Use ? operator to propagate errors
fn outer() -> Result<Data, Error> {
let data = inner()?; // ? unwraps Ok or returns Err
Ok(data)
}
// Or match explicitly
fn outer() -> Result<Data, Error> {
match inner() {
Ok(data) => Ok(data),
Err(e) => Err(e),
}
}
Why this matters: Rust errors must be explicitly handled or propagated with ?.
Tooling
Code Translation Tools
| Tool | Purpose | Notes |
|---|---|---|
py2rs |
Python → Rust transpiler | Experimental, limited support |
PyO3 |
Python ↔ Rust FFI | Call Rust from Python or vice versa |
maturin |
Build Python extensions in Rust | For keeping Python interface, Rust backend |
| Manual translation | Full control | Recommended for production code |
Type Checking and Linting
| Python | Rust | Purpose |
|---|---|---|
mypy |
rustc |
Static type checking |
pylint |
clippy |
Linting and best practices |
black |
rustfmt |
Code formatting |
isort |
- | Import sorting (built into rustfmt) |
Testing Frameworks
| Python | Rust | Purpose |
|---|---|---|
pytest |
Built-in #[test] + cargo test |
Unit testing |
hypothesis |
proptest |
Property-based testing |
unittest.mock |
mockall |
Mocking |
pytest-benchmark |
criterion |
Benchmarking |
Async Runtime
| Python | Rust | Purpose |
|---|---|---|
asyncio |
tokio |
Async runtime (most popular) |
trio |
async-std |
Alternative async runtime |
uvloop |
- | Faster event loop (not needed in Rust) |
Common Crate Equivalents
| Python Package | Rust Crate | Purpose |
|---|---|---|
requests |
reqwest |
HTTP client |
aiohttp |
reqwest (async) |
Async HTTP client |
flask / fastapi |
axum, actix-web |
Web framework |
pydantic |
serde |
Serialization/validation |
click / argparse |
clap |
CLI argument parsing |
logging |
tracing, log |
Logging/tracing |
datetime |
chrono |
Date/time handling |
pathlib |
std::path |
Path manipulation |
json |
serde_json |
JSON parsing |
re |
regex |
Regular expressions |
sqlite3 |
rusqlite |
SQLite database |
sqlalchemy |
diesel, sqlx |
ORM / SQL toolkit |
pytest |
cargo test |
Testing framework |
…(truncated)