返回 Skill 列表
extension
分类: 开发与工程无需 API Key

convert-python-rust

将Python代码转换为地道的Rust代码。在将Python项目迁移到Rust、将Python模式翻译成地道的Rust模式,或为了性能、安全性和并发性重构Python代码库时使用。扩展了meta-convert-dev以包含特定的Python到Rust转换模式。

person作者: jakexiaohubgithub

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

  1. Analyze source thoroughly before writing target
  2. Map types first - create type equivalence table
  3. Handle arbitrary-precision integers - decide if i64 is enough or if you need BigInt
  4. Preserve semantics over syntax similarity
  5. Adopt Rust idioms - don't write "Python code in Rust syntax"
  6. Handle edge cases - None, exceptions, dynamic typing assumptions
  7. 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 explicit Option<T>
  • Rust's combinator methods (map, unwrap_or) are more explicit about handling the None case
  • as_ref() converts Option<T> to Option<&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's setdefault() 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, including 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 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 uses Drop trait
  • Rust's ownership system guarantees cleanup at scope exit (compile-time enforced)
  • No need for explicit with statement - 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, HashMap for 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 thiserror crate provides Display and Error trait 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/await syntax
  • 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 to asyncio.gather
  • tokio::spawn creates a separate task (like create_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's StreamExt::next() in a loop
  • Rust requires async-stream crate for generator-like syntax
  • Streams yield Result for 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 uses tokio::time::timeout
  • Rust's timeout returns Result<Result<T, E>, Elapsed> (nested Results)
  • Cancellation in Rust happens via abort() on JoinHandle

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) or Arc<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 TypedDict is 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 (String not &str for 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 Union is 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 Protocol uses structural typing (method signature match)
  • Rust's traits require explicit impl Trait for Type declarations
  • 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
  • requestsreqwest (async by default)
  • Exception handling → Result<T, E> + ? operator
  • async/await syntax 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:

  • @dataclassstruct with #[derive(Deserialize)]
  • Default values via #[serde(default = "fn")]
  • Custom error enum with thiserror for better error messages
  • Port validation simplified via u16 type (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.Semaphoretokio::sync::Semaphore (same pattern)
  • Rate limiter uses tokio::sync::Mutex for shared state
  • aiohttpreqwest (async HTTP client)
  • BeautifulSoupscraper crate (HTML parsing)
  • loggingtracing (structured logging)
  • asyncio.gatherfutures::future::join_all
  • Error handling via Result + thiserror instead 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 examples
  • lang-python-dev - Python development patterns
  • lang-rust-dev - Rust development patterns