πŸ¦€/🐍/16. Best Practices

Idiomatic Rust for Python Developers

What you'll learn: Top 10 habits to build, common pitfalls with fixes, a structured 3-month learning path, the complete Python→Rust "Rosetta Stone" reference table, and recommended learning resources.

Difficulty: 🟑 Intermediate

flowchart LR
    A["🟒 Week 1-2\nFoundations\n'Why won't this compile?'"] --> B["🟑 Week 3-4\nCore Concepts\n'Oh, it's protecting me'"] 
    B --> C["🟑 Month 2\nIntermediate\n'I see why this matters'"] 
    C --> D["πŸ”΄ Month 3+\nAdvanced\n'Caught a bug at compile time!'"] 
    D --> E["πŸ† Month 6\nFluent\n'Better programmer everywhere'"]
    style A fill:#d4edda
    style B fill:#fff3cd
    style C fill:#fff3cd
    style D fill:#f8d7da
    style E fill:#c3e6cb,stroke:#28a745

Top 10 Habits to Build

  1. Use match on enums instead of if isinstance()

    # Python                              # Rust
    if isinstance(shape, Circle): ...     match shape { Shape::Circle(r) => ... }
    
  2. Let the compiler guide you β€” Read error messages carefully. Rust's compiler is the best in any language. It tells you what's wrong AND how to fix it.

  3. Prefer &str over String in function parameters β€” Accept the most general type. &str works with both String and string literals.

  4. Use iterators instead of index loops β€” Iterator chains are more idiomatic and often faster than for i in 0..vec.len().

  5. Embrace Option and Result β€” Don't .unwrap() everything. Use ?, map, and_then, unwrap_or_else.

  6. Derive traits liberally β€” #[derive(Debug, Clone, PartialEq)] should be on most structs. It's free and makes testing easier.

  7. Use cargo clippy religiously β€” It catches hundreds of style and correctness issues. Treat it like ruff for Rust.

  8. Don't fight the borrow checker β€” If you're fighting it, you're probably structuring data wrong. Refactor to make ownership clear.

  9. Use enums for state machines β€” Instead of string flags or booleans, use enums. The compiler ensures you handle every state.

  10. Clone first, optimize later β€” When learning, use .clone() freely to avoid ownership complexity. Optimize only when profiling shows a need.

Common Mistakes from Python Developers

MistakeWhyFix
.unwrap() everywherePanics at runtimeUse ? or match
String instead of &strUnnecessary allocationUse &str for params
for i in 0..vec.len()Not idiomaticfor item in &vec
Ignoring clippy warningsMiss easy improvementscargo clippy
Too many .clone() callsPerformance overheadRefactor ownership
Giant main() functionHard to testExtract into lib.rs
Not using #[derive()]Re-inventing the wheelDerive common traits
Panicking on errorsNot recoverableReturn Result<T, E>

Performance Comparison

Benchmark: Common Operations

Operation              Python 3.12    Rust (release)    Speedup
─────────────────────  ────────────   ──────────────    ─────────
Fibonacci(40)          ~25s           ~0.3s             ~80x
Sort 10M integers      ~5.2s          ~0.6s             ~9x
JSON parse 100MB       ~8.5s          ~0.4s             ~21x
Regex 1M matches       ~3.1s          ~0.3s             ~10x
HTTP server (req/s)    ~5,000         ~150,000          ~30x
SHA-256 1GB file       ~12s           ~1.2s             ~10x
CSV parse 1M rows      ~4.5s          ~0.2s             ~22x
String concatenation   ~2.1s          ~0.05s            ~42x

Note: Python with C extensions (NumPy, etc.) dramatically narrows the gap for numerical work. These benchmarks compare pure Python vs pure Rust.

Memory Usage

Python:                                 Rust:
─────────                               ─────
- Object header: 28 bytes/object       - No object header
- int: 28 bytes (even for 0)           - i32: 4 bytes, i64: 8 bytes
- str "hello": 54 bytes                - &str "hello": 16 bytes (ptr + len)
- list of 1000 ints: ~36 KB            - Vec<i32>: ~4 KB
  (8 KB pointers + 28 KB int objects)
- dict of 100 items: ~5.5 KB           - HashMap of 100: ~2.4 KB

Total for typical application:
- Python: 50-200 MB baseline           - Rust: 1-5 MB baseline

Common Pitfalls and Solutions

Pitfall 1: "The Borrow Checker Won't Let Me"

// Problem: trying to iterate and modify
let mut items = vec![1, 2, 3, 4, 5];
// for item in &items {
//     if *item > 3 { items.push(*item * 2); }  // ❌ Can't borrow mut while borrowed
// }

// Solution 1: collect changes, apply after
let additions: Vec<i32> = items.iter()
    .filter(|&&x| x > 3)
    .map(|&x| x * 2)
    .collect();
items.extend(additions);

// Solution 2: use retain/extend
items.retain(|&x| x <= 3);

Pitfall 2: "Too Many String Types"

// When in doubt:
// - &str for function parameters
// - String for struct fields and return values
// - &str literals ("hello") work everywhere &str is expected

fn process(input: &str) -> String {    // Accept &str, return String
    format!("Processed: {}", input)
}

Pitfall 3: "I Miss Python's Simplicity"

// Python one-liner:
// result = [x**2 for x in data if x > 0]

