Python-to-Rust Mapping Reference

July 13, 2026 · View on GitHub

Exhaustive mapping table for Python-to-Rust constructs. Organized by category with code examples for each mapping. Use this as a lookup reference during porting -- ctrl-F to find the Rust equivalent of any Python construct.

For porting methodology (principles, workflow, pitfalls, acceptance criteria), see Python-to-Rust Porting Rules.

Related: Python-to-Rust Porting Guide

Last update: 2026-05-27

Types

Primitives

PythonRustExample
str&str (borrow) / String (owned)Accept &str, return String
inti32 / i64 / usizeMatch semantic range
floatf64 / f32f64 for double precision
boolboolSame
NoneOption::NoneUse Option<T>
bytes&[u8] / Vec<u8>
complex(no built-in)Use num-complex crate

Collections

PythonRustNotes
list[T]Vec<T>
tuple[A, B, C](A, B, C)Named struct if >3 fields
dict[K, V]HashMap<K, V>No insertion order! Use indexmap::IndexMap if order matters
set[T]HashSet<T>BTreeSet for sorted
frozensetHashSet<T>Immutability via let binding, not the type. For use as hash key, need newtype implementing Hash
dequeVecDeque<T>
defaultdict(list)HashMap<K, Vec<V>> + .entry().or_default()
CounterHashMap<T, usize>
OrderedDictIndexMap<K, V> (indexmap crate)
namedtupleStruct with named fields

Type Hints to Rust Types

PythonRustNotes
Optional[T]Option<T>
Union[A, B]enum { A(A), B(B) }
AnyGenerics or Box<dyn Trait>Avoid if possible
TypeVar('T')<T> generics
ProtocoltraitRust traits are nominal -- must write explicit impl Trait for Type
TypedDictStruct with named fieldsAdd serde derives only if deserializing from data formats
Literal["a", "b"]enum { A, B }
Callable[[A], B]Fn(A) -> B / FnMut / FnOnceFn -- no mutation, callable repeatedly. FnMut -- mutates captures. FnOnce -- consumes captures. Use Box<dyn Fn(A) -> B> to store callbacks in structs

Control Flow

Conditionals

# Python
if x > 0:
    result = "positive"
elif x == 0:
    result = "zero"
else:
    result = "negative"
// Rust
let result = if x > 0 {
    "positive"
} else if x == 0 {
    "zero"
} else {
    "negative"
};

Pattern Matching

# Python (3.10+)
match command:
    case "quit":
        exit()
    case "hello":
        greet()
    case _:
        unknown()
// Rust
match command.as_str() { // .as_str() needed for String; &str can match directly
    "quit" => exit(),
    "hello" => greet(),
    _ => unknown(),
}

Loops

