Agent skill
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.
Install this agent skill to your Project
npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/data/convert-python-rust
SKILL.md
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 |
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→reqwest(async by default)- Exception handling →
Result<T, E>+?operator async/awaitsyntax is similar in both languages
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→structwith#[derive(Deserialize)]- Default values via
#[serde(default = "fn")] - Custom error enum with
thiserrorfor better error messages - Port validation simplified via
u16type (0-65535 range enforced by type) - 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→tokio::sync::Semaphore(same pattern)- Rate limiter uses
tokio::sync::Mutexfor shared state aiohttp→reqwest(async HTTP client)BeautifulSoup→scrapercrate (HTML parsing)logging→tracing(structured logging)asyncio.gather→futures::future::join_all- Error handling via
Result+thiserrorinstead of exceptions Arc<T>for shared ownership across async tasks- Explicit lifetime management (no GC)
See Also
For more examples and patterns, see:
meta-convert-dev- Foundational patterns with cross-language exampleslang-python-dev- Python development patternslang-rust-dev- Rust development patterns
Didn't find tool you were looking for?