// Rust equivalent:
let result: Vec<i32> = data.iter()
    .filter(|&&x| x > 0)
    .map(|&x| x * x)
    .collect();

// It's more verbose, but:
// - Type-safe at compile time
// - 10-100x faster
// - No runtime type errors possible
// - Explicit about memory allocation (.collect())

Pitfall 4: "Where's My REPL?"

// Rust has no REPL. Instead:
// 1. Use `cargo test` as your REPL β€” write small tests to try things
// 2. Use Rust Playground (play.rust-lang.org) for quick experiments
// 3. Use `dbg!()` macro for quick debug output
// 4. Use `cargo watch -x test` for auto-running tests on save

#[test]
fn playground() {
    // Use this as your "REPL" β€” run with `cargo test playground`
    let result = "hello world"
        .split_whitespace()
        .map(|w| w.to_uppercase())
        .collect::<Vec<_>>();
    dbg!(&result);  // Prints: [src/main.rs:5] &result = ["HELLO", "WORLD"]
}

Learning Path and Resources

Week 1-2: Foundations

  • Install Rust, set up VS Code with rust-analyzer
  • Complete chapters 1-4 of this guide (types, control flow)
  • Write 5 small programs converting Python scripts to Rust
  • Get comfortable with cargo build, cargo test, cargo clippy

Week 3-4: Core Concepts

  • Complete chapters 5-8 (structs, enums, ownership, modules)
  • Rewrite a Python data processing script in Rust
  • Practice with Option<T> and Result<T, E> until natural
  • Read compiler error messages carefully β€” they're teaching you

Month 2: Intermediate

  • Complete chapters 9-12 (error handling, traits, iterators)
  • Build a CLI tool with clap and serde
  • Write a PyO3 extension for a Python project hotspot
  • Practice iterator chains until they feel like comprehensions

Month 3: Advanced

  • Complete chapters 13-16 (concurrency, unsafe, testing)
  • Build a web service with axum and tokio
  • Contribute to an open-source Rust project
  • Read "Programming Rust" (O'Reilly) for deeper understanding

Python β†’ Rust Rosetta Stone

PythonRustChapter
listVec<T>5
dictHashMap<K,V>5
setHashSet<T>5
tuple(T1, T2, ...)5
classstruct + impl5
@dataclass#[derive(...)]5, 12a
Enumenum6
NoneOption<T>6
raise/try/exceptResult<T,E> + ?9
Protocol (PEP 544)trait10
TypeVarGenerics <T>10
__dunder__ methodsTraits (Display, Add, etc.)10
lambda|args| body12
generator yieldimpl Iterator12
list comprehension.map().filter().collect()12
@decoratorHigher-order fn or macro12a, 15
asynciotokio13
threadingstd::thread13
multiprocessingrayon13
unittest.mockmockall14a
pytestcargo test + rstest14a
pip installcargo add8
requirements.txtCargo.lock8
pyproject.tomlCargo.toml8
with (context mgr)Scope-based Drop15
json.dumps/loadsserde_json15

Final Thoughts for Python Developers

What you'll miss from Python:
- REPL and interactive exploration
- Rapid prototyping speed
- Rich ML/AI ecosystem (PyTorch, etc.)
- "Just works" dynamic typing
- pip install and immediate use

What you'll gain from Rust:
- "If it compiles, it works" confidence
- 10-100x performance improvement
- No more runtime type errors
- No more None/null crashes
- True parallelism (no GIL!)
- Single binary deployment
- Predictable memory usage
- The best compiler error messages in any language

The journey:
Week 1:   "Why does the compiler hate me?"
Week 2:   "Oh, it's actually protecting me from bugs"
Month 1:  "I see why this matters"
Month 2:  "I caught a bug at compile time that would've been a production incident"
Month 3:  "I don't want to go back to untyped code"
Month 6:  "Rust has made me a better programmer in every language"

Exercises

<details> <summary><strong>πŸ‹οΈ Exercise: Code Review Checklist</strong> (click to expand)</summary>

Challenge: Review this Rust code (written by a Python developer) and identify 5 idiomatic improvements:

fn get_name(names: Vec<String>, index: i32) -> String {
    if index >= 0 && (index as usize) < names.len() {
        return names[index as usize].clone();
    } else {
        return String::from("");
    }
}

fn main() {
    let mut result = String::from("");
    let names = vec!["Alice".to_string(), "Bob".to_string()];
    result = get_name(names.clone(), 0);
    println!("{}", result);
}
<details> <summary>πŸ”‘ Solution</summary>

Five improvements:

// 1. Take &[String] not Vec<String> (don't take ownership of the whole vec)
// 2. Use usize for index (not i32 β€” indices are always non-negative)
// 3. Return Option<&str> instead of empty string (use the type system!)
// 4. Use .get() instead of bounds-checking manually
// 5. Don't clone() in main β€” pass a reference

fn get_name(names: &[String], index: usize) -> Option<&str> {
    names.get(index).map(|s| s.as_str())
}

fn main() {
    let names = vec!["Alice".to_string(), "Bob".to_string()];
    match get_name(&names, 0) {
        Some(name) => println!("{name}"),
        None => println!("Not found"),
    }
}

Key takeaway: Python habits that hurt in Rust: cloning everything (use borrows), using sentinel values like "" (use Option), taking ownership when borrowing suffices, and using signed integers for indices.

</details> </details>

End of Rust for Python Programmers Training Guide