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
| Python | Rust | Example |
|---|
str | &str (borrow) / String (owned) | Accept &str, return String |
int | i32 / i64 / usize | Match semantic range |
float | f64 / f32 | f64 for double precision |
bool | bool | Same |
None | Option::None | Use Option<T> |
bytes | &[u8] / Vec<u8> | |
complex | (no built-in) | Use num-complex crate |
| Python | Rust | Notes |
|---|
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 |
frozenset | HashSet<T> | Immutability via let binding, not the type. For use as hash key, need newtype implementing Hash |
deque | VecDeque<T> | |
defaultdict(list) | HashMap<K, Vec<V>> + .entry().or_default() | |
Counter | HashMap<T, usize> | |
OrderedDict | IndexMap<K, V> (indexmap crate) | |
namedtuple | Struct with named fields | |
| Python | Rust | Notes |
|---|
Optional[T] | Option<T> | |
Union[A, B] | enum { A(A), B(B) } | |
Any | Generics or Box<dyn Trait> | Avoid if possible |
TypeVar('T') | <T> generics | |
Protocol | trait | Rust traits are nominal -- must write explicit impl Trait for Type |
TypedDict | Struct with named fields | Add serde derives only if deserializing from data formats |
Literal["a", "b"] | enum { A, B } | |
Callable[[A], B] | Fn(A) -> B / FnMut / FnOnce | Fn -- no mutation, callable repeatedly. FnMut -- mutates captures. FnOnce -- consumes captures. Use Box<dyn Fn(A) -> B> to store callbacks in structs |
# 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"
};
# 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(),
}
| Python | Rust |
|---|
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 / continue | break / continue |
for/else | No direct equivalent -- use if let Some(item) = iter.find(|x| cond(x)) { ... } else { ... } |
# 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();
| Python | Rust | Notes |
|---|
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 == y | assert_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 |
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.
| Python | Rust | Notes |
|---|
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 generator | impl Drop for cleanup | Or closure-based API: with_resource(|r| { ... }) |
__enter__ / __exit__ | impl Drop | Drop::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)
# 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}")))
}
| Python | Rust | Notes |
|---|
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 (borrowed) / PathBuf (owned) are analogous to str / String.
Accept impl AsRef<Path> for read-only access; accept impl Into<PathBuf> when storing.
| Python | Rust | Notes |
|---|
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).parent | path.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");
| Python | Rust | Notes |
|---|
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 |
| Python | Rust | Notes |
|---|
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.
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.
| Crate | Version | Purpose | When to use |
|---|
walkdir | 2.5 | Recursive directory traversal | Any directory walking; prefer over manual read_dir recursion |
tempfile | 3.27 | Secure temp files and dirs | Atomic writes, test isolation |
dirs | 6.0 | Platform directory locations | Home dir, config dir, etc. (dirs-next is abandoned — use dirs) |
pathdiff | 0.2 | Compute relative paths | When you need os.path.relpath equivalent |
filetime | 0.2 | Read/write file timestamps | Preserving timestamps across copy. For simple cases, std::fs::File::set_times() (stable since Rust 1.75) may suffice |
glob | 0.3 | Simple glob matching | Basic *, ?, [...] patterns. For {a,b} or multi-pattern, use globset 0.4 |
fs-err | 3.3 | Better filesystem error messages | Drop-in std::fs replacement for applications; adds file paths to errors |
| Python | Rust | Notes |
|---|
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 |
Crate: regex 1.12+ (Thompson NFA engine, guaranteed linear time).
| Python | Rust | Notes |
|---|
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/behind | Use 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().
This is a common source of bugs. Python and Rust use different backreference syntax
in replacement strings:
| Feature | Python re.sub | Rust 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 expansion | N/A | regex::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.
| Python | Rust inline | RegexBuilder method | Notes |
|---|
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.
Both Python 3 and Rust regex default to Unicode-aware matching, but the opt-out
mechanisms differ:
| Class | Python 3 default | Rust regex default | ASCII-only (Python) | ASCII-only (Rust) |
|---|
\w | Unicode letters+digits+_ | Unicode letters+digits+_ | re.ASCII → [a-zA-Z0-9_] | (?-u:\w) → [a-zA-Z0-9_] |
\d | Unicode digits | Unicode digits (\p{Nd}) | re.ASCII → [0-9] | (?-u:\d) → [0-9] |
\s | Unicode whitespace | Unicode whitespace | re.ASCII → [\t\n\r\f\v ] | (?-u:\s) → ASCII whitespace |
\b | Unicode word boundary | Unicode word boundary | re.ASCII → ASCII boundary | (?-u:\b) → ASCII boundary |
. | Any char except \n | Any Unicode scalar except \n | Same | Same |
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).
The regex crate deliberately omits features that would break linear-time guarantees.
Use fancy-regex 0.17+ for these:
| Feature | Python re | Rust regex | Rust fancy-regex |
|---|
Positive lookahead (?=...) | Yes | No | Yes |
Negative lookahead (?!...) | Yes | No | Yes |
Positive lookbehind (?<=...) | Yes (fixed-width) | No | Yes (variable-width) |
Negative lookbehind (?<!...) | Yes (fixed-width) | No | Yes (variable-width) |
Backreferences \1 in pattern | Yes | No | Yes |
Atomic groups (?>...) | Yes (3.11+) | No | Yes |
Possessive quantifiers a++ | Yes (3.11+) | Yes | Yes |
Conditional (?(id)yes|no) | Yes | No | Partial (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).
| Aspect | Python re | Rust regex | Rust fancy-regex |
|---|
| Engine | Backtracking | Thompson NFA | Hybrid (NFA + backtracking) |
| Worst-case time | Exponential | O(m × n) guaranteed | Exponential (fancy features only) |
| Compilation cost | Moderate | Expensive (use LazyLock) | Expensive (use LazyLock) |
| Size limits | None | Default ~10 MB (RegexBuilder::size_limit()) | None |
| Untrusted patterns | Vulnerable to ReDoS | Safe (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.
# 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
}
}
# 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" }
}
| Python Dunder | Rust Equivalent | Notes |
|---|
__str__() | impl fmt::Display | Used 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() method | No standard trait; convention only |
__iter__() + __next__() | impl Iterator | Must define type Item |
__getitem__() | impl Index<Idx> | Returns reference, not owned value |
__add__() etc. | impl Add from std::ops | One trait per operator |
__enter__()/__exit__() | impl Drop + RAII scope | See Context Managers to RAII section above |
__call__() | impl Fn/FnMut/FnOnce | Or just a method; Rust closures implement Fn traits |
__contains__() | .contains() method | No standard trait; convention only |
__bool__() | No standard trait | Use explicit .is_empty() or method |
__del__() | impl Drop | Called when value is dropped; cannot fail |
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.
Python @dataclass | Rust Equivalent | Notes |
|---|
@dataclass | #[derive(Debug, Clone, PartialEq)] | Closest equivalent derives |
frozen=True | No &mut self methods | Immutability 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 | Rust | Notes |
|---|
class Color(Enum): | enum Color { ... } | |
Color.RED | Color::Red | Rust uses CamelCase variants |
Color(1) (by value) | Custom from_value() method | No built-in value lookup |
color.name | Use strum crate Display derive | #[derive(strum::Display)] |
Color["RED"] (by name) | Use strum crate EnumString derive | #[derive(strum::EnumString)] |
auto() | Explicit variants | No 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,
}
| Python | Rust | Notes |
|---|
@decorator | Procedural macros or wrapper functions | No direct equivalent; use #[derive(...)] for common cases |
@property | Method returning value / public field | No descriptor protocol; just use pub fn name(&self) -> &T |
@staticmethod | Associated function (no self) | fn foo() in impl block (no self parameter) |
@classmethod | Associated function taking no self | fn new(...) -> Self is the convention |
*args | Variadic: not supported | Use slice &[T] or macro for variable args |
**kwargs | No direct equivalent | Use a builder pattern or config struct |
isinstance(x, T) | Pattern matching or trait bounds | if let Some(v) = x.downcast_ref::<T>() for dyn Any |
getattr(obj, name) | No runtime reflection | Use enum dispatch or trait objects instead |
lambda x: x + 1 | |x| x + 1 | Closures; type inferred |
| Python | Rust | Notes |
|---|
async def foo() | async fn foo() | |
await bar() | bar().await | Postfix 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 cleanup | No async Drop |
| Python | Rust | Notes |
|---|
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.ThreadPoolExecutor | rayon::ThreadPool or tokio::spawn | rayon for CPU, tokio for I/O |
concurrent.futures.ProcessPoolExecutor | rayon::ThreadPool | Rust threads share memory; no process isolation needed |
queue.Queue | std::sync::mpsc::channel() | Or crossbeam::channel for multi-consumer (MPMC) |
threading.Lock | std::sync::Mutex<T> | Mutex wraps data in Rust, not code |
| Python | Rust | Notes |
|---|
def test_foo(): | #[test] fn test_foo() { | |
assert x == y | assert_eq!(x, y) | |
assert x != y | assert_ne!(x, y) | |
assert x | assert!(x) | |
with pytest.raises(E): | #[should_panic] or assert!(result.is_err()) | |
@pytest.mark.skip | #[ignore] | |
@pytest.fixture | Setup in test function or std::sync::LazyLock | |
@pytest.mark.parametrize | rstest crate or macro-generated tests | |
pytest.approx(x) | (x - y).abs() < epsilon | Or use approx crate: assert_relative_eq!, assert_abs_diff_eq! |
| Python | Rust | Notes |
|---|
pyproject.toml | Cargo.toml | |
pip install | cargo install / cargo add | |
requirements.txt | Cargo.toml [dependencies] | |
setup.py | Cargo.toml | |
__init__.py | lib.rs | |
__main__.py | main.rs | |
| PyPI | crates.io | |
pip install -e . | cargo build (automatic) | |
| Virtual environments | Cargo handles isolation via target/ dir | |
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.
Python (pyproject.toml) | Rust (Cargo.toml) | Notes |
|---|
[project] name, version, description | [package] name, version, description | Same fields, different table name |
[project] authors, license | [package] authors, license | Rust 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, homepage | Rust separates homepage from repository |
[project] keywords, classifiers | [package] keywords, categories | Rust categories are from a fixed list on crates.io |
[project.dependencies] | [dependencies] | See version constraints below |
[project.optional-dependencies] | [features] + optional deps | See 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 |
| Python | Rust (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.
| Python | Rust | Notes |
|---|
uv.lock / poetry.lock | Cargo.lock | Same purpose: reproducible builds |
| Commit lock for apps | Commit Cargo.lock for binaries | Same rule: commit for apps, don’t for libraries |
| Don’t commit for libraries | Don’t commit for libraries | Cargo docs say the same as Python community convention |
pip freeze > requirements.txt | Cargo.lock (automatic) | No manual freeze step in Rust |
uv pip compile (lock without install) | cargo generate-lockfile | Rarely needed; cargo auto-generates |
| Python | Rust | Notes |
|---|
uv add requests | cargo add reqwest | Adds to manifest |
uv remove requests | cargo remove reqwest | |
uv sync / pip install -r | cargo build (fetches automatically) | No separate install step |
uv lock / pip compile | cargo update | Regenerate lock file |
uv tree / pipdeptree | cargo tree | Dependency tree |
pip install -e . | cargo build (always editable) | No concept of editable install |
uv run script.py | cargo run | |
uv run pytest | cargo test | |
| Python | Rust | Notes |
|---|
pyenv / uv python install | rustup install | Install toolchain versions |
.python-version | rust-toolchain.toml | Pin project toolchain version |
python -m venv .venv | N/A | No virtualenvs; Cargo isolates via target/ |
source .venv/bin/activate | N/A | No activation step |
VIRTUAL_ENV env var | CARGO_HOME (~/.cargo) | Where tools/caches live |
uv tool install ruff | cargo install ripgrep | Install CLI tools globally |
pipx | cargo install / cargo binstall | Install 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.
| Python | Rust | Notes |
|---|
ruff check | cargo clippy | Linter |
ruff format / black | cargo fmt | Formatter |
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: ignore | N/A | No equivalent needed; compiler errors are real errors |
[tool.ruff] in pyproject.toml | [lints.clippy] in Cargo.toml | Lint config location |
[tool.ruff.format] in pyproject.toml | rustfmt.toml | Format config location |
ruff.toml / .flake8 | clippy.toml (rare; most use Cargo.toml) | |
.pre-commit-config.yaml | justfile precommit recipe or git hooks | No 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.
| Python | Rust | Notes |
|---|
pytest | cargo test (built-in) | No external test runner needed |
conftest.py | tests/common/mod.rs | Shared test utilities |
tests/ directory | tests/ (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 run | nextest for parallel execution |
pytest-cov / coverage.py | cargo-tarpaulin / cargo-llvm-cov | |
pytest --cov --branch | cargo tarpaulin --branch / cargo llvm-cov --branch | Branch coverage |
pytest.fixture (function-scoped) | Setup code in test function | No fixture injection |
pytest.fixture (module/session-scoped) | std::sync::LazyLock for shared state | |
@pytest.mark.parametrize | rstest crate | |
@pytest.mark.skip / @pytest.mark.skipif | #[ignore] | No conditional skip; use #[cfg] |
@pytest.mark.xfail | No direct equivalent | Test expected failures manually |
doctest module | `/// ``` ... ```` (doc-tests) | Rust doc-tests run via cargo test |
Test discovery by test_ prefix | #[test] attribute + tests/*.rs files | Explicit, not naming-convention |
tox / nox (multi-env testing) | CI matrix (OS x toolchain) | No equivalent tool; CI handles it |
| Python | Rust | Notes |
|---|
"""Docstring""" inside function | /// Doc comment before function | Position differs |
| Google/NumPy/reST docstring styles | Markdown with # Examples, # Errors, # Panics | Standardized sections |
| Sphinx (RST-based) | cargo doc / docs.rs | Auto-generated API docs |
| MkDocs (Markdown-based) | mdbook | Book-style documentation |
| ReadTheDocs hosting | docs.rs (automatic for crates.io) | Hosting is free for published crates |
help() / pydoc | cargo doc --open | |
autodoc extracts from source | cargo doc extracts from /// comments | Same concept |
__doc__ attribute | #[doc = "..."] attribute | Rarely used directly |
| Python | Rust | Notes |
|---|
pip-audit | cargo audit | Vulnerability scanning |
safety (legacy) | cargo audit | |
bandit (SAST) | cargo-deny + cargo-geiger | Static security analysis |
pip-licenses / liccheck | cargo deny check licenses | License compliance |
| PyPI advisory database (OSV) | RustSec advisory database | |
| Dependabot / Renovate | Dependabot / Renovate (same tools) | Both ecosystems supported |
| Python | Rust | Notes |
|---|
Makefile | justfile (recommended) or Makefile | just is the modern Rust convention |
tox | N/A (CI matrix) | No Rust equivalent |
nox | just / cargo-xtask | |
hatch scripts / pdm scripts | just recipes | |
invoke / fabric | just / cargo-make | |
pre-commit framework | Git hooks + just precommit | No standard framework; just is common |
| Python CI step | Rust CI step | Notes |
|---|
actions/setup-python@v6 | dtolnay/rust-toolchain@stable | Toolchain setup |
astral-sh/setup-uv@v8 | Swatinem/rust-cache@v2 | Dependency caching |
| Cache pip/uv cache dir | Cache target/ via rust-cache | Different 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 warnings | Lint step |
ruff format --check . | cargo fmt --all -- --check | Format check step |
mypy . | N/A (compiler checks types) | |
pytest | cargo 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 Python | MSRV: dtolnay/rust-toolchain@1.85 (pin to MSRV) | |
pip-audit | rustsec/audit-check@v2 | |
| EmbarkStudios/cargo-deny-action@v2 | No 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.
| Python | Rust | Notes |
|---|
[project.optional-dependencies] extras | [features] in Cargo.toml | Define named feature groups |
pip install package[dev,test] | cargo build --features dev,test | Activate features |
try: import X; except ImportError: | #[cfg(feature = "cli")] | Conditional compilation |
| No default extras | default = ["cli"] in [features] | Cargo supports default features |
| Runtime availability check | Compile-time conditional compilation | Fundamental 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.
| Python | Rust | Notes |
|---|
python -m build / hatch build | cargo build --release | Build step |
sdist / wheel formats | .crate format (for crates.io) | |
twine upload / uv publish | cargo publish | Publish to registry |
setuptools / hatch / flit | cargo (only one build system) | No build backend fragmentation |
setup.py (build-time code) | build.rs (build script) | Arbitrary build-time logic |
MANIFEST.in / include/exclude | include/exclude in Cargo.toml [package] | Control what goes in the package |
__version__ / importlib.metadata | env!("CARGO_PKG_VERSION") | Version at compile time |
setuptools-scm (git-based versioning) | cargo-release (version bumping + tagging) | |
| Purpose | Python | Rust |
|---|
| Package manifest | pyproject.toml | Cargo.toml |
| Lock file | uv.lock / poetry.lock | Cargo.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.json | N/A (compiler) |
| Test runner | [tool.pytest.ini_options] | .config/nextest.toml (optional) |
| Coverage | [tool.coverage] | (cargo-tarpaulin / cargo-llvm-cov flags) |
| Dependency policy | N/A | deny.toml |
| Release automation | N/A | release.toml |
| Build settings | [build-system] | .cargo/config.toml |
| Task runner | Makefile / tox.ini / noxfile.py | justfile |
| Toolchain pin | .python-version | rust-toolchain.toml |
| Editor settings | .editorconfig | .editorconfig (same) |
| Git ignores | .gitignore (add .venv/, __pycache__/) | .gitignore (add target/) |
| Environment vars | PYTHONPATH, VIRTUAL_ENV | CARGO_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.
| Python Package | Rust Crate | Quality | Notes |
|---|
| marko (Markdown) | comrak 0.52+ | Good with workarounds | 12/15 differences worked around; check for API changes in current releases |
| argparse | clap 4.6 (derive) | Excellent | Perfect mapping |
| PyYAML | serde_yaml_ng 0.10+ | Good | serde_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+ | Excellent | Different anchoring and replacement syntax! See Regex section above |
| re (look-arounds, backrefs) | fancy-regex 0.17+ | Good | Backtracking engine; only use when needed |
| textwrap | textwrap (crate) | Good | Also rolled custom for special cases |
| pytest | cargo test (built-in) | Excellent | |
| (dataclasses) | serde Serialize/Deserialize | Excellent | |
| (type hints) | Rust type system | Built-in | |
| (none) | thiserror 2.0 | Excellent | Library error types |
| (none) | color-eyre 0.6 | Excellent | Optional rich CLI error display; anyhow is the simpler default |
| (none) | tracing 0.1 | Excellent | Structured logging |
| (none) | indicatif 0.18 | Good | Progress bars |
| (none) | tempfile 3.27 | Excellent | Atomic file writes |
| (none) | unicode-segmentation 1.11 | Excellent | Unicode-safe text ops |
| (none) | proptest 1.4 | Excellent | Property-based testing |