Rust for Python Programmers: Complete Training Guide / 面向 Python 程序员的 Rust 完整培训指南
A comprehensive guide to learning Rust for developers with Python experience. This guide covers everything from basic syntax to advanced patterns, focusing on the conceptual shifts required when moving from a dynamically-typed, garbage-collected language to a statically-typed systems language with compile-time memory safety.
这是一本面向 Python 开发者的 Rust 学习指南,涵盖从基础语法到高级模式的内容,重点讲解从动态类型、垃圾回收语言迁移到具备编译期内存安全保证的静态类型系统语言时所需要的思维转变。
How to Use This Book / 如何使用本书
Self-study format: Work through Part I (ch 1-6) first - these map closely to Python concepts you already know. Part II (ch 7-12) introduces Rust-specific ideas like ownership and traits. Part III (ch 13-16) covers advanced topics and migration.
自学建议:先学习第一部分(第 1-6 章),这些内容与 Python 中已有概念最接近。第二部分(第 7-12 章)会引入 Rust 特有概念,如所有权和 trait。第三部分(第 13-16 章)讨论高级主题与迁移问题。
Pacing recommendations / 学习节奏建议:
| Chapters / 章节 | Topic / 主题 | Suggested Time / 建议时间 | Checkpoint / 检查点 |
|---|---|---|---|
| 1-4 | Setup, types, control flow / 环境、类型、控制流 | 1 day / 1 天 | You can write a CLI temperature converter in Rust / 你可以用 Rust 写出命令行温度转换器 |
| 5-6 | Data structures, enums, pattern matching / 数据结构、枚举、模式匹配 | 1-2 days / 1-2 天 | You can define an enum with data and match exhaustively on it / 你可以定义携带数据的枚举并用 match 完整匹配 |
| 7 | Ownership and borrowing / 所有权与借用 | 1-2 days / 1-2 天 | You can explain why let s2 = s1 invalidates s1 / 你可以解释为什么 let s2 = s1 会使 s1 失效 |
| 8-9 | Modules, error handling / 模块、错误处理 | 1 day / 1 天 | You can create a multi-file project that propagates errors with ? / 你可以创建一个多文件项目并用 ? 传播错误 |
| 10-12 | Traits, generics, closures, iterators / Trait、泛型、闭包、迭代器 | 1-2 days / 1-2 天 | You can translate a list comprehension to an iterator chain / 你可以把列表推导式翻译成迭代器链 |
| 13 | Concurrency / 并发 | 1 day / 1 天 | You can write a thread-safe counter with Arc<Mutex<T>> / 你可以用 Arc<Mutex<T>> 写出线程安全计数器 |
| 14 | Unsafe, PyO3, testing / Unsafe、PyO3、测试 | 1 day / 1 天 | You can call a Rust function from Python via PyO3 / 你可以通过 PyO3 从 Python 调用 Rust 函数 |
| 15-16 | Migration, best practices / 迁移、最佳实践 | At your own pace / 自定节奏 | Reference material - consult as you write real code / 作为参考材料,在实际开发时按需查阅 |
| 17 | Capstone project / 综合项目 | 2-3 days / 2-3 天 | Build a complete CLI app tying everything together / 构建一个整合各章节内容的完整命令行应用 |
How to use the exercises / 如何使用练习:
- Chapters include hands-on exercises in collapsible
<details>blocks with solutions / 各章包含可折叠<details>区块中的动手练习及答案 - Always try the exercise before expanding the solution. Struggling with the borrow checker is part of learning - the compiler’s error messages are your teacher / 总是先尝试练习,再展开答案。 与借用检查器斗争本身就是学习过程,编译器的报错就是老师
- If you’re stuck for more than 15 minutes, expand the solution, study it, then close it and try again from scratch / 如果卡住超过 15 分钟,就展开答案学习,然后收起并重新独立完成一次
- The Rust Playground lets you run code without a local install / Rust Playground 允许你在未本地安装 Rust 的情况下运行代码
Difficulty indicators / 难度标记:
- 🟢 Beginner - Direct translation from Python concepts / 初级:可以直接从 Python 概念迁移
- 🟡 Intermediate - Requires understanding ownership or traits / 中级:需要理解所有权或 trait
- 🔶 Advanced - Lifetimes, async internals, or unsafe code / 高级:生命周期、async 内部机制或 unsafe 代码
When you hit a wall / 遇到卡点时:
- Read the compiler error message carefully - Rust’s errors are exceptionally helpful / 仔细阅读编译器错误信息,Rust 的错误提示通常非常有帮助
- Re-read the relevant section; concepts like ownership (ch7) often click on the second pass / 重读相关小节,像所有权这样的概念往往第二遍才真正理解
- The Rust standard library docs are excellent - search for any type or method / Rust 标准库文档 非常优秀,遇到类型或方法都值得去查
- For deeper async patterns, see the companion Async Rust Training / 如需更深入的异步内容,请参考配套的 Async Rust Training
Table of Contents / 目录
Part I - Foundations / 第一部分:基础
1. Introduction and Motivation / 1. 引言与动机 🟢
- The Case for Rust for Python Developers / Rust 对 Python 开发者的价值
- Common Python Pain Points That Rust Addresses / Rust 能解决的 Python 常见痛点
- When to Choose Rust Over Python / 何时选择 Rust 而不是 Python
2. Getting Started / 2. 快速开始 🟢
- Installation and Setup / 安装与环境配置
- Your First Rust Program / 你的第一个 Rust 程序
- Cargo vs pip/Poetry / Cargo 与 pip/Poetry 对比
3. Built-in Types and Variables / 3. 内建类型与变量 🟢
- Variables and Mutability / 变量与可变性
- Primitive Types Comparison / 基本类型对比
- String Types: String vs &str / 字符串类型:String 与 &str
4. Control Flow / 4. 控制流 🟢
- Conditional Statements / 条件语句
- Loops and Iteration / 循环与迭代
- Expression Blocks / 表达式块
- Functions and Type Signatures / 函数与类型签名
5. Data Structures and Collections / 5. 数据结构与集合 🟢
- Tuples, Arrays, Slices / 元组、数组与切片
- Structs vs Classes / 结构体与类
- Vec vs list, HashMap vs dict / Vec 与 list,HashMap 与 dict
6. Enums and Pattern Matching / 6. 枚举与模式匹配 🟡
- Algebraic Data Types vs Union Types / 代数数据类型与联合类型
- Exhaustive Pattern Matching / 穷尽模式匹配
- Option for None Safety / 用 Option 实现 None 安全
Part II - Core Concepts / 第二部分:核心概念
7. Ownership and Borrowing / 7. 所有权与借用 🟡
- Understanding Ownership / 理解所有权
- Move Semantics vs Reference Counting / 移动语义与引用计数
- Borrowing and Lifetimes / 借用与生命周期
- Smart Pointers / 智能指针
8. Crates and Modules / 8. Crate 与模块 🟢
9. Error Handling / 9. 错误处理 🟡
- Exceptions vs Result / 异常与 Result
- The ? Operator /
?操作符 - Custom Error Types with thiserror / 使用 thiserror 自定义错误类型
10. Traits and Generics / 10. Trait 与泛型 🟡
- Traits vs Duck Typing / Trait 与鸭子类型
- Protocols (PEP 544) vs Traits / Protocol(PEP 544)与 Trait
- Generic Constraints / 泛型约束
11. From and Into Traits / 11. From 与 Into Trait 🟡
- Type Conversions in Rust / Rust 中的类型转换
- From, Into, TryFrom / From、Into 与 TryFrom
- String Conversion Patterns / 字符串转换模式
12. Closures and Iterators / 12. 闭包与迭代器 🟡
- Closures vs Lambdas / 闭包与 Lambda
- Iterators vs Generators / 迭代器与生成器
- Macros: Code That Writes Code / 宏:生成代码的代码
Part III - Advanced Topics & Migration / 第三部分:高级主题与迁移
13. Concurrency / 13. 并发 🔶
- No GIL: True Parallelism / 没有 GIL:真正的并行
- Thread Safety: Type System Guarantees / 线程安全:由类型系统保证
- async/await Comparison / async/await 对比
14. Unsafe Rust, FFI, and Testing / 14. Unsafe Rust、FFI 与测试 🔶
- When and Why to Use Unsafe / 何时以及为何使用 Unsafe
- PyO3: Rust Extensions for Python / PyO3:为 Python 编写 Rust 扩展
- Unit Tests vs pytest / 单元测试与 pytest
15. Migration Patterns / 15. 迁移模式 🟡
- Common Python Patterns in Rust / Rust 中的常见 Python 模式
- Essential Crates for Python Developers / Python 开发者必备 Crate
- Incremental Adoption Strategy / 渐进式采用策略
16. Best Practices / 16. 最佳实践 🟡
- Idiomatic Rust for Python Developers / 面向 Python 开发者的 Rust 惯用法
- Common Pitfalls and Solutions / 常见陷阱与解决方案
- Python to Rust Rosetta Stone / Python 到 Rust 对照速查
- Learning Path and Resources / 学习路径与资源
Part IV - Capstone / 第四部分:综合项目
17. Capstone Project: CLI Task Manager / 17. 综合项目:命令行任务管理器 🔶
- The Project:
rustdo/ 项目:rustdo - Data Model, Storage, Commands, Business Logic / 数据模型、存储、命令与业务逻辑
- Tests and Stretch Goals / 测试与扩展目标
1. Introduction and Motivation / 1. 引言与动机
Speaker Intro and General Approach / 讲师介绍与整体方法
- Speaker intro / 讲师介绍
- Principal Firmware Architect in Microsoft SCHIE (Silicon and Cloud Hardware Infrastructure Engineering) team / Microsoft SCHIE(Silicon and Cloud Hardware Infrastructure Engineering)团队首席固件架构师
- Industry veteran with expertise in security, systems programming (firmware, operating systems, hypervisors), CPU and platform architecture, and C++ systems / 在安全、系统编程(固件、操作系统、虚拟机监控器)、CPU 与平台架构以及 C++ 系统方面经验丰富
- Started programming in Rust in 2017 (@AWS EC2), and have been in love with the language ever since / 2017 年在 AWS EC2 开始使用 Rust,此后长期深度投入
- This course is intended to be as interactive as possible / 本课程尽量采用高互动式教学
- Assumption: You know Python and its ecosystem / 前提:你熟悉 Python 及其生态
- Examples deliberately map Python concepts to Rust equivalents / 示例会刻意把 Python 概念映射到 Rust 对应概念
- Please feel free to ask clarifying questions at any point of time / 任何时候都欢迎提出澄清性问题
The Case for Rust for Python Developers / Rust 对 Python 开发者的价值
What you’ll learn / 你将学到: Why Python developers are adopting Rust, real-world performance wins (Dropbox, Discord, Pydantic), when Rust is the right choice vs staying with Python, and the core philosophical differences between the two languages.
为什么越来越多 Python 开发者开始采用 Rust、真实世界中的性能收益(Dropbox、Discord、Pydantic)、何时应选择 Rust 而不是继续使用 Python,以及这两门语言在核心设计理念上的差异。
Difficulty / 难度: 🟢 Beginner / 初级
Performance: From Minutes to Milliseconds / 性能:从分钟到毫秒
Python is famously slow for CPU-bound work. Rust provides C-level performance with a high-level feel.
Python 在 CPU 密集型任务上出了名地慢。Rust 则在保留高级语言体验的同时,提供接近 C 的性能。
# Python - ~2 seconds for 10 million calls
import time
def fibonacci(n: int) -> int:
if n <= 1:
return n
a, b = 0, 1
for _ in range(2, n + 1):
a, b = b, a + b
return b
start = time.perf_counter()
results = [fibonacci(n % 30) for n in range(10_000_000)]
elapsed = time.perf_counter() - start
print(f"Elapsed: {elapsed:.2f}s") # ~2s on typical hardware
// Rust - ~0.07 seconds for the same 10 million calls
use std::time::Instant;
fn fibonacci(n: u64) -> u64 {
if n <= 1 {
return n;
}
let (mut a, mut b) = (0u64, 1u64);
for _ in 2..=n {
let temp = b;
b = a + b;
a = temp;
}
b
}
fn main() {
let start = Instant::now();
let results: Vec<u64> = (0..10_000_000).map(|n| fibonacci(n % 30)).collect();
println!("Elapsed: {:.2?}", start.elapsed()); // ~0.07s
}
Note / 说明: Rust should be run in release mode (
cargo run --release) for a fair performance comparison.为了公平比较性能,Rust 应使用 release 模式运行(
cargo run --release)。Why the difference? / 为什么差距这么大? Python dispatches every
+through a dictionary lookup, unboxes integers from heap objects, and checks types at every operation. Rust compilesfibonaccidirectly to a handful of x86add/movinstructions - the same code a C compiler would produce.Python 中每次
+运算都要经过字典查找、从堆对象中拆箱整数,并在每一步执行类型检查。Rust 则会把fibonacci直接编译成少量 x86add/mov指令,本质上就是 C 编译器会生成的那类代码。
Memory Safety Without a Garbage Collector / 没有垃圾回收器的内存安全
Python’s reference-counting GC has known issues: circular references, unpredictable __del__ timing, and memory fragmentation. Rust eliminates these at compile time.
Python 的引用计数 GC 有几个已知问题:循环引用、__del__ 调用时机不可预测,以及内存碎片。Rust 在编译期就能避免这些问题。
# Python - circular reference that CPython's ref counter can't free
class Node:
def __init__(self, value):
self.value = value
self.parent = None
self.children = []
def add_child(self, child):
self.children.append(child)
child.parent = self # Circular reference!
# These two nodes reference each other - ref count never reaches 0.
# CPython's cycle detector will *eventually* clean them up,
# but you can't control when, and it adds GC pause overhead.
root = Node("root")
child = Node("child")
root.add_child(child)
// Rust - ownership prevents circular references by design
struct Node {
value: String,
children: Vec<Node>, // Children are OWNED - no cycles possible
}
impl Node {
fn new(value: &str) -> Self {
Node {
value: value.to_string(),
children: Vec::new(),
}
}
fn add_child(&mut self, child: Node) {
self.children.push(child); // Ownership transfers here
}
}
fn main() {
let mut root = Node::new("root");
let child = Node::new("child");
root.add_child(child);
// When root is dropped, all children are dropped too.
// Deterministic, zero overhead, no GC.
}
Key insight / 核心洞见: In Rust, the child doesn’t hold a reference back to the parent. If you truly need cross-references (like a graph), you use explicit mechanisms like
Rc<RefCell<T>>or indices - making the complexity visible and intentional.在 Rust 中,子节点默认不会反向持有父节点引用。如果你确实需要交叉引用(例如图结构),就必须显式使用
Rc<RefCell<T>>或索引等方式,让复杂性显性化、可审查。
Common Python Pain Points That Rust Addresses / Rust 能解决的常见 Python 痛点
1. Runtime Type Errors / 运行时类型错误
The most common Python production bug: passing the wrong type to a function. Type hints help, but they aren’t enforced.
Python 生产环境里最常见的问题之一,就是把错误类型传给函数。类型提示能提供帮助,但它们本身并不具备强制力。
# Python - type hints are suggestions, not rules
def process_user(user_id: int, name: str) -> dict:
return {"id": user_id, "name": name.upper()}
# These all "work" at the call site - fail at runtime
process_user("not-a-number", 42) # TypeError at .upper()
process_user(None, "Alice") # Works until you use user_id as int
# Even with mypy, you can still bypass types:
data = json.loads('{"id": "oops"}') # Always returns Any
process_user(data["id"], data["name"]) # mypy can't catch this
#![allow(unused)]
fn main() {
// Rust - the compiler catches all of these before the program runs
fn process_user(user_id: i64, name: &str) -> User {
User {
id: user_id,
name: name.to_uppercase(),
}
}
// process_user("not-a-number", 42); // Compile error: expected i64, found &str
// process_user(None, "Alice"); // Compile error: expected i64, found Option
// Extra arguments are always a compile error.
// Deserializing JSON is type-safe too:
#[derive(Deserialize)]
struct UserInput {
id: i64, // Must be a number in the JSON
name: String, // Must be a string in the JSON
}
let input: UserInput = serde_json::from_str(json_str)?; // Returns Err if types mismatch
process_user(input.id, &input.name); // Guaranteed correct types
}
2. None: The Billion Dollar Mistake (Python Edition) / None:Python 版的“十亿美元错误”
None can appear anywhere a value is expected. Python has no compile-time way to prevent AttributeError: 'NoneType' object has no attribute ....
在 Python 中,None 可以出现在任何本应出现值的位置。Python 没有编译期机制来阻止 AttributeError: 'NoneType' object has no attribute ... 这类错误。
# Python - None sneaks in everywhere
def find_user(user_id: int) -> dict | None:
users = {1: {"name": "Alice"}, 2: {"name": "Bob"}}
return users.get(user_id)
user = find_user(999) # Returns None
print(user["name"]) # TypeError: 'NoneType' object is not subscriptable
# Even with Optional type hint, nothing enforces the check:
from typing import Optional
def get_name(user_id: int) -> Optional[str]:
return None
name: Optional[str] = get_name(1)
print(name.upper()) # AttributeError - mypy warns, runtime doesn't care
#![allow(unused)]
fn main() {
// Rust - None is impossible unless explicitly handled
fn find_user(user_id: i64) -> Option<User> {
let users = HashMap::from([
(1, User { name: "Alice".into() }),
(2, User { name: "Bob".into() }),
]);
users.get(&user_id).cloned()
}
let user = find_user(999); // Returns None variant of Option<User>
// println!("{}", user.name); // Compile error: Option<User> has no field `name`
// You MUST handle the None case:
match find_user(999) {
Some(user) => println!("{}", user.name),
None => println!("User not found"),
}
// Or use combinators:
let name = find_user(999)
.map(|u| u.name)
.unwrap_or_else(|| "Unknown".to_string());
}
3. The GIL: Python’s Concurrency Ceiling / GIL:Python 并发能力的天花板
Python’s Global Interpreter Lock means threads don’t run Python code in parallel. threading is only useful for I/O-bound work; CPU-bound work requires multiprocessing (with its serialization overhead) or C extensions.
Python 的全局解释器锁(GIL)意味着多个线程无法真正并行执行 Python 代码。threading 主要适用于 I/O 密集型任务;如果是 CPU 密集型任务,通常要依赖 multiprocessing(同时承担序列化开销)或 C 扩展。
# Python - threads DON'T speed up CPU work because of the GIL
import threading
import time
def cpu_work(n):
total = 0
for i in range(n):
total += i * i
return total
start = time.perf_counter()
threads = [threading.Thread(target=cpu_work, args=(10_000_000,)) for _ in range(4)]
for t in threads:
t.start()
for t in threads:
t.join()
elapsed = time.perf_counter() - start
print(f"4 threads: {elapsed:.2f}s") # About the SAME as 1 thread! GIL prevents parallelism.
# multiprocessing "works" but serializes data between processes:
from multiprocessing import Pool
with Pool(4) as p:
results = p.map(cpu_work, [10_000_000] * 4) # ~4x faster, but pickle overhead
// Rust - true parallelism, no GIL, no serialization overhead
use std::thread;
fn cpu_work(n: u64) -> u64 {
(0..n).map(|i| i * i).sum()
}
fn main() {
let start = std::time::Instant::now();
let handles: Vec<_> = (0..4)
.map(|_| thread::spawn(|| cpu_work(10_000_000)))
.collect();
let results: Vec<u64> = handles.into_iter()
.map(|h| h.join().unwrap())
.collect();
println!("4 threads: {:.2?}", start.elapsed()); // ~4x faster than single thread
}
With Rayon / 使用 Rayon: parallelism is even simpler.
借助 Rayon,并行写法会更简单:
#![allow(unused)] fn main() { use rayon::prelude::*; let results: Vec<u64> = inputs.par_iter().map(|&n| cpu_work(n)).collect(); }
4. Deployment and Distribution Pain / 部署与分发的痛点
Python deployment is notoriously difficult: venvs, system Python conflicts, pip install failures, C extension wheels, Docker images with full Python runtime.
Python 部署一直以复杂著称:虚拟环境、系统 Python 冲突、pip install 失败、C 扩展 wheel、以及带完整 Python 运行时的 Docker 镜像。
# Python deployment checklist:
# 1. Which Python version? 3.9? 3.10? 3.11? 3.12?
# 2. Virtual environment: venv, conda, poetry, pipenv?
# 3. C extensions: need compiler? manylinux wheels?
# 4. System dependencies: libssl, libffi, etc.?
# 5. Docker: full python:3.12 image is 1.0 GB
# 6. Startup time: 200-500ms for import-heavy apps
# Docker image: ~1 GB
# FROM python:3.12-slim
# COPY requirements.txt .
# RUN pip install -r requirements.txt
# COPY . .
# CMD ["python", "app.py"]
#![allow(unused)]
fn main() {
// Rust deployment: single static binary, no runtime needed
// cargo build --release -> one binary, ~5-20 MB
// Copy it anywhere - no Python, no venv, no dependencies
// Docker image: ~5 MB (from scratch or distroless)
// FROM scratch
// COPY target/release/my_app /my_app
// CMD ["/my_app"]
// Startup time: <1ms
// Cross-compile: cargo build --target x86_64-unknown-linux-musl
}
When to Choose Rust Over Python / 何时选择 Rust 而不是 Python
Choose Rust When / 在这些场景下选择 Rust:
- Performance is critical: Data pipelines, real-time processing, compute-heavy services / 性能关键:数据流水线、实时处理、计算密集型服务
- Correctness matters: Financial systems, safety-critical code, protocol implementations / 正确性关键:金融系统、安全关键代码、协议实现
- Deployment simplicity: Single binary, no runtime dependencies / 部署简洁:单一二进制,无运行时依赖
- Low-level control: Hardware interaction, OS integration, embedded systems / 需要底层控制:硬件交互、操作系统集成、嵌入式系统
- True concurrency: CPU-bound parallelism without GIL workarounds / 真正并发:不依赖绕过 GIL 的 CPU 并行
- Memory efficiency: Reduce cloud costs for memory-intensive services / 内存效率:降低内存密集型服务的云成本
- Long-running services: Where predictable latency matters (no GC pauses) / 长时间运行的服务:需要稳定延迟,没有 GC 停顿
Stay with Python When / 在这些场景下继续使用 Python:
- Rapid prototyping: Exploratory data analysis, scripts, one-off tools / 快速原型开发:探索式数据分析、脚本、一次性工具
- ML/AI workflows: PyTorch, TensorFlow, scikit-learn ecosystem / ML/AI 工作流:PyTorch、TensorFlow、scikit-learn 生态
- Glue code: Connecting APIs, data transformation scripts / 胶水代码:串接 API、数据转换脚本
- Team expertise: When Rust learning curve doesn’t justify benefits / 团队经验因素:Rust 学习成本大于收益时
- Time to market: When development speed trumps execution speed / 上市速度更重要:开发速度优先于执行速度
- Interactive work: Jupyter notebooks, REPL-driven development / 交互式工作流:Jupyter、REPL 驱动开发
- Scripting: Automation, sys-admin tasks, quick utilities / 脚本任务:自动化、系统管理、快速小工具
Consider Both (Hybrid Approach with PyO3) / 可以考虑混合方案(结合 PyO3):
- Compute-heavy code in Rust: Called from Python via PyO3/maturin / 把重计算代码写在 Rust 中,通过 PyO3/maturin 从 Python 调用
- Business logic and orchestration in Python: Familiar, productive / 业务逻辑和编排保留在 Python,保持熟悉和高效率
- Gradual migration: Identify hotspots, replace with Rust extensions / 渐进式迁移:先识别热点,再用 Rust 扩展替换
- Best of both: Python’s ecosystem + Rust’s performance / 两者结合:Python 的生态加上 Rust 的性能
Real-World Impact: Why Companies Choose Rust / 真实世界影响:为什么公司选择 Rust
Dropbox: Storage Infrastructure / Dropbox:存储基础设施
- Before (Python): High CPU usage, memory overhead in sync engine / 之前(Python):同步引擎 CPU 占用高、内存开销大
- After (Rust): 10x performance improvement, 50% memory reduction / 之后(Rust):性能提升 10 倍,内存减少 50%
- Result: Millions saved in infrastructure costs / 结果:节省了数百万基础设施成本
Discord: Voice/Video Backend / Discord:语音视频后端
- Before (Python -> Go): GC pauses causing audio drops / 之前(Python -> Go):GC 停顿导致音频卡顿
- After (Rust): Consistent low-latency performance / 之后(Rust):持续稳定的低延迟表现
- Result: Better user experience, reduced server costs / 结果:用户体验更好,服务器成本更低
Cloudflare: Edge Workers / Cloudflare:边缘 Workers
- Why Rust: WebAssembly compilation, predictable performance at edge / 为什么选 Rust:便于编译为 WebAssembly,边缘场景下性能可预测
- Result: Workers run with microsecond cold starts / 结果:Worker 冷启动达到微秒级
Pydantic V2 / Pydantic V2
- Before: Pure Python validation - slow for large payloads / 之前:纯 Python 校验,大型负载下速度较慢
- After: Rust core (via PyO3) - 5-50x faster validation / 之后:用 Rust 核心(通过 PyO3)实现,校验速度提升 5-50 倍
- Result: Same Python API, dramatically faster execution / 结果:保持同样的 Python API,但执行速度显著提升
Why This Matters for Python Developers / 这对 Python 开发者意味着什么:
- Complementary skills: Rust and Python solve different problems / 技能互补:Rust 和 Python 解决不同类别的问题
- PyO3 bridge: Write Rust extensions callable from Python / PyO3 桥梁:可编写可被 Python 调用的 Rust 扩展
- Performance understanding: Learn why Python is slow and how to fix hotspots / 性能理解:理解 Python 为什么慢,以及如何优化热点
- Career growth: Systems programming expertise increasingly valuable / 职业成长:系统编程能力越来越有价值
- Cloud costs: 10x faster code = significantly lower infrastructure spend / 云成本:10 倍性能提升往往意味着显著更低的基础设施开销
Language Philosophy Comparison / 语言设计理念对比
Python Philosophy / Python 的理念
- Readability counts: Clean syntax, “one obvious way to do it” / 可读性优先:语法简洁,强调“显而易见的一种方式”
- Batteries included: Extensive standard library, rapid prototyping / 自带电池:标准库丰富,适合快速原型
- Duck typing: “If it walks like a duck and quacks like a duck…” / 鸭子类型:“如果它走起来像鸭子、叫起来像鸭子……”
- Developer velocity: Optimize for writing speed, not execution speed / 开发效率优先:优先提升编写速度,而非执行速度
- Dynamic everything: Modify classes at runtime, monkey-patching, metaclasses / 高度动态:运行时修改类、猴子补丁、元类
Rust Philosophy / Rust 的理念
- Performance without sacrifice: Zero-cost abstractions, no runtime overhead / 不牺牲性能:零成本抽象、没有运行时负担
- Correctness first: If it compiles, entire categories of bugs are impossible / 正确性优先:如果能编译通过,整类 bug 就不可能存在
- Explicit over implicit: No hidden behavior, no implicit conversions / 显式优于隐式:没有隐藏行为,也没有隐式转换
- Ownership: Resources have exactly one owner - memory, files, sockets / 所有权:资源总有明确所有者,例如内存、文件、socket
- Fearless concurrency: The type system prevents data races at compile time / 无畏并发:类型系统在编译期阻止数据竞争
graph LR
subgraph PY["Python"]
direction TB
PY_CODE["Your Code"] --> PY_INTERP["Interpreter<br/>CPython VM"]
PY_INTERP --> PY_GC["Garbage Collector<br/>ref count + GC"]
PY_GC --> PY_GIL["GIL<br/>no true parallelism"]
PY_GIL --> PY_OS["OS / Hardware"]
end
subgraph RS["Rust"]
direction TB
RS_CODE["Your Code"] --> RS_NONE["No runtime overhead"]
RS_NONE --> RS_OWN["Ownership<br/>compile-time, zero-cost"]
RS_OWN --> RS_THR["Native threads<br/>true parallelism"]
RS_THR --> RS_OS["OS / Hardware"]
end
style PY_INTERP fill:#fff3e0,color:#000,stroke:#e65100
style PY_GC fill:#fff3e0,color:#000,stroke:#e65100
style PY_GIL fill:#ffcdd2,color:#000,stroke:#c62828
style RS_NONE fill:#c8e6c9,color:#000,stroke:#2e7d32
style RS_OWN fill:#c8e6c9,color:#000,stroke:#2e7d32
style RS_THR fill:#c8e6c9,color:#000,stroke:#2e7d32
Quick Reference: Rust vs Python / 速查:Rust 与 Python 对比
| Concept / 概念 | Python | Rust | Key Difference / 关键差异 |
|---|---|---|---|
| Typing / 类型系统 | Dynamic (duck typing) / 动态类型 | Static (compile-time) / 静态类型(编译期) | Errors caught before runtime / 错误在运行前就被捕获 |
| Memory / 内存管理 | Garbage collected (ref counting + cycle GC) / 垃圾回收(引用计数 + 循环 GC) | Ownership system / 所有权系统 | Zero-cost, deterministic cleanup / 零成本、确定性清理 |
| None/null / 空值 | None anywhere / None 可随处出现 | Option<T> | Compile-time None safety / 编译期 None 安全 |
| Error handling / 错误处理 | raise/try/except | Result<T, E> | Explicit, no hidden control flow / 显式表达,没有隐藏控制流 |
| Mutability / 可变性 | Everything mutable / 一切默认可变 | Immutable by default / 默认不可变 | Opt-in to mutation / 必须显式选择可变 |
| Speed / 速度 | Interpreted (~10-100x slower) / 解释执行 | Compiled (C/C++ speed) / 编译执行(接近 C/C++) | Orders of magnitude faster / 通常快几个数量级 |
| Concurrency / 并发 | GIL limits threads / GIL 限制线程并发 | No GIL, Send/Sync traits / 无 GIL,依靠 Send/Sync | True parallelism by default / 默认支持真正并行 |
| Dependencies / 依赖管理 | pip install / poetry add | cargo add | Built-in dependency management / 内建依赖管理 |
| Build system / 构建系统 | setuptools/poetry/hatch | Cargo | Single unified tool / 统一工具链 |
| Packaging / 打包配置 | pyproject.toml | Cargo.toml | Similar declarative config / 都是声明式配置 |
| REPL / 交互环境 | python interactive | No REPL (use tests/cargo run) / 无标准 REPL | Compile-first workflow / 先编译后运行的工作流 |
| Type hints / 类型提示 | Optional, not enforced / 可选且不强制 | Required, compiler-enforced / 必需且由编译器强制 | Types are not decorative / 类型不是装饰品 |
Exercises / 练习
Exercise: Mental Model Check / 练习:心智模型检查 (click to expand / 点击展开)
Challenge / 挑战: For each Python snippet, predict what Rust would require differently. Don’t write code - just describe the constraint.
对下面每个 Python 片段,判断在 Rust 中会有哪些不同要求。不要写代码,只描述约束。
x = [1, 2, 3]; y = x; x.append(4)- What happens in Rust?x = [1, 2, 3]; y = x; x.append(4):在 Rust 中会发生什么?data = None; print(data.upper())- How does Rust prevent this?data = None; print(data.upper()):Rust 如何阻止这种情况?import threading; shared = []; threading.Thread(target=shared.append, args=(1,)).start()- What does Rust demand?import threading; shared = []; threading.Thread(target=shared.append, args=(1,)).start():Rust 会要求你做什么?
Solution / 参考答案
- Ownership move / 所有权移动:
let y = x;movesx-x.push(4)is a compile error. You’d needlet y = x.clone();or borrow withlet y = &x;.let y = x;会移动x,因此后续x.push(4)会成为编译错误。你需要显式clone(),或者改为借用&x。 - No null / 没有 null:
datacan’t beNoneunless it’sOption<String>. You mustmatchor use.unwrap()/if let- no surpriseNoneTypeerrors.
除非类型是Option<String>,否则data不可能是None。你必须显式match,或使用.unwrap()/if let等方式处理。 - Send + Sync /
Send与Sync: The compiler requiressharedto be wrapped inArc<Mutex<Vec<i32>>>. Forgetting the lock = compile error, not a race condition.
编译器会要求shared被包装为Arc<Mutex<Vec<i32>>>之类的安全共享结构。漏掉锁不会变成竞态条件,而是直接编译不过。
Key takeaway / 关键结论: Rust shifts runtime failures to compile-time errors. The “friction” you feel is the compiler catching real bugs.
Rust 把许多运行时失败前移成编译期错误。你感受到的“阻力”,本质上是编译器在替你抓住真实 bug。
2. Getting Started / 2. 快速开始
Installation and Setup / 安装与环境配置
What you’ll learn / 你将学到: How to install Rust and its toolchain, the Cargo build system vs pip/Poetry, IDE setup, your first
Hello, world!program, and essential Rust keywords mapped to Python equivalents.如何安装 Rust 及其工具链、Cargo 与 pip/Poetry 的区别、IDE 配置、你的第一个
Hello, world!程序,以及若干对 Python 开发者特别重要的 Rust 关键字。Difficulty / 难度: 🟢 Beginner / 初级
Installing Rust / 安装 Rust
# Install Rust via rustup (Linux/macOS/WSL)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# Verify installation
rustc --version # Rust compiler
cargo --version # Build tool + package manager (like pip + setuptools combined)
# Update Rust
rustup update
Rust Tools vs Python Tools / Rust 工具与 Python 工具对照
| Purpose / 用途 | Python | Rust |
|---|---|---|
| Language runtime / 语言执行核心 | python (interpreter) / 解释器 | rustc (compiler, rarely called directly) / 编译器(通常不直接手动调用) |
| Package manager / 包管理 | pip / poetry / uv | cargo (built-in) / 内建 cargo |
| Project config / 项目配置 | pyproject.toml | Cargo.toml |
| Lock file / 锁文件 | poetry.lock / requirements.txt | Cargo.lock |
| Virtual env / 虚拟环境 | venv / conda | Not needed (deps are per-project) / 不需要(依赖天然按项目隔离) |
| Formatter / 格式化工具 | black / ruff format | rustfmt (built-in: cargo fmt) |
| Linter / 静态检查 | ruff / flake8 / pylint | clippy (built-in: cargo clippy) |
| Type checker / 类型检查 | mypy / pyright | Built into compiler (always on) / 编译器内建、始终启用 |
| Test runner / 测试运行器 | pytest | cargo test (built-in) |
| Docs / 文档生成 | sphinx / mkdocs | cargo doc (built-in) |
| REPL / 交互解释器 | python / ipython | None (use cargo test or Rust Playground) / 无标准 REPL(可用 cargo test 或 Rust Playground) |
IDE Setup / IDE 配置
VS Code (recommended / 推荐):
Extensions to install:
- rust-analyzer -> Essential: IDE features, type hints, completions
- Even Better TOML -> Syntax highlighting for Cargo.toml
- CodeLLDB -> Debugger support
# Python equivalent mapping:
# rust-analyzer ~= Pylance (but with 100% type coverage, always)
# cargo clippy ~= ruff (but checks correctness, not just style)
Your First Rust Program / 你的第一个 Rust 程序
Python Hello World / Python 版本 Hello World
# hello.py - just run it
print("Hello, World!")
# Run:
# python hello.py
Rust Hello World / Rust 版本 Hello World
// src/main.rs - must be compiled first
fn main() {
println!("Hello, World!"); // println! is a macro (note the !)
}
// Build and run:
// cargo run
Key Differences for Python Developers / 面向 Python 开发者的关键差异
Python: Rust:
-------- -----
- No main() needed - fn main() is the entry point
- Indentation = blocks - Curly braces {} = blocks
- print() is a function - println!() is a macro (the ! matters)
- No semicolons - Semicolons end statements
- No type declarations - Types inferred but always known
- Interpreted (run directly) - Compiled (cargo build, then run)
- Errors at runtime - Most errors at compile time
Creating Your First Project / 创建你的第一个项目
# Python # Rust
mkdir myproject cargo new myproject
cd myproject cd myproject
python -m venv .venv # No virtual env needed
source .venv/bin/activate # No activation needed
# Create files manually # src/main.rs already created
# Python project structure: Rust project structure:
# myproject/ myproject/
# ├── pyproject.toml ├── Cargo.toml (like pyproject.toml)
# ├── src/ ├── src/
# │ └── myproject/ │ └── main.rs (entry point)
# │ ├── __init__.py └── (no __init__.py needed)
# │ └── main.py
# └── tests/
# └── test_main.py
graph TD
subgraph Python ["Python Project"]
PP["pyproject.toml"] --- PS["src/"]
PS --- PM["myproject/"]
PM --- PI["__init__.py"]
PM --- PMN["main.py"]
PP --- PT["tests/"]
end
subgraph Rust ["Rust Project"]
RC["Cargo.toml"] --- RS["src/"]
RS --- RM["main.rs"]
RC --- RTG["target/ (auto-generated)"]
end
style Python fill:#ffeeba
style Rust fill:#d4edda
Key difference / 关键差异: Rust projects are simpler - no
__init__.py, no virtual environments, nosetup.pyvssetup.cfgvspyproject.tomlconfusion. JustCargo.toml+src/.Rust 项目结构通常更简单:没有
__init__.py,不需要虚拟环境,也没有setup.py、setup.cfg、pyproject.toml的多套配置混乱。核心就是Cargo.toml加src/。
Cargo vs pip/Poetry / Cargo 与 pip/Poetry 对比
Project Configuration / 项目配置
# Python - pyproject.toml
[project]
name = "myproject"
version = "0.1.0"
requires-python = ">=3.10"
dependencies = [
"requests>=2.28",
"pydantic>=2.0",
]
[project.optional-dependencies]
dev = ["pytest", "ruff", "mypy"]
# Rust - Cargo.toml
[package]
name = "myproject"
version = "0.1.0"
edition = "2021" # Rust edition (like Python version)
[dependencies]
reqwest = "0.12" # HTTP client (like requests)
serde = { version = "1.0", features = ["derive"] } # Serialization (like pydantic)
[dev-dependencies]
# Test dependencies - only compiled for `cargo test`
# (No separate test config needed - `cargo test` is built in)
Common Cargo Commands / 常用 Cargo 命令
# Python equivalent # Rust
pip install requests cargo add reqwest
pip install -r requirements.txt cargo build # auto-installs deps
pip install -e . cargo build # always "editable"
python -m pytest cargo test
python -m mypy . # Built into compiler - always runs
ruff check . cargo clippy
ruff format . cargo fmt
python main.py cargo run
python -c "..." # No equivalent - use cargo run or tests
# Rust-specific:
cargo new myproject # Create new project
cargo build --release # Optimized build (10-100x faster than debug)
cargo doc --open # Generate and browse API docs
cargo update # Update deps (like pip install --upgrade)
Essential Rust Keywords for Python Developers / 面向 Python 开发者的 Rust 核心关键字
Variable and Mutability Keywords / 变量与可变性关键字
#![allow(unused)]
fn main() {
// let - declare a variable (like Python assignment, but immutable by default)
let name = "Alice"; // Python: name = "Alice" (but mutable)
// name = "Bob"; // Compile error! Immutable by default
// mut - opt into mutability
let mut count = 0; // Python: count = 0 (always mutable in Python)
count += 1; // Allowed because of `mut`
// const - compile-time constant (like Python's convention of UPPER_CASE, but enforced)
const MAX_SIZE: usize = 1024; // Python: MAX_SIZE = 1024 (convention only)
// static - global variable (use sparingly; Python has module-level globals)
static VERSION: &str = "1.0";
}
Ownership and Borrowing Keywords / 所有权与借用关键字
#![allow(unused)]
fn main() {
// These have NO Python equivalents - they're Rust-specific concepts
// & - borrow (read-only reference)
fn print_name(name: &str) { } // Python: def print_name(name: str) - but Python passes references always
// &mut - mutable borrow
fn append(list: &mut Vec<i32>) { } // Python: def append(lst: list) - always mutable in Python
// move - transfer ownership (happens implicitly in Rust, never in Python)
let s1 = String::from("hello");
let s2 = s1; // s1 is MOVED to s2 - s1 is no longer valid
// println!("{}", s1); // Compile error: value moved
}
Type Definition Keywords / 类型定义关键字
#![allow(unused)]
fn main() {
// struct - like a Python dataclass or NamedTuple
struct Point { // @dataclass
x: f64, // class Point:
y: f64, // x: float
} // y: float
// enum - like Python's enum but MUCH more powerful (carries data)
enum Shape { // No direct Python equivalent
Circle(f64), // Each variant can hold different data
Rectangle(f64, f64),
}
// impl - attach methods to a type (like defining methods in a class)
impl Point { // class Point:
fn distance(&self) -> f64 { // def distance(self) -> float:
(self.x.powi(2) + self.y.powi(2)).sqrt()
}
}
// trait - like Python's ABC or Protocol (PEP 544)
trait Drawable { // class Drawable(Protocol):
fn draw(&self); // def draw(self) -> None: ...
}
// type - type alias (like Python's TypeAlias)
type UserId = i64; // UserId = int (or TypeAlias)
}
Control Flow Keywords / 控制流关键字
#![allow(unused)]
fn main() {
// match - exhaustive pattern matching (like Python 3.10+ match, but enforced)
match value {
1 => println!("one"),
2 | 3 => println!("two or three"),
_ => println!("other"), // _ = wildcard (like Python's case _:)
}
// if let - destructure + conditional
if let Some(x) = optional_value {
println!("{}", x);
}
// loop - infinite loop (like while True:)
loop {
break; // Must break to exit
}
// for - iteration (like Python's for, but needs .iter() more often)
for item in collection.iter() { // for item in collection:
println!("{}", item);
}
// while let - loop with destructuring
while let Some(item) = stack.pop() {
process(item);
}
}
Visibility Keywords / 可见性关键字
#![allow(unused)]
fn main() {
// pub - public (Python has no real private; uses _ convention)
pub fn greet() { } // def greet(): - everything is "public" in Python
// pub(crate) - visible within the crate only
pub(crate) fn internal() { } // def _internal(): - single underscore convention
// (no keyword) - private to the module
fn private_helper() { } // def __private(): - double underscore name mangling
// In Python, "private" is a gentleman's agreement.
// In Rust, private is enforced by the compiler.
}
Exercises / 练习
Exercise: First Rust Program / 练习:第一个 Rust 程序 (click to expand / 点击展开)
Challenge / 挑战: Create a new Rust project and write a program that:
创建一个新的 Rust 项目,并编写程序完成以下任务:
- Declares a variable
namewith your name (type&str)
声明一个变量name,保存你的名字(类型为&str) - Declares a mutable variable
countstarting at 0
声明一个可变变量count,初始值为 0 - Uses a
forloop from 1..=5 to incrementcountand print"Hello, {name}! (count: {count})"
使用1..=5的for循环递增count,并打印"Hello, {name}! (count: {count})" - After the loop, print whether count is even or odd using a
matchexpression
循环结束后,使用match表达式判断count是偶数还是奇数并打印
Solution / 参考答案
cargo new hello_rust && cd hello_rust
// src/main.rs
fn main() {
let name = "Pythonista";
let mut count = 0u32;
for _ in 1..=5 {
count += 1;
println!("Hello, {name}! (count: {count})");
}
let parity = match count % 2 {
0 => "even",
_ => "odd",
};
println!("Final count {count} is {parity}");
}
Key takeaways / 关键要点:
letis immutable by default (you needmutto changecount)let默认不可变;如果要修改count,必须显式加mut1..=5is inclusive range (Python’srange(1, 6))1..=5是包含结束值的区间,对应 Python 的range(1, 6)matchis an expression that returns a valuematch是能返回值的表达式- No
self, noif __name__ == "__main__"- justfn main()
不需要self,也不需要if __name__ == "__main__",只需fn main()
3. Built-in Types and Variables / 3. 内建类型与变量
Variables and Mutability
What you’ll learn: Immutable-by-default variables, explicit
mut, primitive numeric types vs Python’s arbitrary-precisionint,Stringvs&str(the hardest early concept), string formatting, and Rust’s required type annotations.Difficulty: 🟢 Beginner
Python Variable Declaration
# Python — everything is mutable, dynamically typed
count = 0 # Mutable, type inferred as int
count = 5 # ✅ Works
count = "hello" # ✅ Works — type can change! (dynamic typing)
# "Constants" are just convention:
MAX_SIZE = 1024 # Nothing prevents MAX_SIZE = 999 later
Rust Variable Declaration
#![allow(unused)]
fn main() {
// Rust — immutable by default, statically typed
let count = 0; // Immutable, type inferred as i32
// count = 5; // ❌ Compile error: cannot assign twice to immutable variable
// count = "hello"; // ❌ Compile error: expected integer, found &str
let mut count = 0; // Explicitly mutable
count = 5; // ✅ Works
// count = "hello"; // ❌ Still can't change type
const MAX_SIZE: usize = 1024; // True constant — enforced by compiler
}
Key Mental Shift for Python Developers
#![allow(unused)]
fn main() {
// Python: variables are labels that point to objects
// Rust: variables are named storage locations that OWN their values
// Variable shadowing — unique to Rust, very useful
let input = "42"; // &str
let input = input.parse::<i32>().unwrap(); // Now it's i32 — new variable, same name
let input = input * 2; // Now it's 84 — another new variable
// In Python, you'd just reassign and lose the old type:
input = "42"
input = int(input) # Same name, different type — Python allows this too
But in Rust, each `let` creates a genuinely new binding. The old one is gone.
}
Practical Example: Counter
# Python version
class Counter:
def __init__(self):
self.value = 0
def increment(self):
self.value += 1
def get_value(self):
return self.value
c = Counter()
c.increment()
print(c.get_value()) # 1
// Rust version
struct Counter {
value: i64,
}
impl Counter {
fn new() -> Self {
Counter { value: 0 }
}
fn increment(&mut self) { // &mut self = I will modify this
self.value += 1;
}
fn get_value(&self) -> i64 { // &self = I only read this
self.value
}
}
fn main() {
let mut c = Counter::new(); // Must be `mut` to call increment()
c.increment();
println!("{}", c.get_value()); // 1
}
Key difference: In Rust,
&mut selfin the method signature tells you (and the compiler) thatincrementmodifies the counter. In Python, any method can mutate anything — you have to read the code to know.
Primitive Types Comparison
flowchart LR
subgraph Python ["Python Types"]
PI["int\n(arbitrary precision)"]
PF["float\n(64-bit only)"]
PB["bool"]
PS["str\n(Unicode)"]
end
subgraph Rust ["Rust Types"]
RI["i8 / i16 / i32 / i64 / i128\nu8 / u16 / u32 / u64 / u128"]
RF["f32 / f64"]
RB["bool"]
RS["String / &str"]
end
PI -->|"fixed-size"| RI
PF -->|"choose precision"| RF
PB -->|"same"| RB
PS -->|"owned vs borrowed"| RS
style Python fill:#ffeeba
style Rust fill:#d4edda
Numeric Types
| Python | Rust | Notes |
|---|---|---|
int (arbitrary precision) | i8, i16, i32, i64, i128, isize | Rust integers have fixed size |
int (unsigned: no separate type) | u8, u16, u32, u64, u128, usize | Explicit unsigned types |
float (64-bit IEEE 754) | f32, f64 | Python only has 64-bit float |
bool | bool | Same concept |
complex | No built-in (use num crate) | Rare in systems code |
# Python — one integer type, arbitrary precision
x = 42 # int — can grow to any size
big = 2 ** 1000 # Still works — thousands of digits
y = 3.14 # float — always 64-bit
#![allow(unused)]
fn main() {
// Rust — explicit sizes, overflow is a compile/runtime error
let x: i32 = 42; // 32-bit signed integer
let y: f64 = 3.14; // 64-bit float (Python's float equivalent)
let big: i128 = 2_i128.pow(100); // 128-bit max — no arbitrary precision
// For arbitrary precision: use the `num-bigint` crate
// Underscores for readability (like Python's 1_000_000):
let million = 1_000_000; // Same syntax as Python!
// Type suffix syntax:
let a = 42u8; // u8
let b = 3.14f32; // f32
}
Size Types (Important!)
#![allow(unused)]
fn main() {
// usize and isize — pointer-sized integers, used for indexing
let length: usize = vec![1, 2, 3].len(); // .len() returns usize
let index: usize = 0; // Array indices are always usize
// In Python, len() returns int and indices are int — no distinction.
// In Rust, mixing i32 and usize requires explicit conversion:
let i: i32 = 5;
// let item = vec[i]; // ❌ Error: expected usize, found i32
let item = vec[i as usize]; // ✅ Explicit conversion
}
Type Inference
#![allow(unused)]
fn main() {
// Rust infers types but they're FIXED — not dynamic
let x = 42; // Compiler infers i32 (default integer type)
let y = 3.14; // Compiler infers f64 (default float type)
let s = "hello"; // Compiler infers &str (string slice)
let v = vec![1, 2]; // Compiler infers Vec<i32>
// You can always be explicit:
let x: i64 = 42;
let y: f32 = 3.14;
// Unlike Python, the type can NEVER change after inference:
let x = 42;
// x = "hello"; // ❌ Error: expected integer, found &str
}
String Types: String vs &str
This is one of the biggest surprises for Python developers. Rust has two main string types where Python has one.
Python String Handling
# Python — one string type, immutable, reference counted
name = "Alice" # str — immutable, heap allocated
greeting = f"Hello, {name}!" # f-string formatting
chars = list(name) # Convert to list of characters
upper = name.upper() # Returns new string (immutable)
Rust String Types
#![allow(unused)]
fn main() {
// Rust has TWO string types:
// 1. &str (string slice) — borrowed, immutable, like a "view" into string data
let name: &str = "Alice"; // Points to string data in the binary
// Closest to Python's str, but it's a REFERENCE
// 2. String (owned string) — heap-allocated, growable, owned
let mut greeting = String::from("Hello, "); // Owned, can be modified
greeting.push_str(name);
greeting.push('!');
// greeting is now "Hello, Alice!"
}
When to Use Which?
#![allow(unused)]
fn main() {
// Think of it like this:
// &str = "I'm looking at a string someone else owns" (read-only view)
// String = "I own this string and can modify it" (owned data)
// Function parameters: prefer &str (accepts both types)
fn greet(name: &str) -> String { // accepts &str AND &String
format!("Hello, {}!", name) // format! creates a new String
}
let s1 = "world"; // &str literal
let s2 = String::from("Rust"); // String
greet(s1); // ✅ &str works directly
greet(&s2); // ✅ &String auto-converts to &str (Deref coercion)
}
Practical Examples
# Python string operations
name = "alice"
upper = name.upper() # "ALICE"
contains = "lic" in name # True
parts = "a,b,c".split(",") # ["a", "b", "c"]
joined = "-".join(["a", "b", "c"]) # "a-b-c"
stripped = " hello ".strip() # "hello"
replaced = name.replace("a", "A") # "Alice"
#![allow(unused)]
fn main() {
// Rust equivalents
let name = "alice";
let upper = name.to_uppercase(); // String — new allocation
let contains = name.contains("lic"); // bool
let parts: Vec<&str> = "a,b,c".split(',').collect(); // Vec<&str>
let joined = ["a", "b", "c"].join("-"); // String
let stripped = " hello ".trim(); // &str — no allocation!
let replaced = name.replace("a", "A"); // String
// Key insight: some operations return &str (no allocation), others return String.
// .trim() returns a slice of the original — efficient!
// .to_uppercase() must create a new String — allocation required.
}
Python Developers: Think of it This Way
Python str ≈ Rust &str (you usually read strings)
Python str ≈ Rust String (when you need to own/modify)
Rule of thumb:
- Function parameters → use &str (most flexible)
- Struct fields → use String (struct owns its data)
- Return values → use String (caller needs to own it)
- String literals → automatically &str
Printing and String Formatting
Basic Output
# Python
print("Hello, World!")
print("Name:", name, "Age:", age) # Space-separated
print(f"Name: {name}, Age: {age}") # f-string
#![allow(unused)]
fn main() {
// Rust
println!("Hello, World!");
println!("Name: {} Age: {}", name, age); // Positional {}
println!("Name: {name}, Age: {age}"); // Inline variables (Rust 1.58+, like f-strings!)
}
Format Specifiers
# Python formatting
print(f"{3.14159:.2f}") # "3.14" — 2 decimal places
print(f"{42:05d}") # "00042" — zero-padded
print(f"{255:#x}") # "0xff" — hex
print(f"{42:>10}") # " 42" — right-aligned
print(f"{'left':<10}|") # "left |" — left-aligned
#![allow(unused)]
fn main() {
// Rust formatting (very similar to Python!)
println!("{:.2}", 3.14159); // "3.14" — 2 decimal places
println!("{:05}", 42); // "00042" — zero-padded
println!("{:#x}", 255); // "0xff" — hex
println!("{:>10}", 42); // " 42" — right-aligned
println!("{:<10}|", "left"); // "left |" — left-aligned
}
Debug Printing
# Python — repr() and pprint
print(repr([1, 2, 3])) # "[1, 2, 3]"
from pprint import pprint
pprint({"key": [1, 2, 3]}) # Pretty-printed
#![allow(unused)]
fn main() {
// Rust — {:?} and {:#?}
println!("{:?}", vec![1, 2, 3]); // "[1, 2, 3]" — Debug format
println!("{:#?}", vec![1, 2, 3]); // Pretty-printed Debug format
// To make your types printable, derive Debug:
#[derive(Debug)]
struct Point { x: f64, y: f64 }
let p = Point { x: 1.0, y: 2.0 };
println!("{:?}", p); // "Point { x: 1.0, y: 2.0 }"
println!("{p:?}"); // Same, with inline syntax
}
Quick Reference
| Python | Rust | Notes |
|---|---|---|
print(x) | println!("{}", x) or println!("{x}") | Display format |
print(repr(x)) | println!("{:?}", x) | Debug format |
f"Hello {name}" | format!("Hello {name}") | Returns String |
print(x, end="") | print!("{x}") | No newline (print! vs println!) |
print(x, file=sys.stderr) | eprintln!("{x}") | Print to stderr |
sys.stdout.write(s) | print!("{s}") | No newline |
Type Annotations: Optional vs Required
Python Type Hints (Optional, Not Enforced)
# Python — type hints are documentation, not enforcement
def add(a: int, b: int) -> int:
return a + b
add(1, 2) # ✅
add("a", "b") # ✅ Python doesn't care — returns "ab"
add(1, "2") # ✅ Until it crashes at runtime: TypeError
# Union types, Optional
def find(key: str) -> int | None:
...
# Generic types
def first(items: list[int]) -> int | None:
return items[0] if items else None
# Type aliases
UserId = int
Mapping = dict[str, list[int]]
Rust Type Declarations (Required, Compiler-Enforced)
#![allow(unused)]
fn main() {
// Rust — types are enforced. Always. No exceptions.
fn add(a: i32, b: i32) -> i32 {
a + b
}
add(1, 2); // ✅
// add("a", "b"); // ❌ Compile error: expected i32, found &str
// Optional values use Option<T>
fn find(key: &str) -> Option<i32> {
// Returns Some(value) or None
Some(42)
}
// Generic types
fn first(items: &[i32]) -> Option<i32> {
items.first().copied()
}
// Type aliases
type UserId = i64;
type Mapping = HashMap<String, Vec<i32>>;
}
Key insight: In Python, type hints help your IDE and mypy but don’t affect runtime. In Rust, types ARE the program — the compiler uses them to guarantee memory safety, prevent data races, and eliminate null pointer errors.
📌 See also: Ch. 6 — Enums and Pattern Matching shows how Rust’s type system replaces Python’s
Uniontypes andisinstance()checks.
Exercises
🏋️ Exercise: Temperature Converter (click to expand)
Challenge: Write a function celsius_to_fahrenheit(c: f64) -> f64 and a function classify(temp_f: f64) -> &'static str that returns “cold”, “mild”, or “hot” based on thresholds. Print the result for 0, 20, and 35 degrees Celsius. Use string formatting.
🔑 Solution
fn celsius_to_fahrenheit(c: f64) -> f64 {
c * 9.0 / 5.0 + 32.0
}
fn classify(temp_f: f64) -> &'static str {
if temp_f < 50.0 { "cold" }
else if temp_f < 77.0 { "mild" }
else { "hot" }
}
fn main() {
for c in [0.0, 20.0, 35.0] {
let f = celsius_to_fahrenheit(c);
println!("{c:.1}°C = {f:.1}°F — {}", classify(f));
}
}
Key takeaway: Rust requires explicit f64 (no implicit int→float), for iterates over arrays directly (no range()), and if/else blocks are expressions.
4. Control Flow / 4. 控制流
Conditional Statements
What you’ll learn:
if/elsewithout parentheses (but with braces),loop/while/forvs Python’s iteration model, expression blocks (everything returns a value), and function signatures with mandatory return types.Difficulty: 🟢 Beginner
if/else
# Python
if temperature > 100:
print("Too hot!")
elif temperature < 0:
print("Too cold!")
else:
print("Just right")
# Ternary
status = "hot" if temperature > 100 else "ok"
#![allow(unused)]
fn main() {
// Rust — braces required, no colons, `else if` not `elif`
if temperature > 100 {
println!("Too hot!");
} else if temperature < 0 {
println!("Too cold!");
} else {
println!("Just right");
}
// if is an EXPRESSION — returns a value (like Python ternary, but more powerful)
let status = if temperature > 100 { "hot" } else { "ok" };
}
Important Differences
#![allow(unused)]
fn main() {
// 1. Condition must be a bool — no truthy/falsy
let x = 42;
// if x { } // ❌ Error: expected bool, found integer
if x != 0 { } // ✅ Explicit comparison required
// In Python, these are all truthy/falsy:
// if []: → False (empty list)
// if "": → False (empty string)
// if 0: → False (zero)
// if None: → False
// In Rust, ONLY bool works in conditions:
let items: Vec<i32> = vec![];
// if items { } // ❌ Error
if !items.is_empty() { } // ✅ Explicit check
let name = "";
// if name { } // ❌ Error
if !name.is_empty() { } // ✅ Explicit check
}
Loops and Iteration
for Loops
# Python
for i in range(5):
print(i)
for item in ["a", "b", "c"]:
print(item)
for i, item in enumerate(["a", "b", "c"]):
print(f"{i}: {item}")
for key, value in {"x": 1, "y": 2}.items():
print(f"{key} = {value}")
#![allow(unused)]
fn main() {
// Rust
for i in 0..5 { // range(5) → 0..5
println!("{}", i);
}
for item in ["a", "b", "c"] { // Direct iteration
println!("{}", item);
}
for (i, item) in ["a", "b", "c"].iter().enumerate() { // enumerate()
println!("{}: {}", i, item);
}
// HashMap iteration
use std::collections::HashMap;
let map = HashMap::from([("x", 1), ("y", 2)]);
for (key, value) in &map { // & borrows the map
println!("{} = {}", key, value);
}
}
Range Syntax
#![allow(unused)]
fn main() {
Python: Rust: Notes:
range(5) 0..5 Half-open (excludes end)
range(1, 10) 1..10 Half-open
range(1, 11) 1..=10 Inclusive (includes end)
range(0, 10, 2) (0..10).step_by(2) Step (method, not syntax)
}
while Loops
# Python
count = 0
while count < 5:
print(count)
count += 1
# Infinite loop
while True:
data = get_input()
if data == "quit":
break
#![allow(unused)]
fn main() {
// Rust
let mut count = 0;
while count < 5 {
println!("{}", count);
count += 1;
}
// Infinite loop — use `loop`, not `while true`
loop {
let data = get_input();
if data == "quit" {
break;
}
}
// loop can return a value! (unique to Rust)
let result = loop {
let input = get_input();
if let Ok(num) = input.parse::<i32>() {
break num; // `break` with a value — like return for loops
}
println!("Not a number, try again");
};
}
List Comprehensions vs Iterator Chains
# Python — list comprehensions
squares = [x ** 2 for x in range(10)]
evens = [x for x in range(20) if x % 2 == 0]
pairs = [(x, y) for x in range(3) for y in range(3)]
#![allow(unused)]
fn main() {
// Rust — iterator chains (.map, .filter, .collect)
let squares: Vec<i32> = (0..10).map(|x| x * x).collect();
let evens: Vec<i32> = (0..20).filter(|x| x % 2 == 0).collect();
let pairs: Vec<(i32, i32)> = (0..3)
.flat_map(|x| (0..3).map(move |y| (x, y)))
.collect();
// These are LAZY — nothing runs until .collect()
// Python comprehensions are eager (run immediately)
// Rust iterators can be more efficient for large datasets
}
Expression Blocks
Everything in Rust is an expression (or can be). This is a big shift from Python,
where if/for are statements.
# Python — if is a statement (except ternary)
if condition:
result = "yes"
else:
result = "no"
# Or ternary (limited to one expression):
result = "yes" if condition else "no"
#![allow(unused)]
fn main() {
// Rust — if is an expression (returns a value)
let result = if condition { "yes" } else { "no" };
// Blocks are expressions — the last line (without semicolon) is the return value
let value = {
let x = 5;
let y = 10;
x + y // No semicolon → this is the value of the block (15)
};
// match is an expression too
let description = match temperature {
t if t > 100 => "boiling",
t if t > 50 => "hot",
t if t > 20 => "warm",
_ => "cold",
};
}
The following diagram illustrates the core difference between Python’s statement-based and Rust’s expression-based control flow:
flowchart LR
subgraph Python ["Python — Statements"]
P1["if condition:"] --> P2["result = 'yes'"]
P1 --> P3["result = 'no'"]
P2 --> P4["result used later"]
P3 --> P4
end
subgraph Rust ["Rust — Expressions"]
R1["let result = if cond"] --> R2["{ 'yes' }"]
R1 --> R3["{ 'no' }"]
R2 --> R4["value returned directly"]
R3 --> R4
end
style Python fill:#ffeeba
style Rust fill:#d4edda
The semicolon rule: In Rust, the last expression in a block without a semicolon is the block’s return value. Adding a semicolon makes it a statement (returns
()). This trips up Python developers initially — it’s like an implicitreturn.
Functions and Type Signatures
Python Functions
# Python — types optional, dynamic dispatch
def greet(name: str, greeting: str = "Hello") -> str:
return f"{greeting}, {name}!"
# Default args, *args, **kwargs
def flexible(*args, **kwargs):
pass
# First-class functions
def apply(f, x):
return f(x)
result = apply(lambda x: x * 2, 5) # 10
Rust Functions
#![allow(unused)]
fn main() {
// Rust — types REQUIRED on function signatures, no defaults
fn greet(name: &str, greeting: &str) -> String {
format!("{}, {}!", greeting, name)
}
// No default arguments — use builder pattern or Option
fn greet_with_default(name: &str, greeting: Option<&str>) -> String {
let greeting = greeting.unwrap_or("Hello");
format!("{}, {}!", greeting, name)
}
// No *args/**kwargs — use slices or structs
fn sum_all(numbers: &[i32]) -> i32 {
numbers.iter().sum()
}
// First-class functions and closures
fn apply(f: fn(i32) -> i32, x: i32) -> i32 {
f(x)
}
let result = apply(|x| x * 2, 5); // 10
}
Return Values
# Python — return is explicit, None is implicit
def divide(a, b):
if b == 0:
return None # Or raise an exception
return a / b
#![allow(unused)]
fn main() {
// Rust — last expression is the return value (no semicolon)
fn divide(a: f64, b: f64) -> Option<f64> {
if b == 0.0 {
None // Early return (could also write `return None;`)
} else {
Some(a / b) // Last expression — implicit return
}
}
}
Multiple Return Values
# Python — return a tuple
def min_max(numbers):
return min(numbers), max(numbers)
lo, hi = min_max([3, 1, 4, 1, 5])
#![allow(unused)]
fn main() {
// Rust — return a tuple (same concept!)
fn min_max(numbers: &[i32]) -> (i32, i32) {
let min = *numbers.iter().min().unwrap();
let max = *numbers.iter().max().unwrap();
(min, max)
}
let (lo, hi) = min_max(&[3, 1, 4, 1, 5]);
}
Methods: self vs &self vs &mut self
#![allow(unused)]
fn main() {
// In Python, `self` is always a mutable reference to the object.
// In Rust, you choose:
impl MyStruct {
fn new() -> Self { ... } // No self — "static method" / "classmethod"
fn read_only(&self) { ... } // &self — borrows immutably (can't modify)
fn modify(&mut self) { ... } // &mut self — borrows mutably (can modify)
fn consume(self) { ... } // self — takes ownership (object is moved)
}
// Python equivalent:
// class MyStruct:
// @classmethod
// def new(cls): ... # No instance needed
// def read_only(self): ... # All three are the same in Python:
// def modify(self): ... # Python self is always mutable
// def consume(self): ... # Python never "consumes" self
}
Exercises
🏋️ Exercise: FizzBuzz with Expressions (click to expand)
Challenge: Write FizzBuzz for 1..=30 using Rust’s expression-based match. Each number should print “Fizz”, “Buzz”, “FizzBuzz”, or the number. Use match (n % 3, n % 5) as the expression.
🔑 Solution
fn main() {
for n in 1..=30 {
let result = match (n % 3, n % 5) {
(0, 0) => String::from("FizzBuzz"),
(0, _) => String::from("Fizz"),
(_, 0) => String::from("Buzz"),
_ => n.to_string(),
};
println!("{result}");
}
}
Key takeaway: match is an expression that returns a value — no need for if/elif/else chains. The _ wildcard replaces Python’s case _: default.
5. Data Structures and Collections / 5. 数据结构与集合
Tuples and Destructuring
What you’ll learn: Rust tuples vs Python tuples, arrays and slices, structs (Rust’s replacement for classes),
Vec<T>vslist,HashMap<K,V>vsdict, and the newtype pattern for domain modeling.Difficulty: 🟢 Beginner
Python Tuples
# Python — tuples are immutable sequences
point = (3.0, 4.0)
x, y = point # Unpacking
print(f"x={x}, y={y}")
# Tuples can hold mixed types
record = ("Alice", 30, True)
name, age, active = record
# Named tuples for clarity
from typing import NamedTuple
class Point(NamedTuple):
x: float
y: float
p = Point(3.0, 4.0)
print(p.x) # Named access
Rust Tuples
#![allow(unused)]
fn main() {
// Rust — tuples are fixed-size, typed, can hold mixed types
let point: (f64, f64) = (3.0, 4.0);
let (x, y) = point; // Destructuring (same as Python unpacking)
println!("x={x}, y={y}");
// Mixed types
let record: (&str, i32, bool) = ("Alice", 30, true);
let (name, age, active) = record;
// Access by index (unlike Python, uses .0 .1 .2 syntax)
let first = record.0; // "Alice"
let second = record.1; // 30
// Python: record[0]
// Rust: record.0 ← dot-index, not bracket-index
}
When to Use Tuples vs Structs
#![allow(unused)]
fn main() {
// Tuples: quick grouping, function returns, temporary values
fn min_max(data: &[i32]) -> (i32, i32) {
(*data.iter().min().unwrap(), *data.iter().max().unwrap())
}
let (lo, hi) = min_max(&[3, 1, 4, 1, 5]);
// Structs: named fields, clear intent, methods
struct Point { x: f64, y: f64 }
// Rule of thumb:
// - 2-3 same-type fields → tuple is fine
// - Named fields needed → use struct
// - Methods needed → use struct
// (Same guidance as Python: tuple vs namedtuple vs dataclass)
}
Arrays and Slices
Python Lists vs Rust Arrays
# Python — lists are dynamic, heterogeneous
numbers = [1, 2, 3, 4, 5] # Can grow, shrink, hold mixed types
numbers.append(6)
mixed = [1, "two", 3.0] # Mixed types allowed
#![allow(unused)]
fn main() {
// Rust has TWO fixed-size vs dynamic concepts:
// 1. Array — fixed size, stack-allocated (no Python equivalent)
let numbers: [i32; 5] = [1, 2, 3, 4, 5]; // Size is part of the type!
// numbers.push(6); // ❌ Arrays can't grow
// Initialize all elements to same value:
let zeros = [0; 10]; // [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
// 2. Slice — a view into an array or Vec (like Python slicing, but borrowed)
let slice: &[i32] = &numbers[1..4]; // [2, 3, 4] — a reference, not a copy!
// Python: numbers[1:4] creates a NEW list (copy)
// Rust: &numbers[1..4] creates a VIEW (no copy, no allocation)
}
Practical Comparison
# Python slicing — creates copies
data = [10, 20, 30, 40, 50]
first_three = data[:3] # New list: [10, 20, 30]
last_two = data[-2:] # New list: [40, 50]
reversed_data = data[::-1] # New list: [50, 40, 30, 20, 10]
#![allow(unused)]
fn main() {
// Rust slicing — creates views (references)
let data = [10, 20, 30, 40, 50];
let first_three = &data[..3]; // &[i32], view: [10, 20, 30]
let last_two = &data[3..]; // &[i32], view: [40, 50]
// No negative indexing — use .len()
let last_two = &data[data.len()-2..]; // &[i32], view: [40, 50]
// Reverse: use an iterator
let reversed: Vec<i32> = data.iter().rev().copied().collect();
}
Structs vs Classes
Python Classes
# Python — class with __init__, methods, properties
from dataclasses import dataclass
@dataclass
class Rectangle:
width: float
height: float
def area(self) -> float:
return self.width * self.height
def perimeter(self) -> float:
return 2.0 * (self.width + self.height)
def scale(self, factor: float) -> "Rectangle":
return Rectangle(self.width * factor, self.height * factor)
def __str__(self) -> str:
return f"Rectangle({self.width} x {self.height})"
r = Rectangle(10.0, 5.0)
print(r.area()) # 50.0
print(r) # Rectangle(10.0 x 5.0)
Rust Structs
// Rust — struct + impl blocks (no inheritance!)
#[derive(Debug, Clone)]
struct Rectangle {
width: f64,
height: f64,
}
impl Rectangle {
// "Constructor" — associated function (no self)
fn new(width: f64, height: f64) -> Self {
Rectangle { width, height } // Field shorthand when names match
}
fn area(&self) -> f64 {
self.width * self.height
}
fn perimeter(&self) -> f64 {
2.0 * (self.width + self.height)
}
fn scale(&self, factor: f64) -> Rectangle {
Rectangle::new(self.width * factor, self.height * factor)
}
}
// Display trait = Python's __str__
impl std::fmt::Display for Rectangle {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Rectangle({} x {})", self.width, self.height)
}
}
fn main() {
let r = Rectangle::new(10.0, 5.0);
println!("{}", r.area()); // 50.0
println!("{}", r); // Rectangle(10 x 5)
}
flowchart LR
subgraph Python ["Python Object (Heap)"]
PH["PyObject Header\n(refcount + type ptr)"] --> PW["width: float obj"]
PH --> PHT["height: float obj"]
PH --> PD["__dict__"]
end
subgraph Rust ["Rust Struct (Stack)"]
RW["width: f64\n(8 bytes)"] --- RH["height: f64\n(8 bytes)"]
end
style Python fill:#ffeeba
style Rust fill:#d4edda
Memory insight: A Python
Rectangleobject has a 56-byte header + separate heap-allocated float objects. A RustRectangleis exactly 16 bytes on the stack — no indirection, no GC pressure.📌 See also: Ch. 10 — Traits and Generics covers implementing traits like
Display,Debug, and operator overloading for your structs.
Key Mapping: Python Dunder Methods → Rust Traits
| Python | Rust | Purpose |
|---|---|---|
__str__ | impl Display | Human-readable string |
__repr__ | #[derive(Debug)] | Debug representation |
__eq__ | #[derive(PartialEq)] | Equality comparison |
__hash__ | #[derive(Hash)] | Hashable (for dict keys / HashSet) |
__lt__, __le__, etc. | #[derive(PartialOrd, Ord)] | Ordering |
__add__ | impl Add | + operator |
__iter__ | impl Iterator | Iteration |
__len__ | .len() method | Length |
__enter__/__exit__ | impl Drop | Cleanup (automatic in Rust) |
__init__ | fn new() (convention) | Constructor |
__getitem__ | impl Index | Indexing with [] |
__contains__ | .contains() method | in operator |
No Inheritance — Composition Instead
# Python — inheritance
class Animal:
def __init__(self, name: str):
self.name = name
def speak(self) -> str:
raise NotImplementedError
class Dog(Animal):
def speak(self) -> str:
return f"{self.name} says Woof!"
class Cat(Animal):
def speak(self) -> str:
return f"{self.name} says Meow!"
#![allow(unused)]
fn main() {
// Rust — traits + composition (no inheritance)
trait Animal {
fn name(&self) -> &str;
fn speak(&self) -> String;
}
struct Dog { name: String }
struct Cat { name: String }
impl Animal for Dog {
fn name(&self) -> &str { &self.name }
fn speak(&self) -> String {
format!("{} says Woof!", self.name)
}
}
impl Animal for Cat {
fn name(&self) -> &str { &self.name }
fn speak(&self) -> String {
format!("{} says Meow!", self.name)
}
}
// Use trait objects for polymorphism (like Python's duck typing):
fn animal_roll_call(animals: &[&dyn Animal]) {
for a in animals {
println!("{}", a.speak());
}
}
}
Mental model: Python says “inherit behavior”. Rust says “implement contracts”. The result is similar, but Rust avoids the diamond problem and fragile base class issues.
Vec vs list
Vec<T> is Rust’s growable, heap-allocated array — the closest equivalent to Python’s list.
Creating Vectors
# Python
numbers = [1, 2, 3]
empty = []
repeated = [0] * 10
from_range = list(range(1, 6))
#![allow(unused)]
fn main() {
// Rust
let numbers = vec![1, 2, 3]; // vec! macro (like a list literal)
let empty: Vec<i32> = Vec::new(); // Empty vec (type annotation needed)
let repeated = vec![0; 10]; // [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
let from_range: Vec<i32> = (1..6).collect(); // [1, 2, 3, 4, 5]
}
Common Operations
# Python list operations
nums = [1, 2, 3]
nums.append(4) # [1, 2, 3, 4]
nums.extend([5, 6]) # [1, 2, 3, 4, 5, 6]
nums.insert(0, 0) # [0, 1, 2, 3, 4, 5, 6]
last = nums.pop() # 6, nums = [0, 1, 2, 3, 4, 5]
length = len(nums) # 6
nums.sort() # In-place sort
sorted_copy = sorted(nums) # New sorted list
nums.reverse() # In-place reverse
contains = 3 in nums # True
index = nums.index(3) # Index of first 3
#![allow(unused)]
fn main() {
// Rust Vec operations
let mut nums = vec![1, 2, 3];
nums.push(4); // [1, 2, 3, 4]
nums.extend([5, 6]); // [1, 2, 3, 4, 5, 6]
nums.insert(0, 0); // [0, 1, 2, 3, 4, 5, 6]
let last = nums.pop(); // Some(6), nums = [0, 1, 2, 3, 4, 5]
let length = nums.len(); // 6
nums.sort(); // In-place sort
let mut sorted_copy = nums.clone();
sorted_copy.sort(); // Sort a clone
nums.reverse(); // In-place reverse
let contains = nums.contains(&3); // true
let index = nums.iter().position(|&x| x == 3); // Some(index) or None
}
Quick Reference
| Python | Rust | Notes |
|---|---|---|
lst.append(x) | vec.push(x) | |
lst.extend(other) | vec.extend(other) | |
lst.pop() | vec.pop() | Returns Option<T> |
lst.insert(i, x) | vec.insert(i, x) | |
lst.remove(x) | vec.retain(|v| v != &x) | |
del lst[i] | vec.remove(i) | Returns the removed element |
len(lst) | vec.len() | |
x in lst | vec.contains(&x) | |
lst.sort() | vec.sort() | |
sorted(lst) | Clone + sort, or iterator | |
lst[i] | vec[i] | Panics if out of bounds |
lst.get(i, default) | vec.get(i) | Returns Option<&T> |
lst[1:3] | &vec[1..3] | Returns a slice (no copy) |
HashMap vs dict
HashMap<K, V> is Rust’s hash map — equivalent to Python’s dict.
Creating HashMaps
# Python
scores = {"Alice": 100, "Bob": 85}
empty = {}
from_pairs = dict([("x", 1), ("y", 2)])
comprehension = {k: v for k, v in zip(keys, values)}
#![allow(unused)]
fn main() {
// Rust
use std::collections::HashMap;
let scores = HashMap::from([("Alice", 100), ("Bob", 85)]);
let empty: HashMap<String, i32> = HashMap::new();
let from_pairs: HashMap<&str, i32> = [("x", 1), ("y", 2)].into_iter().collect();
let comprehension: HashMap<_, _> = keys.iter().zip(values.iter()).collect();
}
Common Operations
# Python dict operations
d = {"a": 1, "b": 2}
d["c"] = 3 # Insert
val = d["a"] # 1 (KeyError if missing)
val = d.get("z", 0) # 0 (default if missing)
del d["b"] # Remove
exists = "a" in d # True
keys = list(d.keys()) # ["a", "c"]
values = list(d.values()) # [1, 3]
items = list(d.items()) # [("a", 1), ("c", 3)]
length = len(d) # 2
# setdefault / defaultdict
from collections import defaultdict
word_count = defaultdict(int)
for word in words:
word_count[word] += 1
#![allow(unused)]
fn main() {
// Rust HashMap operations
use std::collections::HashMap;
let mut d = HashMap::new();
d.insert("a", 1);
d.insert("b", 2);
d.insert("c", 3); // Insert or overwrite
let val = d["a"]; // 1 (panics if missing)
let val = d.get("z").copied().unwrap_or(0); // 0 (safe access)
d.remove("b"); // Remove
let exists = d.contains_key("a"); // true
let keys: Vec<_> = d.keys().collect();
let values: Vec<_> = d.values().collect();
let length = d.len();
// entry API = Python's setdefault / defaultdict pattern
let mut word_count: HashMap<&str, i32> = HashMap::new();
for word in words {
*word_count.entry(word).or_insert(0) += 1;
}
}
Quick Reference
| Python | Rust | Notes |
|---|---|---|
d[key] = val | d.insert(key, val) | Returns Option<V> (old value) |
d[key] | d[&key] | Panics if missing |
d.get(key) | d.get(&key) | Returns Option<&V> |
d.get(key, default) | d.get(&key).unwrap_or(&default) | |
key in d | d.contains_key(&key) | |
del d[key] | d.remove(&key) | Returns Option<V> |
d.keys() | d.keys() | Iterator |
d.values() | d.values() | Iterator |
d.items() | d.iter() | Iterator of (&K, &V) |
len(d) | d.len() | |
d.update(other) | d.extend(other) | |
defaultdict(int) | .entry().or_insert(0) | Entry API |
d.setdefault(k, v) | d.entry(k).or_insert(v) | Entry API |
Other Collections
| Python | Rust | Notes |
|---|---|---|
set() | HashSet<T> | use std::collections::HashSet; |
collections.deque | VecDeque<T> | use std::collections::VecDeque; |
heapq | BinaryHeap<T> | Max-heap by default |
collections.OrderedDict | IndexMap (crate) | HashMap doesn’t preserve order |
sortedcontainers.SortedList | BTreeSet<T> / BTreeMap<K,V> | Tree-based, sorted |
Exercises
🏋️ Exercise: Word Frequency Counter (click to expand)
Challenge: Write a function that takes a &str sentence and returns a HashMap<String, usize> of word frequencies (case-insensitive). In Python this is Counter(s.lower().split()). Translate it to Rust.
🔑 Solution
use std::collections::HashMap;
fn word_frequencies(text: &str) -> HashMap<String, usize> {
let mut counts = HashMap::new();
for word in text.split_whitespace() {
let key = word.to_lowercase();
*counts.entry(key).or_insert(0) += 1;
}
counts
}
fn main() {
let text = "the quick brown fox jumps over the lazy fox";
let freq = word_frequencies(text);
for (word, count) in &freq {
println!("{word}: {count}");
}
}
Key takeaway: HashMap::entry().or_insert() is Rust’s equivalent of Python’s defaultdict or Counter. The * dereference is needed because or_insert returns &mut usize.
6. Enums and Pattern Matching / 6. 枚举与模式匹配
Algebraic Data Types vs Union Types
What you’ll learn: Rust enums with data vs Python
Uniontypes, exhaustivematchvsmatch/case,Option<T>as a compile-time replacement forNone, and guard patterns.Difficulty: 🟡 Intermediate
Python 3.10 introduced match statements and type unions. Rust’s enums go further —
each variant can carry different data, and the compiler ensures you handle every case.
Python Union Types and Match
# Python 3.10+ — structural pattern matching
from typing import Union
from dataclasses import dataclass
@dataclass
class Circle:
radius: float
@dataclass
class Rectangle:
width: float
height: float
@dataclass
class Triangle:
base: float
height: float
Shape = Union[Circle, Rectangle, Triangle] # Type alias
def area(shape: Shape) -> float:
match shape:
case Circle(radius=r):
return 3.14159 * r * r
case Rectangle(width=w, height=h):
return w * h
case Triangle(base=b, height=h):
return 0.5 * b * h
# No compiler warning if you miss a case!
# Adding a new shape? grep the codebase and hope you find all match blocks.
Rust Enums — Data-Carrying Variants
#![allow(unused)]
fn main() {
// Rust — enum variants carry data, compiler enforces exhaustive matching
enum Shape {
Circle(f64), // Circle carries radius
Rectangle(f64, f64), // Rectangle carries width, height
Triangle { base: f64, height: f64 }, // Named fields also work
}
fn area(shape: &Shape) -> f64 {
match shape {
Shape::Circle(r) => std::f64::consts::PI * r * r,
Shape::Rectangle(w, h) => w * h,
Shape::Triangle { base, height } => 0.5 * base * height,
// ❌ If you add Shape::Pentagon and forget to handle it here,
// the compiler refuses to build. No grep needed.
}
}
}
Key insight: Rust’s
matchis exhaustive — the compiler verifies you handle every variant. Add a new variant to an enum and the compiler tells you exactly whichmatchblocks need updating. Python’smatchhas no such guarantee.
Enums Replace Multiple Python Patterns
# Python — several patterns that Rust enums replace:
# 1. String constants
STATUS_PENDING = "pending"
STATUS_ACTIVE = "active"
STATUS_CLOSED = "closed"
# 2. Python Enum (no data)
from enum import Enum
class Status(Enum):
PENDING = "pending"
ACTIVE = "active"
CLOSED = "closed"
# 3. Tagged unions (class + type field)
class Message:
def __init__(self, kind, **data):
self.kind = kind
self.data = data
# Message(kind="text", content="hello")
# Message(kind="image", url="...", width=100)
#![allow(unused)]
fn main() {
// Rust — one enum does all three and more
// 1. Simple enum (like Python's Enum)
enum Status {
Pending,
Active,
Closed,
}
// 2. Data-carrying enum (tagged union — type-safe!)
enum Message {
Text(String),
Image { url: String, width: u32, height: u32 },
Quit, // No data
Move { x: i32, y: i32 },
}
}
flowchart TD
E["enum Message"] --> T["Text(String)\n🏷️ tag=0 + String data"]
E --> I["Image { url, width, height }\n🏷️ tag=1 + 3 fields"]
E --> Q["Quit\n🏷️ tag=2 + no data"]
E --> M["Move { x, y }\n🏷️ tag=3 + 2 fields"]
style E fill:#d4edda,stroke:#28a745
style T fill:#fff3cd
style I fill:#fff3cd
style Q fill:#fff3cd
style M fill:#fff3cd
Memory insight: Rust enums are “tagged unions” — the compiler stores a discriminant tag + enough space for the largest variant. Python’s equivalent (
Union[str, dict, None]) has no compact representation.📌 See also: Ch. 9 — Error Handling uses enums extensively —
Result<T, E>andOption<T>are just enums withmatch.
#![allow(unused)]
fn main() {
fn process(msg: &Message) {
match msg {
Message::Text(content) => println!("Text: {content}"),
Message::Image { url, width, height } => {
println!("Image: {url} ({width}x{height})")
}
Message::Quit => println!("Quitting"),
Message::Move { x, y } => println!("Moving to ({x}, {y})"),
}
}
}
Exhaustive Pattern Matching
Python’s match — Not Exhaustive
# Python — the wildcard case is optional, no compiler help
def describe(value):
match value:
case 0:
return "zero"
case 1:
return "one"
# If you forget the default, Python returns None silently.
# No warning, no error.
describe(42) # Returns None — a silent bug
Rust’s match — Compiler-Enforced
#![allow(unused)]
fn main() {
// Rust — MUST handle every possible case
fn describe(value: i32) -> &'static str {
match value {
0 => "zero",
1 => "one",
// ❌ Compile error: non-exhaustive patterns: `i32::MIN..=-1_i32`
// and `2_i32..=i32::MAX` not covered
_ => "other", // _ = catch-all (required for open-ended types)
}
}
// For enums, NO catch-all needed — compiler knows all variants:
enum Color { Red, Green, Blue }
fn color_hex(c: Color) -> &'static str {
match c {
Color::Red => "#ff0000",
Color::Green => "#00ff00",
Color::Blue => "#0000ff",
// No _ needed — all variants covered
// Add Color::Yellow later → compiler error HERE
}
}
}
Pattern Matching Features
#![allow(unused)]
fn main() {
// Multiple values (like Python's case 1 | 2 | 3:)
match value {
1 | 2 | 3 => println!("small"),
4..=9 => println!("medium"), // Range patterns
_ => println!("large"),
}
// Guards (like Python's case x if x > 0:)
match temperature {
t if t > 100 => println!("boiling"),
t if t < 0 => println!("freezing"),
t => println!("normal: {t}°"),
}
// Nested destructuring
let point = (3, (4, 5));
match point {
(0, _) => println!("on y-axis"),
(_, (0, _)) => println!("y=0"),
(x, (y, z)) => println!("x={x}, y={y}, z={z}"),
}
}
Option for None Safety
Option<T> is the most important Rust enum for Python developers. It replaces
None with a type-safe alternative.
Python None
# Python — None is a value that can appear anywhere
def find_user(user_id: int) -> dict | None:
users = {1: {"name": "Alice"}}
return users.get(user_id)
user = find_user(999)
# user is None — but nothing forces you to check!
print(user["name"]) # 💥 TypeError at runtime
Rust Option
#![allow(unused)]
fn main() {
// Rust — Option<T> forces you to handle the None case
fn find_user(user_id: i64) -> Option<User> {
let users = HashMap::from([(1, User { name: "Alice".into() })]);
users.get(&user_id).cloned()
}
let user = find_user(999);
// user is Option<User> — you CANNOT use it without handling None
// Method 1: match
match find_user(999) {
Some(user) => println!("Found: {}", user.name),
None => println!("Not found"),
}
// Method 2: if let (like Python's if (x := expr) is not None)
if let Some(user) = find_user(1) {
println!("Found: {}", user.name);
}
// Method 3: unwrap_or
let name = find_user(999)
.map(|u| u.name)
.unwrap_or_else(|| "Unknown".to_string());
// Method 4: ? operator (in functions that return Option)
fn get_user_name(id: i64) -> Option<String> {
let user = find_user(id)?; // Returns None early if not found
Some(user.name)
}
}
Option Methods — Python Equivalents
| Pattern | Python | Rust |
|---|---|---|
| Check if exists | if x is not None: | if let Some(x) = opt { |
| Default value | x or default | opt.unwrap_or(default) |
| Default factory | x or compute() | opt.unwrap_or_else(|| compute()) |
| Transform if exists | f(x) if x else None | opt.map(f) |
| Chain lookups | x and x.attr and x.attr.method() | opt.and_then(|x| x.method()) |
| Crash if None | Not possible to prevent | opt.unwrap() (panic) or opt.expect("msg") |
| Get or raise | x if x else raise | opt.ok_or(Error)? |
Exercises
🏋️ Exercise: Shape Area Calculator (click to expand)
Challenge: Define an enum Shape with variants Circle(f64) (radius), Rectangle(f64, f64) (width, height), and Triangle(f64, f64) (base, height). Implement a method fn area(&self) -> f64 using match. Create one of each and print the area.
🔑 Solution
use std::f64::consts::PI;
enum Shape {
Circle(f64),
Rectangle(f64, f64),
Triangle(f64, f64),
}
impl Shape {
fn area(&self) -> f64 {
match self {
Shape::Circle(r) => PI * r * r,
Shape::Rectangle(w, h) => w * h,
Shape::Triangle(b, h) => 0.5 * b * h,
}
}
}
fn main() {
let shapes = [
Shape::Circle(5.0),
Shape::Rectangle(4.0, 6.0),
Shape::Triangle(3.0, 8.0),
];
for shape in &shapes {
println!("Area: {:.2}", shape.area());
}
}
Key takeaway: Rust enums replace Python’s Union[Circle, Rectangle, Triangle] + isinstance() checks. The compiler ensures you handle every variant — adding a new shape without updating area() is a compile error.
7. Ownership and Borrowing / 7. 所有权与借用
Understanding Ownership
What you’ll learn: Why Rust has ownership (no GC!), move semantics vs Python’s reference counting, borrowing (
&and&mut), lifetime basics, and smart pointers (Box,Rc,Arc).Difficulty: 🟡 Intermediate
This is the hardest concept for Python developers. In Python, you never think about who “owns” data — the garbage collector handles it. In Rust, every value has exactly one owner, and the compiler tracks this at compile time.
Python: Shared References Everywhere
# Python — everything is a reference, gc cleans up
a = [1, 2, 3]
b = a # b and a point to the SAME list
b.append(4)
print(a) # [1, 2, 3, 4] — surprise! a changed too
# Who owns the list? Both a and b reference it.
# The garbage collector frees it when no references remain.
# You never think about this.
Rust: Single Ownership
#![allow(unused)]
fn main() {
// Rust — every value has exactly ONE owner
let a = vec![1, 2, 3];
let b = a; // Ownership MOVES from a to b
// println!("{:?}", a); // ❌ Compile error: value used after move
// a no longer exists. b is the sole owner.
println!("{:?}", b); // ✅ [1, 2, 3]
// When b goes out of scope, the Vec is freed. Deterministic. No GC.
}
The Three Ownership Rules
#![allow(unused)]
fn main() {
1. Each value has exactly ONE owner variable.
2. When the owner goes out of scope, the value is dropped (freed).
3. Ownership can be transferred (moved) but not duplicated (unless Clone).
}
Move Semantics — The Biggest Python Shock
# Python — assignment copies the reference, not the data
def process(data):
data.append(42)
# Original list is modified!
my_list = [1, 2, 3]
process(my_list)
print(my_list) # [1, 2, 3, 42] — modified by process!
#![allow(unused)]
fn main() {
// Rust — passing to a function MOVES ownership (for non-Copy types)
fn process(mut data: Vec<i32>) -> Vec<i32> {
data.push(42);
data // Must return it to give ownership back!
}
let my_vec = vec![1, 2, 3];
let my_vec = process(my_vec); // Ownership moves in and back out
println!("{:?}", my_vec); // [1, 2, 3, 42]
// Or better — borrow instead of moving:
fn process_borrowed(data: &mut Vec<i32>) {
data.push(42);
}
let mut my_vec = vec![1, 2, 3];
process_borrowed(&mut my_vec); // Lend it temporarily
println!("{:?}", my_vec); // [1, 2, 3, 42] — still ours
}
Ownership Visualized
Python: Rust:
a ──────┐ a ──→ [1, 2, 3]
├──→ [1, 2, 3]
b ──────┘ After: let b = a;
(a and b share one object) a (invalid, moved)
(refcount = 2) b ──→ [1, 2, 3]
(only b owns the data)
del a → refcount = 1 drop(b) → data freed
del b → refcount = 0 → freed (deterministic, no GC)
stateDiagram-v2
state "Python (Reference Counting)" as PY {
[*] --> a_owns: a = [1,2,3]
a_owns --> shared: b = a
shared --> b_only: del a (refcount 2→1)
b_only --> freed: del b (refcount 1→0)
note right of shared: Both a and b point\nto the SAME object
}
state "Rust (Ownership Move)" as RS {
[*] --> a_owns2: let a = vec![1,2,3]
a_owns2 --> b_owns: let b = a (MOVE)
b_owns --> freed2: b goes out of scope
note right of b_owns: a is INVALID after move\nCompile error if used
}
Move Semantics vs Reference Counting
Copy vs Move
#![allow(unused)]
fn main() {
// Simple types (integers, floats, bools, chars) are COPIED, not moved
let x = 42;
let y = x; // x is COPIED to y (both valid)
println!("{x} {y}"); // ✅ 42 42
// Heap-allocated types (String, Vec, HashMap) are MOVED
let s1 = String::from("hello");
let s2 = s1; // s1 is MOVED to s2
// println!("{s1}"); // ❌ Error: value used after move
// To explicitly copy heap data, use .clone()
let s1 = String::from("hello");
let s2 = s1.clone(); // Deep copy
println!("{s1} {s2}"); // ✅ hello hello (both valid)
}
Python Developer’s Mental Model
Python: Rust:
───────── ─────
int, float, bool Copy types (i32, f64, bool, char)
→ copied on assignment → copied on assignment (similar behavior)
(Note: Python caches small ints; Rust copies are always predictable)
list, dict, str Move types (Vec, HashMap, String)
→ shared reference → ownership transfer (different behavior!)
→ gc cleans up → owner drops data
→ clone with list(x) → clone with x.clone()
or copy.deepcopy(x)
When Python’s Sharing Model Causes Bugs
# Python — accidental aliasing
def remove_duplicates(items):
seen = set()
result = []
for item in items:
if item not in seen:
seen.add(item)
result.append(item)
return result
original = [1, 2, 2, 3, 3, 3]
alias = original # Alias, NOT a copy
unique = remove_duplicates(alias)
# original is still [1, 2, 2, 3, 3, 3] — but only because we didn't mutate
# If remove_duplicates modified the input, original would be affected too
#![allow(unused)]
fn main() {
use std::collections::HashSet;
// Rust — ownership prevents accidental aliasing
fn remove_duplicates(items: &[i32]) -> Vec<i32> {
let mut seen = HashSet::new();
items.iter()
.filter(|&&item| seen.insert(item))
.copied()
.collect()
}
let original = vec![1, 2, 2, 3, 3, 3];
let unique = remove_duplicates(&original); // Borrows — can't modify
// original is guaranteed unchanged — compiler prevented mutation via &
}
Borrowing and Lifetimes
Borrowing = Lending a Book
#![allow(unused)]
fn main() {
Think of ownership like a physical book:
Python: Everyone has a photocopy (shared references + GC)
Rust: One person owns the book. Others can:
- &book = look at it (immutable borrow, many allowed)
- &mut book = write in it (mutable borrow, exclusive)
- book = give it away (move)
}
Borrowing Rules
flowchart TD
R["Borrowing Rules"] --> IMM["✅ Many &T\n(shared/immutable)"]
R --> MUT["✅ One &mut T\n(exclusive/mutable)"]
R --> CONFLICT["❌ &T + &mut T\n(NEVER at same time)"]
IMM --> SAFE["Multiple readers, safe"]
MUT --> SAFE2["Single writer, safe"]
CONFLICT --> ERR["Compile error!"]
style IMM fill:#d4edda
style MUT fill:#d4edda
style CONFLICT fill:#f8d7da
style ERR fill:#f8d7da,stroke:#dc3545
#![allow(unused)]
fn main() {
// Rule 1: You can have MANY immutable borrows OR ONE mutable borrow (not both)
let mut data = vec![1, 2, 3];
// Multiple immutable borrows — fine
let a = &data;
let b = &data;
println!("{:?} {:?}", a, b); // ✅
// Mutable borrow — must be exclusive
let c = &mut data;
c.push(4);
// println!("{:?}", a); // ❌ Error: can't use immutable borrow while mutable exists
// This prevents data races at compile time!
// Python has no equivalent — it's why Python dict modified-during-iteration crashes at runtime.
}
Lifetimes — A Brief Introduction
#![allow(unused)]
fn main() {
// Lifetimes answer: "How long does this reference live?"
// Usually the compiler infers them. You rarely write them explicitly.
// Simple case — compiler handles it:
fn first_word(s: &str) -> &str {
s.split_whitespace().next().unwrap_or("")
}
// The compiler knows: the returned &str lives as long as the input &str
// When you need explicit lifetimes (rare):
fn longest<'a>(a: &'a str, b: &'a str) -> &'a str {
if a.len() > b.len() { a } else { b }
}
// 'a says: "the return value lives as long as both inputs"
}
For Python developers: Don’t worry about lifetimes initially. The compiler will tell you when you need them, and 95% of the time it infers them automatically. Think of lifetime annotations as hints you give the compiler when it can’t figure out the relationships on its own.
Smart Pointers
For cases where single ownership is too restrictive, Rust provides smart pointers. These are closer to Python’s reference model — but explicit and opt-in.
#![allow(unused)]
fn main() {
// Box<T> — heap allocation with single owner (like Python's normal allocation)
let boxed = Box::new(42); // Heap-allocated i32
// Rc<T> — reference counted (like Python's refcount!)
use std::rc::Rc;
let shared = Rc::new(vec![1, 2, 3]);
let clone1 = Rc::clone(&shared); // Increment refcount
let clone2 = Rc::clone(&shared); // Increment refcount
// All three point to the same Vec. When all are dropped, Vec is freed.
// Similar to Python's reference counting, but Rc does NOT handle cycles —
// use Weak<T> to break cycles (Python's GC handles cycles automatically)
// Arc<T> — atomic reference counting (Rc for multi-threaded code)
use std::sync::Arc;
let thread_safe = Arc::new(vec![1, 2, 3]);
// Use Arc when sharing across threads (Rc is single-threaded)
// RefCell<T> — runtime borrow checking (like Python's "anything goes" model)
use std::cell::RefCell;
let cell = RefCell::new(42);
*cell.borrow_mut() = 99; // Mutable borrow at runtime (panics if double-borrowed)
}
When to Use Each
| Smart Pointer | Python Analogy | Use Case |
|---|---|---|
Box<T> | Normal allocation | Large data, recursive types, trait objects |
Rc<T> | Python’s default refcount | Shared ownership, single-threaded |
Arc<T> | Thread-safe refcount | Shared ownership, multi-threaded |
RefCell<T> | Python’s “just mutate it” | Interior mutability (escape hatch) |
Rc<RefCell<T>> | Python’s normal object model | Shared + mutable (graph structures) |
Key insight:
Rc<RefCell<T>>gives you Python-like semantics (shared, mutable data) but you have to opt in explicitly. Rust’s default (owned, moved) is faster and avoids the overhead of reference counting. For graph-like structures with cycles, useWeak<T>to break reference loops — unlike Python, Rust’sRchas no cycle collector.
📌 See also: Ch. 13 — Concurrency covers
Arc<Mutex<T>>for multi-threaded shared state.
Exercises
🏋️ Exercise: Spot the Borrow Checker Error (click to expand)
Challenge: The following code has 3 borrow checker errors. Identify each one and fix them without using .clone():
fn main() {
let mut names = vec!["Alice".to_string(), "Bob".to_string()];
let first = &names[0];
names.push("Charlie".to_string());
println!("First: {first}");
let greeting = make_greeting(names[0]);
println!("{greeting}");
}
fn make_greeting(name: String) -> String {
format!("Hello, {name}!")
}
🔑 Solution
fn main() {
let mut names = vec!["Alice".to_string(), "Bob".to_string()];
let first = &names[0];
println!("First: {first}"); // Use borrow BEFORE mutating
names.push("Charlie".to_string()); // Now safe — no live immutable borrow
let greeting = make_greeting(&names[0]); // Pass reference, not owned
println!("{greeting}");
}
fn make_greeting(name: &str) -> String { // Accept &str, not String
format!("Hello, {name}!")
}
Errors fixed:
- Immutable borrow + mutation:
firstborrowsnames, thenpushmutates it. Fix: usefirstbefore pushing. - Move out of Vec:
names[0]tries to move a String out of Vec (not allowed). Fix: borrow with&names[0]. - Function takes ownership:
make_greeting(String)consumes the value. Fix: take&strinstead.
8. Crates and Modules / 8. Crate 与模块
Rust Modules vs Python Packages
What you’ll learn:
modandusevsimport, visibility (pub) vs Python’s convention-based privacy, Cargo.toml vs pyproject.toml, crates.io vs PyPI, and workspaces vs monorepos.Difficulty: 🟢 Beginner
Python Module System
# Python — files are modules, directories with __init__.py are packages
# myproject/
# ├── __init__.py # Makes it a package
# ├── main.py
# ├── utils/
# │ ├── __init__.py # Makes utils a sub-package
# │ ├── helpers.py
# │ └── validators.py
# └── models/
# ├── __init__.py
# ├── user.py
# └── product.py
# Importing:
from myproject.utils.helpers import format_name
from myproject.models.user import User
import myproject.utils.validators as validators
Rust Module System
#![allow(unused)]
fn main() {
// Rust — mod declarations create the module tree, files provide content
// src/
// ├── main.rs # Crate root — declares modules
// ├── utils/
// │ ├── mod.rs # Module declaration (like __init__.py)
// │ ├── helpers.rs
// │ └── validators.rs
// └── models/
// ├── mod.rs
// ├── user.rs
// └── product.rs
// In src/main.rs:
mod utils; // Tells Rust to look for src/utils/mod.rs
mod models; // Tells Rust to look for src/models/mod.rs
use utils::helpers::format_name;
use models::user::User;
// In src/utils/mod.rs:
pub mod helpers; // Declares and re-exports helpers.rs
pub mod validators; // Declares and re-exports validators.rs
}
graph TD
A["main.rs<br/>(crate root)"] --> B["mod utils"]
A --> C["mod models"]
B --> D["utils/mod.rs"]
D --> E["helpers.rs"]
D --> F["validators.rs"]
C --> G["models/mod.rs"]
G --> H["user.rs"]
G --> I["product.rs"]
style A fill:#d4edda,stroke:#28a745
style D fill:#fff3cd,stroke:#ffc107
style G fill:#fff3cd,stroke:#ffc107
Python equivalent: Think of
mod.rsas__init__.py— it declares what the module exports. The crate root (main.rs/lib.rs) is like your top-level package__init__.py.
Key Differences
| Concept | Python | Rust |
|---|---|---|
| Module = file | ✅ Automatic | Must declare with mod |
| Package = directory | __init__.py | mod.rs |
| Public by default | ✅ Everything | ❌ Private by default |
| Make public | _prefix convention | pub keyword |
| Import syntax | from x import y | use x::y; |
| Wildcard import | from x import * | use x::*; (discouraged) |
| Relative imports | from . import sibling | use super::sibling; |
| Re-export | __all__ or explicit | pub use inner::Thing; |
Visibility — Private by Default
# Python — "we're all adults here"
class User:
def __init__(self):
self.name = "Alice" # Public (by convention)
self._age = 30 # "Private" (convention: single underscore)
self.__secret = "shhh" # Name-mangled (not truly private)
# Nothing stops you from accessing _age or even __secret
print(user._age) # Works fine
print(user._User__secret) # Works too (name mangling)
#![allow(unused)]
fn main() {
// Rust — private is enforced by the compiler
pub struct User {
pub name: String, // Public — anyone can access
age: i32, // Private — only this module can access
}
impl User {
pub fn new(name: &str, age: i32) -> Self {
User { name: name.to_string(), age }
}
pub fn age(&self) -> i32 { // Public getter
self.age
}
fn validate(&self) -> bool { // Private method
self.age > 0
}
}
// Outside the module:
let user = User::new("Alice", 30);
println!("{}", user.name); // ✅ Public
// println!("{}", user.age); // ❌ Compile error: field is private
println!("{}", user.age()); // ✅ Public method (getter)
}
Crates vs PyPI Packages
Python Packages (PyPI)
# Python
pip install requests # Install from PyPI
pip install "requests>=2.28" # Version constraint
pip freeze > requirements.txt # Lock versions
pip install -r requirements.txt # Reproduce environment
Rust Crates (crates.io)
# Rust
cargo add reqwest # Install from crates.io (adds to Cargo.toml)
cargo add reqwest@0.12 # Version constraint
# Cargo.lock is auto-generated — no manual step
cargo build # Downloads and compiles dependencies
Cargo.toml vs pyproject.toml
# Rust — Cargo.toml
[package]
name = "my-project"
version = "0.1.0"
edition = "2021"
[dependencies]
serde = { version = "1.0", features = ["derive"] } # With feature flags
reqwest = { version = "0.12", features = ["json"] }
tokio = { version = "1", features = ["full"] }
log = "0.4"
[dev-dependencies]
mockall = "0.13"
Essential Crates for Python Developers
| Python Library | Rust Crate | Purpose |
|---|---|---|
requests | reqwest | HTTP client |
json (stdlib) | serde_json | JSON parsing |
pydantic | serde | Serialization/validation |
pathlib | std::path (stdlib) | Path handling |
os / shutil | std::fs (stdlib) | File operations |
re | regex | Regular expressions |
logging | tracing / log | Logging |
click / argparse | clap | CLI argument parsing |
asyncio | tokio | Async runtime |
datetime | chrono | Date and time |
pytest | Built-in + rstest | Testing |
dataclasses | #[derive(...)] | Data structures |
typing.Protocol | Traits | Structural typing |
subprocess | std::process (stdlib) | Run external commands |
sqlite3 | rusqlite | SQLite |
sqlalchemy | diesel / sqlx | ORM / SQL toolkit |
fastapi | axum / actix-web | Web framework |
Workspaces vs Monorepos
Python Monorepo (typical)
# Python monorepo (various approaches, no standard)
myproject/
├── pyproject.toml # Root project
├── packages/
│ ├── core/
│ │ ├── pyproject.toml # Each package has its own config
│ │ └── src/core/...
│ ├── api/
│ │ ├── pyproject.toml
│ │ └── src/api/...
│ └── cli/
│ ├── pyproject.toml
│ └── src/cli/...
# Tools: poetry workspaces, pip -e ., uv workspaces — no standard
Rust Workspace
# Rust — Cargo.toml at root
[workspace]
members = [
"core",
"api",
"cli",
]
# Shared dependencies across workspace
[workspace.dependencies]
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
# Rust workspace structure — standardized, built into Cargo
myproject/
├── Cargo.toml # Workspace root
├── Cargo.lock # Single lock file for all crates
├── core/
│ ├── Cargo.toml # [dependencies] serde.workspace = true
│ └── src/lib.rs
├── api/
│ ├── Cargo.toml
│ └── src/lib.rs
└── cli/
├── Cargo.toml
└── src/main.rs
# Workspace commands
cargo build # Build everything
cargo test # Test everything
cargo build -p core # Build just the core crate
cargo test -p api # Test just the api crate
cargo clippy --all # Lint everything
Key insight: Rust workspaces are first-class, built into Cargo. Python monorepos require third-party tools (poetry, uv, pants) with varying levels of support. In a Rust workspace, all crates share a single
Cargo.lock, ensuring consistent dependency versions across the project.
Exercises
🏋️ Exercise: Module Visibility (click to expand)
Challenge: Given this module structure, predict which lines compile and which don’t:
mod kitchen {
fn secret_recipe() -> &'static str { "42 spices" }
pub fn menu() -> &'static str { "Today's special" }
pub mod staff {
pub fn cook() -> String {
format!("Cooking with {}", super::secret_recipe())
}
}
}
fn main() {
println!("{}", kitchen::menu()); // Line A
println!("{}", kitchen::secret_recipe()); // Line B
println!("{}", kitchen::staff::cook()); // Line C
}
🔑 Solution
- Line A: ✅ Compiles —
menu()ispub - Line B: ❌ Compile error —
secret_recipe()is private tokitchen - Line C: ✅ Compiles —
staff::cook()ispub, andcook()can accesssecret_recipe()viasuper::(child modules can access parent’s private items)
Key takeaway: In Rust, child modules can see parent’s privates (like Python’s _private convention, but enforced). Outsiders cannot. This is the opposite of Python where _private is just a hint.
9. Error Handling / 9. 错误处理
Exceptions vs Result
What you’ll learn:
Result<T, E>vstry/except, the?operator for concise error propagation, custom error types withthiserror,anyhowfor applications, and why explicit errors prevent hidden bugs.Difficulty: 🟡 Intermediate
This is one of the biggest mindset changes for Python developers. Python uses exceptions
for error handling — errors can be thrown from anywhere and caught anywhere (or not at all).
Rust uses Result<T, E> — errors are values that must be explicitly handled.
Python Exception Handling
# Python — exceptions can be thrown from anywhere
import json
def load_config(path: str) -> dict:
try:
with open(path) as f:
data = json.load(f) # Can raise JSONDecodeError
if "version" not in data:
raise ValueError("Missing version field")
return data
except FileNotFoundError:
print(f"Config file not found: {path}")
return {}
except json.JSONDecodeError as e:
print(f"Invalid JSON: {e}")
return {}
# What other exceptions can this throw?
# IOError? PermissionError? UnicodeDecodeError?
# You can't tell from the function signature!
Rust Result-Based Error Handling
#![allow(unused)]
fn main() {
// Rust — errors are return values, visible in the function signature
use std::fs;
use serde_json::Value;
fn load_config(path: &str) -> Result<Value, ConfigError> {
let contents = fs::read_to_string(path) // Returns Result
.map_err(|e| ConfigError::FileError(e.to_string()))?;
let data: Value = serde_json::from_str(&contents) // Returns Result
.map_err(|e| ConfigError::ParseError(e.to_string()))?;
if data.get("version").is_none() {
return Err(ConfigError::MissingField("version".to_string()));
}
Ok(data)
}
#[derive(Debug)]
enum ConfigError {
FileError(String),
ParseError(String),
MissingField(String),
}
}
Key Differences
Python: Rust:
───────── ─────
- Errors are exceptions (thrown) - Errors are values (returned)
- Hidden control flow (stack unwinding) - Explicit control flow (? operator)
- Can't tell what errors from signature- MUST see errors in return type
- Uncaught exceptions crash at runtime - Unhandled Results are compile warnings
- try/except is optional - Handling Result is required
- Broad except catches everything - match arms are exhaustive
The Two Result Variants
#![allow(unused)]
fn main() {
// Result<T, E> has exactly two variants:
enum Result<T, E> {
Ok(T), // Success — contains the value (like Python's return value)
Err(E), // Failure — contains the error (like Python's raised exception)
}
// Using Result:
fn divide(a: f64, b: f64) -> Result<f64, String> {
if b == 0.0 {
Err("Division by zero".to_string()) // Like: raise ValueError("...")
} else {
Ok(a / b) // Like: return a / b
}
}
// Handling Result — like try/except but explicit
match divide(10.0, 0.0) {
Ok(result) => println!("Result: {result}"),
Err(msg) => println!("Error: {msg}"),
}
}
The ? Operator
The ? operator is Rust’s equivalent of letting exceptions propagate up the call stack,
but it’s visible and explicit.
Python — Implicit Propagation
# Python — exceptions propagate silently up the call stack
def read_username() -> str:
with open("config.txt") as f: # FileNotFoundError propagates
return f.readline().strip() # IOError propagates
def greet():
name = read_username() # If this throws, greet() also throws
print(f"Hello, {name}!") # This is skipped on error
# The error propagation is INVISIBLE — you have to read the implementation
# to know what exceptions might escape.
Rust — Explicit Propagation with ?
#![allow(unused)]
fn main() {
// Rust — ? propagates errors, but it's visible in the code AND the signature
use std::fs;
use std::io;
fn read_username() -> Result<String, io::Error> {
let contents = fs::read_to_string("config.txt")?; // ? = propagate on Err
Ok(contents.lines().next().unwrap_or("").to_string())
}
fn greet() -> Result<(), io::Error> {
let name = read_username()?; // ? = if Err, return Err immediately
println!("Hello, {name}!"); // Only reached on Ok
Ok(())
}
// The ? says: "if this is Err, return it from THIS function immediately."
// It's like Python's exception propagation, but:
// 1. It's visible (you see the ?)
// 2. It's in the return type (Result<..., io::Error>)
// 3. The compiler ensures you handle it somewhere
}
Chaining with ?
# Python — multiple operations that might fail
def process_file(path: str) -> dict:
with open(path) as f: # Might fail
text = f.read() # Might fail
data = json.loads(text) # Might fail
validate(data) # Might fail
return transform(data) # Might fail
# Any of these can throw — and the exception type varies!
#![allow(unused)]
fn main() {
// Rust — same chain, but explicit
fn process_file(path: &str) -> Result<Data, AppError> {
let text = fs::read_to_string(path)?; // ? propagates io::Error
let data: Value = serde_json::from_str(&text)?; // ? propagates serde error
let validated = validate(&data)?; // ? propagates validation error
let result = transform(&validated)?; // ? propagates transform error
Ok(result)
}
// Every ? is a potential early return — and they're all visible!
}
flowchart TD
A["read_to_string(path)?"] -->|Ok| B["serde_json::from_str?"]
A -->|Err| X["Return Err(io::Error)"]
B -->|Ok| C["validate(&data)?"]
B -->|Err| Y["Return Err(serde::Error)"]
C -->|Ok| D["transform(&validated)?"]
C -->|Err| Z["Return Err(ValidationError)"]
D -->|Ok| E["Ok(result) ✅"]
D -->|Err| W["Return Err(TransformError)"]
style E fill:#d4edda,stroke:#28a745
style X fill:#f8d7da,stroke:#dc3545
style Y fill:#f8d7da,stroke:#dc3545
style Z fill:#f8d7da,stroke:#dc3545
style W fill:#f8d7da,stroke:#dc3545
Each
?is an exit point — unlike Python’s try/except where you can’t see which line might throw without reading the docs.📌 See also: Ch. 15 — Migration Patterns covers translating Python try/except patterns to Rust in real codebases.
Custom Error Types with thiserror
graph TD
AE["AppError (enum)"] --> NF["NotFound\n{ entity, id }"]
AE --> VE["Validation\n{ field, message }"]
AE --> IO["Io(std::io::Error)\n#[from]"]
AE --> JSON["Json(serde_json::Error)\n#[from]"]
IO2["std::io::Error"] -->|"auto-convert via From"| IO
JSON2["serde_json::Error"] -->|"auto-convert via From"| JSON
style AE fill:#d4edda,stroke:#28a745
style NF fill:#fff3cd
style VE fill:#fff3cd
style IO fill:#fff3cd
style JSON fill:#fff3cd
style IO2 fill:#f8d7da
style JSON2 fill:#f8d7da
The
#[from]attribute auto-generatesimpl From<io::Error> for AppError, so?converts library errors into your app errors automatically.
Python Custom Exceptions
# Python — custom exception classes
class AppError(Exception):
pass
class NotFoundError(AppError):
def __init__(self, entity: str, id: int):
self.entity = entity
self.id = id
super().__init__(f"{entity} with id {id} not found")
class ValidationError(AppError):
def __init__(self, field: str, message: str):
self.field = field
super().__init__(f"Validation error on {field}: {message}")
# Usage:
def find_user(user_id: int) -> dict:
if user_id not in users:
raise NotFoundError("User", user_id)
return users[user_id]
Rust Custom Errors with thiserror
#![allow(unused)]
fn main() {
// Rust — error enums with thiserror (most popular approach)
// Cargo.toml: thiserror = "2"
use thiserror::Error;
#[derive(Debug, Error)]
enum AppError {
#[error("{entity} with id {id} not found")]
NotFound { entity: String, id: i64 },
#[error("Validation error on {field}: {message}")]
Validation { field: String, message: String },
#[error("IO error: {0}")]
Io(#[from] std::io::Error), // Auto-convert from io::Error
#[error("JSON error: {0}")]
Json(#[from] serde_json::Error), // Auto-convert from serde error
}
// Usage:
fn find_user(user_id: i64) -> Result<User, AppError> {
users.get(&user_id)
.cloned()
.ok_or(AppError::NotFound {
entity: "User".to_string(),
id: user_id,
})
}
// The #[from] attribute means ? auto-converts io::Error → AppError::Io
fn load_users(path: &str) -> Result<Vec<User>, AppError> {
let data = fs::read_to_string(path)?; // io::Error → AppError::Io automatically
let users: Vec<User> = serde_json::from_str(&data)?; // → AppError::Json
Ok(users)
}
}
Error Handling Quick Reference
| Python | Rust | Notes |
|---|---|---|
raise ValueError("msg") | return Err(AppError::Validation {...}) | Explicit return |
try: ... except: | match result { Ok(v) => ..., Err(e) => ... } | Exhaustive |
except ValueError as e: | Err(AppError::Validation { .. }) => | Pattern match |
raise ... from e | #[from] attribute or .map_err() | Error chaining |
finally: | Drop trait (automatic) | Deterministic cleanup |
with open(...): | Scope-based drop (automatic) | RAII pattern |
| Exception propagates silently | ? propagates visibly | Always in return type |
isinstance(e, ValueError) | matches!(e, AppError::Validation {..}) | Type checking |
Exercises
🏋️ Exercise: Parse Config Value (click to expand)
Challenge: Write a function parse_port(s: &str) -> Result<u16, String> that:
- Rejects empty strings with error
"empty input" - Parses the string to
u16, mapping the parse error to"invalid number: {original_error}" - Rejects ports below 1024 with
"port {n} is privileged"
Call it with "", "hello", "80", and "8080" and print the results.
🔑 Solution
fn parse_port(s: &str) -> Result<u16, String> {
if s.is_empty() {
return Err("empty input".to_string());
}
let port: u16 = s.parse().map_err(|e| format!("invalid number: {e}"))?;
if port < 1024 {
return Err(format!("port {port} is privileged"));
}
Ok(port)
}
fn main() {
for input in ["", "hello", "80", "8080"] {
match parse_port(input) {
Ok(port) => println!("✅ {input} → {port}"),
Err(e) => println!("❌ {input:?} → {e}"),
}
}
}
Key takeaway: ? with .map_err() is Rust’s replacement for try/except ValueError as e: raise ConfigError(...) from e. Every error path is visible in the return type.
10. Traits and Generics / 10. Trait 与泛型
Traits vs Duck Typing
What you’ll learn: Traits as explicit contracts (vs Python duck typing),
Protocol(PEP 544) ≈ Trait, generic type bounds withwhereclauses, trait objects (dyn Trait) vs static dispatch, and common std traits.Difficulty: 🟡 Intermediate
This is where Rust’s type system really shines for Python developers. Python’s “duck typing” says: “if it walks like a duck and quacks like a duck, it’s a duck.” Rust’s traits say: “I’ll tell you exactly which duck behaviors I need, at compile time.”
Python Duck Typing
# Python — duck typing: anything with the right methods works
def total_area(shapes):
"""Works with anything that has an .area() method."""
return sum(shape.area() for shape in shapes)
class Circle:
def __init__(self, radius): self.radius = radius
def area(self): return 3.14159 * self.radius ** 2
class Rectangle:
def __init__(self, w, h): self.w, self.h = w, h
def area(self): return self.w * self.h
# Works at runtime — no inheritance needed!
shapes = [Circle(5), Rectangle(3, 4)]
print(total_area(shapes)) # 90.54
# But what if something doesn't have .area()?
class Dog:
def bark(self): return "Woof!"
total_area([Dog()]) # 💥 AttributeError: 'Dog' has no attribute 'area'
# Error happens at RUNTIME, not at definition time
Rust Traits — Explicit Duck Typing
#![allow(unused)]
fn main() {
// Rust — traits make the "duck" contract explicit
trait HasArea {
fn area(&self) -> f64; // Any type that implements this trait has .area()
}
struct Circle { radius: f64 }
struct Rectangle { width: f64, height: f64 }
impl HasArea for Circle {
fn area(&self) -> f64 {
std::f64::consts::PI * self.radius * self.radius
}
}
impl HasArea for Rectangle {
fn area(&self) -> f64 {
self.width * self.height
}
}
// The trait constraint is explicit — compiler checks at compile time
fn total_area(shapes: &[&dyn HasArea]) -> f64 {
shapes.iter().map(|s| s.area()).sum()
}
// Using it:
let shapes: Vec<&dyn HasArea> = vec![&Circle { radius: 5.0 }, &Rectangle { width: 3.0, height: 4.0 }];
println!("{}", total_area(&shapes)); // 90.54
// struct Dog;
// total_area(&[&Dog {}]); // ❌ Compile error: Dog doesn't implement HasArea
}
Key insight: Python’s duck typing defers errors to runtime. Rust’s traits catch them at compile time. Same flexibility, earlier error detection.
Protocols (PEP 544) vs Traits
Python 3.8 introduced Protocol (PEP 544) for structural subtyping — it’s the
closest Python concept to Rust traits.
Python Protocol
# Python — Protocol (structural typing, like Rust traits)
from typing import Protocol, runtime_checkable
@runtime_checkable
class Printable(Protocol):
def to_string(self) -> str: ...
class User:
def __init__(self, name: str):
self.name = name
def to_string(self) -> str:
return f"User({self.name})"
class Product:
def __init__(self, name: str, price: float):
self.name = name
self.price = price
def to_string(self) -> str:
return f"Product({self.name}, ${self.price:.2f})"
def print_all(items: list[Printable]) -> None:
for item in items:
print(item.to_string())
# Works because User and Product both have to_string()
print_all([User("Alice"), Product("Widget", 9.99)])
# BUT: mypy checks this, Python runtime does NOT enforce it
# print_all([42]) # mypy warns, but Python runs it and crashes
Rust Trait (Equivalent, but enforced!)
#![allow(unused)]
fn main() {
// Rust — traits are enforced at compile time
trait Printable {
fn to_string(&self) -> String;
}
struct User { name: String }
struct Product { name: String, price: f64 }
impl Printable for User {
fn to_string(&self) -> String {
format!("User({})", self.name)
}
}
impl Printable for Product {
fn to_string(&self) -> String {
format!("Product({}, ${:.2})", self.name, self.price)
}
}
fn print_all(items: &[&dyn Printable]) {
for item in items {
println!("{}", item.to_string());
}
}
// print_all(&[&42i32]); // ❌ Compile error: i32 doesn't implement Printable
}
Comparison Table
| Feature | Python Protocol | Rust Trait |
|---|---|---|
| Structural typing | ✅ (implicit) | ❌ (explicit impl) |
| Checked at | Runtime (or mypy) | Compile time (always) |
| Default implementations | ❌ | ✅ |
| Can add to foreign types | ❌ | ✅ (within limits) |
| Multiple protocols | ✅ | ✅ (multiple traits) |
| Associated types | ❌ | ✅ |
| Generic constraints | ✅ (with TypeVar) | ✅ (trait bounds) |
Generic Constraints
Python Generics
# Python — TypeVar for generic functions
from typing import TypeVar, Sequence
T = TypeVar('T')
def first(items: Sequence[T]) -> T | None:
return items[0] if items else None
# Bounded TypeVar
from typing import SupportsFloat
T = TypeVar('T', bound=SupportsFloat)
def average(items: Sequence[T]) -> float:
return sum(float(x) for x in items) / len(items)
Rust Generics with Trait Bounds
#![allow(unused)]
fn main() {
// Rust — generics with trait bounds
fn first<T>(items: &[T]) -> Option<&T> {
items.first()
}
// With trait bounds — "T must implement these traits"
fn average<T>(items: &[T]) -> f64
where
T: Into<f64> + Copy, // T must convert to f64 and be copyable
{
let sum: f64 = items.iter().map(|&x| x.into()).sum();
sum / items.len() as f64
}
// Multiple bounds — "T must implement Display AND Debug AND Clone"
fn log_and_clone<T: std::fmt::Display + std::fmt::Debug + Clone>(item: &T) -> T {
println!("Display: {}", item);
println!("Debug: {:?}", item);
item.clone()
}
// Shorthand with impl Trait (for simple cases)
fn print_it(item: &impl std::fmt::Display) {
println!("{}", item);
}
}
Generics Quick Reference
| Python | Rust | Notes |
|---|---|---|
TypeVar('T') | <T> | Unbounded generic |
TypeVar('T', bound=X) | <T: X> | Bounded generic |
Union[int, str] | enum or trait object | Rust has no union types |
Sequence[T] | &[T] (slice) | Borrowed sequence |
Callable[[A], R] | Fn(A) -> R | Function trait |
Optional[T] | Option<T> | Built into the language |
Common Standard Library Traits
These are Rust’s version of Python’s “dunder methods” — they define how types behave in common situations.
Display and Debug (Printing)
#![allow(unused)]
fn main() {
use std::fmt;
// Debug — like __repr__ (auto-derivable)
#[derive(Debug)]
struct Point { x: f64, y: f64 }
// Now you can: println!("{:?}", point);
// Display — like __str__ (must implement manually)
impl fmt::Display for Point {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "({}, {})", self.x, self.y)
}
}
// Now you can: println!("{}", point);
}
Comparison Traits
#![allow(unused)]
fn main() {
// PartialEq — like __eq__
// Eq — total equality (f64 is PartialEq but not Eq because NaN != NaN)
// PartialOrd — like __lt__, __le__, etc.
// Ord — total ordering
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)]
struct Student {
name: String,
grade: i32,
}
// Now students can be: compared, sorted, used as HashMap keys, cloned
let mut students = vec![
Student { name: "Charlie".into(), grade: 85 },
Student { name: "Alice".into(), grade: 92 },
];
students.sort(); // Uses Ord — sorts by name then grade (struct field order)
}
Iterator Trait
#![allow(unused)]
fn main() {
// Implementing Iterator — like Python's __iter__/__next__
struct Countdown { value: i32 }
impl Iterator for Countdown {
type Item = i32; // What the iterator yields
fn next(&mut self) -> Option<Self::Item> {
if self.value > 0 {
self.value -= 1;
Some(self.value + 1)
} else {
None // Iteration complete
}
}
}
// Usage:
for n in (Countdown { value: 5 }) {
println!("{n}"); // 5, 4, 3, 2, 1
}
}
Common Traits at a Glance
| Rust Trait | Python Equivalent | Purpose |
|---|---|---|
Display | __str__ | Human-readable string |
Debug | __repr__ | Debug string (derivable) |
Clone | copy.deepcopy | Deep copy |
Copy | (int/float auto-copy) | Implicit copy for simple types |
PartialEq / Eq | __eq__ | Equality comparison |
PartialOrd / Ord | __lt__ etc. | Ordering |
Hash | __hash__ | Hashable (for dict keys) |
Default | Default __init__ | Default values |
From / Into | __init__ overloads | Type conversions |
Iterator | __iter__ / __next__ | Iteration |
Drop | __del__ / __exit__ | Cleanup |
Add, Sub, Mul | __add__, __sub__, __mul__ | Operator overloading |
Index | __getitem__ | Indexing with [] |
Deref | (no equivalent) | Smart pointer dereferencing |
Send / Sync | (no equivalent) | Thread safety markers |
flowchart TB
subgraph Static ["Static Dispatch (impl Trait)"]
G["fn notify(item: &impl Summary)"] --> M1["Compiled: notify_Article()"]
G --> M2["Compiled: notify_Tweet()"]
M1 --> O1["Inlined, zero-cost"]
M2 --> O2["Inlined, zero-cost"]
end
subgraph Dynamic ["Dynamic Dispatch (dyn Trait)"]
D["fn notify(item: &dyn Summary)"] --> VT["vtable lookup"]
VT --> I1["Article::summarize()"]
VT --> I2["Tweet::summarize()"]
end
style Static fill:#d4edda
style Dynamic fill:#fff3cd
Python equivalent: Python always uses dynamic dispatch (
getattrat runtime). Rust defaults to static dispatch (monomorphization — the compiler generates specialized code for each concrete type). Usedyn Traitonly when you need runtime polymorphism.📌 See also: Ch. 11 — From/Into Traits covers the conversion traits (
From,Into,TryFrom) in depth.
Associated Types
Rust traits can define associated types — type placeholders that each implementor fills in. Python has no equivalent:
#![allow(unused)]
fn main() {
// Iterator defines an associated type 'Item'
trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
}
struct Countdown { remaining: u32 }
impl Iterator for Countdown {
type Item = u32; // This iterator yields u32 values
fn next(&mut self) -> Option<u32> {
if self.remaining > 0 {
self.remaining -= 1;
Some(self.remaining)
} else {
None
}
}
}
}
In Python, __iter__ / __next__ return Any — there’s no way to declare “this iterator yields int” and have it enforced (type hints with Iterator[int] are advisory only).
Operator Overloading: __add__ → impl Add
Python uses magic methods (__add__, __mul__). Rust uses trait implementations — same idea, but type-checked at compile time:
# Python
class Vec2:
def __init__(self, x, y):
self.x, self.y = x, y
def __add__(self, other):
return Vec2(self.x + other.x, self.y + other.y) # No type checking on 'other'
#![allow(unused)]
fn main() {
use std::ops::Add;
#[derive(Debug, Clone, Copy)]
struct Vec2 { x: f64, y: f64 }
impl Add for Vec2 {
type Output = Vec2; // Associated type: what does + return?
fn add(self, rhs: Vec2) -> Vec2 {
Vec2 { x: self.x + rhs.x, y: self.y + rhs.y }
}
}
let a = Vec2 { x: 1.0, y: 2.0 };
let b = Vec2 { x: 3.0, y: 4.0 };
let c = a + b; // Type-safe: only Vec2 + Vec2 is allowed
}
Key difference: Python’s __add__ accepts any other at runtime (you check types manually or get a TypeError). Rust’s Add trait enforces the operand types at compile time — Vec2 + i32 is a compile error unless you explicitly impl Add<i32> for Vec2.
Exercises
🏋️ Exercise: Generic Summary Trait (click to expand)
Challenge: Define a trait Summary with a method fn summarize(&self) -> String. Implement it for two structs: Article { title: String, body: String } and Tweet { username: String, content: String }. Then write a function fn notify(item: &impl Summary) that prints the summary.
🔑 Solution
trait Summary {
fn summarize(&self) -> String;
}
struct Article { title: String, body: String }
struct Tweet { username: String, content: String }
impl Summary for Article {
fn summarize(&self) -> String {
format!("{} — {}...", self.title, &self.body[..20.min(self.body.len())])
}
}
impl Summary for Tweet {
fn summarize(&self) -> String {
format!("@{}: {}", self.username, self.content)
}
}
fn notify(item: &impl Summary) {
println!("📢 {}", item.summarize());
}
fn main() {
let article = Article {
title: "Rust is great".into(),
body: "Here is why Rust beats Python for systems...".into(),
};
let tweet = Tweet {
username: "rustacean".into(),
content: "Just shipped my first crate!".into(),
};
notify(&article);
notify(&tweet);
}
Key takeaway: &impl Summary is the Rust equivalent of Python’s Protocol with a summarize method. But Rust checks it at compile time — passing a type that doesn’t implement Summary is a compile error, not a runtime AttributeError.
11. From and Into Traits / 11. From 与 Into Trait
Type Conversions in Rust
What you’ll learn:
FromandIntotraits for zero-cost type conversions,TryFromfor fallible conversions, howimpl From<A> for Bauto-generatesInto, and string conversion patterns.Difficulty: 🟡 Intermediate
Python handles type conversions with constructor calls (int("42"), str(42),
float("3.14")). Rust uses the From and Into traits for type-safe conversions.
Python Type Conversion
# Python — explicit constructors for conversion
x = int("42") # str → int (can raise ValueError)
s = str(42) # int → str
f = float("3.14") # str → float
lst = list((1, 2, 3)) # tuple → list
# Custom conversion via __init__ or class methods
class Celsius:
def __init__(self, temp: float):
self.temp = temp
@classmethod
def from_fahrenheit(cls, f: float) -> "Celsius":
return cls((f - 32.0) * 5.0 / 9.0)
c = Celsius.from_fahrenheit(212.0) # 100.0°C
Rust From/Into
#![allow(unused)]
fn main() {
// Rust — From trait defines conversions
// Implementing From<T> gives you Into<U> automatically!
struct Celsius(f64);
struct Fahrenheit(f64);
impl From<Fahrenheit> for Celsius {
fn from(f: Fahrenheit) -> Self {
Celsius((f.0 - 32.0) * 5.0 / 9.0)
}
}
// Now both work:
let c1 = Celsius::from(Fahrenheit(212.0)); // Explicit From
let c2: Celsius = Fahrenheit(212.0).into(); // Into (automatically derived)
// String conversions:
let s: String = String::from("hello"); // &str → String
let s: String = "hello".to_string(); // Same thing
let s: String = "hello".into(); // Also works (From is implemented)
let num: i64 = 42i32.into(); // i32 → i64 (lossless, so From exists)
// let small: i32 = 42i64.into(); // ❌ i64 → i32 might lose data — no From
// For fallible conversions, use TryFrom:
let n: Result<i32, _> = "42".parse(); // str → i32 (might fail)
let n: i32 = "42".parse().unwrap(); // Panic if not a number
let n: i32 = "42".parse()?; // Propagate error with ?
}
The From/Into Relationship
flowchart LR
A["impl From<A> for B"] -->|"auto-generates"| B["impl Into<B> for A"]
C["Celsius::from(Fahrenheit(212.0))"] ---|"same as"| D["Fahrenheit(212.0).into()"]
style A fill:#d4edda
style B fill:#d4edda
Rule of thumb: Always implement
From, never implementIntodirectly. ImplementingFrom<A> for Bgives youInto<B> for Afor free.
When to Use From/Into
#![allow(unused)]
fn main() {
// Implement From<T> for your types to enable ergonomic API design:
#[derive(Debug)]
struct UserId(i64);
impl From<i64> for UserId {
fn from(id: i64) -> Self {
UserId(id)
}
}
// Now functions can accept anything convertible to UserId:
fn find_user(id: impl Into<UserId>) -> Option<String> {
let user_id = id.into();
// ... lookup logic
Some(format!("User #{:?}", user_id))
}
find_user(42i64); // ✅ i64 auto-converts to UserId
find_user(UserId(42)); // ✅ UserId stays as-is
}
TryFrom — Fallible Conversions
Not all conversions can succeed. Python raises exceptions; Rust uses TryFrom which returns a Result:
# Python — fallible conversions raise exceptions
try:
port = int("not_a_number") # ValueError
except ValueError as e:
print(f"Invalid: {e}")
# Custom validation in __init__
class Port:
def __init__(self, value: int):
if not (1 <= value <= 65535):
raise ValueError(f"Invalid port: {value}")
self.value = value
try:
p = Port(99999) # ValueError at runtime
except ValueError:
pass
#![allow(unused)]
fn main() {
use std::num::ParseIntError;
// TryFrom for built-in types
let n: Result<i32, ParseIntError> = "42".try_into(); // Ok(42)
let n: Result<i32, ParseIntError> = "bad".try_into(); // Err(...)
// Custom TryFrom for validation
#[derive(Debug)]
struct Port(u16);
#[derive(Debug)]
enum PortError {
OutOfRange(u16),
Zero,
}
impl TryFrom<u16> for Port {
type Error = PortError;
fn try_from(value: u16) -> Result<Self, Self::Error> {
match value {
0 => Err(PortError::Zero),
1..=65535 => Ok(Port(value)),
// Note: u16 max is 65535, so this covers all cases
}
}
}
impl std::fmt::Display for PortError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
PortError::Zero => write!(f, "port cannot be zero"),
PortError::OutOfRange(v) => write!(f, "port {v} out of range"),
}
}
}
// Usage:
let p: Result<Port, _> = 8080u16.try_into(); // Ok(Port(8080))
let p: Result<Port, _> = 0u16.try_into(); // Err(PortError::Zero)
}
Python → Rust mental model:
TryFrom=__init__that validates and can fail. But instead of raising an exception, it returnsResult— so callers must handle the error case.
String Conversion Patterns
Strings are the most common source of conversion confusion for Python developers:
#![allow(unused)]
fn main() {
// String → &str (borrowing, free)
let s = String::from("hello");
let r: &str = &s; // Automatic Deref coercion
let r: &str = s.as_str(); // Explicit
// &str → String (allocating, costs memory)
let r: &str = "hello";
let s1 = String::from(r); // From trait
let s2 = r.to_string(); // ToString trait (via Display)
let s3: String = r.into(); // Into trait
// Number → String
let s = 42.to_string(); // "42" — like Python's str(42)
let s = format!("{:.2}", 3.14); // "3.14" — like Python's f"{3.14:.2f}"
// String → Number
let n: i32 = "42".parse().unwrap(); // like Python's int("42")
let f: f64 = "3.14".parse().unwrap(); // like Python's float("3.14")
// Custom types → String (implement Display)
use std::fmt;
struct Point { x: f64, y: f64 }
impl fmt::Display for Point {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "({}, {})", self.x, self.y)
}
}
let p = Point { x: 1.0, y: 2.0 };
println!("{p}"); // (1, 2) — like Python's __str__
let s = p.to_string(); // Also works! Display gives you ToString for free.
}
Conversion Quick Reference
| Python | Rust | Notes |
|---|---|---|
str(x) | x.to_string() | Requires Display impl |
int("42") | "42".parse::<i32>() | Returns Result |
float("3.14") | "3.14".parse::<f64>() | Returns Result |
list(iter) | iter.collect::<Vec<_>>() | Type annotation needed |
dict(pairs) | pairs.collect::<HashMap<_,_>>() | Type annotation needed |
bool(x) | No direct equivalent | Use explicit checks |
MyClass(x) | MyClass::from(x) | Implement From<T> |
MyClass(x) (validates) | MyClass::try_from(x)? | Implement TryFrom<T> |
Conversion Chains and Error Handling
Real-world code often chains multiple conversions. Compare the approaches:
# Python — chain of conversions with try/except
def parse_config(raw: str) -> tuple[str, int]:
try:
host, port_str = raw.split(":")
port = int(port_str)
if not (1 <= port <= 65535):
raise ValueError(f"Bad port: {port}")
return (host, port)
except (ValueError, AttributeError) as e:
raise ConfigError(f"Invalid config: {e}") from e
fn parse_config(raw: &str) -> Result<(String, u16), String> {
let (host, port_str) = raw
.split_once(':')
.ok_or_else(|| "missing ':' separator".to_string())?;
let port: u16 = port_str
.parse()
.map_err(|e| format!("invalid port: {e}"))?;
if port == 0 {
return Err("port cannot be zero".to_string());
}
Ok((host.to_string(), port))
}
fn main() {
match parse_config("localhost:8080") {
Ok((host, port)) => println!("Connecting to {host}:{port}"),
Err(e) => eprintln!("Config error: {e}"),
}
}
Key insight: Each
?is a visible exit point. In Python, any line insidetrycould be the one that throws — in Rust, only lines ending with?can fail.📌 See also: Ch. 9 — Error Handling covers
Result,?, and custom error types withthiserrorin depth.
Exercises
🏋️ Exercise: Temperature Conversion Library (click to expand)
Challenge: Build a mini temperature conversion library:
- Define
Celsius(f64),Fahrenheit(f64), andKelvin(f64)structs - Implement
From<Celsius> for FahrenheitandFrom<Celsius> for Kelvin - Implement
TryFrom<f64> for Kelvinthat rejects values below absolute zero (-273.15°C = 0K) - Implement
Displayfor all three types (e.g.,"100.00°C")
🔑 Solution
use std::fmt;
struct Celsius(f64);
struct Fahrenheit(f64);
struct Kelvin(f64);
impl From<Celsius> for Fahrenheit {
fn from(c: Celsius) -> Self {
Fahrenheit(c.0 * 9.0 / 5.0 + 32.0)
}
}
impl From<Celsius> for Kelvin {
fn from(c: Celsius) -> Self {
Kelvin(c.0 + 273.15)
}
}
#[derive(Debug)]
struct BelowAbsoluteZero;
impl fmt::Display for BelowAbsoluteZero {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "temperature below absolute zero")
}
}
impl TryFrom<f64> for Kelvin {
type Error = BelowAbsoluteZero;
fn try_from(value: f64) -> Result<Self, Self::Error> {
if value < 0.0 {
Err(BelowAbsoluteZero)
} else {
Ok(Kelvin(value))
}
}
}
impl fmt::Display for Celsius { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{:.2}°C", self.0) } }
impl fmt::Display for Fahrenheit { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{:.2}°F", self.0) } }
impl fmt::Display for Kelvin { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{:.2}K", self.0) } }
fn main() {
let boiling = Celsius(100.0);
let f: Fahrenheit = Celsius(100.0).into();
let k: Kelvin = Celsius(100.0).into();
println!("{boiling} = {f} = {k}");
match Kelvin::try_from(-10.0) {
Ok(k) => println!("{k}"),
Err(e) => println!("Error: {e}"),
}
}
Key takeaway: From handles infallible conversions (Celsius→Fahrenheit always works). TryFrom handles fallible ones (negative Kelvin is impossible). Python conflates both in __init__ — Rust makes the distinction explicit in the type system.
12. Closures and Iterators / 12. 闭包与迭代器
Rust Closures vs Python Lambdas
What you’ll learn: Multi-line closures (not just one-expression lambdas),
Fn/FnMut/FnOncecapture semantics, iterator chains vs list comprehensions,map/filter/fold, andmacro_rules!basics.Difficulty: 🟡 Intermediate
Python Closures and Lambdas
# Python — lambdas are one-expression anonymous functions
double = lambda x: x * 2
result = double(5) # 10
# Full closures capture variables from enclosing scope:
def make_adder(n):
def adder(x):
return x + n # Captures `n` from outer scope
return adder
add_5 = make_adder(5)
print(add_5(10)) # 15
# Higher-order functions:
numbers = [1, 2, 3, 4, 5]
doubled = list(map(lambda x: x * 2, numbers))
evens = list(filter(lambda x: x % 2 == 0, numbers))
Rust Closures
#![allow(unused)]
fn main() {
// Rust — closures use |args| body syntax
let double = |x: i32| x * 2;
let result = double(5); // 10
// Closures capture variables from enclosing scope:
fn make_adder(n: i32) -> impl Fn(i32) -> i32 {
move |x| x + n // `move` transfers ownership of `n` into the closure
}
let add_5 = make_adder(5);
println!("{}", add_5(10)); // 15
// Higher-order functions with iterators:
let numbers = vec![1, 2, 3, 4, 5];
let doubled: Vec<i32> = numbers.iter().map(|x| x * 2).collect();
let evens: Vec<i32> = numbers.iter().filter(|&&x| x % 2 == 0).copied().collect();
}
Closure Syntax Comparison
Python: Rust:
───────── ─────
lambda x: x * 2 |x| x * 2
lambda x, y: x + y |x, y| x + y
lambda: 42 || 42
# Multi-line
def f(x): |x| {
y = x * 2 let y = x * 2;
return y + 1 y + 1
}
Closure Capture — How Rust Differs
# Python — closures capture by reference (late binding!)
funcs = [lambda: i for i in range(3)]
print([f() for f in funcs]) # [2, 2, 2] — surprise! All captured the same `i`
# Fix with default arg trick:
funcs = [lambda i=i: i for i in range(3)]
print([f() for f in funcs]) # [0, 1, 2]
#![allow(unused)]
fn main() {
// Rust — closures capture correctly (no late-binding gotcha)
let funcs: Vec<Box<dyn Fn() -> i32>> = (0..3)
.map(|i| Box::new(move || i) as Box<dyn Fn() -> i32>)
.collect();
let results: Vec<i32> = funcs.iter().map(|f| f()).collect();
println!("{:?}", results); // [0, 1, 2] — correct!
// `move` captures a COPY of `i` for each closure — no late-binding surprise.
}
Three Closure Traits
#![allow(unused)]
fn main() {
// Rust closures implement one or more of these traits:
// Fn — can be called multiple times, doesn't mutate captures (most common)
fn apply(f: impl Fn(i32) -> i32, x: i32) -> i32 { f(x) }
// FnMut — can be called multiple times, MAY mutate captures
fn apply_mut(mut f: impl FnMut(i32) -> i32, x: i32) -> i32 { f(x) }
// FnOnce — can only be called ONCE (consumes captures)
fn apply_once(f: impl FnOnce() -> String) -> String { f() }
// Python has no equivalent — closures are always Fn-like.
// In Rust, the compiler automatically determines which trait to use.
}
Iterators vs Generators
Python Generators
# Python — generators with yield
def fibonacci():
a, b = 0, 1
while True:
yield a
a, b = b, a + b
# Lazy — values computed on demand
fib = fibonacci()
first_10 = [next(fib) for _ in range(10)]
# Generator expressions — like lazy list comprehensions
squares = (x ** 2 for x in range(1000000)) # No memory allocation
first_5 = [next(squares) for _ in range(5)]
Rust Iterators
#![allow(unused)]
fn main() {
// Rust — Iterator trait (similar concept, different syntax)
struct Fibonacci {
a: u64,
b: u64,
}
impl Fibonacci {
fn new() -> Self {
Fibonacci { a: 0, b: 1 }
}
}
impl Iterator for Fibonacci {
type Item = u64;
fn next(&mut self) -> Option<Self::Item> {
let current = self.a;
self.a = self.b;
self.b = current + self.b;
Some(current)
}
}
// Lazy — values computed on demand (just like Python generators)
let first_10: Vec<u64> = Fibonacci::new().take(10).collect();
// Iterator chains — like generator expressions
let squares: Vec<u64> = (0..1_000_000u64).map(|x| x * x).take(5).collect();
}
Comprehensions vs Iterator Chains
This section maps Python’s comprehension syntax to Rust’s iterator chains.
List Comprehension → map/filter/collect
# Python comprehensions:
squares = [x ** 2 for x in range(10)]
evens = [x for x in range(20) if x % 2 == 0]
names = [user.name for user in users if user.active]
pairs = [(x, y) for x in range(3) for y in range(3)]
flat = [item for sublist in nested for item in sublist]
flowchart LR
A["Source\n[1,2,3,4,5]"] -->|.iter\(\)| B["Iterator"]
B -->|.filter\(\|x\| x%2==0\)| C["[2, 4]"]
C -->|.map\(\|x\| x*x\)| D["[4, 16]"]
D -->|.collect\(\)| E["Vec<i32>\n[4, 16]"]
style A fill:#ffeeba
style E fill:#d4edda
Key insight: Rust iterators are lazy — nothing happens until
.collect(). Python’s generators work similarly, but list comprehensions evaluate eagerly.
#![allow(unused)]
fn main() {
// Rust iterator chains:
let squares: Vec<i32> = (0..10).map(|x| x * x).collect();
let evens: Vec<i32> = (0..20).filter(|x| x % 2 == 0).collect();
let names: Vec<&str> = users.iter()
.filter(|u| u.active)
.map(|u| u.name.as_str())
.collect();
let pairs: Vec<(i32, i32)> = (0..3)
.flat_map(|x| (0..3).map(move |y| (x, y)))
.collect();
let flat: Vec<i32> = nested.iter()
.flat_map(|sublist| sublist.iter().copied())
.collect();
}
Dict Comprehension → collect into HashMap
# Python
word_lengths = {word: len(word) for word in words}
inverted = {v: k for k, v in mapping.items()}
#![allow(unused)]
fn main() {
// Rust
let word_lengths: HashMap<&str, usize> = words.iter()
.map(|w| (*w, w.len()))
.collect();
let inverted: HashMap<&V, &K> = mapping.iter()
.map(|(k, v)| (v, k))
.collect();
}
Set Comprehension → collect into HashSet
# Python
unique_lengths = {len(word) for word in words}
#![allow(unused)]
fn main() {
// Rust
let unique_lengths: HashSet<usize> = words.iter()
.map(|w| w.len())
.collect();
}
Common Iterator Methods
| Python | Rust | Notes |
|---|---|---|
map(f, iter) | .map(f) | Transform each element |
filter(f, iter) | .filter(f) | Keep matching elements |
sum(iter) | .sum() | Sum all elements |
min(iter) / max(iter) | .min() / .max() | Returns Option |
any(f(x) for x in iter) | .any(f) | True if any match |
all(f(x) for x in iter) | .all(f) | True if all match |
enumerate(iter) | .enumerate() | Index + value |
zip(a, b) | a.zip(b) | Pair elements |
len(list) | .count() (consumes!) or .len() | Count elements |
list(reversed(x)) | .rev() | Reverse iteration |
itertools.chain(a, b) | a.chain(b) | Concatenate iterators |
next(iter) | .next() | Get next element |
next(iter, default) | .next().unwrap_or(default) | With default |
list(iter) | .collect::<Vec<_>>() | Materialize into collection |
sorted(iter) | Collect, then .sort() | No lazy sorted iterator |
functools.reduce(f, iter) | .fold(init, f) or .reduce(f) | Accumulate |
Key Differences
Python iterators: Rust iterators:
───────────────── ──────────────
- Lazy by default (generators) - Lazy by default (all iterator chains)
- yield creates generators - impl Iterator { fn next() }
- StopIteration to end - None to end
- Can be consumed once - Can be consumed once
- No type safety - Fully type-safe
- Slightly slower (interpreter) - Zero-cost (compiled away)
Why Macros Exist in Rust
Python has no macro system — it uses decorators, metaclasses, and runtime introspection for metaprogramming. Rust uses macros for compile-time code generation.
Python Metaprogramming vs Rust Macros
# Python — decorators and metaclasses for metaprogramming
from dataclasses import dataclass
from functools import wraps
@dataclass # Generates __init__, __repr__, __eq__ at import time
class Point:
x: float
y: float
# Custom decorator
def log_calls(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}")
return func(*args, **kwargs)
return wrapper
@log_calls
def process(data):
return data.upper()
#![allow(unused)]
fn main() {
// Rust — derive macros and declarative macros for code generation
#[derive(Debug, Clone, PartialEq)] // Generates Debug, Clone, PartialEq impls at COMPILE time
struct Point {
x: f64,
y: f64,
}
// Declarative macro (like a template)
macro_rules! log_call {
($func_name:expr, $body:expr) => {
println!("Calling {}", $func_name);
$body
};
}
fn process(data: &str) -> String {
log_call!("process", data.to_uppercase())
}
}
Common Built-in Macros
#![allow(unused)]
fn main() {
// These macros are used everywhere in Rust:
println!("Hello, {}!", name); // Print with formatting
format!("Value: {}", x); // Create formatted String
vec![1, 2, 3]; // Create a Vec
assert_eq!(2 + 2, 4); // Test assertion
assert!(value > 0, "must be positive"); // Boolean assertion
dbg!(expression); // Debug print: prints expression AND value
todo!(); // Placeholder — compiles but panics if reached
unimplemented!(); // Mark code as unimplemented
panic!("something went wrong"); // Crash with message (like raise RuntimeError)
// Why are these macros instead of functions?
// - println! accepts variable arguments (Rust functions can't)
// - vec! generates code for any type and size
// - assert_eq! knows the SOURCE CODE of what you compared
// - dbg! knows the FILE NAME and LINE NUMBER
}
Writing a Simple Macro with macro_rules!
#![allow(unused)]
fn main() {
// Python dict() equivalent
// Python: d = dict(a=1, b=2)
// Rust: let d = hashmap!{ "a" => 1, "b" => 2 };
macro_rules! hashmap {
($($key:expr => $value:expr),* $(,)?) => {
{
let mut map = std::collections::HashMap::new();
$(map.insert($key, $value);)*
map
}
};
}
let scores = hashmap! {
"Alice" => 100,
"Bob" => 85,
"Charlie" => 90,
};
}
Derive Macros — Auto-Implementing Traits
#![allow(unused)]
fn main() {
// #[derive(...)] is the Rust equivalent of Python's @dataclass decorator
// Python:
// @dataclass(frozen=True, order=True)
// class Student:
// name: str
// grade: int
// Rust:
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
struct Student {
name: String,
grade: i32,
}
// Common derive macros:
// Debug → {:?} formatting (like __repr__)
// Clone → .clone() deep copy
// Copy → implicit copy (only for simple types)
// PartialEq, Eq → == comparison (like __eq__)
// PartialOrd, Ord → <, >, sorting (like __lt__ etc.)
// Hash → usable as HashMap key (like __hash__)
// Default → MyType::default() (like __init__ with no args)
// Crate-provided derive macros:
// Serialize, Deserialize (serde) → JSON/YAML/TOML serialization
// (like Python's json.dumps/loads but type-safe)
}
Python Decorator vs Rust Derive
| Python Decorator | Rust Derive | Purpose |
|---|---|---|
@dataclass | #[derive(Debug, Clone, PartialEq)] | Data class |
@dataclass(frozen=True) | Immutable by default | Immutability |
@dataclass(order=True) | #[derive(Ord, PartialOrd)] | Comparison/sorting |
@total_ordering | #[derive(PartialOrd, Ord)] | Full ordering |
JSON json.dumps(obj.__dict__) | #[derive(Serialize)] | Serialization |
JSON MyClass(**json.loads(s)) | #[derive(Deserialize)] | Deserialization |
Exercises
🏋️ Exercise: Derive and Custom Debug (click to expand)
Challenge: Create a User struct with fields name: String, email: String, and password_hash: String. Derive Clone and PartialEq, but implement Debug manually so it prints the name and email but redacts the password (shows "***" instead).
🔑 Solution
use std::fmt;
#[derive(Clone, PartialEq)]
struct User {
name: String,
email: String,
password_hash: String,
}
impl fmt::Debug for User {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("User")
.field("name", &self.name)
.field("email", &self.email)
.field("password_hash", &"***")
.finish()
}
}
fn main() {
let user = User {
name: "Alice".into(),
email: "alice@example.com".into(),
password_hash: "a1b2c3d4e5f6".into(),
};
println!("{user:?}");
// Output: User { name: "Alice", email: "alice@example.com", password_hash: "***" }
}
Key takeaway: Unlike Python’s __repr__, Rust lets you derive Debug for free — but you can override it for sensitive fields. This is safer than Python where print(user) might accidentally leak secrets.
13. Concurrency / 13. 并发
No GIL: True Parallelism
What you’ll learn: Why the GIL limits Python concurrency, Rust’s
Send/Synctraits for compile-time thread safety,Arc<Mutex<T>>vs Pythonthreading.Lock, channels vsqueue.Queue, and async/await differences.Difficulty: 🔴 Advanced
The GIL (Global Interpreter Lock) is Python’s biggest limitation for CPU-bound work. Rust has no GIL — threads run truly in parallel, and the type system prevents data races at compile time.
gantt
title CPU-bound Work: Python GIL vs Rust Threads
dateFormat X
axisFormat %s
section Python (GIL)
Thread 1 :a1, 0, 4
Thread 2 :a2, 4, 8
Thread 3 :a3, 8, 12
Thread 4 :a4, 12, 16
section Rust (no GIL)
Thread 1 :b1, 0, 4
Thread 2 :b2, 0, 4
Thread 3 :b3, 0, 4
Thread 4 :b4, 0, 4
Key insight: Python threads run sequentially for CPU work (GIL serializes them). Rust threads run truly in parallel — 4 threads = ~4x speedup.
📌 Prerequisite: Make sure you’re comfortable with Ch. 7 — Ownership and Borrowing before tackling this chapter.
Arc,Mutex, and move closures all build on ownership concepts.
Python’s GIL Problem
# Python — threads don't help for CPU-bound work
import threading
import time
counter = 0
def increment(n):
global counter
for _ in range(n):
counter += 1 # NOT thread-safe! But GIL "protects" simple operations
threads = [threading.Thread(target=increment, args=(1_000_000,)) for _ in range(4)]
start = time.perf_counter()
for t in threads:
t.start()
for t in threads:
t.join()
elapsed = time.perf_counter() - start
print(f"Counter: {counter}") # Might not be 4,000,000!
print(f"Time: {elapsed:.2f}s") # About the SAME as single-threaded (GIL)
# For true parallelism, Python requires multiprocessing:
from multiprocessing import Pool
with Pool(4) as pool:
results = pool.map(cpu_work, data) # Separate processes, pickle overhead
Rust — True Parallelism, Compile-Time Safety
use std::sync::atomic::{AtomicI64, Ordering};
use std::sync::Arc;
use std::thread;
fn main() {
let counter = Arc::new(AtomicI64::new(0));
let handles: Vec<_> = (0..4).map(|_| {
let counter = Arc::clone(&counter);
thread::spawn(move || {
for _ in 0..1_000_000 {
counter.fetch_add(1, Ordering::Relaxed);
}
})
}).collect();
for h in handles {
h.join().unwrap();
}
println!("Counter: {}", counter.load(Ordering::Relaxed)); // Always 4,000,000
// Runs on ALL cores — true parallelism, no GIL
}
Thread Safety: Type System Guarantees
Python — Runtime Errors
# Python — data races caught at runtime (or not at all)
import threading
shared_list = []
def append_items(items):
for item in items:
shared_list.append(item) # "Thread-safe" due to GIL for append
# But complex operations are NOT safe:
# if item not in shared_list:
# shared_list.append(item) # RACE CONDITION!
# Using Lock for safety:
lock = threading.Lock()
def safe_append(items):
for item in items:
with lock:
if item not in shared_list:
shared_list.append(item)
# Forgetting the lock? No compiler warning. Bug discovered in production.
Rust — Compile-Time Errors
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
// Trying to share a Vec across threads without protection:
// let shared = vec![];
// thread::spawn(move || shared.push(1));
// ❌ Compile error: Vec is not Send/Sync without protection
// With Mutex (Rust's equivalent of threading.Lock):
let shared = Arc::new(Mutex::new(Vec::new()));
let handles: Vec<_> = (0..4).map(|i| {
let shared = Arc::clone(&shared);
thread::spawn(move || {
let mut data = shared.lock().unwrap(); // Lock is REQUIRED to access
data.push(i);
// Lock is automatically released when `data` goes out of scope
// No "forgetting to unlock" — RAII guarantees it
})
}).collect();
for h in handles {
h.join().unwrap();
}
println!("{:?}", shared.lock().unwrap()); // [0, 1, 2, 3] (order may vary)
}
Send and Sync Traits
#![allow(unused)]
fn main() {
// Rust uses two marker traits to enforce thread safety:
// Send — "this type can be transferred to another thread"
// Most types are Send. Rc<T> is NOT (use Arc<T> for threads).
// Sync — "this type can be referenced from multiple threads"
// Most types are Sync. Cell<T>/RefCell<T> are NOT (use Mutex<T>).
// The compiler checks these automatically:
// thread::spawn(move || { ... })
// ↑ The closure's captures must be Send
// ↑ Shared references must be Sync
// ↑ If they're not → compile error
// Python has no equivalent. Thread safety bugs are discovered at runtime.
// Rust catches them at compile time. This is "fearless concurrency."
}
Concurrency Primitives Comparison
| Python | Rust | Purpose |
|---|---|---|
threading.Lock() | Mutex<T> | Mutual exclusion |
threading.RLock() | Mutex<T> (no reentrant) | Reentrant lock (use differently) |
threading.RWLock (N/A) | RwLock<T> | Multiple readers OR one writer |
threading.Event() | Condvar | Condition variable |
queue.Queue() | mpsc::channel() | Thread-safe channel |
multiprocessing.Pool | rayon::ThreadPool | Thread pool |
concurrent.futures | rayon / tokio::spawn | Task-based parallelism |
threading.local() | thread_local! | Thread-local storage |
| N/A | Atomic* types | Lock-free counters and flags |
Mutex Poisoning
If a thread panics while holding a Mutex, the lock becomes poisoned. Python has no equivalent — if a thread crashes holding a threading.Lock(), the lock stays stuck.
#![allow(unused)]
fn main() {
use std::sync::{Arc, Mutex};
use std::thread;
let data = Arc::new(Mutex::new(vec![1, 2, 3]));
let data2 = Arc::clone(&data);
let _ = thread::spawn(move || {
let mut guard = data2.lock().unwrap();
guard.push(4);
panic!("oops!"); // Lock is now poisoned
}).join();
// Subsequent lock attempts return Err(PoisonError)
match data.lock() {
Ok(guard) => println!("Data: {guard:?}"),
Err(poisoned) => {
println!("Lock was poisoned! Recovering...");
let guard = poisoned.into_inner();
println!("Recovered: {guard:?}"); // [1, 2, 3, 4]
}
}
}
Atomic Ordering (brief note)
The Ordering parameter on atomic operations controls memory visibility guarantees:
| Ordering | When to use |
|---|---|
Relaxed | Simple counters where ordering doesn’t matter |
Acquire/Release | Producer-consumer: writer uses Release, reader uses Acquire |
SeqCst | When in doubt — strictest ordering, most intuitive |
Python’s threading module hides these details behind the GIL. In Rust, you choose explicitly — use SeqCst until profiling shows you need something weaker.
async/await Comparison
Python and Rust both have async/await syntax, but they work very differently
under the hood.
Python async/await
# Python — asyncio for concurrent I/O
import asyncio
import aiohttp
async def fetch_url(session, url):
async with session.get(url) as resp:
return await resp.text()
async def main():
urls = ["https://example.com", "https://httpbin.org/get"]
async with aiohttp.ClientSession() as session:
tasks = [fetch_url(session, url) for url in urls]
results = await asyncio.gather(*tasks)
for url, result in zip(urls, results):
print(f"{url}: {len(result)} bytes")
asyncio.run(main())
# Python async is single-threaded (still GIL)!
# It only helps with I/O-bound work (waiting for network/disk).
# CPU-bound work in async still blocks the event loop.
Rust async/await
// Rust — tokio for concurrent I/O (and CPU parallelism!)
use reqwest;
use tokio;
async fn fetch_url(url: &str) -> Result<String, reqwest::Error> {
reqwest::get(url).await?.text().await
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let urls = vec!["https://example.com", "https://httpbin.org/get"];
let tasks: Vec<_> = urls.iter()
.map(|url| tokio::spawn(fetch_url(url))) // No GIL limitation
.collect(); // Can use all CPU cores
let results = futures::future::join_all(tasks).await;
for (url, result) in urls.iter().zip(results) {
match result {
Ok(Ok(body)) => println!("{url}: {} bytes", body.len()),
Ok(Err(e)) => println!("{url}: error {e}"),
Err(e) => println!("{url}: task failed {e}"),
}
}
Ok(())
}
Key Differences
| Aspect | Python asyncio | Rust tokio |
|---|---|---|
| GIL | Still applies | No GIL |
| CPU parallelism | ❌ Single-threaded | ✅ Multi-threaded |
| Runtime | Built-in (asyncio) | External crate (tokio) |
| Ecosystem | aiohttp, asyncpg, etc. | reqwest, sqlx, etc. |
| Performance | Good for I/O | Excellent for I/O AND CPU |
| Error handling | Exceptions | Result<T, E> |
| Cancellation | task.cancel() | Drop the future |
| Color problem | Sync ↔ async boundary | Same issue exists |
Simple Parallelism with Rayon
# Python — multiprocessing for CPU parallelism
from multiprocessing import Pool
def process_item(item):
return heavy_computation(item)
with Pool(8) as pool:
results = pool.map(process_item, items)
#![allow(unused)]
fn main() {
// Rust — rayon for effortless CPU parallelism (one line change!)
use rayon::prelude::*;
// Sequential:
let results: Vec<_> = items.iter().map(|item| heavy_computation(item)).collect();
// Parallel (change .iter() to .par_iter() — that's it!):
let results: Vec<_> = items.par_iter().map(|item| heavy_computation(item)).collect();
// No pickle, no process overhead, no serialization.
// Rayon automatically distributes work across cores.
}
💼 Case Study: Parallel Image Processing Pipeline
A data science team processes 50,000 satellite images nightly. Their Python pipeline uses multiprocessing.Pool:
# Python — multiprocessing for CPU-bound image work
import multiprocessing
from PIL import Image
import numpy as np
def process_image(path: str) -> dict:
img = np.array(Image.open(path))
# CPU-intensive: histogram equalization, edge detection, classification
histogram = np.histogram(img, bins=256)[0]
edges = detect_edges(img) # ~200ms per image
label = classify(edges) # ~100ms per image
return {"path": path, "label": label, "edge_count": len(edges)}
# Problem: each subprocess copies the full Python interpreter
# Memory: 50MB per worker × 16 workers = 800MB overhead
# Startup: 2-3 seconds to fork and pickle arguments
with multiprocessing.Pool(16) as pool:
results = pool.map(process_image, image_paths) # ~4.5 hours for 50k images
Pain points: 800MB memory overhead from forking, pickle serialization of arguments/results, GIL prevents using threads, error handling is opaque (exceptions in workers are hard to debug).
use rayon::prelude::*;
use image::GenericImageView;
struct ImageResult {
path: String,
label: String,
edge_count: usize,
}
fn process_image(path: &str) -> Result<ImageResult, image::ImageError> {
let img = image::open(path)?;
let histogram = compute_histogram(&img); // ~50ms (no numpy overhead)
let edges = detect_edges(&img); // ~40ms (SIMD-optimized)
let label = classify(&edges); // ~20ms
Ok(ImageResult {
path: path.to_string(),
label,
edge_count: edges.len(),
})
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let paths: Vec<String> = load_image_paths()?;
// Rayon automatically uses all CPU cores — no forking, no pickle, no GIL
let results: Vec<ImageResult> = paths
.par_iter() // Parallel iterator
.filter_map(|p| process_image(p).ok()) // Skip errors gracefully
.collect(); // Collect in parallel
println!("Processed {} images", results.len());
Ok(())
}
// 50k images in ~35 minutes (vs 4.5 hours in Python)
// Memory: ~50MB total (shared threads, no forking)
Results:
| Metric | Python (multiprocessing) | Rust (rayon) |
|---|---|---|
| Time (50k images) | ~4.5 hours | ~35 minutes |
| Memory overhead | 800MB (16 workers) | ~50MB (shared) |
| Error handling | Opaque pickle errors | Result<T, E> at every step |
| Startup cost | 2–3s (fork + pickle) | None (threads) |
Key lesson: For CPU-bound parallel work, Rust’s threads + rayon replace Python’s
multiprocessingwith zero serialization overhead, shared memory, and compile-time safety.
Exercises
🏋️ Exercise: Thread-Safe Counter (click to expand)
Challenge: In Python, you might use threading.Lock to protect a shared counter. Translate this to Rust: spawn 10 threads, each incrementing a shared counter 1000 times. Print the final value (should be 10000). Use Arc<Mutex<u64>>.
🔑 Solution
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let counter = Arc::new(Mutex::new(0u64));
let mut handles = vec![];
for _ in 0..10 {
let counter = Arc::clone(&counter);
handles.push(thread::spawn(move || {
for _ in 0..1000 {
let mut num = counter.lock().unwrap();
*num += 1;
}
}));
}
for handle in handles {
handle.join().unwrap();
}
println!("Final count: {}", *counter.lock().unwrap());
}
Key takeaway: Arc<Mutex<T>> is Rust’s equivalent of Python’s lock = threading.Lock() + shared variable — but Rust won’t compile if you forget the Arc or Mutex. Python happily runs a racy program and gives you wrong answers silently.
14. Unsafe Rust and FFI / 14. Unsafe Rust 与 FFI
When and Why to Use Unsafe
What you’ll learn: What
unsafepermits and why it exists, writing Python extensions with PyO3 (the killer feature for Python devs), Rust’s testing framework vs pytest, mocking with mockall, and benchmarking.Difficulty: 🔴 Advanced
unsafe in Rust is an escape hatch — it tells the compiler “I’m doing something
you can’t verify, but I promise it’s correct.” Python has no equivalent because
Python never gives you direct memory access.
flowchart TB
subgraph Safe ["Safe Rust (99% of code)"]
S1["Your application logic"]
S2["pub fn safe_api\(&self\) -> Result"]
end
subgraph Unsafe ["unsafe block (minimal, audited)"]
U1["Raw pointer dereference"]
U2["FFI call to C/Python"]
end
subgraph External ["External (C / Python / OS)"]
E1["libc / PyO3 / system calls"]
end
S1 --> S2
S2 --> U1
S2 --> U2
U1 --> E1
U2 --> E1
style Safe fill:#d4edda,stroke:#28a745
style Unsafe fill:#fff3cd,stroke:#ffc107
style External fill:#f8d7da,stroke:#dc3545
The pattern: Safe API wraps a small
unsafeblock. Callers never seeunsafe. Python’sctypeshas no such boundary — every FFI call is implicitly unsafe.📌 See also: Ch. 13 — Concurrency covers
Send/Synctraits which areunsafeauto-traits that the compiler checks for thread safety.
What unsafe Allows
// unsafe lets you do FIVE things that safe Rust forbids:
// 1. Dereference raw pointers
// 2. Call unsafe functions/methods
// 3. Access mutable static variables
// 4. Implement unsafe traits
// 5. Access union fields
// Example: calling a C function
extern "C" {
fn abs(input: i32) -> i32;
}
fn main() {
// SAFETY: abs() is a well-defined C standard library function.
let result = unsafe { abs(-42) }; // Safe Rust can't verify C code
println!("{result}"); // 42
}
When to Use unsafe
#![allow(unused)]
fn main() {
// 1. FFI — calling C libraries (most common reason)
// 2. Performance-critical inner loops (rare)
// 3. Data structures the borrow checker can't express (rare)
// As a Python developer, you'll mostly encounter unsafe in:
// - PyO3 internals (Python ↔ Rust bridge)
// - C library bindings
// - Low-level system calls
// Rule of thumb: if you're writing application code (not library code),
// you should almost never need unsafe. If you think you do, ask in the
// Rust community first — there's usually a safe alternative.
}
PyO3: Rust Extensions for Python
PyO3 is the bridge between Python and Rust. It lets you write Rust functions and classes that are callable from Python — perfect for replacing slow Python hotspots.
Creating a Python Extension in Rust
# Setup
pip install maturin # Build tool for Rust Python extensions
maturin init # Creates project structure
# Project structure:
# my_extension/
# ├── Cargo.toml
# ├── pyproject.toml
# └── src/
# └── lib.rs
# Cargo.toml
[package]
name = "my_extension"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"] # Shared library for Python
[dependencies]
pyo3 = { version = "0.22", features = ["extension-module"] }
#![allow(unused)]
fn main() {
// src/lib.rs — Rust functions callable from Python
use pyo3::prelude::*;
/// A fast Fibonacci function written in Rust.
#[pyfunction]
fn fibonacci(n: u64) -> u64 {
let (mut a, mut b) = (0u64, 1u64);
for _ in 0..n {
let temp = b;
b = a.wrapping_add(b);
a = temp;
}
a
}
/// Find all prime numbers up to n (Sieve of Eratosthenes).
#[pyfunction]
fn primes_up_to(n: usize) -> Vec<usize> {
let mut is_prime = vec![true; n + 1];
is_prime[0] = false;
if n > 0 { is_prime[1] = false; }
for i in 2..=((n as f64).sqrt() as usize) {
if is_prime[i] {
for j in (i * i..=n).step_by(i) {
is_prime[j] = false;
}
}
}
(2..=n).filter(|&i| is_prime[i]).collect()
}
/// A Rust class usable from Python.
#[pyclass]
struct Counter {
value: i64,
}
#[pymethods]
impl Counter {
#[new]
fn new(start: i64) -> Self {
Counter { value: start }
}
fn increment(&mut self) {
self.value += 1;
}
fn get_value(&self) -> i64 {
self.value
}
fn __repr__(&self) -> String {
format!("Counter(value={})", self.value)
}
}
/// The Python module definition.
#[pymodule]
fn my_extension(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_function(wrap_pyfunction!(fibonacci, m)?)?;
m.add_function(wrap_pyfunction!(primes_up_to, m)?)?;
m.add_class::<Counter>()?;
Ok(())
}
}
Using from Python
# Build and install:
maturin develop --release # Builds and installs into current venv
# Python — use the Rust extension like any Python module
import my_extension
# Call Rust function
result = my_extension.fibonacci(50)
print(result) # 12586269025 — computed in microseconds
# Use Rust class
counter = my_extension.Counter(0)
counter.increment()
counter.increment()
print(counter.get_value()) # 2
print(counter) # Counter(value=2)
# Performance comparison:
import time
# Python version
def py_primes(n):
sieve = [True] * (n + 1)
for i in range(2, int(n**0.5) + 1):
if sieve[i]:
for j in range(i*i, n+1, i):
sieve[j] = False
return [i for i in range(2, n+1) if sieve[i]]
start = time.perf_counter()
py_result = py_primes(10_000_000)
py_time = time.perf_counter() - start
start = time.perf_counter()
rs_result = my_extension.primes_up_to(10_000_000)
rs_time = time.perf_counter() - start
print(f"Python: {py_time:.3f}s") # ~3.5s
print(f"Rust: {rs_time:.3f}s") # ~0.05s — 70x faster!
print(f"Same results: {py_result == rs_result}") # True
PyO3 Quick Reference
| Python Concept | PyO3 Attribute | Notes |
|---|---|---|
| Function | #[pyfunction] | Exposed to Python |
| Class | #[pyclass] | Python-visible class |
| Method | #[pymethods] | Methods on a pyclass |
__init__ | #[new] | Constructor |
__repr__ | fn __repr__() | String representation |
__str__ | fn __str__() | Display string |
__len__ | fn __len__() | Length |
__getitem__ | fn __getitem__() | Indexing |
| Property | #[getter] / #[setter] | Attribute access |
| Static method | #[staticmethod] | No self |
| Class method | #[classmethod] | Takes cls |
FFI Safety Patterns
When exposing Rust to Python (via PyO3 or raw C FFI), these rules prevent the most common bugs:
-
Never let a panic cross the FFI boundary — a Rust panic unwinding into Python (or C) is undefined behavior. PyO3 handles this automatically for
#[pyfunction], but rawextern "C"functions need explicit protection:#![allow(unused)] fn main() { #[no_mangle] pub extern "C" fn raw_ffi_function() -> i32 { match std::panic::catch_unwind(|| { // actual logic 42 }) { Ok(result) => result, Err(_) => -1, // Return error code instead of panicking into C/Python } } } -
#[repr(C)]for shared structs — if Python/C reads struct fields directly, you must use#[repr(C)]to guarantee C-compatible layout. If you’re passing opaque pointers (which PyO3 does for#[pyclass]), it’s not needed. -
extern "C"— required for raw FFI functions so the calling convention matches what C/Python expects. PyO3’s#[pyfunction]handles this for you.
PyO3 advantage: PyO3 wraps most of these safety concerns for you — panic catching, type conversion, GIL management. Prefer PyO3 over raw FFI unless you have a specific reason not to.
Unit Tests vs pytest
Python Testing with pytest
# test_calculator.py
import pytest
from calculator import add, divide
def test_add():
assert add(2, 3) == 5
def test_add_negative():
assert add(-1, 1) == 0
def test_divide():
assert divide(10, 2) == 5.0
def test_divide_by_zero():
with pytest.raises(ZeroDivisionError):
divide(1, 0)
# Parameterized tests
@pytest.mark.parametrize("a,b,expected", [
(1, 2, 3),
(0, 0, 0),
(-1, -1, -2),
(100, 200, 300),
])
def test_add_parametrized(a, b, expected):
assert add(a, b) == expected
# Fixtures
@pytest.fixture
def sample_data():
return [1, 2, 3, 4, 5]
def test_sum(sample_data):
assert sum(sample_data) == 15
# Running tests
pytest # Run all tests
pytest test_calculator.py # Run one file
pytest -k "test_add" # Run matching tests
pytest -v # Verbose output
pytest --tb=short # Short tracebacks
Rust Built-in Testing
#![allow(unused)]
fn main() {
// src/calculator.rs — tests live in the SAME file!
fn add(a: i32, b: i32) -> i32 {
a + b
}
fn divide(a: f64, b: f64) -> Result<f64, String> {
if b == 0.0 {
Err("Division by zero".to_string())
} else {
Ok(a / b)
}
}
// Tests go in a #[cfg(test)] module — only compiled during `cargo test`
#[cfg(test)]
mod tests {
use super::*; // Import everything from parent module
#[test]
fn test_add() {
assert_eq!(add(2, 3), 5);
}
#[test]
fn test_add_negative() {
assert_eq!(add(-1, 1), 0);
}
#[test]
fn test_divide() {
assert_eq!(divide(10.0, 2.0), Ok(5.0));
}
#[test]
fn test_divide_by_zero() {
assert!(divide(1.0, 0.0).is_err());
}
// Test that something panics (like pytest.raises)
#[test]
#[should_panic(expected = "out of bounds")]
fn test_out_of_bounds() {
let v = vec![1, 2, 3];
let _ = v[99]; // Panics
}
}
}
# Running tests
cargo test # Run all tests
cargo test test_add # Run matching tests
cargo test -- --nocapture # Show println! output
cargo test -p my_crate # Test one crate in workspace
cargo test -- --test-threads=1 # Sequential (for tests with side effects)
Testing Quick Reference
| pytest | Rust | Notes |
|---|---|---|
assert x == y | assert_eq!(x, y) | Equality |
assert x != y | assert_ne!(x, y) | Inequality |
assert condition | assert!(condition) | Boolean |
assert condition, "msg" | assert!(condition, "msg") | With message |
pytest.raises(E) | #[should_panic] | Expect panic |
@pytest.fixture | Setup in test or helper fn | No built-in fixtures |
@pytest.mark.parametrize | rstest crate | Parameterized tests |
conftest.py | tests/common/mod.rs | Shared test helpers |
pytest.skip() | #[ignore] | Skip a test |
tmp_path fixture | tempfile crate | Temporary directories |
Parameterized Tests with rstest
#![allow(unused)]
fn main() {
// Cargo.toml: rstest = "0.23"
use rstest::rstest;
// Like @pytest.mark.parametrize
#[rstest]
#[case(1, 2, 3)]
#[case(0, 0, 0)]
#[case(-1, -1, -2)]
#[case(100, 200, 300)]
fn test_add(#[case] a: i32, #[case] b: i32, #[case] expected: i32) {
assert_eq!(add(a, b), expected);
}
// Like @pytest.fixture
use rstest::fixture;
#[fixture]
fn sample_data() -> Vec<i32> {
vec![1, 2, 3, 4, 5]
}
#[rstest]
fn test_sum(sample_data: Vec<i32>) {
assert_eq!(sample_data.iter().sum::<i32>(), 15);
}
}
Mocking with mockall
# Python — mocking with unittest.mock
from unittest.mock import Mock, patch
def test_fetch_user():
mock_db = Mock()
mock_db.get_user.return_value = {"name": "Alice"}
result = fetch_user_name(mock_db, 1)
assert result == "Alice"
mock_db.get_user.assert_called_once_with(1)
#![allow(unused)]
fn main() {
// Rust — mocking with mockall crate
// Cargo.toml: mockall = "0.13"
use mockall::{automock, predicate::*};
#[automock] // Generates MockDatabase automatically
trait Database {
fn get_user(&self, id: i64) -> Option<User>;
}
fn fetch_user_name(db: &dyn Database, id: i64) -> Option<String> {
db.get_user(id).map(|u| u.name)
}
#[test]
fn test_fetch_user() {
let mut mock = MockDatabase::new();
mock.expect_get_user()
.with(eq(1)) // assert_called_with(1)
.times(1) // assert_called_once
.returning(|_| Some(User { name: "Alice".into() }));
let result = fetch_user_name(&mock, 1);
assert_eq!(result, Some("Alice".to_string()));
}
}
Exercises
🏋️ Exercise: Safe Wrapper Around Unsafe (click to expand)
Challenge: Write a safe function split_at_mid that takes a &mut [i32] and returns two mutable slices (&mut [i32], &mut [i32]) split at the midpoint. Internally, use unsafe with raw pointers (simulating what split_at_mut does). Then wrap it in a safe API.
🔑 Solution
fn split_at_mid(slice: &mut [i32]) -> (&mut [i32], &mut [i32]) {
let mid = slice.len() / 2;
let ptr = slice.as_mut_ptr();
let len = slice.len();
assert!(mid <= len); // Safety check before unsafe
// SAFETY: mid <= len (asserted above), and ptr comes from a valid &mut slice,
// so both sub-slices are within bounds and non-overlapping.
unsafe {
(
std::slice::from_raw_parts_mut(ptr, mid),
std::slice::from_raw_parts_mut(ptr.add(mid), len - mid),
)
}
}
fn main() {
let mut data = vec![1, 2, 3, 4, 5, 6];
let (left, right) = split_at_mid(&mut data);
left[0] = 99;
right[0] = 88;
println!("left: {left:?}, right: {right:?}");
// left: [99, 2, 3], right: [88, 5, 6]
}
Key takeaway: The unsafe block is small and guarded by the assert!. The public API is fully safe — callers never see unsafe. This is the Rust pattern: unsafe internals, safe interfaces. Python’s ctypes gives you no such guarantees.
15. Migration Patterns / 15. 迁移模式
Common Python Patterns in Rust
What you’ll learn: How to translate dict→struct, class→struct+impl, list comprehension→iterator chain, decorator→trait, and context manager→Drop/RAII. Plus essential crates and an incremental adoption strategy.
Difficulty: 🟡 Intermediate
Dictionary → Struct
# Python — dict as data container (very common)
user = {
"name": "Alice",
"age": 30,
"email": "alice@example.com",
"active": True,
}
print(user["name"])
#![allow(unused)]
fn main() {
// Rust — struct with named fields
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
struct User {
name: String,
age: i32,
email: String,
active: bool,
}
let user = User {
name: "Alice".into(),
age: 30,
email: "alice@example.com".into(),
active: true,
};
println!("{}", user.name);
}
Context Manager → RAII (Drop)
# Python — context manager for resource cleanup
class FileManager:
def __init__(self, path):
self.file = open(path, 'w')
def __enter__(self):
return self.file
def __exit__(self, *args):
self.file.close()
with FileManager("output.txt") as f:
f.write("hello")
# File automatically closed when exiting `with`
#![allow(unused)]
fn main() {
// Rust — RAII: Drop trait runs when value goes out of scope
use std::fs::File;
use std::io::Write;
fn write_file() -> std::io::Result<()> {
let mut file = File::create("output.txt")?;
file.write_all(b"hello")?;
Ok(())
// File automatically closed when `file` goes out of scope
// No `with` needed — RAII handles it!
}
}
Decorator → Higher-Order Function or Macro
# Python — decorator for timing
import functools, time
def timed(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
elapsed = time.perf_counter() - start
print(f"{func.__name__} took {elapsed:.4f}s")
return result
return wrapper
@timed
def slow_function():
time.sleep(1)
#![allow(unused)]
fn main() {
// Rust — no decorators, use wrapper functions or macros
use std::time::Instant;
fn timed<F, R>(name: &str, f: F) -> R
where
F: FnOnce() -> R,
{
let start = Instant::now();
let result = f();
println!("{} took {:.4?}", name, start.elapsed());
result
}
// Usage:
let result = timed("slow_function", || {
std::thread::sleep(std::time::Duration::from_secs(1));
42
});
}
Iterator Pipeline (Data Processing)
# Python — chain of transformations
import csv
from collections import Counter
def analyze_sales(filename):
with open(filename) as f:
reader = csv.DictReader(f)
sales = [
row for row in reader
if float(row["amount"]) > 100
]
by_region = Counter(sale["region"] for sale in sales)
top_regions = by_region.most_common(5)
return top_regions
#![allow(unused)]
fn main() {
// Rust — iterator chains with strong types
use std::collections::HashMap;
#[derive(Debug, serde::Deserialize)]
struct Sale {
region: String,
amount: f64,
}
fn analyze_sales(filename: &str) -> Vec<(String, usize)> {
let data = std::fs::read_to_string(filename).unwrap();
let mut reader = csv::Reader::from_reader(data.as_bytes());
let mut by_region: HashMap<String, usize> = HashMap::new();
for sale in reader.deserialize::<Sale>().flatten() {
if sale.amount > 100.0 {
*by_region.entry(sale.region).or_insert(0) += 1;
}
}
let mut top: Vec<_> = by_region.into_iter().collect();
top.sort_by(|a, b| b.1.cmp(&a.1));
top.truncate(5);
top
}
}
Global Config / Singleton
# Python — module-level singleton (common pattern)
# config.py
import json
class Config:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
with open("config.json") as f:
cls._instance.data = json.load(f)
return cls._instance
config = Config() # Module-level singleton
#![allow(unused)]
fn main() {
// Rust — OnceLock for lazy static initialization (Rust 1.70+)
use std::sync::OnceLock;
use serde_json::Value;
static CONFIG: OnceLock<Value> = OnceLock::new();
fn get_config() -> &'static Value {
CONFIG.get_or_init(|| {
let data = std::fs::read_to_string("config.json")
.expect("Failed to read config");
serde_json::from_str(&data)
.expect("Failed to parse config")
})
}
// Usage anywhere:
let db_host = get_config()["database"]["host"].as_str().unwrap();
}
Essential Crates for Python Developers
Data Processing & Serialization
| Task | Python | Rust Crate | Notes |
|---|---|---|---|
| JSON | json | serde_json | Type-safe serialization |
| CSV | csv, pandas | csv | Streaming, low memory |
| YAML | pyyaml | serde_yaml | Config files |
| TOML | tomllib | toml | Config files |
| Data validation | pydantic | serde + custom | Compile-time validation |
| Date/time | datetime | chrono | Full timezone support |
| Regex | re | regex | Very fast |
| UUID | uuid | uuid | Same concept |
Web & Network
| Task | Python | Rust Crate | Notes |
|---|---|---|---|
| HTTP client | requests | reqwest | Async-first |
| Web framework | FastAPI/Flask | axum / actix-web | Very fast |
| WebSocket | websockets | tokio-tungstenite | Async |
| gRPC | grpcio | tonic | Full support |
| Database (SQL) | sqlalchemy | sqlx / diesel | Compile-time checked SQL |
| Redis | redis-py | redis | Async support |
CLI & System
| Task | Python | Rust Crate | Notes |
|---|---|---|---|
| CLI args | argparse/click | clap | Derive macros |
| Colored output | colorama | colored | Terminal colors |
| Progress bar | tqdm | indicatif | Same UX |
| File watching | watchdog | notify | Cross-platform |
| Logging | logging | tracing | Structured, async-ready |
| Env vars | os.environ | std::env + dotenvy | .env support |
| Subprocess | subprocess | std::process::Command | Built-in |
| Temp files | tempfile | tempfile | Same name! |
Testing
| Task | Python | Rust Crate | Notes |
|---|---|---|---|
| Test framework | pytest | Built-in + rstest | cargo test |
| Mocking | unittest.mock | mockall | Trait-based |
| Property testing | hypothesis | proptest | Similar API |
| Snapshot testing | syrupy | insta | Snapshot approval |
| Benchmarking | pytest-benchmark | criterion | Statistical |
| Code coverage | coverage.py | cargo-tarpaulin | LLVM-based |
Incremental Adoption Strategy
flowchart LR
A["1️⃣ Profile Python\n(find hotspots)"] --> B["2️⃣ Write Rust Extension\n(PyO3 + maturin)"]
B --> C["3️⃣ Replace Python Call\n(same API)"]
C --> D["4️⃣ Expand Gradually\n(more functions)"]
D --> E{"Full rewrite\nworth it?"}
E -->|Yes| F["Pure Rust🦀"]
E -->|No| G["Hybrid🐍+🦀"]
style A fill:#ffeeba
style B fill:#fff3cd
style C fill:#d4edda
style D fill:#d4edda
style F fill:#c3e6cb
style G fill:#c3e6cb
📌 See also: Ch. 14 — Unsafe Rust and FFI covers the low-level FFI details needed for PyO3 bindings.
Step 1: Identify Hotspots
# Profile your Python code first
import cProfile
cProfile.run('main()') # Find the CPU-intensive functions
# Or use py-spy for sampling profiler:
# py-spy top --pid <python-pid>
# py-spy record -o profile.svg -- python main.py
Step 2: Write Rust Extension for Hotspot
# Create a Rust extension with maturin
cd my_python_project
maturin init --bindings pyo3
# Write the hot function in Rust (see PyO3 section above)
# Build and install:
maturin develop --release
Step 3: Replace Python Call with Rust Call
# Before:
result = python_hot_function(data) # Slow
# After:
import my_rust_extension
result = my_rust_extension.hot_function(data) # Fast!
# Same API, same tests, 10-100x faster
Step 4: Expand Gradually
#![allow(unused)]
fn main() {
Week 1-2: Replace one CPU-bound function with Rust
Week 3-4: Replace data parsing/validation layer
Month 2: Replace core data pipeline
Month 3+: Consider full Rust rewrite if benefits justify it
Key principle: keep Python for orchestration, use Rust for computation.
}
💼 Case Study: Accelerating a Data Pipeline with PyO3
A fintech startup has a Python data pipeline that processes 2GB of daily transaction CSV files. The critical bottleneck is a validation + transformation step:
# Python — the slow part (~12 minutes for 2GB)
import csv
from decimal import Decimal
from datetime import datetime
def validate_and_transform(filepath: str) -> list[dict]:
results = []
with open(filepath) as f:
reader = csv.DictReader(f)
for row in reader:
# Parse and validate each field
amount = Decimal(row["amount"])
if amount < 0:
raise ValueError(f"Negative amount: {amount}")
date = datetime.strptime(row["date"], "%Y-%m-%d")
category = categorize(row["merchant"]) # String matching, ~50 rules
results.append({
"amount_cents": int(amount * 100),
"date": date.isoformat(),
"category": category,
"merchant": row["merchant"].strip().lower(),
})
return results
# ~12 minutes for 15M rows. Tried pandas — got to ~8 minutes but 6GB RAM.
Step 1: Profile and identify the hotspot (CSV parsing + Decimal conversion + string matching = 95% of time).
Step 2: Write the Rust extension:
#![allow(unused)]
fn main() {
// src/lib.rs — PyO3 extension
use pyo3::prelude::*;
use pyo3::types::PyList;
use std::fs::File;
use std::io::BufReader;
#[derive(Debug)]
struct Transaction {
amount_cents: i64,
date: String,
category: String,
merchant: String,
}
fn categorize(merchant: &str) -> &'static str {
// Aho-Corasick or simple rules — compiled once, blazing fast
if merchant.contains("amazon") { "shopping" }
else if merchant.contains("uber") || merchant.contains("lyft") { "transport" }
else if merchant.contains("starbucks") { "food" }
else { "other" }
}
#[pyfunction]
fn process_transactions(path: &str) -> PyResult<Vec<(i64, String, String, String)>> {
let file = File::open(path).map_err(|e| pyo3::exceptions::PyIOError::new_err(e.to_string()))?;
let mut reader = csv::Reader::from_reader(BufReader::new(file));
let mut results = Vec::with_capacity(15_000_000); // Pre-allocate
for record in reader.records() {
let record = record.map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?;
let amount_str = &record[0];
let amount_cents = parse_amount_cents(amount_str)?; // Custom parser, no Decimal
let date = &record[1]; // Already in ISO format, just validate
let merchant = record[2].trim().to_lowercase();
let category = categorize(&merchant).to_string();
results.push((amount_cents, date.to_string(), category, merchant));
}
Ok(results)
}
#[pymodule]
fn fast_pipeline(_py: Python, m: &PyModule) -> PyResult<()> {
m.add_function(wrap_pyfunction!(process_transactions, m)?)?;
Ok(())
}
}
Step 3: Replace one line in Python:
# Before:
results = validate_and_transform("transactions.csv") # 12 minutes
# After:
import fast_pipeline
results = fast_pipeline.process_transactions("transactions.csv") # 45 seconds
# Same Python orchestration, same tests, same deployment
# Just one function replaced
Results:
| Metric | Python (csv + Decimal) | Rust (PyO3 + csv crate) |
|---|---|---|
| Time (2GB / 15M rows) | 12 minutes | 45 seconds |
| Peak memory | 6GB (pandas) / 2GB (csv) | 200MB |
| Lines changed in Python | — | 1 (import + call) |
| Rust code written | — | ~60 lines |
| Tests passing | 47/47 | 47/47 (unchanged) |
Key lesson: You don’t need to rewrite your whole application. Find the 5% of code that takes 95% of the time, rewrite that in Rust with PyO3, and keep everything else in Python. The team went from “we need to add more servers” to “one server is enough.”
Exercises
🏋️ Exercise: Migration Decision Matrix (click to expand)
Challenge: You have a Python web application with these components. For each one, decide: Keep in Python, Rewrite in Rust, or PyO3 bridge. Justify each choice.
- Flask route handlers (request parsing, JSON responses)
- Image thumbnail generation (CPU-bound, processes 10k images/day)
- Database ORM queries (SQLAlchemy)
- CSV parser for 2GB financial files (runs nightly)
- Admin dashboard (Jinja2 templates)
🔑 Solution
| Component | Decision | Rationale |
|---|---|---|
| Flask route handlers | 🐍 Keep Python | I/O-bound, framework-heavy, low benefit from Rust |
| Image thumbnail generation | 🦀 PyO3 bridge | CPU-bound hot path, keep Python API, Rust internals |
| Database ORM queries | 🐍 Keep Python | SQLAlchemy is mature, queries are I/O-bound |
| CSV parser (2GB) | 🦀 PyO3 bridge or full Rust | CPU + memory bound, Rust’s zero-copy parsing shines |
| Admin dashboard | 🐍 Keep Python | UI/template code, no performance concern |
Key takeaway: The migration sweet spot is CPU-bound, performance-critical code that has a clean boundary. Don’t rewrite glue code or I/O-bound handlers — the gains don’t justify the cost.
16. Best Practices / 16. 最佳实践
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
-
Use
matchon enums instead ofif isinstance()# Python # Rust if isinstance(shape, Circle): ... match shape { Shape::Circle(r) => ... } -
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.
-
Prefer
&stroverStringin function parameters — Accept the most general type.&strworks with bothStringand string literals. -
Use iterators instead of index loops — Iterator chains are more idiomatic and often faster than
for i in 0..vec.len(). -
Embrace
OptionandResult— Don’t.unwrap()everything. Use?,map,and_then,unwrap_or_else. -
Derive traits liberally —
#[derive(Debug, Clone, PartialEq)]should be on most structs. It’s free and makes testing easier. -
Use
cargo clippyreligiously — It catches hundreds of style and correctness issues. Treat it likerufffor Rust. -
Don’t fight the borrow checker — If you’re fighting it, you’re probably structuring data wrong. Refactor to make ownership clear.
-
Use enums for state machines — Instead of string flags or booleans, use enums. The compiler ensures you handle every state.
-
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
| Mistake | Why | Fix |
|---|---|---|
.unwrap() everywhere | Panics at runtime | Use ? or match |
| String instead of &str | Unnecessary allocation | Use &str for params |
for i in 0..vec.len() | Not idiomatic | for item in &vec |
| Ignoring clippy warnings | Miss easy improvements | cargo clippy |
Too many .clone() calls | Performance overhead | Refactor ownership |
| Giant main() function | Hard to test | Extract into lib.rs |
Not using #[derive()] | Re-inventing the wheel | Derive common traits |
| Panicking on errors | Not recoverable | Return 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”
#![allow(unused)]
fn main() {
// 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”
#![allow(unused)]
fn main() {
// 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”
#![allow(unused)]
fn main() {
// 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?”
#![allow(unused)]
fn main() {
// 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>andResult<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
clapandserde - 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
axumandtokio - Contribute to an open-source Rust project
- Read “Programming Rust” (O’Reilly) for deeper understanding
Recommended Resources
- The Rust Book: https://doc.rust-lang.org/book/ (official, excellent)
- Rust by Example: https://doc.rust-lang.org/rust-by-example/ (learn by doing)
- Rustlings: https://github.com/rust-lang/rustlings (exercises)
- Rust Playground: https://play.rust-lang.org/ (online compiler)
- This Week in Rust: https://this-week-in-rust.org/ (newsletter)
- PyO3 Guide: https://pyo3.rs/ (Python ↔ Rust bridge)
- Comprehensive Rust (Google): https://google.github.io/comprehensive-rust/
Python → Rust Rosetta Stone
| Python | Rust | Chapter |
|---|---|---|
list | Vec<T> | 5 |
dict | HashMap<K,V> | 5 |
set | HashSet<T> | 5 |
tuple | (T1, T2, ...) | 5 |
class | struct + impl | 5 |
@dataclass | #[derive(...)] | 5, 12a |
Enum | enum | 6 |
None | Option<T> | 6 |
raise/try/except | Result<T,E> + ? | 9 |
Protocol (PEP 544) | trait | 10 |
TypeVar | Generics <T> | 10 |
__dunder__ methods | Traits (Display, Add, etc.) | 10 |
lambda | |args| body | 12 |
generator yield | impl Iterator | 12 |
| list comprehension | .map().filter().collect() | 12 |
@decorator | Higher-order fn or macro | 12a, 15 |
asyncio | tokio | 13 |
threading | std::thread | 13 |
multiprocessing | rayon | 13 |
unittest.mock | mockall | 14a |
pytest | cargo test + rstest | 14a |
pip install | cargo add | 8 |
requirements.txt | Cargo.lock | 8 |
pyproject.toml | Cargo.toml | 8 |
with (context mgr) | Scope-based Drop | 15 |
json.dumps/loads | serde_json | 15 |
Final Thoughts for Python Developers
#![allow(unused)]
fn main() {
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
🏋️ Exercise: Code Review Checklist (click to expand)
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);
}
🔑 Solution
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.
End of Rust for Python Programmers Training Guide
17. Capstone Project: CLI Task Manager / 17. 综合项目:命令行任务管理器
Capstone Project: Build a CLI Task Manager
What you’ll learn: Tie together everything from the course by building a complete Rust CLI application that a Python developer would typically write with
argparse+json+pathlib.Difficulty: 🔴 Advanced
This capstone project exercises concepts from every major chapter:
- Ch. 3: Types and variables (structs, enums)
- Ch. 5: Collections (
Vec,HashMap) - Ch. 6: Enums and pattern matching (task status, commands)
- Ch. 7: Ownership and borrowing (passing references)
- Ch. 9: Error handling (
Result,?, custom errors) - Ch. 10: Traits (
Display,FromStr) - Ch. 11: Type conversions (
From,TryFrom) - Ch. 12: Iterators and closures (filtering, mapping)
- Ch. 8: Modules (organized project structure)
The Project: rustdo
A command-line task manager (like Python’s todo.txt tools) that stores tasks in a JSON file.
Python Equivalent (what you’d write in Python)
#!/usr/bin/env python3
"""A simple CLI task manager — the Python version."""
import json
import sys
from pathlib import Path
from datetime import datetime
from enum import Enum
TASK_FILE = Path.home() / ".rustdo.json"
class Priority(Enum):
LOW = "low"
MEDIUM = "medium"
HIGH = "high"
class Task:
def __init__(self, id: int, title: str, priority: Priority, done: bool = False):
self.id = id
self.title = title
self.priority = priority
self.done = done
self.created = datetime.now().isoformat()
def load_tasks() -> list[Task]:
if not TASK_FILE.exists():
return []
data = json.loads(TASK_FILE.read_text())
return [Task(**t) for t in data]
def save_tasks(tasks: list[Task]):
TASK_FILE.write_text(json.dumps([t.__dict__ for t in tasks], indent=2))
# Commands: add, list, done, remove, stats
# ... (you know how this goes in Python)
Your Rust Implementation
Build this step-by-step. Each step maps to concepts from specific chapters.
Step 1: Define the Data Model (Ch. 3, 6, 10, 11)
#![allow(unused)]
fn main() {
// src/task.rs
use std::fmt;
use std::str::FromStr;
use serde::{Deserialize, Serialize};
use chrono::Local;
/// Task priority — maps to Python's Priority(Enum)
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Priority {
Low,
Medium,
High,
}
// Display trait (Python's __str__)
impl fmt::Display for Priority {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Priority::Low => write!(f, "low"),
Priority::Medium => write!(f, "medium"),
Priority::High => write!(f, "high"),
}
}
}
// FromStr trait (parsing "high" → Priority::High)
impl FromStr for Priority {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"low" | "l" => Ok(Priority::Low),
"medium" | "med" | "m" => Ok(Priority::Medium),
"high" | "h" => Ok(Priority::High),
other => Err(format!("unknown priority: '{other}' (use low/medium/high)")),
}
}
}
/// A single task — maps to Python's Task class
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Task {
pub id: u32,
pub title: String,
pub priority: Priority,
pub done: bool,
pub created: String,
}
impl Task {
pub fn new(id: u32, title: String, priority: Priority) -> Self {
Self {
id,
title,
priority,
done: false,
created: Local::now().format("%Y-%m-%dT%H:%M:%S").to_string(),
}
}
}
impl fmt::Display for Task {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let status = if self.done { "✅" } else { "⬜" };
let priority_icon = match self.priority {
Priority::Low => "🟢",
Priority::Medium => "🟡",
Priority::High => "🔴",
};
write!(f, "{} {} [{}] {} ({})", status, self.id, priority_icon, self.title, self.created)
}
}
}
Python comparison: In Python you’d use
@dataclass+Enum. In Rust,struct+enum+derivemacros give you serialization, display, and parsing for free.
Step 2: Storage Layer (Ch. 9, 7)
#![allow(unused)]
fn main() {
// src/storage.rs
use std::fs;
use std::path::PathBuf;
use crate::task::Task;
/// Get the path to the task file (~/.rustdo.json)
fn task_file_path() -> PathBuf {
let home = dirs::home_dir().expect("Could not determine home directory");
home.join(".rustdo.json")
}
/// Load tasks from disk — returns empty Vec if file doesn't exist
pub fn load_tasks() -> Result<Vec<Task>, Box<dyn std::error::Error>> {
let path = task_file_path();
if !path.exists() {
return Ok(Vec::new());
}
let content = fs::read_to_string(&path)?; // ? propagates io::Error
let tasks: Vec<Task> = serde_json::from_str(&content)?; // ? propagates serde error
Ok(tasks)
}
/// Save tasks to disk
pub fn save_tasks(tasks: &[Task]) -> Result<(), Box<dyn std::error::Error>> {
let path = task_file_path();
let json = serde_json::to_string_pretty(tasks)?;
fs::write(&path, json)?;
Ok(())
}
}
Python comparison: Python uses
Path.read_text()+json.loads(). Rust usesfs::read_to_string()+serde_json::from_str(). Note the?— every error is explicit and propagated.
Step 3: Command Enum (Ch. 6)
#![allow(unused)]
fn main() {
// src/command.rs
use crate::task::Priority;
/// All possible commands — one enum variant per action
pub enum Command {
Add { title: String, priority: Priority },
List { show_done: bool },
Done { id: u32 },
Remove { id: u32 },
Stats,
Help,
}
impl Command {
/// Parse command-line arguments into a Command
/// (In production, you'd use `clap` — this is educational)
pub fn parse(args: &[String]) -> Result<Self, String> {
match args.first().map(|s| s.as_str()) {
Some("add") => {
let title = args.get(1)
.ok_or("usage: rustdo add <title> [priority]")?
.clone();
let priority = args.get(2)
.map(|p| p.parse::<Priority>())
.transpose()
.map_err(|e| e.to_string())?
.unwrap_or(Priority::Medium);
Ok(Command::Add { title, priority })
}
Some("list") => {
let show_done = args.get(1).map(|s| s == "--all").unwrap_or(false);
Ok(Command::List { show_done })
}
Some("done") => {
let id: u32 = args.get(1)
.ok_or("usage: rustdo done <id>")?
.parse()
.map_err(|_| "id must be a number")?;
Ok(Command::Done { id })
}
Some("remove") => {
let id: u32 = args.get(1)
.ok_or("usage: rustdo remove <id>")?
.parse()
.map_err(|_| "id must be a number")?;
Ok(Command::Remove { id })
}
Some("stats") => Ok(Command::Stats),
_ => Ok(Command::Help),
}
}
}
}
Python comparison: Python uses
argparseorclick. This hand-rolled parser shows howmatchon enum-like patterns replaces Python’s if/elif chains. For real projects, use theclapcrate.
Step 4: Business Logic (Ch. 5, 12, 7)
#![allow(unused)]
fn main() {
// src/actions.rs
use crate::task::{Task, Priority};
use crate::storage;
pub fn add_task(title: String, priority: Priority) -> Result<(), Box<dyn std::error::Error>> {
let mut tasks = storage::load_tasks()?;
let next_id = tasks.iter().map(|t| t.id).max().unwrap_or(0) + 1;
let task = Task::new(next_id, title.clone(), priority);
println!("Added: {task}");
tasks.push(task);
storage::save_tasks(&tasks)?;
Ok(())
}
pub fn list_tasks(show_done: bool) -> Result<(), Box<dyn std::error::Error>> {
let tasks = storage::load_tasks()?;
let filtered: Vec<&Task> = tasks.iter()
.filter(|t| show_done || !t.done) // Iterator + closure (Ch. 12)
.collect();
if filtered.is_empty() {
println!("No tasks! 🎉");
return Ok(());
}
for task in &filtered {
println!(" {task}"); // Uses Display trait (Ch. 10)
}
println!("\n{} task(s) shown", filtered.len());
Ok(())
}
pub fn complete_task(id: u32) -> Result<(), Box<dyn std::error::Error>> {
let mut tasks = storage::load_tasks()?;
let task = tasks.iter_mut()
.find(|t| t.id == id) // Iterator::find (Ch. 12)
.ok_or(format!("No task with id {id}"))?;
task.done = true;
println!("Completed: {task}");
storage::save_tasks(&tasks)?;
Ok(())
}
pub fn remove_task(id: u32) -> Result<(), Box<dyn std::error::Error>> {
let mut tasks = storage::load_tasks()?;
let len_before = tasks.len();
tasks.retain(|t| t.id != id); // Vec::retain (Ch. 5)
if tasks.len() == len_before {
return Err(format!("No task with id {id}").into());
}
println!("Removed task {id}");
storage::save_tasks(&tasks)?;
Ok(())
}
pub fn show_stats() -> Result<(), Box<dyn std::error::Error>> {
let tasks = storage::load_tasks()?;
let total = tasks.len();
let done = tasks.iter().filter(|t| t.done).count();
let pending = total - done;
// Group by priority using iterators (Ch. 12)
let high = tasks.iter().filter(|t| !t.done && t.priority == Priority::High).count();
let medium = tasks.iter().filter(|t| !t.done && t.priority == Priority::Medium).count();
let low = tasks.iter().filter(|t| !t.done && t.priority == Priority::Low).count();
println!("📊 Task Statistics");
println!(" Total: {total}");
println!(" Done: {done} ✅");
println!(" Pending: {pending}");
println!(" 🔴 High: {high}");
println!(" 🟡 Medium: {medium}");
println!(" 🟢 Low: {low}");
Ok(())
}
}
Key Rust patterns used:
iter().map().max(),iter().filter().collect(),iter_mut().find(),retain(),iter().filter().count(). These replace Python’s list comprehensions,next(x for x in ...), andCounter.
Step 5: Wire It Together (Ch. 8)
// src/main.rs
mod task;
mod storage;
mod command;
mod actions;
use command::Command;
fn main() {
let args: Vec<String> = std::env::args().skip(1).collect();
let command = match Command::parse(&args) {
Ok(cmd) => cmd,
Err(e) => {
eprintln!("Error: {e}");
std::process::exit(1);
}
};
let result = match command {
Command::Add { title, priority } => actions::add_task(title, priority),
Command::List { show_done } => actions::list_tasks(show_done),
Command::Done { id } => actions::complete_task(id),
Command::Remove { id } => actions::remove_task(id),
Command::Stats => actions::show_stats(),
Command::Help => {
print_help();
Ok(())
}
};
if let Err(e) = result {
eprintln!("Error: {e}");
std::process::exit(1);
}
}
fn print_help() {
println!("rustdo — a task manager for Pythonistas learning Rust\n");
println!("USAGE:");
println!(" rustdo add <title> [low|medium|high] Add a task");
println!(" rustdo list [--all] List pending tasks");
println!(" rustdo done <id> Mark task complete");
println!(" rustdo remove <id> Remove a task");
println!(" rustdo stats Show statistics");
}
graph TD
CLI["main.rs<br/>(CLI entry)"] --> CMD["command.rs<br/>(parse args)"]
CMD --> ACT["actions.rs<br/>(business logic)"]
ACT --> STORE["storage.rs<br/>(JSON persistence)"]
ACT --> TASK["task.rs<br/>(data model)"]
STORE --> TASK
style CLI fill:#d4edda
style CMD fill:#fff3cd
style ACT fill:#fff3cd
style STORE fill:#ffeeba
style TASK fill:#ffeeba
Step 6: Cargo.toml Dependencies
[package]
name = "rustdo"
version = "0.1.0"
edition = "2021"
[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"
chrono = "0.4"
dirs = "5"
Python equivalent: This is your
pyproject.toml[project.dependencies].cargo add serde serde_json chrono dirsis likepip install.
Step 7: Tests (Ch. 14)
#![allow(unused)]
fn main() {
// src/task.rs — add at the bottom
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_priority() {
assert_eq!("high".parse::<Priority>().unwrap(), Priority::High);
assert_eq!("H".parse::<Priority>().unwrap(), Priority::High);
assert_eq!("med".parse::<Priority>().unwrap(), Priority::Medium);
assert!("invalid".parse::<Priority>().is_err());
}
#[test]
fn task_display() {
let task = Task::new(1, "Write Rust".to_string(), Priority::High);
let display = format!("{task}");
assert!(display.contains("Write Rust"));
assert!(display.contains("🔴"));
assert!(display.contains("⬜")); // Not done yet
}
#[test]
fn task_serialization_roundtrip() {
let task = Task::new(1, "Test".to_string(), Priority::Low);
let json = serde_json::to_string(&task).unwrap();
let recovered: Task = serde_json::from_str(&json).unwrap();
assert_eq!(recovered.title, "Test");
assert_eq!(recovered.priority, Priority::Low);
}
}
}
Python equivalent:
pytesttests. Run withcargo testinstead ofpytest. No test discovery magic needed —#[test]marks test functions explicitly.
Stretch Goals
Once you have the basic version working, try these enhancements:
-
Add
clapfor argument parsing — Replace the hand-rolled parser withclap’s derive macros:#![allow(unused)] fn main() { #[derive(Parser)] enum Command { Add { title: String, #[arg(default_value = "medium")] priority: Priority }, List { #[arg(long)] all: bool }, Done { id: u32 }, Remove { id: u32 }, Stats, } } -
Add colored output — Use the
coloredcrate for terminal colors (like Python’scolorama). -
Add due dates — Add an
Option<NaiveDate>field and filter overdue tasks. -
Add tags/categories — Use
Vec<String>for tags and filter with.iter().any(). -
Make it a library + binary — Split into
lib.rs+main.rsso the logic is reusable (Ch. 8 module pattern).
What You Practiced
| Chapter | Concept | Where It Appeared |
|---|---|---|
| Ch. 3 | Types and variables | Task struct fields, u32, String, bool |
| Ch. 5 | Collections | Vec<Task>, retain(), push() |
| Ch. 6 | Enums + match | Priority, Command, exhaustive matching |
| Ch. 7 | Ownership + borrowing | &[Task] vs Vec<Task>, &mut for completion |
| Ch. 8 | Modules | mod task; mod storage; mod command; mod actions; |
| Ch. 9 | Error handling | Result<T, E>, ? operator, .ok_or() |
| Ch. 10 | Traits | Display, FromStr, Serialize, Deserialize |
| Ch. 11 | From/Into | FromStr for Priority, .into() for error conversion |
| Ch. 12 | Iterators | filter, map, find, count, collect |
| Ch. 14 | Testing | #[test], #[cfg(test)], assertion macros |
🎓 Congratulations! If you’ve built this project, you’ve used every major Rust concept covered in this book. You’re no longer a Python developer learning Rust — you’re a Rust developer who also knows Python.