PythonRust
for x in items:for x in &items {
for i, x in enumerate(items):for (i, x) in items.iter().enumerate() {
for x in range(10):for x in 0..10 {
while condition:while condition {
break / continuebreak / continue
for/elseNo direct equivalent -- use if let Some(item) = iter.find(|x| cond(x)) { ... } else { ... }

Comprehensions to Iterators

# Python
result = [x * 2 for x in items if x > 0]
// Rust
let result: Vec<_> = items.iter()
    .filter(|&&x| x > 0)
    .map(|&x| x * 2)
    .collect();
# Dict comprehension
d = {k: v for k, v in pairs if v > 0}
// Rust
let d: HashMap<_, _> = pairs.iter()
    .filter(|(_, v)| *v > 0)
    .cloned()
    .collect();

Error Handling

PythonRustNotes
try: / except:match result { Ok(v) => ..., Err(e) => ... }Or use ?
raise ValueError(msg)return Err(Error::Validation(msg))
raise (re-raise)return Err(e) or just ?
finally:Drop trait / scope guards
with context_manager:Scope + Drop / closure patterns
assert x == yassert_eq!(x, y)Not debug_assert_eq! -- debug asserts are stripped in release builds. Use debug_assert! only for hot-path invariants you’ve verified via other means

Context Managers to RAII

Python’s with statement maps to Rust’s RAII (Resource Acquisition Is Initialization) pattern, where resources are cleaned up when they go out of scope via the Drop trait.

PythonRustNotes
with open(f) as fh:let content = std::fs::read_to_string(path)?;For simple read-all; or use { let file = File::open(path)?; ... } (RAII scope)
with lock:let _guard = mutex.lock().unwrap();Guard released on scope exit
@contextmanager generatorimpl Drop for cleanupOr closure-based API: with_resource(|r| { ... })
__enter__ / __exit__impl DropDrop::drop() called automatically at scope end
# Python
with open("data.txt") as f:
    content = f.read()
    process(content)
# file closed here
// Rust: simple case -- just read the file
let content = std::fs::read_to_string("data.txt")?;
process(&content);

// Rust: RAII scope when you need the file handle
{
    let mut file = File::open("data.txt")?;
    let mut content = String::new();
    file.read_to_string(&mut content)?;
    process(&content);
} // file closed here (Drop called)
# Python: lock
with lock:
    shared_data.append(item)
// Rust: Mutex guard
{
    let mut data = mutex.lock().unwrap();
    data.push(item);
} // lock released here (guard dropped)

Error Propagation

# Python
def process(path):
    try:
        data = read_file(path)
        return transform(data)
    except FileNotFoundError:
        return default_data()
    except ValueError as e:
        raise RuntimeError(f"transform failed: {e}")
// Rust
fn process(path: &Path) -> Result<Data> {
    let data = match read_file(path) {
        Ok(d) => d,
        Err(Error::NotFound(_)) => return Ok(default_data()),
        Err(e) => return Err(e),
    };
    transform(&data).map_err(|e| Error::Runtime(format!("transform failed: {e}")))
}

I/O

Basic I/O

PythonRustNotes
open(path).read()std::fs::read_to_string(path)?
open(path, 'rb').read()std::fs::read(path)?Returns Vec<u8>
open(path, 'w').write(data)std::fs::write(path, data)?
sys.stdin.read()std::io::stdin().read_to_string(&mut buf)?
print(x)println!("{x}")
print(x, file=sys.stderr)eprintln!("{x}")
logging.info(msg)tracing::info!(msg) or log::info!(msg)tracing crate preferred; log crate for simple cases
json.dumps(obj)serde_json::to_string(&obj)?Requires #[derive(Serialize)]
json.loads(s)serde_json::from_str(s)?Requires #[derive(Deserialize)]

Tip: Consider fs_err 3.x as a drop-in replacement for std::fs in applications. It wraps every function to include the file path in error messages: use fs_err as fs; then use fs::read_to_string(path)? normally.

Path Operations

Path (borrowed) / PathBuf (owned) are analogous to str / String. Accept impl AsRef<Path> for read-only access; accept impl Into<PathBuf> when storing.

PythonRustNotes
Path(path).exists()path.exists()Follows symlinks; converts I/O errors to false (can hide permission errors). Prefer fs::exists(path)? (stable since Rust 1.81) for explicit error handling
Path(path).is_file()path.is_file()Follows symlinks
Path(path).is_dir()path.is_dir()Follows symlinks
os.path.islink(path)path.is_symlink()Does NOT follow symlinks
Path(path).parentpath.parent()Returns Option<&Path>
os.path.join(a, b)path.join(b)Returns PathBuf
os.path.basename(path)path.file_name()Returns Option<&OsStr>
os.path.splitext(path)path.file_stem() + path.extension()Each returns Option<&OsStr>
os.path.abspath(path)std::path::absolute(path)?Stable since Rust 1.79. Does NOT resolve symlinks (unlike canonicalize)
os.path.realpath(path)std::fs::canonicalize(path)?Resolves all symlinks and .. components; path must exist
os.path.relpath(path, base)pathdiff::diff_paths(path, base)Need pathdiff 0.2 crate
os.path.expanduser("~")dirs::home_dir()Need dirs 6.x crate
str(path)path.display() or path.to_string_lossy()Paths are NOT guaranteed UTF-8 in Rust

Extension manipulation pitfall: path.with_extension("orig") replaces the last extension, so foo.tar.gz becomes foo.tar.orig. To append an extension (like .orig for backups), use:

let backup = PathBuf::from(format!("{}.orig", path.display()));
// or: let mut backup = path.to_path_buf(); backup.as_mut_os_string().push(".orig");

Filesystem Operations

PythonRustNotes
os.rename(src, dst)std::fs::rename(src, dst)?Fails across filesystems (EXDEV). Fallback: copy + delete. See fs_extra crate for cross-device moves
shutil.copy2(src, dst)std::fs::copy(src, dst)?Copies content + permissions only, NOT timestamps. Need filetime 0.2 crate for full metadata preservation
shutil.rmtree(path)std::fs::remove_dir_all(path)?Direct equivalent
os.remove(path)std::fs::remove_file(path)?Rust 1.86+: removes read-only files on Windows
os.makedirs(path, exist_ok=True)std::fs::create_dir_all(path)?Direct equivalent
os.listdir(path)std::fs::read_dir(path)?Returns Result<DirEntry> iterator, not a Vec
os.stat(path)std::fs::metadata(path)?Use symlink_metadata() to not follow symlinks
os.path.getsize(path)path.metadata()?.len()
os.chmod(path, mode)std::fs::set_permissions(path, perms)?Unix-only mode bits: need use std::os::unix::fs::PermissionsExt
open(path, 'x') (exclusive create)File::create_new(path)?Stable since Rust 1.77. Atomic; avoids TOCTOU race
fcntl.flock(f, LOCK_EX)file.lock()?Stable since Rust 1.89. Also: lock_shared(), try_lock(), unlock()
tempfile.NamedTemporaryFile()tempfile::NamedTempFile::new()?3.x crate. Auto-deletes on drop by default. Use persist() / keep() to retain
tempfile.mkdtemp()tempfile::TempDir::new()?Recursively deleted on drop
glob.glob("*.md")glob::glob("*.md")?0.3 crate. For {a,b} patterns or richer matching, use globset 0.4

Directory Walking

PythonRustNotes
os.walk(dir)walkdir::WalkDir::new(dir)walkdir 2.5 crate. Yields DirEntry; Python yields (dirpath, dirnames, filenames) tuples — API shape differs significantly

walkdir builder pattern:

use walkdir::WalkDir;

// Basic: iterate all entries recursively
for entry in WalkDir::new(root) {
    let entry = entry?;
    println!("{}", entry.path().display());
}

// With filtering: filter_entry prunes traversal (skips entire subtrees)
let walker = WalkDir::new(root).into_iter()
    .filter_entry(|e| !is_hidden(e));  // skips descending into hidden dirs

// With options
WalkDir::new(root)
    .follow_links(false)          // default: don't follow symlinks
    .max_depth(10)                // limit recursion
    .sort_by_file_name()          // deterministic output
    .into_iter()
    .filter_entry(|e| !is_excluded(e))

Key difference from Python os.walk: filter_entry() prunes the traversal tree (skips descending into non-matching directories). Standard .filter() does NOT prune — it still descends into filtered-out directories. Always use filter_entry() for exclude-pattern logic.

Atomic File Operations

For safe in-place file modification, use tempfile::NamedTempFile + persist():

use std::io::Write;
use tempfile::NamedTempFile;

fn atomic_write(path: &Path, content: &str) -> anyhow::Result<()> {
    // MUST create temp file on same filesystem as target
    let dir = path.parent().unwrap_or(Path::new("."));
    let mut tmp = NamedTempFile::new_in(dir)?;
    tmp.write_all(content.as_bytes())?;
    tmp.as_file().sync_all()?;  // flush to disk for crash safety
    tmp.persist(path)?;          // atomic rename
    Ok(())
}

persist() atomically replaces the target. If it fails, the PersistError contains the original NamedTempFile for recovery. Use persist_noclobber() to fail instead of replace if the target exists.

CrateVersionPurposeWhen to use
walkdir2.5Recursive directory traversalAny directory walking; prefer over manual read_dir recursion
tempfile3.27Secure temp files and dirsAtomic writes, test isolation
dirs6.0Platform directory locationsHome dir, config dir, etc. (dirs-next is abandoned — use dirs)
pathdiff0.2Compute relative pathsWhen you need os.path.relpath equivalent
filetime0.2Read/write file timestampsPreserving timestamps across copy. For simple cases, std::fs::File::set_times() (stable since Rust 1.75) may suffice
glob0.3Simple glob matchingBasic *, ?, [...] patterns. For {a,b} or multi-pattern, use globset 0.4
fs-err3.3Better filesystem error messagesDrop-in std::fs replacement for applications; adds file paths to errors

String Operations

PythonRustNotes
s.strip()s.trim()
s.lstrip() / s.rstrip()s.trim_start() / s.trim_end()
s.split(sep)s.split(sep)Returns iterator
s.splitlines()s.lines()
sep.join(items)items.join(sep)On slices
s.replace(old, new)s.replace(old, new)Returns new String
s.startswith(p)s.starts_with(p)
s.endswith(p)s.ends_with(p)
s.upper() / s.lower()s.to_uppercase() / s.to_lowercase()
s.find(sub)s.find(sub)Returns Option<usize>. Returns byte offset, not char index! Different for non-ASCII
s[start:end]&s[start..end]Panics if not char boundary!
len(s)s.len() (bytes) / s.chars().count() (chars)Different!
f"hello {name}"format!("hello {name}")
s.encode('utf-8')s.as_bytes()Rust strings are always UTF-8

Regex

Crate: regex 1.12+ (Thompson NFA engine, guaranteed linear time).

Core API Mapping

PythonRustNotes
re.compile(pattern)Regex::new(pattern)?Use LazyLock for statics
re.match(pattern, s)Regex::new(&format!("^(?:{pattern})"))Must add ^ anchor -- Rust has no start-anchored match. Use captures() for groups, is_match() for bool
re.search(pattern, s)regex.find(s)Direct equivalent; matches anywhere
re.fullmatch(pattern, s)regex.is_match(s) with ^...$Wrap pattern: format!("^(?:{pattern})$")
re.sub(pattern, repl, s)regex.replace_all(s, repl)Use regex.replace() for count=1. Replacement syntax differs -- see below
re.sub(pattern, repl, s, count=1)regex.replace(s, repl)Replaces only first match
re.findall(pattern, s)regex.find_iter(s).map(|m| m.as_str()).collect::<Vec<_>>()find_iter() returns Match objects, not strings
re.findall(pattern, s) (with groups)regex.captures_iter(s)Returns Captures objects; Python returns list of group tuples, API shape differs
re.split(pattern, s)regex.split(s).collect::<Vec<_>>()
match.group(0)m.as_str() or caps.get_match()get_match() added in regex 1.12
match.group(1)caps.get(1).map(|m| m.as_str())
match.group("name")caps.name("name").map(|m| m.as_str())
Look-ahead/behindUse fancy-regex 0.17+Not in standard regex; never will be (by design)
Backreferences in pattern (\1)Use fancy-regex 0.17+Not in standard regex

Critical difference: Python re.match() anchors to string start. Rust is_match() matches anywhere. Always add ^ for patterns that were used with re.match().

Replacement String Syntax

This is a common source of bugs. Python and Rust use different backreference syntax in replacement strings:

FeaturePython re.subRust regex replace
Numbered backreference\1, \2$1, $2
Named backreference\g<name>$name or ${name}
Whole match\g<0>$0
Literal $ in replacement$ (no escaping needed)$$
Literal \ in replacement\\\ (no special meaning in Rust replacements)
Disable all expansionN/Aregex::NoExpand("literal \$1")
# Python
re.sub(r"figure (\d+)", r"Figure \1", text)
re.sub(r"(?P<y>\d{4})-(?P<m>\d{2})", r"\g<m>/\g<y>", text)
// Rust -- note \$1 not \1, $name not \g<name>
regex.replace_all(text, "Figure \$1");
regex.replace_all(text, "$m/$y");
// Use ${1} to disambiguate: "${1}st" means group 1 + "st"
regex.replace_all(text, "${1}st");

When porting, mechanically translate all \N to $N and all \g<name> to $name in replacement strings.

Flag Mapping

PythonRust inlineRegexBuilder methodNotes
re.IGNORECASE / re.I(?i).case_insensitive(true)Equivalent
re.DOTALL / re.S(?s).dot_matches_new_line(true)Equivalent
re.MULTILINE / re.M(?m).multi_line(true)^/$ match line boundaries
re.VERBOSE / re.X(?x).ignore_whitespace(true)Equivalent
re.ASCII / re.A(?-u).unicode(false)Rust is Unicode by default; disable with (?-u)
re.UNICODE / re.U(default)(default)Rust regex is Unicode-aware by default
Combined re.I | re.S(?is)Chain builder methods
N/A(?U).swap_greed(true)Swap greedy/lazy (Rust-only)
N/A(?R).crlf(true)CRLF line terminator mode (Rust-only)

Flags can be scoped: (?i:pattern) applies case-insensitive only to pattern. Flags can be negated: (?-i) disables case-insensitive matching.

Unicode Behavior

Both Python 3 and Rust regex default to Unicode-aware matching, but the opt-out mechanisms differ:

ClassPython 3 defaultRust regex defaultASCII-only (Python)ASCII-only (Rust)
\wUnicode letters+digits+_Unicode letters+digits+_re.ASCII[a-zA-Z0-9_](?-u:\w)[a-zA-Z0-9_]
\dUnicode digitsUnicode digits (\p{Nd})re.ASCII[0-9](?-u:\d)[0-9]
\sUnicode whitespaceUnicode whitespacere.ASCII[\t\n\r\f\v ](?-u:\s) → ASCII whitespace
\bUnicode word boundaryUnicode word boundaryre.ASCII → ASCII boundary(?-u:\b) → ASCII boundary
.Any char except \nAny Unicode scalar except \nSameSame

Key difference: Python uses re.ASCII flag (global), Rust uses (?-u) (can be scoped to subexpressions). Disabling Unicode globally via RegexBuilder::unicode(false) changes . to match any single byte -- use regex::bytes::Regex if needed.

Named groups: Python (?P<name>...) works in Rust too (both (?P<name>...) and the shorter (?<name>...) are supported).

Feature Availability

The regex crate deliberately omits features that would break linear-time guarantees. Use fancy-regex 0.17+ for these:

FeaturePython reRust regexRust fancy-regex
Positive lookahead (?=...)YesNoYes
Negative lookahead (?!...)YesNoYes
Positive lookbehind (?<=...)Yes (fixed-width)NoYes (variable-width)
Negative lookbehind (?<!...)Yes (fixed-width)NoYes (variable-width)
Backreferences \1 in patternYesNoYes
Atomic groups (?>...)Yes (3.11+)NoYes
Possessive quantifiers a++Yes (3.11+)YesYes
Conditional (?(id)yes|no)YesNoPartial (group-based only)

fancy-regex delegates simple patterns to regex (linear time) and uses backtracking only for fancy features (potentially exponential -- catastrophic backtracking possible, same as Python).

Performance and Compilation

AspectPython reRust regexRust fancy-regex
EngineBacktrackingThompson NFAHybrid (NFA + backtracking)
Worst-case timeExponentialO(m × n) guaranteedExponential (fancy features only)
Compilation costModerateExpensive (use LazyLock)Expensive (use LazyLock)
Size limitsNoneDefault ~10 MB (RegexBuilder::size_limit())None
Untrusted patternsVulnerable to ReDoSSafe (linear time)Vulnerable to ReDoS (fancy features)

For untrusted patterns, use RegexBuilder::size_limit() and dfa_size_limit() to bound resource usage. Increase size_limit() if known-large patterns fail to compile.

Classes to Structs

# Python
class Config:
    def __init__(self, width: int = 80, verbose: bool = False):
        self.width = width
        self.verbose = verbose

    def with_width(self, width: int) -> 'Config':
        return Config(width=width, verbose=self.verbose)
// Rust
#[derive(Debug, Clone)]
pub struct Config {
    pub width: usize,
    pub verbose: bool,
}

impl Default for Config {
    fn default() -> Self {
        Self { width: 80, verbose: false }
    }
}

impl Config {
    pub fn with_width(mut self, width: usize) -> Self {
        self.width = width;
        self
    }
}

Inheritance to Composition/Traits

# Python
class Animal:
    def speak(self) -> str: ...

class Dog(Animal):
    def speak(self) -> str:
        return "woof"
// Rust
trait Animal {
    fn speak(&self) -> &str;
}

struct Dog;

impl Animal for Dog {
    fn speak(&self) -> &str { "woof" }
}

Dunder Methods to Rust Traits

Python DunderRust EquivalentNotes
__str__()impl fmt::DisplayUsed by format!, println!
__repr__()#[derive(Debug)]Used by {:?} format
__eq__()/__ne__()#[derive(PartialEq)]Add Eq if no floating-point fields
__hash__()#[derive(Hash)]Requires PartialEq + Eq
__lt__() etc.impl PartialOrd / #[derive(Ord)]Ord requires Eq
__len__().len() methodNo standard trait; convention only
__iter__() + __next__()impl IteratorMust define type Item
__getitem__()impl Index<Idx>Returns reference, not owned value
__add__() etc.impl Add from std::opsOne trait per operator
__enter__()/__exit__()impl Drop + RAII scopeSee Context Managers to RAII section above
__call__()impl Fn/FnMut/FnOnceOr just a method; Rust closures implement Fn traits
__contains__().contains() methodNo standard trait; convention only
__bool__()No standard traitUse explicit .is_empty() or method
__del__()impl DropCalled when value is dropped; cannot fail

Generators to Iterators

Python generators map to Rust iterators, but require explicit state management.

Simple generator (closure-based):

# Python
def evens(n):
    for i in range(n):
        if i % 2 == 0:
            yield i
// Rust: idiomatic -- use iterator combinators when possible
fn evens(n: usize) -> impl Iterator<Item = usize> {
    (0..n).filter(|x| x % 2 == 0)
}

// Rust: using std::iter::from_fn for complex yield logic
fn evens_from_fn(n: usize) -> impl Iterator<Item = usize> {
    let mut i = 0;
    std::iter::from_fn(move || {
        while i < n {
            let current = i;
            i += 1;
            if current % 2 == 0 {
                return Some(current);
            }
        }
        None
    })
}

Stateful generator -- struct implementing Iterator:

# Python
def fibonacci():
    a, b = 0, 1
    while True:
        yield a
        a, b = b, a + b
// Rust
struct Fibonacci { a: u64, b: u64 }

impl Fibonacci {
    fn new() -> Self { Self { a: 0, b: 1 } }
}

impl Iterator for Fibonacci {
    type Item = u64;
    fn next(&mut self) -> Option<Self::Item> {
        let result = self.a;
        (self.a, self.b) = (self.b, self.a + self.b);
        Some(result)
    }
}

Note: gen blocks are a reserved keyword in Edition 2024 and an experimental feature. When stabilized, they will allow Python-like yield syntax in Rust.

Dataclasses to Structs with Derives

Python @dataclassRust EquivalentNotes
@dataclass#[derive(Debug, Clone, PartialEq)]Closest equivalent derives
frozen=TrueNo &mut self methodsImmutability enforced by API design, not annotation
order=True#[derive(PartialOrd, Ord)]Ord requires Eq; use PartialOrd alone if fields contain floats
field(default=...)impl Default or #[derive(Default)]
# Python
@dataclass(frozen=True, order=True)
class Point:
    x: float
    y: float
    label: str = "origin"
// Rust
#[derive(Debug, Clone, PartialEq, PartialOrd)]
pub struct Point {
    pub x: f64,
    pub y: f64,
    pub label: String,
}

impl Default for Point {
    fn default() -> Self {
        Self { x: 0.0, y: 0.0, label: "origin".to_string() }
    }
}

Python Enums to Rust Enums

PythonRustNotes
class Color(Enum):enum Color { ... }
Color.REDColor::RedRust uses CamelCase variants
Color(1) (by value)Custom from_value() methodNo built-in value lookup
color.nameUse strum crate Display derive#[derive(strum::Display)]
Color["RED"] (by name)Use strum crate EnumString derive#[derive(strum::EnumString)]
auto()Explicit variantsNo auto-numbering
# Python
from enum import Enum

class Color(Enum):
    RED = "red"
    GREEN = "green"
    BLUE = "blue"
// Rust (with strum for Display/FromStr)
use strum::{Display, EnumString};

#[derive(Debug, Clone, Copy, PartialEq, Display, EnumString)]
pub enum Color {
    #[strum(serialize = "red")]
    Red,
    #[strum(serialize = "green")]
    Green,
    #[strum(serialize = "blue")]
    Blue,
}

Common Python Patterns

PythonRustNotes
@decoratorProcedural macros or wrapper functionsNo direct equivalent; use #[derive(...)] for common cases
@propertyMethod returning value / public fieldNo descriptor protocol; just use pub fn name(&self) -> &T
@staticmethodAssociated function (no self)fn foo() in impl block (no self parameter)
@classmethodAssociated function taking no selffn new(...) -> Self is the convention
*argsVariadic: not supportedUse slice &[T] or macro for variable args
**kwargsNo direct equivalentUse a builder pattern or config struct
isinstance(x, T)Pattern matching or trait boundsif let Some(v) = x.downcast_ref::<T>() for dyn Any
getattr(obj, name)No runtime reflectionUse enum dispatch or trait objects instead
lambda x: x + 1|x| x + 1Closures; type inferred

Async Patterns

PythonRustNotes
async def foo()async fn foo()
await bar()bar().awaitPostfix syntax in Rust
asyncio.run(main())#[tokio::main] on async fn main()Requires tokio runtime
asyncio.gather(a, b)tokio::join!(a, b)For fixed count; use futures::future::join_all for dynamic list
asyncio.create_task(coro)tokio::spawn(future)Returns JoinHandle
async for item in aiter:while let Some(item) = stream.next().await {Requires tokio-stream or futures
async with resource:Scope + .await on cleanupNo async Drop

Parallelism Patterns

PythonRustNotes
multiprocessing.Pool.map(f, items)items.par_iter().map(f).collect()rayon crate; data parallelism
threading.Thread(target=f)std::thread::spawn(f)OS-level threads
concurrent.futures.ThreadPoolExecutorrayon::ThreadPool or tokio::spawnrayon for CPU, tokio for I/O
concurrent.futures.ProcessPoolExecutorrayon::ThreadPoolRust threads share memory; no process isolation needed
queue.Queuestd::sync::mpsc::channel()Or crossbeam::channel for multi-consumer (MPMC)
threading.Lockstd::sync::Mutex<T>Mutex wraps data in Rust, not code

Testing

PythonRustNotes
def test_foo():#[test] fn test_foo() {
assert x == yassert_eq!(x, y)
assert x != yassert_ne!(x, y)
assert xassert!(x)
with pytest.raises(E):#[should_panic] or assert!(result.is_err())
@pytest.mark.skip#[ignore]
@pytest.fixtureSetup in test function or std::sync::LazyLock
@pytest.mark.parametrizerstest crate or macro-generated tests
pytest.approx(x)(x - y).abs() < epsilonOr use approx crate: assert_relative_eq!, assert_abs_diff_eq!

Packaging

PythonRustNotes
pyproject.tomlCargo.toml
pip installcargo install / cargo add
requirements.txtCargo.toml [dependencies]
setup.pyCargo.toml
__init__.pylib.rs
__main__.pymain.rs
PyPIcrates.io
pip install -e .cargo build (automatic)
Virtual environmentsCargo handles isolation via target/ dir

Project Setup Mapping

This section maps the full Python project infrastructure to Rust equivalents. Not for education -- for agents and engineers who know both languages but need the specific equivalences and pitfalls.

Manifest Structure: pyproject.toml vs Cargo.toml

Python (pyproject.toml)Rust (Cargo.toml)Notes
[project] name, version, description[package] name, version, descriptionSame fields, different table name
[project] authors, license[package] authors, licenseRust uses SPDX expressions: "MIT OR Apache-2.0"
requires-python = ">=3.11"rust-version = "1.85"MSRV. Enforced by cargo and CI
[project] readme, urls[package] readme, repository, homepageRust separates homepage from repository
[project] keywords, classifiers[package] keywords, categoriesRust categories are from a fixed list on crates.io
[project.dependencies][dependencies]See version constraints below
[project.optional-dependencies][features] + optional depsSee feature flags below
[dependency-groups] (PEP 735)[dev-dependencies], [build-dependencies]Rust separates dev and build deps explicitly
[build-system]N/A (cargo is the only build system)No equivalent of setuptools/hatch/flit/maturin
[tool.ruff], [tool.mypy], etc.Separate files: rustfmt.toml, deny.toml, etc.Python centralizes; Rust distributes config

Version Constraint Syntax

PythonRust (Cargo)Semantics
>=1.0,<2.0">=1.0, <2.0"Exact range (same)
~=1.4"^1.4"Compatible release (>=1.4, <2.0); caret is Cargo default
==1.4.*"~1.4"Exact minor (>=1.4, <1.5); tilde in Cargo
>=1.4">=1.4"Minimum version (same)
==1.4.0"=1.4.0"Exact pin (note single = in Cargo)
No default"1.4" means ^1.4 (>=1.4.0, <2.0.0)Cargo’s default is permissive semver-compatible

Pitfall: The ~= mapping depends on the number of version components:

  • Python ~=1.4 (2 parts) means >=1.4, <2.0 -- maps to Cargo ^1.4 (or bare "1.4").
  • Python ~=1.4.2 (3 parts) means >=1.4.2, <1.5.0 -- maps to Cargo ~1.4.2.

Cargo’s default ^1.4.2 means >=1.4.2, <2.0.0 -- much more permissive than ~1.4.2. When porting 3-component ~=, use ~ in Cargo, not the bare version.

Lock Files

PythonRustNotes
uv.lock / poetry.lockCargo.lockSame purpose: reproducible builds
Commit lock for appsCommit Cargo.lock for binariesSame rule: commit for apps, don’t for libraries
Don’t commit for librariesDon’t commit for librariesCargo docs say the same as Python community convention
pip freeze > requirements.txtCargo.lock (automatic)No manual freeze step in Rust
uv pip compile (lock without install)cargo generate-lockfileRarely needed; cargo auto-generates

Dependency Management Commands

PythonRustNotes
uv add requestscargo add reqwestAdds to manifest
uv remove requestscargo remove reqwest
uv sync / pip install -rcargo build (fetches automatically)No separate install step
uv lock / pip compilecargo updateRegenerate lock file
uv tree / pipdeptreecargo treeDependency tree
pip install -e .cargo build (always editable)No concept of editable install
uv run script.pycargo run
uv run pytestcargo test

Environment and Toolchain Management

PythonRustNotes
pyenv / uv python installrustup installInstall toolchain versions
.python-versionrust-toolchain.tomlPin project toolchain version
python -m venv .venvN/ANo virtualenvs; Cargo isolates via target/
source .venv/bin/activateN/ANo activation step
VIRTUAL_ENV env varCARGO_HOME (~/.cargo)Where tools/caches live
uv tool install ruffcargo install ripgrepInstall CLI tools globally
pipxcargo install / cargo binstallInstall binaries from registry

Key difference: Python requires creating and activating virtual environments per project. Rust has no equivalent -- cargo builds into a project-local target/ directory and resolves dependencies per-project from the lock file. There is no global dependency state to isolate from.

Linting and Formatting

PythonRustNotes
ruff checkcargo clippyLinter
ruff format / blackcargo fmtFormatter
mypy / pyright(compiler)Rust’s type checker is the compiler itself
isort(rustfmt)Import ordering is handled by rustfmt
# noqa: E501#[allow(clippy::too_many_lines)]Suppress specific lint
# type: ignoreN/ANo equivalent needed; compiler errors are real errors
[tool.ruff] in pyproject.toml[lints.clippy] in Cargo.tomlLint config location
[tool.ruff.format] in pyproject.tomlrustfmt.tomlFormat config location
ruff.toml / .flake8clippy.toml (rare; most use Cargo.toml)
.pre-commit-config.yamljustfile precommit recipe or git hooksNo standard pre-commit framework

Key difference: Python needs 3+ tools (ruff/black + mypy + isort) to achieve what Rust gets from 2 (rustfmt + clippy) plus the compiler. The compiler subsumes the role of Python type checkers entirely. There is no “gradually typed” -- it’s all checked.

Testing

PythonRustNotes
pytestcargo test (built-in)No external test runner needed
conftest.pytests/common/mod.rsShared test utilities
tests/ directorytests/ (integration) + #[cfg(test)] (unit)Rust has two test locations
pytest.ini / [tool.pytest].config/nextest.toml (if using nextest)Minimal test config in Rust
pytest-xdist (parallel)cargo nextest runnextest for parallel execution
pytest-cov / coverage.pycargo-tarpaulin / cargo-llvm-cov
pytest --cov --branchcargo tarpaulin --branch / cargo llvm-cov --branchBranch coverage
pytest.fixture (function-scoped)Setup code in test functionNo fixture injection
pytest.fixture (module/session-scoped)std::sync::LazyLock for shared state
@pytest.mark.parametrizerstest crate
@pytest.mark.skip / @pytest.mark.skipif#[ignore]No conditional skip; use #[cfg]
@pytest.mark.xfailNo direct equivalentTest expected failures manually
doctest module`/// ``` ... ```` (doc-tests)Rust doc-tests run via cargo test
Test discovery by test_ prefix#[test] attribute + tests/*.rs filesExplicit, not naming-convention
tox / nox (multi-env testing)CI matrix (OS x toolchain)No equivalent tool; CI handles it

Documentation

PythonRustNotes
"""Docstring""" inside function/// Doc comment before functionPosition differs
Google/NumPy/reST docstring stylesMarkdown with # Examples, # Errors, # PanicsStandardized sections
Sphinx (RST-based)cargo doc / docs.rsAuto-generated API docs
MkDocs (Markdown-based)mdbookBook-style documentation
ReadTheDocs hostingdocs.rs (automatic for crates.io)Hosting is free for published crates
help() / pydoccargo doc --open
autodoc extracts from sourcecargo doc extracts from /// commentsSame concept
__doc__ attribute#[doc = "..."] attributeRarely used directly

Security Auditing

PythonRustNotes
pip-auditcargo auditVulnerability scanning
safety (legacy)cargo audit
bandit (SAST)cargo-deny + cargo-geigerStatic security analysis
pip-licenses / liccheckcargo deny check licensesLicense compliance
PyPI advisory database (OSV)RustSec advisory database
Dependabot / RenovateDependabot / Renovate (same tools)Both ecosystems supported

Task Runners

PythonRustNotes
Makefilejustfile (recommended) or Makefilejust is the modern Rust convention
toxN/A (CI matrix)No Rust equivalent
noxjust / cargo-xtask
hatch scripts / pdm scriptsjust recipes
invoke / fabricjust / cargo-make
pre-commit frameworkGit hooks + just precommitNo standard framework; just is common

CI/CD Patterns

Python CI stepRust CI stepNotes
actions/setup-python@v6dtolnay/rust-toolchain@stableToolchain setup
astral-sh/setup-uv@v8Swatinem/rust-cache@v2Dependency caching
Cache pip/uv cache dirCache target/ via rust-cacheDifferent cache strategies
uv sync / pip install -r requirements.txt(automatic on first cargo command)No explicit install step
ruff check .cargo clippy --all-targets --all-features -- -D warningsLint step
ruff format --check .cargo fmt --all -- --checkFormat check step
mypy .N/A (compiler checks types)
pytestcargo test --all-features --locked--locked ensures Cargo.lock is used
pytest -n auto (parallel)cargo nextest run
Matrix across Python versions (3.11, 3.12)Matrix across OS (ubuntu, macos, windows)Different matrix axes
MSRV: test with oldest PythonMSRV: dtolnay/rust-toolchain@1.85 (pin to MSRV)
pip-auditrustsec/audit-check@v2
EmbarkStudios/cargo-deny-action@v2No Python equivalent

Pitfall: CI build time. Python CI is fast (no compilation). Rust CI is slow (compilation from scratch takes minutes). Mitigate with Swatinem/rust-cache@v2, separate CI jobs (fmt and clippy are fast, test is slow), and cargo check before cargo test where possible.

Feature Flags vs Optional Dependencies

PythonRustNotes
[project.optional-dependencies] extras[features] in Cargo.tomlDefine named feature groups
pip install package[dev,test]cargo build --features dev,testActivate features
try: import X; except ImportError:#[cfg(feature = "cli")]Conditional compilation
No default extrasdefault = ["cli"] in [features]Cargo supports default features
Runtime availability checkCompile-time conditional compilationFundamental difference
# Rust: Cargo.toml features example (from flowmark-rs)
[features]
default = ["cli"]
cli = ["clap", "color-eyre", "tracing", "tempfile", "indicatif", "ctrlc"]

# Dependencies gated behind features
[dependencies]
clap = { version = "4.6", features = ["derive"], optional = true }

Key difference: Python optional dependencies exist at runtime -- you check if they’re importable. Rust features are compile-time -- code behind #[cfg(feature = "...")] is not compiled at all. This means Rust features affect binary size and compilation time, not just runtime behavior.

Build and Release

PythonRustNotes
python -m build / hatch buildcargo build --releaseBuild step
sdist / wheel formats.crate format (for crates.io)
twine upload / uv publishcargo publishPublish to registry
setuptools / hatch / flitcargo (only one build system)No build backend fragmentation
setup.py (build-time code)build.rs (build script)Arbitrary build-time logic
MANIFEST.in / include/excludeinclude/exclude in Cargo.toml [package]Control what goes in the package
__version__ / importlib.metadataenv!("CARGO_PKG_VERSION")Version at compile time
setuptools-scm (git-based versioning)cargo-release (version bumping + tagging)

Config File Locations

PurposePythonRust
Package manifestpyproject.tomlCargo.toml
Lock fileuv.lock / poetry.lockCargo.lock
Formatter[tool.ruff.format] or [tool.black]rustfmt.toml
Linter[tool.ruff] or .flake8[lints.clippy] in Cargo.toml
Type checker[tool.mypy] or pyrightconfig.jsonN/A (compiler)
Test runner[tool.pytest.ini_options].config/nextest.toml (optional)
Coverage[tool.coverage](cargo-tarpaulin / cargo-llvm-cov flags)
Dependency policyN/Adeny.toml
Release automationN/Arelease.toml
Build settings[build-system].cargo/config.toml
Task runnerMakefile / tox.ini / noxfile.pyjustfile
Toolchain pin.python-versionrust-toolchain.toml
Editor settings.editorconfig.editorconfig (same)
Git ignores.gitignore (add .venv/, __pycache__/).gitignore (add target/)
Environment varsPYTHONPATH, VIRTUAL_ENVCARGO_HOME, RUSTFLAGS, RUST_LOG

Key insight: Python consolidates most tool config into pyproject.toml via [tool.*] sections. Rust distributes config across separate files (rustfmt.toml, deny.toml, release.toml, .cargo/config.toml). When setting up a Rust project, expect to create 4-6 config files where Python would have one.

Dependency Mapping (Flowmark-Specific)

Python PackageRust CrateQualityNotes
marko (Markdown)comrak 0.52+Good with workarounds12/15 differences worked around; check for API changes in current releases
argparseclap 4.6 (derive)ExcellentPerfect mapping
PyYAMLserde_yaml_ng 0.10+Goodserde_yaml is archived. serde_norway is an alternative that tracks a maintained libyaml fork; prefer it if C-lib supply-chain maintenance matters
re (regex)regex 1.12+ExcellentDifferent anchoring and replacement syntax! See Regex section above
re (look-arounds, backrefs)fancy-regex 0.17+GoodBacktracking engine; only use when needed
textwraptextwrap (crate)GoodAlso rolled custom for special cases
pytestcargo test (built-in)Excellent
(dataclasses)serde Serialize/DeserializeExcellent
(type hints)Rust type systemBuilt-in
(none)thiserror 2.0ExcellentLibrary error types
(none)color-eyre 0.6ExcellentOptional rich CLI error display; anyhow is the simpler default
(none)tracing 0.1ExcellentStructured logging
(none)indicatif 0.18GoodProgress bars
(none)tempfile 3.27ExcellentAtomic file writes
(none)unicode-segmentation 1.11ExcellentUnicode-safe text ops
(none)proptest 1.4ExcellentProperty-based testing