Claude-skill-registry convert-python-rust
Convert Python code to idiomatic Rust. Use when migrating Python projects to Rust, translating Python patterns to idiomatic Rust, or refactoring Python codebases for performance, safety, and concurrency. Extends meta-convert-dev with Python-to-Rust specific patterns.
git clone https://github.com/majiayu000/claude-skill-registry
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/convert-python-rust" ~/.claude/skills/majiayu000-claude-skill-registry-convert-python-rust && rm -rf "$T"
skills/data/convert-python-rust/SKILL.mdConvert 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
- Foundational conversion patterns (APTV workflow, testing strategies)meta-convert-dev
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 |
|---|---|---|
| , , , | Python has arbitrary precision |
| | Default float |
| | Direct mapping |
| , | Owned vs borrowed |
| , | Owned vs borrowed |
| | Growable array |
| | Fixed-size tuple |
| , | Hash vs ordered |
| , | Hash vs ordered |
| | Explicit nullable |
| | Tagged union |
| | Function trait |
| | Async function |
| struct | Data classes |
| | Error handling |
When Converting Code
- Analyze source thoroughly before writing target
- Map types first - create type equivalence table
- Handle arbitrary-precision integers - decide if
is enough or if you needi64BigInt - 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 |
|---|---|---|
| | Default for small integers |
| | Large integers (64-bit) |
| | Very large integers (128-bit) |
| | Python default - arbitrary precision |
| | IEEE 754 double precision |
| | Direct mapping |
| | Owned, heap-allocated UTF-8 |
| | Borrowed string slice |
| | Owned byte vector |
| | Borrowed byte slice |
| | Mutable byte vector |
| | Use 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 |
|---|---|---|
| | Owned, growable, ordered |
| | Fixed-size, immutable |
| | Variable-length tuple → Vec |
| | Hash-based, unordered |
| | Tree-based, ordered |
| | Hash-based, unique values |
| | Tree-based, ordered unique |
| | Immutable by default in Rust |
| | Double-ended queue |
| | Insertion-order map |
| + API | Use pattern |
| | Count occurrences |
Composite Types
| Python | Rust | Notes |
|---|---|---|
(data) | | Data containers |
(behavior) | + | Behavior contracts |
| struct | Auto-derive common traits |
| | Structural types → nominal traits |
| | Named fields |
| or tuple | Prefer struct for clarity |
| | Algebraic data types |
| | Literal types → enums |
| | Tagged union |
| | Nullable types |
| | Function types |
| | Generic types |
Type Annotations → Generics + Traits
| Python | Rust | Notes |
|---|---|---|
| | Unconstrained generic |
| | Trait bound |
| | Slice for sequences |
| Avoid - use generics | is a code smell |
| 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 (
) while Rust uses explicitif userOption<T> - Rust's combinator methods (
,map
) are more explicit about handling theunwrap_or
caseNone
convertsas_ref()
toOption<T>
to avoid consuming the valueOption<&T>
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
to materialize into a collectioncollect() - Terminal operations like
consume the iterator automaticallysum()
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
API is more efficient than Python'sentry()
for expensive defaultssetdefault()
takes a closure, only calling it if the key is missingor_insert_with()- Rust's iterator
can build many collection types, includingcollect()HashMap
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
macro is compile-time checked for type safetyformat!
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__
protocols; Rust uses__exit__
traitDrop - Rust's ownership system guarantees cleanup at scope exit (compile-time enforced)
- No need for explicit
statement - scope-based cleanup is automaticwith
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,
for truly dynamic dataHashMap - 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
transforms errors explicitlymap_err()
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
for common patterns#[derive(...)] - 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 |
|---|---|---|
| | Exceptions → Result |
| | Pattern matching |
| Anti-pattern - always specify error type | No catch-all |
| RAII / Drop trait | Automatic cleanup |
| Nested error types or | Error chains |
| | 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
crate providesthiserror
andDisplay
trait implementationsError
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 function |
| | Await syntax |
| | Run async code |
| or | Concurrent execution |
| | Background task |
| | Async sleep |
| | Timeout |
| | Async channel |
| | 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
syntaxawait - Python's context managers become RAII in Rust (automatic cleanup)
- Rust requires explicit error handling (
+Result
)?
macro sets up the async runtime automatically#[tokio::main]
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:
is macro-based (compile-time), similar totokio::join!asyncio.gather
creates a separate task (liketokio::spawn
)create_task- Spawned tasks return
, requiring doubleJoinHandle
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
→ Rust'sasync for
in a loopStreamExt::next() - Rust requires
crate for generator-like syntaxasync-stream - Streams yield
for error handling (Python would raise exceptions)Result
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
; Rust usesasyncio.wait_fortokio::time::timeout - Rust's
returnstimeout
(nested Results)Result<Result<T, E>, Elapsed> - Cancellation in Rust happens via
onabort()JoinHandle
Memory & Ownership
Python GC → Rust Ownership
| Python Model | Rust Model | Translation |
|---|---|---|
| Reference counting + cycle detection | Ownership + borrowing | Explicit ownership transfer |
| Shared references everywhere | (immutable) or (mutable) | Borrow checker enforces aliasing rules |
| Mutable by default | Immutable by default ( vs ) | Explicit mutability |
| No lifetime tracking | Explicit lifetimes () | Compiler ensures references are valid |
or rely on GC | 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
(single-threaded) orRc<RefCell<T>>
(multi-threaded)Arc<Mutex<T>>
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
) over cloning for performance&mut T
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
is for type hints; Rust's structs are enforced at compile timeTypedDict - Rust's
macros auto-generate common trait implementations#[derive] - Rust structs require owned data (
notString
for struct fields)&str
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
is a type hint checked by mypy/pyrightUnion - 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
uses structural typing (method signature match)Protocol - Rust's traits require explicit
declarationsimpl Trait for Type - 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 |
|---|---|---|
| Python → Rust transpiler | Experimental, limited support |
| Python ↔ Rust FFI | Call Rust from Python or vice versa |
| 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 |
|---|---|---|
| | Static type checking |
| | Linting and best practices |
| | Code formatting |
| - | Import sorting (built into ) |
Testing Frameworks
| Python | Rust | Purpose |
|---|---|---|
| Built-in + | Unit testing |
| | Property-based testing |
| | Mocking |
| | Benchmarking |
Async Runtime
| Python | Rust | Purpose |
|---|---|---|
| | Async runtime (most popular) |
| | Alternative async runtime |
| - | Faster event loop (not needed in Rust) |
Common Crate Equivalents
| Python Package | Rust Crate | Purpose |
|---|---|---|
| | HTTP client |
| (async) | Async HTTP client |
/ | , | Web framework |
| | Serialization/validation |
/ | | CLI argument parsing |
| , | Logging/tracing |
| | Date/time handling |
| | Path manipulation |
| | JSON parsing |
| | Regular expressions |
| | SQLite database |
| , | ORM / SQL toolkit |
| | Testing framework |
Examples
Example 1: Simple - HTTP GET Request
Before (Python):
import requests def fetch_user(user_id: int) -> dict: """Fetch user data from API.""" response = requests.get(f"https://api.example.com/users/{user_id}") response.raise_for_status() return response.json() # Usage try: user = fetch_user(123) print(f"User: {user['name']}") except requests.HTTPError as e: print(f"HTTP error: {e}") except Exception as e: print(f"Error: {e}")
After (Rust):
use reqwest; use serde::Deserialize; #[derive(Debug, Deserialize)] struct User { name: String, // other fields... } async fn fetch_user(user_id: u32) -> Result<User, reqwest::Error> { let url = format!("https://api.example.com/users/{}", user_id); let user = reqwest::get(&url) .await? .error_for_status()? .json::<User>() .await?; Ok(user) } // Usage #[tokio::main] async fn main() { match fetch_user(123).await { Ok(user) => println!("User: {}", user.name), Err(e) => eprintln!("Error: {}", e), } }
Key changes:
- Python dict → Rust struct with
serde::Deserialize
→requests
(async by default)reqwest- Exception handling →
+Result<T, E>
operator?
/async
syntax is similar in both languagesawait
Example 2: Medium - Configuration Parser with Validation
Before (Python):
from pathlib import Path from typing import Optional import json from dataclasses import dataclass @dataclass class Config: host: str port: int timeout: int = 30 def validate(self): if not (1 <= self.port <= 65535): raise ValueError(f"Invalid port: {self.port}") if self.timeout < 0: raise ValueError(f"Invalid timeout: {self.timeout}") def load_config(path: Path) -> Config: """Load and validate configuration from JSON file.""" if not path.exists(): raise FileNotFoundError(f"Config file not found: {path}") with path.open() as f: data = json.load(f) config = Config(**data) config.validate() return config # Usage try: config = load_config(Path("config.json")) print(f"Server: {config.host}:{config.port}") except (FileNotFoundError, ValueError, json.JSONDecodeError) as e: print(f"Configuration error: {e}") exit(1)
After (Rust):
use serde::{Deserialize, Serialize}; use std::fs; use std::path::Path; use thiserror::Error; #[derive(Debug, Error)] enum ConfigError { #[error("Config file not found: {0}")] NotFound(String), #[error("Failed to read config: {0}")] Io(#[from] std::io::Error), #[error("Failed to parse config: {0}")] Parse(#[from] serde_json::Error), #[error("Invalid port: {0} (must be 1-65535)")] InvalidPort(u16), #[error("Invalid timeout: {0} (must be non-negative)")] InvalidTimeout(i32), } #[derive(Debug, Deserialize, Serialize)] struct Config { host: String, port: u16, #[serde(default = "default_timeout")] timeout: u32, } fn default_timeout() -> u32 { 30 } impl Config { fn validate(&self) -> Result<(), ConfigError> { if self.port == 0 { return Err(ConfigError::InvalidPort(self.port)); } // port is u16, so max is already 65535 Ok(()) } } fn load_config(path: &Path) -> Result<Config, ConfigError> { if !path.exists() { return Err(ConfigError::NotFound(path.display().to_string())); } let content = fs::read_to_string(path)?; let config: Config = serde_json::from_str(&content)?; config.validate()?; Ok(config) } // Usage fn main() { match load_config(Path::new("config.json")) { Ok(config) => { println!("Server: {}:{}", config.host, config.port); } Err(e) => { eprintln!("Configuration error: {}", e); std::process::exit(1); } } }
Key changes:
→@dataclass
withstruct#[derive(Deserialize)]- Default values via
#[serde(default = "fn")] - Custom error enum with
for better error messagesthiserror - Port validation simplified via
type (0-65535 range enforced by type)u16 - File I/O errors automatically converted via
#[from]
Example 3: Complex - Concurrent Web Scraper with Rate Limiting
Before (Python):
import asyncio import aiohttp from typing import List, Dict, Optional from dataclasses import dataclass from bs4 import BeautifulSoup import logging logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) @dataclass class Article: title: str url: str excerpt: str class RateLimiter: """Token bucket rate limiter.""" def __init__(self, rate: int, per: float): self.rate = rate self.per = per self.allowance = rate self.last_check = asyncio.get_event_loop().time() async def acquire(self): """Acquire a token, waiting if necessary.""" current = asyncio.get_event_loop().time() elapsed = current - self.last_check self.last_check = current self.allowance += elapsed * (self.rate / self.per) if self.allowance > self.rate: self.allowance = self.rate if self.allowance < 1.0: sleep_time = (1.0 - self.allowance) * (self.per / self.rate) await asyncio.sleep(sleep_time) self.allowance = 0.0 else: self.allowance -= 1.0 class Scraper: def __init__(self, base_url: str, max_concurrent: int = 5, rate_limit: int = 10): self.base_url = base_url self.semaphore = asyncio.Semaphore(max_concurrent) self.rate_limiter = RateLimiter(rate=rate_limit, per=1.0) async def fetch_page(self, session: aiohttp.ClientSession, url: str) -> Optional[str]: """Fetch a single page with rate limiting.""" await self.rate_limiter.acquire() async with self.semaphore: try: logger.info(f"Fetching {url}") async with session.get(url, timeout=10) as response: response.raise_for_status() return await response.text() except aiohttp.ClientError as e: logger.error(f"Failed to fetch {url}: {e}") return None except asyncio.TimeoutError: logger.error(f"Timeout fetching {url}") return None async def parse_article(self, html: str, url: str) -> Optional[Article]: """Parse article from HTML.""" try: soup = BeautifulSoup(html, 'html.parser') title = soup.find('h1').get_text(strip=True) excerpt = soup.find('p').get_text(strip=True)[:200] return Article(title=title, url=url, excerpt=excerpt) except Exception as e: logger.error(f"Failed to parse {url}: {e}") return None async def scrape_articles(self, paths: List[str]) -> List[Article]: """Scrape multiple articles concurrently.""" async with aiohttp.ClientSession() as session: tasks = [] for path in paths: url = f"{self.base_url}{path}" tasks.append(self.fetch_and_parse(session, url)) results = await asyncio.gather(*tasks) return [article for article in results if article is not None] async def fetch_and_parse(self, session: aiohttp.ClientSession, url: str) -> Optional[Article]: """Fetch and parse a single article.""" html = await self.fetch_page(session, url) if html: return await self.parse_article(html, url) return None # Usage async def main(): scraper = Scraper("https://example.com", max_concurrent=5, rate_limit=10) paths = [f"/article/{i}" for i in range(20)] articles = await scraper.scrape_articles(paths) logger.info(f"Scraped {len(articles)} articles") for article in articles[:5]: print(f"{article.title}: {article.excerpt}") if __name__ == "__main__": asyncio.run(main())
After (Rust):
use reqwest; use scraper::{Html, Selector}; use tokio; use tokio::sync::Semaphore; use tokio::time::{sleep, Duration, Instant}; use std::sync::Arc; use thiserror::Error; use tracing::{info, error}; #[derive(Debug, Clone)] struct Article { title: String, url: String, excerpt: String, } #[derive(Debug, Error)] enum ScraperError { #[error("HTTP request failed: {0}")] Request(#[from] reqwest::Error), #[error("Failed to parse HTML")] Parse, #[error("Timeout")] Timeout, } /// Token bucket rate limiter struct RateLimiter { rate: f64, per: f64, allowance: tokio::sync::Mutex<(f64, Instant)>, } impl RateLimiter { fn new(rate: usize, per: f64) -> Self { Self { rate: rate as f64, per, allowance: tokio::sync::Mutex::new((rate as f64, Instant::now())), } } async fn acquire(&self) { let mut guard = self.allowance.lock().await; let (mut allowance, mut last_check) = *guard; let current = Instant::now(); let elapsed = current.duration_since(last_check).as_secs_f64(); last_check = current; allowance += elapsed * (self.rate / self.per); if allowance > self.rate { allowance = self.rate; } if allowance < 1.0 { let sleep_time = (1.0 - allowance) * (self.per / self.rate); drop(guard); // Release lock before sleeping sleep(Duration::from_secs_f64(sleep_time)).await; allowance = 0.0; } else { allowance -= 1.0; } *guard = (allowance, last_check); } } struct Scraper { base_url: String, client: reqwest::Client, semaphore: Arc<Semaphore>, rate_limiter: Arc<RateLimiter>, } impl Scraper { fn new(base_url: String, max_concurrent: usize, rate_limit: usize) -> Self { Self { base_url, client: reqwest::Client::new(), semaphore: Arc::new(Semaphore::new(max_concurrent)), rate_limiter: Arc::new(RateLimiter::new(rate_limit, 1.0)), } } async fn fetch_page(&self, url: &str) -> Result<String, ScraperError> { self.rate_limiter.acquire().await; let _permit = self.semaphore.acquire().await.unwrap(); info!("Fetching {}", url); let response = tokio::time::timeout( Duration::from_secs(10), self.client.get(url).send() ) .await .map_err(|_| ScraperError::Timeout)??; let html = response.error_for_status()?.text().await?; Ok(html) } fn parse_article(&self, html: &str, url: String) -> Result<Article, ScraperError> { let document = Html::parse_document(html); let title_selector = Selector::parse("h1").unwrap(); let p_selector = Selector::parse("p").unwrap(); let title = document .select(&title_selector) .next() .ok_or(ScraperError::Parse)? .text() .collect::<String>() .trim() .to_string(); let excerpt = document .select(&p_selector) .next() .ok_or(ScraperError::Parse)? .text() .collect::<String>() .chars() .take(200) .collect(); Ok(Article { title, url, excerpt }) } async fn fetch_and_parse(&self, url: String) -> Option<Article> { match self.fetch_page(&url).await { Ok(html) => { match self.parse_article(&html, url.clone()) { Ok(article) => Some(article), Err(e) => { error!("Failed to parse {}: {}", url, e); None } } } Err(e) => { error!("Failed to fetch {}: {}", url, e); None } } } async fn scrape_articles(&self, paths: &[&str]) -> Vec<Article> { let tasks: Vec<_> = paths .iter() .map(|path| { let url = format!("{}{}", self.base_url, path); self.fetch_and_parse(url) }) .collect(); let results = futures::future::join_all(tasks).await; results.into_iter().flatten().collect() } } #[tokio::main] async fn main() { tracing_subscriber::fmt::init(); let scraper = Scraper::new( "https://example.com".to_string(), 5, // max_concurrent 10, // rate_limit ); let paths: Vec<_> = (0..20).map(|i| format!("/article/{}", i)).collect(); let path_refs: Vec<&str> = paths.iter().map(|s| s.as_str()).collect(); let articles = scraper.scrape_articles(&path_refs).await; info!("Scraped {} articles", articles.len()); for article in articles.iter().take(5) { println!("{}: {}", article.title, article.excerpt); } } // Cargo.toml dependencies: // [dependencies] // reqwest = { version = "0.11", features = ["json"] } // tokio = { version = "1", features = ["full"] } // scraper = "0.17" // thiserror = "1" // tracing = "0.1" // tracing-subscriber = "0.3" // futures = "0.3"
Key changes:
→asyncio.Semaphore
(same pattern)tokio::sync::Semaphore- Rate limiter uses
for shared statetokio::sync::Mutex
→aiohttp
(async HTTP client)reqwest
→BeautifulSoup
crate (HTML parsing)scraper
→logging
(structured logging)tracing
→asyncio.gatherfutures::future::join_all- Error handling via
+Result
instead of exceptionsthiserror
for shared ownership across async tasksArc<T>- Explicit lifetime management (no GC)
See Also
For more examples and patterns, see:
- Foundational patterns with cross-language examplesmeta-convert-dev
- Python development patternslang-python-dev
- Rust development patternslang-rust-dev