Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

English Original

面向 Python 程序员的 Rust 完整培训指南 🟢

这是一本面向 Python 开发者的 Rust 学习指南,涵盖从基础语法到高级模式的内容,重点讲解从动态类型、垃圾回收语言迁移到具备编译期内存安全保证的静态类型系统语言时所需要的思维转变。

如何使用本书

自学建议:先学习第一部分(第 1-6 章),这些内容与 Python 中已有概念最接近。第二部分(第 7-12 章)会引入 Rust 特有概念,如所有权和 trait。第三部分(第 13-16 章)讨论高级主题与迁移问题。

学习节奏建议:

章节主题建议时间检查点
1-4环境、类型、控制流1 天你可以用 Rust 写出命令行温度转换器
5-6数据结构、枚举、模式匹配1-2 天你可以定义携带数据的枚举并用 match 完整匹配
7所有权与借用1-2 天你可以解释为什么 let s2 = s1 会使 s1 失效
8-9模块、错误处理1 天你可以创建一个多文件项目并用 ? 传播错误
10-12Trait、泛型、闭包、迭代器1-2 天你可以把列表推导式翻译成迭代器链
13并发1 天你可以用 Arc<Mutex<T>> 写出线程安全计数器
14Unsafe、PyO3、测试1 天你可以通过 PyO3 从 Python 调用 Rust 函数
15-16迁移、最佳实践自定节奏作为参考材料,在实际开发时按需查阅
17综合项目2-3 天构建一个整合各章节内容的完整命令行应用

如何使用练习:

  • 各章包含可折叠 <details> 区块中的动手练习及答案
  • 总是先尝试练习,再展开答案。 与借用检查器斗争本身就是学习过程,编译器的报错就是老师
  • 如果卡住超过 15 分钟,就展开答案学习,然后收起并重新独立完成一次
  • Rust Playground 允许你在未本地安装 Rust 的情况下运行代码

难度标记:

  • 🟢 初级:可以直接从 Python 概念迁移
  • 🟡 中级:需要理解所有权或 trait
  • 🔶 高级:生命周期、async 内部机制或 unsafe 代码

遇到卡点时:

  • 仔细阅读编译器错误信息,Rust 的错误提示通常非常有帮助
  • 重读相关小节,像所有权这样的概念往往第二遍才真正理解
  • Rust 标准库文档 非常优秀,遇到类型或方法都值得去查
  • 如需更深入的异步内容,请参考配套的 Async Rust Training

目录

第一部分:基础

1. 引言与动机 🟢

2. 快速开始 🟢

3. 内建类型与变量 🟢

4. 控制流 🟢

5. 数据结构与集合 🟢

6. 枚举与模式匹配 🟡

第二部分:核心概念

7. 所有权与借用 🟡

8. Crate 与模块 🟢

9. 错误处理 🟡

10. Trait 与泛型 🟡

11. From 与 Into Trait 🟡

12. 闭包与迭代器 🟡

第三部分:高级主题与迁移

13. 并发 🔶

14. Unsafe Rust、FFI 与测试 🔶

15. 迁移模式 🟡

16. 最佳实践 🟡


第四部分:综合项目

17. 综合项目:命令行任务管理器 🔶


1. 引言与动机

English Original

讲师介绍与整体方法

  • 讲师介绍
    • Microsoft SCHIE(Silicon and Cloud Hardware Infrastructure Engineering,硅与云硬件基础设施工程)团队首席固件架构师
    • 行业资深专家,专长于安全、系统编程(固件、操作系统、虚拟机管理程序)、CPU 与平台架构以及 C++ 系统
    • 2017 年在 AWS EC2 开始使用 Rust 编程,从此爱上了这门语言
  • 本课程旨在尽可能保持高度互动
    • 前提假设:你熟悉 Python 及其生态系统
    • 示例会刻意将 Python 概念映射到 Rust 对应概念
    • 欢迎随时提出任何澄清性问题

Rust 对 Python 开发者的价值

你将学到: 为什么 Python 开发者开始采用 Rust、真实世界中的性能收益(Dropbox、Discord、Pydantic)、何时应选择 Rust 而不是继续使用 Python,以及这两门语言在核心设计理念上的差异。

难度: 🟢 初级

性能:从分钟到毫秒

Python 在处理 CPU 密集型任务时出了名的慢。Rust 则在提供高级语言体验的同时,具备接近 C 的原生性能。

# Python — 处理 1000 万次调用约需 2 秒
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:.2f}s")  # 在典型硬件上约为 2s
// Rust — 处理同样的 1000 万次调用约需 0.07 秒
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!("耗时: {:.2?}", start.elapsed());  // 约为 0.07s
}

注意:为了公平比较性能,Rust 应在发布模式下运行(cargo run --release)。 为什么差距这么大? Python 的每一次 + 操作都要经过字典查找、从堆对象中解包整数,并在每次操作时进行类型检查。而 Rust 会将 fibonacci 直接编译成少量 x86 add/mov 指令 —— 这与 C 编译器生成的代码几乎一致。

没有垃圾回收器的内存安全

Python 的引用计数垃圾回收(GC)存在一些已知问题:循环引用、不可预测的 __del__ 触发时机以及内存碎片。Rust 则在编译时消除了这些隐患。

# Python — CPython 的引用计数器无法释放的循环引用
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  # 循环引用!

# 这两个节点相互引用 —— 引用计数永远不会降为 0。
# CPython 的循环检测器最终会清理它们,
# 但你无法控制清理时机,且会带来 GC 停顿开销。
root = Node("root")
child = Node("child")
root.add_child(child)
// Rust — 架构设计上就防止了循环引用
struct Node {
    value: String,
    children: Vec<Node>,  // 子节点被“拥有” (OWNED) —— 不可能产生循环
}

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);  // 所有权在此转移
    }
}

fn main() {
    let mut root = Node::new("root");
    let child = Node::new("child");
    root.add_child(child);
    // 当 root 被丢弃 (dropped) 时,所有子节点也会随之被丢弃。
    // 确定性、零开销、无需 GC。
}

核心洞见:在 Rust 中,子节点不会持有指向父节点的引用。如果你确实需要交叉引用(如在图结构中),则必须显式使用 Rc<RefCell<T>> 或索引等机制 —— 从而使代码的复杂性显性化且可控。


Rust 解决的常见 Python 痛点

1. 运行时类型错误

最常见的 Python 生产环境 Bug:向函数传递了错误的类型。类型提示 (Type hints) 虽然有帮助,但在运行时并不具备强制力。

# Python — 类型提示只是建议,而非规则
def process_user(user_id: int, name: str) -> dict:
    return {"id": user_id, "name": name.upper()}

# 这些调用在调用方都能“正常运行” —— 但在运行时会崩溃
process_user("not-a-number", 42)        # TypeError: int 类型没有 .upper() 方法
process_user(None, "Alice")             # 静默将 None 存为 id —— Bug 隐藏到下游代码期望 int 时才爆发

# 即使用了 mypy,依然能通过各种方式绕过类型检查:
import json
data = json.loads('{"id": "oops", "name": "Alice"}') # 总是返回 Any
process_user(data["id"], data["name"])               # mypy 无法捕捉到这里的问题
#![allow(unused)]
fn main() {
// Rust — 编译器在程序运行前就会抓住这些错误
fn process_user(user_id: i64, name: &str) -> User {
    User {
        id: user_id,
        name: name.to_uppercase(),
    }
}

// process_user("not-a-number", 42);     // ❌ 编译错误:期望 i64,却是 &str
// process_user(None, "Alice");           // ❌ 编译错误:期望 i64,却是 Option
// 参数个数不对总是会导致编译错误。

// 反序列化 JSON 也是类型安全的:
#[derive(serde::Deserialize)]
struct UserInput {
    id: i64,      // JSON 中必须为数字
    name: String, // JSON 中必须为字符串
}
let input: UserInput = serde_json::from_str(json_str)?; // 类型不匹配则返回 Err
process_user(input.id, &input.name); // ✅ 保证类型正确
}

2. None:Python 版的“十亿美元错误”

在 Python 中,任何期望值的地方都可能出现 None。Python 无法在编译时防止 AttributeError: 'NoneType' object has no attribute ... 这样的错误。

# Python — None 潜伏在每一个角落
def find_user(user_id: int) -> dict | None:
    users = {1: {"name": "Alice"}, 2: {"name": "Bob"}}
    return users.get(user_id)

user = find_user(999)         # 返回 None
print(user["name"])           # 💥 TypeError: 'NoneType' object is not subscriptable

# 即使有 Optional 类型提示,也没有东西强制执行检查:
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 虽然警告,但运行时无动于衷
#![allow(unused)]
fn main() {
// Rust — 除非显式处理,否则 None 是不可能出现的
fn find_user(user_id: i64) -> Option<User> {
    let users = std::collections::HashMap::from([
        (1, User { name: "Alice".into() }),
        (2, User { name: "Bob".into() }),
    ]);
    users.get(&user_id).cloned()
}

let user = find_user(999);  // 返回 Option<User> 的 None 变体 (variant)
// println!("{}", user.name);  // ❌ 编译错误:Option<User> 没有 name 字段

// 你必须显式处理 None 的情况:
match find_user(999) {
    Some(user) => println!("{}", user.name),
    None => println!("未找到用户"),
}

// 也可以使用组合算子:
let name = find_user(999)
    .map(|u| u.name)
    .unwrap_or_else(|| "未知用户".to_string());
}

3. GIL:Python 的并发天花板

Python 的全局解释器锁 (Global Interpreter Lock) 意味着其线程无法真正并行执行 Python 代码。threading 库只对 I/O 密集型工作有效;而处理 CPU 密集型任务则需要 multiprocessing(带有序列化开销)或 C 扩展。

# Python — 线程由于 GIL 的存在并不能加速 CPU 密集型任务
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 线程耗时: {elapsed:.2f}s")  # 与单线程基本一致!GIL 阻止了真正并行。

# multiprocessing 虽然起效,但需要在进程间序列化数据:
from multiprocessing import Pool
with Pool(4) as p:
    results = p.map(cpu_work, [10_000_000] * 4)  # 约为 4 倍加速,但有 pickle 开销。
// Rust — 真正的并行,无 GIL,无序列化开销
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 线程耗时: {:.2?}", start.elapsed());  // 约为单线程 4 倍速
}

配合 Rayon (Rust 的并行迭代器库),实现并行会更简单:

#![allow(unused)]
fn main() {
use rayon::prelude::*;
let results: Vec<u64> = inputs.par_iter().map(|&n| cpu_work(n)).collect();
}

4. 部署与分发的重重阻碍

Python 部署出了名的困难:虚拟环境 (venvs)、系统 Python 版本冲突、pip install 各种报错、C 扩展的 Wheel 文件、以及带有庞大 Python 运行时的 Docker 镜像。

# Python 部署检查表:
# 1. 哪个 Python 版本? 3.10? 3.11? 3.12?
# 2. 虚拟环境:venv, conda, poetry, pipenv?
# 3. C 扩展:需要编译器吗?manylinux wheels 匹配吗?
# 4. 系统依赖:libssl, libffi 等?
# 5. Docker:python:3.12-slim 镜像大概 200MB 以上,完整版则接近 1GB
# 6. 启动时间:由于大量 import,通常需要 200-500ms
#![allow(unused)]
fn main() {
// Rust 部署:单一静态二进制文件,无需运行时
// cargo build --release → 生成单个二进制执行文件,通常在 5-20 MB
// 复制到任何地方即可 —— 无需 Python,无需 venv,无依赖报错

// Docker 镜像:从 scratch 或 distroless 基础镜像开始,通常只有几 MB
// FROM scratch
// COPY target/release/my_app /my_app
// CMD ["/my_app"]

// 启动时间:< 1ms
// 跨平台编译 (Cross-compile): cargo build --target x86_64-unknown-linux-musl
}

何时选择 Rust 而不是 Python

建议选择 Rust 的场景:

  • 性能至关重要:数据流水线、实时处理、计算密集型服务。
  • 正确性极其重要:金融系统、安全关键型代码、协议实现。
  • 追求部署简便:单个二进制文件,无运行时依赖。
  • 底层控制:硬件交互、操作系统集成、嵌入式系统。
  • 真正的并发:无需 GIL 绕过方案的 CPU 密集型并行。
  • 内存效率:降低内存密集型服务的云成本。
  • 长时间运行的服务:延迟可预测非常重要(无 GC 停顿)。

建议保留 Python 的场景:

  • 快速原型开发:探索性数据分析、脚本、一次性工具。
  • ML/AI 工作流:PyTorch、TensorFlow、scikit-learn 生态系统。
  • 胶水代码:连接 API、数据转换脚本。
  • 团队专业能力:Rust 的学习曲线带来的收益不足以抵消其成本时。
  • 上市时间优先:开发速度比执行速度更重要时。
  • 交互式工作:Jupyter 笔记本、REPL 驱动开发。
  • 自动化脚本:运维自动化、系统管理任务、快速工具。

考虑结合使用(配合 PyO3 的混合方案):

  • 计算密集型代码用 Rust 编写:通过 PyO3/maturin 从 Python 调用。
  • 业务逻辑和编排用 Python 编写:熟悉且高效。
  • 渐进式迁移:识别性能热点,并用 Rust 扩展替换。
  • 取长补短:Python 的生态系统 + Rust 的性能。

真实世界的影响:为什么大厂选择 Rust?

Dropbox:存储基础设施

  • 此前(Python):同步引擎中的 CPU 使用率高,内存开销大。
  • 此后(Rust):性能提升 10 倍,内存占用减少 50%。
  • 结果:节省了数百万美元的基础设施成本。

Discord:语音/视频后端

  • 此前(Python → Go):GC 停顿导致音频掉帧。
  • 此后(Rust):获得了极其稳定的低延迟性能。
  • 结果:由于用户体验更好,且服务器成本降低。

Cloudflare:边缘计算 (Edge Workers)

  • 原因:WebAssembly 编译支持,边缘侧性能可预测。
  • 结果:Worker 的冷启动时间缩短到微秒级。

Pydantic V2

  • 此前:纯 Python 验证 —— 处理大型负载时速度较慢。
  • 此后:Rust 核心(通过 PyO3)—— 验证速度提升 5–50 倍。
  • 结果:保持同样的 Python API,执行速度大幅提升。

这对 Python 开发者意味着什么:

  1. 技能互补:Rust 和 Python 解决不同的问题。
  2. PyO3 桥梁:编写可从 Python 调用的 Rust 扩展。
  3. 深入理解性能:了解为什么 Python 慢,以及如何修复性能热点。
  4. 职业成长:系统编程专业知识的价值日益凸显。
  5. 云端成本:代码提速 10 倍 = 显著降低基础设施支出。

语言哲学对比

Python 哲学

  • 可读性至上:语法简洁,“完成一件事应该只有一种显而易见的方法”。
  • 内置电池 (Batteries included):内容丰富的标准库,支持快速原型开发。
  • 鸭子类型 (Duck typing):“如果它走起来像鸭子,叫起来也像鸭子…”
  • 开发效率:针对编写速度而非执行速度进行优化。
  • 动态特性:运行时修改类、猴子补丁 (monkey-patching)、元类。

Rust 哲学

  • 高性能且不失安全:零成本抽象,无运行时开销。
  • 正确性第一:只要能编译通过,整类 Bug 都不可能出现。
  • 显式优于隐式:无隐藏行为,无隐式类型转换。
  • 所有权:资源(内存、文件、套接字)有且仅有一个所有者。
  • 无畏并发:通过类型系统在编译时防止数据竞争。
graph LR
    subgraph PY["🐍 Python"]
        direction TB
        PY_CODE["你的代码"] --> PY_INTERP["解释器 — CPython VM"]
        PY_INTERP --> PY_GC["垃圾回收器 — 引用计数 + GC"]
        PY_GC --> PY_GIL["GIL — 无真正并行"]
        PY_GIL --> PY_OS["操作系统 / 硬件"]
    end

    subgraph RS["🦀 Rust"]
        direction TB
        RS_CODE["你的代码"] --> RS_NONE["零运行时开销"]
        RS_NONE --> RS_OWN["所有权 — 编译时检查,零成本"]
        RS_NONE --> RS_THR["原生线程 — 真正并行"]
        RS_THR --> RS_OS["操作系统 / 硬件"]
    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

快速对照:Rust vs Python

概念PythonRust核心差异
类型系统动态 (duck typing)静态(编译时)Bug 在运行前就被抓住
内存管理垃圾回收(引用计数 + 循环检测 GC)所有权系统零成本、确定性的清理
None/nullNone 随处可见Option<T>编译时保证 None 安全
错误处理raise/try/exceptResult<T, E>显式,无隐藏的控制流
可变性切皆可变默认不可变手动开启可变性
速度解释执行(约慢 10–100 倍)编译执行(C/C++ 级别的速度)数量级的性能提升
并发GIL 限制了线程无 GIL,有 Send/Sync Trait默认即支持真正并行
依赖管理pip install / poetry addcargo add内建的依赖管理工具
构建系统setuptools/poetry/hatchCargo统一的构建工具
配置方式pyproject.tomlCargo.toml类似的声明式配置
交互式环境python 交互模式无 REPL(使用测试或 cargo run)编译优先的工作流
类型提示可选,不强制必须提供,编译器强制类型并非装饰品

练习

🏋️ 练习:思维模型检查(点击展开)

挑战:对于以下每个 Python 代码片段,预测 Rust 会有何种不同的要求。不需要写代码 —— 只需要描述约束条件。

  1. x = [1, 2, 3]; y = x; x.append(4) —— 在 Rust 中会发生什么?
  2. data = None; print(data.upper()) —— Rust 如何防止这种情况?
  3. import threading; shared = []; threading.Thread(target=shared.append, args=(1,)).start() —— Rust 要求什么?
🔑 答案
  1. 所有权转移 (Move):let y = x; 会转移 x 的所有权 —— 导致 x.push(4) 报编译错误。你需要使用 let y = x.clone(); 或者通过 let y = &x; 进行借用。
  2. 没有 Null:除非声明为 Option<String>,否则 data 不能为空。你必须使用 match 或 .unwrap() / if let 来处理 —— 不会出现意外的 NoneType 错误。
  3. Send + Sync:编译器要求 shared 必须被包裹在 Arc<Mutex<Vec<i32>>> 中。忘了加锁 = 编译错误,而不是出现竞争条件。

核心要点:Rust 将运行时故障转变为编译时错误。你感受到的“阻力”其实是编译器在帮你捉虫。


2. 快速开始

English Original

安装与环境配置

你将学到: 如何安装 Rust 及其工具链、Cargo 构建系统与 pip/Poetry 的对比、IDE 配置、你的第一个 Hello, world! 程序,以及映射到 Python等价概念的核心 Rust 关键字。

难度: 🟢 初级

安装 Rust

# 在 Linux/macOS/WSL 上通过 rustup 安装 Rust
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

# 验证安装
rustc --version     # Rust 编译器
cargo --version     # 构建工具 + 包管理器(相当于 pip + setuptools 的结合体)

# 更新 Rust
rustup update

Rust 工具 vs Python 工具

用途PythonRust
语言运行时python (解释器)rustc (编译器,很少直接调用)
包管理器pip / poetry / uvcargo (内建)
项目配置pyproject.tomlCargo.toml
锁文件poetry.lock / requirements.txtCargo.lock
虚拟环境venv / conda不需要(依赖按项目管理)
格式化工具black / ruff formatrustfmt (内建: cargo fmt)
静态分析 (Linter)ruff / flake8 / pylintclippy (内建: cargo clippy)
类型检查器mypy / pyright已集成进编译器 (始终开启)
测试运行器pytestcargo test (内建)
文档工具sphinx / mkdocscargo doc (内建)
REPLpython / ipython无 (使用 cargo test 或 Rust Playground)

IDE 配置

VS Code (推荐):

需要安装的扩展:
- rust-analyzer        ← 核心: 提供 IDE 功能、类型提示、自动补全
- Even Better TOML     ← Cargo.toml 的语法高亮
- CodeLLDB             ← 调试器支持

# 与 Python 扩展的等价映射:
# rust-analyzer ≈ Pylance (但具备 100% 类型覆盖率)
# cargo clippy  ≈ ruff (但除了风格外更注重正确性检查)

你的第一个 Rust 程序

Python 版 Hello World

# hello.py — 直接运行即可
print("Hello, World!")

# 运行:
# python hello.py

Rust 版 Hello World

// src/main.rs — 必须先进行编译
fn main() {
    println!("Hello, World!");   // println! 是一个宏 (注意末尾的 !)
}

// 构建并运行:
// cargo run

给 Python 开发者的关键差异点

Python:                              Rust:
─────────                            ─────
- 不需要 main()                      - fn main() 是程序的入口
- 缩进即代码块                       - 花括号 {} 即代码块
- print() 是一个函数                 - println!() 是一个宏 (末尾有 !)
- 不需要分号                         - 使用分号结束语句
- 无需类型声明                       - 类型虽然是推导的,但始终明确
- 解释执行 (直接运行)                - 编译执行 (cargo build 后运行)
- 报错发生在运行时                    - 大多数错误发生在编译时

创建你的第一个项目

# Python                              # Rust
mkdir myproject                        cargo new myproject
cd myproject                           cd myproject
python -m venv .venv                   # 无需虚拟环境
source .venv/bin/activate              # 无需激活
# 需要手动创建文件                      # src/main.rs 已自动创建

# Python 项目结构:                    Rust 项目结构:
# myproject/                           myproject/
# ├── pyproject.toml                   ├── Cargo.toml        (类似 pyproject.toml)
# ├── src/                             ├── src/
# │   └── myproject/                   │   └── main.rs       (程序入口)
# │       ├── __init__.py              └── (无需 __init__.py 文件)
# │       └── main.py
# └── tests/
#     └── test_main.py
graph TD
    subgraph Python ["Python 项目"]
        PP["pyproject.toml"] --- PS["src/"]
        PS --- PM["myproject/"]
        PM --- PI["__init__.py"]
        PM --- PMN["main.py"]
        PP --- PT["tests/"]
    end
    subgraph Rust ["Rust 项目"]
        RC["Cargo.toml"] --- RS["src/"]
        RS --- RM["main.rs"]
        RC --- RTG["target/ (自动生成)"]
    end
    style Python fill:#ffeeba
    style Rust fill:#d4edda

关键差异:Rust 项目结构更简洁 —— 没有 __init__.py,没有虚拟环境,也没有 setup.py vs setup.cfg vs pyproject.toml 之间的混乱。只有 Cargo.toml + src/。


Cargo vs pip/Poetry

项目配置

# 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 版本 (类似 Python 3.10)

[dependencies]
reqwest = "0.12"          # HTTP 客户端 (类似 requests)
serde = { version = "1.0", features = ["derive"] }  # 序列化 (类似 pydantic)

[dev-dependencies]
# 测试依赖 — 仅在 `cargo test` 时编译
# (无需单独配置测试运行器 — `cargo test` 是内建的)

常用 Cargo 命令对比

# Python 等价                      # Rust
pip install requests               cargo add reqwest
pip install -r requirements.txt    cargo build           # 自动安装依赖
pip install -e .                   cargo build            # 总是“可编辑的”
python -m pytest                   cargo test
python -m mypy .                   # 集成进编译器 — 总是运行
ruff check .                       cargo clippy
ruff format .                      cargo fmt
python main.py                     cargo run
python -c "..."                    # 无等价物 — 使用 cargo run 或测试

# Rust 特有:
cargo new myproject                # 创建新项目
cargo build --release              # 优化构建 (比 debug 慢,但运行快 10-100 倍)
cargo doc --open                   # 生成并浏览 API 文档
cargo update                       # 更新依赖 (类似 pip install --upgrade)

给 Python 开发者的 Rust 核心关键字

变量与可变性关键字

#![allow(unused)]
fn main() {
// let — 声明变量 (类似 Python 赋值,但默认不可变)
let name = "Alice";          // Python: name = "Alice" (但 Python 总是可变的)
// name = "Bob";             // ❌ 编译错误!默认不可变

// mut — 声明可变性
let mut count = 0;           // Python: count = 0 (在 Python 中总是可变的)
count += 1;                  // ✅ 允许,因为带有 `mut`

// const — 编译时常量 (类似 Python 全大写的常量约定,但 Rust 强制执行)
const MAX_SIZE: usize = 1024;   // Python: MAX_SIZE = 1024 (仅为约定)

// static — 全局变量 (谨慎使用;Python 有模块级全局变量)
static VERSION: &str = "1.0";
}

所有权与借用关键字

#![allow(unused)]
fn main() {
// 这里的概念在 Python 中没有等价物 — 它们是 Rust 特有的核心

// & — 借用 (只读引用)
fn print_name(name: &str) { }    // Python: def print_name(name: str) — 但 Python 总是传引用

// &mut — 可变借用
fn append(list: &mut Vec<i32>) { }  // Python: def append(lst: list) — Python 中总是可变的

// move — 转移所有权 (在 Rust 中隐式发生,在 Python 中绝不发生)
let s1 = String::from("hello");
let s2 = s1;    // s1 被转移 (MOVED) 给了 s2 — s1 后续不再有效
// println!("{}", s1);  // ❌ 编译错误: value moved
}

类型定义关键字

#![allow(unused)]
fn main() {
// struct — 类似 Python 的 dataclass 或 NamedTuple
struct Point {               // @dataclass
    x: f64,                  // class Point:
    y: f64,                  //     x: float
}                            //     y: float

// enum — 类似 Python 的 enum 但远更强大 (可携带数据)
enum Shape {                 // 无直接 Python 等价物
    Circle(f64),             // 每个变体可以持有不同的数据
    Rectangle(f64, f64),
}

// impl — 为类型关联方法 (类似在类中定义方法)
impl Point {                 // class Point:
    fn distance(&self) -> f64 {  //     def distance(self) -> float:
        (self.x.powi(2) + self.y.powi(2)).sqrt()
    }
}

// trait — 类似 Python 的 ABC 或 Protocol (PEP 544)
trait Drawable {             // class Drawable(Protocol):
    fn draw(&self);          //     def draw(self) -> None: ...
}

// type — 类型别名 (类似 Python 的 TypeAlias)
type UserId = i64;           // UserId = int  (or TypeAlias)
}

控制流关键字

#![allow(unused)]
fn main() {
// match — 穷尽模式匹配 (类似 Python 3.10+ 的 match,但由编译器强制执行)
match value {
    1 => println!("one"),
    2 | 3 => println!("two or three"),
    _ => println!("other"),          // _ = 通配符 (类似 Python 的 case _:)
}

// if let — 解构 + 条件 (类似 Python: if (m := regex.match(s)):)
if let Some(x) = optional_value {
    println!("{}", x);
}

// loop — 无限循环 (类似 while True:)
loop {
    break;  // 必须使用 break 退出
}

// for — 迭代 (类似 Python 的 for,但通常需要使用 .iter())
for item in collection.iter() {      // for item in collection:
    println!("{}", item);
}

// while let — 带有解构的循环
while let Some(item) = stack.pop() {
    process(item);
}
}

可见性关键字

#![allow(unused)]
fn main() {
// pub — 公有的 (Python 中没有真正的私有;通常使用 _ 约定)
pub fn greet() { }           // def greet():  — Python 中一切都是“公有”的

// pub(crate) — 仅在当前项目 (crate) 内可见
pub(crate) fn internal() { } // def _internal():  — 单下划线约定

// (默认无关键字) — 对当前模块为私有
fn private_helper() { }      // def __private():  — 双下划线名称改写 (name mangling)

// 在 Python 中,“私有”只是“君子协定”
// 在 Rust 中,私有是由编译器强制执行的
}

练习

🏋️ 练习:你的第一个 Rust 程序(点击展开)

挑战:创建一个新的 Rust 项目并编写一段程序:

  1. 声明一个名为 name 的变量并存储你的名字 (类型为 &str)
  2. 声明一个从 0 开始的可变变量 count
  3. 使用 for 循环(范围为 1..=5)递增 count 并打印 "Hello, {name}! (count: {count})"
  4. 循环结束后,使用 match 表达式判断 count 是奇数还是偶数
🔑 答案
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}");
}

核心要点:

  • let 默认不可变(你需要 mut 来改变 count)
  • 1..=5 是包含上限的范围 (相当于 Python 的 range(1, 6))
  • match 是一个表达式,可以返回一个值
  • 没有 self,没有 if __name__ == "__main__" —— 入口即是 fn main()

3. 内建类型与变量

English Original

变量与可变性

你将学到: 默认不可变的变量、显式使用 mut、原生数值类型与 Python 任意精度 int 的对比、String 与 &str(初学者最难理解的概念)、字符串格式化,以及 Rust 强制要求的类型注解。

难度: 🟢 初级

Python 变量声明

# Python — 一切皆可变,动态类型
count = 0          # 可变,类型推导为 int
count = 5          # ✅ 可行
count = "hello"    # ✅ 可行 — 类型可以改变!(动态类型)

# “常量”仅是一种约定:
MAX_SIZE = 1024    # 后续没有任何东西能阻止 MAX_SIZE = 999

Rust 变量声明

#![allow(unused)]
fn main() {
// Rust — 默认不可变,静态类型
let count = 0;           // 不可变,类型推导为 i32
// count = 5;            // ❌ 编译错误:不能对不可变变量进行二次赋值
// count = "hello";      // ❌ 编译错误:期望 integer,却是 &str

let mut count = 0;       // 显式声明为可变
count = 5;               // ✅ 可行
// count = "hello";      // ❌ 依然不能改变类型
}

给 Python 开发者的关键思维转变

#![allow(unused)]
fn main() {
// Python:变量是指向对象的标签 (labels)
// Rust:变量是命名的存储位置,且拥有 (OWN) 它们的值

// 变量遮蔽 (Variable shadowing) — Rust 特有且非常有用的功能
let input = "42";              // &str
let input = input.parse::<i32>().unwrap();  // 现在它是 i32 — 新变量,同名
let input = input * 2;         // 现在它是 84 — 又一个同名新变量

// 在 Python 中,你只需重新赋值并丢失旧类型:
input = "42"
input = int(input)   # 同名,不同类型 — Python 也允许这样做
但在 Rust 中,每个 `let` 都会创建一个全新的绑定。旧的绑定会被遮蔽。
}

实践示例:计数器

# Python 版本
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 版本
struct Counter {
    value: i64,
}

impl Counter {
    fn new() -> Self {
        Counter { value: 0 }
    }

    fn increment(&mut self) {     // &mut self = 我会修改此对象
        self.value += 1;
    }

    fn get_value(&self) -> i64 {  // &self = 我只读取此对象
        self.value
    }
}

fn main() {
    let mut c = Counter::new();   // 必须为 `mut` 才能调用 increment()
    c.increment();
    println!("{}", c.get_value()); // 1
}

关键差异:在 Rust 中,方法签名中的 &mut self 会明确告诉你(以及编译器)increment 会修改计数器。而在 Python 中,任何方法都可以修改任何东西 —— 你必须通读代码才能确定。


原生类型对比

flowchart LR
    subgraph Python ["Python 类型"]
        PI["int\n(任意精度)"] 
        PF["float\n(仅支持 64 位)"]
        PB["bool"]
        PS["str\n(Unicode)"]
    end
    subgraph Rust ["Rust 类型"]
        RI["i8 / i16 / i32 / i64 / i128\nu8 / u16 / u32 / u64 / u128"]
        RF["f32 / f64"]
        RB["bool"]
        RS["String / &str"]
    end
    PI -->|"固定大小"| RI
    PF -->|"选择精度"| RF
    PB -->|"相同"| RB
    PS -->|"拥有 vs 借用"| RS
    style Python fill:#ffeeba
    style Rust fill:#d4edda

数值类型

PythonRust说明
int (任意精度)i8, i16, i32, i64, i128, isizeRust 整数具有固定大小
int (无符号:无独立类型)u8, u16, u32, u64, u128, usize显式的无符号类型
float (64 位 IEEE 754)f32, f64Python 仅有 64 位浮点数
boolbool概念相同
complex无内建支持 (使用 num crate)在系统代码中很少见
# Python — 只有一种整数类型,任意精度
x = 42                     # int — 可以增长到任意大小
big = 2 ** 1000            # 依然可行 — 拥有数千位数字
y = 3.14                   # float — 总是 64 位
#![allow(unused)]
fn main() {
// Rust — 显式大小,溢出会导致编译/运行时错误
let x: i32 = 42;           // 32 位有符号整数
let y: f64 = 3.14;         // 64 位浮点数 (等价于 Python 的 float)
let big: i128 = 2_i128.pow(100); // 最大支持 128 位 — 无内建的任意精度
// 如需任意精度:请使用 `num-bigint` crate

// 为了可读性使用下划线 (类似 Python 的 1_000_000):
let million = 1_000_000;   // 语法与 Python 一致!

// 类型后缀语法:
let a = 42u8;              // u8
let b = 3.14f32;           // f32
}

大小类型 (重要!)

#![allow(unused)]
fn main() {
// usize 和 isize — 指针大小的整数,用于索引
let length: usize = vec![1, 2, 3].len();  // .len() 返回 usize
let index: usize = 0;                     // 数组索引总是 usize

// 在 Python 中,len() 返回 int 且索引也是 int — 两者无区别。
// 在 Rust 中,混合使用 i32 和 usize 需要显式转换:
let i: i32 = 5;
// let item = vec[i];    // ❌ 错误:期望 usize,却是 i32
let item = vec[i as usize]; // ✅ 显式转换
}

类型推导

#![allow(unused)]
fn main() {
// Rust 虽然支持类型推导,但类型是固定的 (FIXED) — 而非动态
let x = 42;          // 编译器推导为 i32 (默认整数类型)
let y = 3.14;        // 编译器推导为 f64 (默认浮点类型)
let s = "hello";     // 编译器推导为 &str (字符串切片)
let v = vec![1, 2];  // 编译器推导为 Vec<i32>

// 你也可以始终显式声明:
let x: i64 = 42;
let y: f32 = 3.14;

// 与 Python 不同,推导出的类型永远不能改变:
let x = 42;
// x = "hello";      // ❌ 错误:期望整数,却是 &str
}

字符串类型:String vs &str

这是让 Python 开发者最意外的特性之一。Rust 有 两种 主要的字符串类型,而 Python 只有一种。

Python 字符串处理

# Python — 一种字符串类型,不可变且引用计数
name = "Alice"          # str — 不可变,且在堆上分配空间
greeting = f"Hello, {name}!"  # f-string 格式化
chars = list(name)      # 转换为字符列表
upper = name.upper()    # 返回新字符串(不可变)

Rust 字符串类型

#![allow(unused)]
fn main() {
// Rust 有两种字符串类型:

// 1. &str (字符串切片) — 借用、不可变,类似对字符串数据的“视图” (view)
let name: &str = "Alice";           // 指向二进制文件中的字符串数据
                                     // 最接近 Python 的 str,但它是一个引用 (REFERENCE)

// 2. String (拥有所有权的字符串) — 在堆上分配、可增长且由程序“拥有”
let mut greeting = String::from("Hello, ");  // 拥有的字符串 (owned),可修改
greeting.push_str(name);
greeting.push('!');
// greeting 现在是 "Hello, Alice!"
}

应该在何时使用哪个?

#![allow(unused)]
fn main() {
// 你可以这样理解:
// &str  = “我正在看一段别人拥有的字符串”  (只读视图)
// String = “我拥有这段字符串数据,可以随时修改它” (拥有的数据)

// 函数参数:倾向于使用 &str (因为它能同时接受两种类型)
fn greet(name: &str) -> String {          // 接受 &str 以及 &String
    format!("Hello, {}!", name)           // format! 宏会创建一个新 String
}

let s1 = "world";                         // &str 字面量
let s2 = String::from("Rust");            // String

greet(s1);      // ✅ &str 直接可行
greet(&s2);     // ✅ &String 会自动转换为 &str (解引用强制转换)
}

实践示例

# Python 字符串操作
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 等价写法
let name = "alice";
let upper = name.to_uppercase();           // String — 发生了新内存分配
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 — 无需新分配内存!
let replaced = name.replace("a", "A");     // String

// 核心洞见:有些操作返回 &str (无分配),而有些操作返回 String。
// .trim() 返回原始字符串的一个切片 — 非常高效!
// .to_uppercase() 必须创建一个新 String — 必须进行内存分配。
}

给 Python 开发者的建议

Python str     ≈ Rust &str     (当你只是读取字符串时)
Python str     ≈ Rust String   (当你需要拥有/修改字符串时)

经验法则:
- 函数参数 → 使用 &str (最灵活)
- 结构体字段 → 使用 String (结构体应该拥有它所持有的数据)
- 返回值 → 使用 String (调用者需要拥有返回的结果数据)
- 字符串字面量 → 自动即为 &str

打印与字符串格式化

基础输出

# Python
print("Hello, World!")
print("Name:", name, "Age:", age)    # 空格分隔
print(f"Name: {name}, Age: {age}")   # f-string
#![allow(unused)]
fn main() {
// Rust
println!("Hello, World!");
println!("Name: {} Age: {}", name, age);    // 位置占位符 {}
println!("Name: {name}, Age: {age}");       // 内联变量 (Rust 1.58+ 支持,类似 f-strings!)
}

格式限定符

# Python 格式化
print(f"{3.14159:.2f}")          # "3.14" — 保留 2 位小数
print(f"{42:05d}")               # "00042" — 零填充 (zero-padded)
print(f"{255:#x}")               # "0xff" — 十六进制
print(f"{42:>10}")               # "        42" — 右对齐
print(f"{'left':<10}|")          # "left      |" — 左对齐
#![allow(unused)]
fn main() {
// Rust 格式化 (与 Python 非常相似!)
println!("{:.2}", 3.14159);         // "3.14" — 保留 2 位小数
println!("{:05}", 42);              // "00042" — 零填充
println!("{:#x}", 255);             // "0xff" — 十六进制
println!("{:>10}", 42);             // "        42" — 右对齐
println!("{:<10}|", "left");        // "left      |" — 左对齐
}

调试打印 (Debug Printing)

# Python — repr() 和 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 — {:?} 和 {:#?}
println!("{:?}", vec![1, 2, 3]);       // "[1, 2, 3]" — 使用 Debug 格式
println!("{:#?}", vec![1, 2, 3]);      // 使用 Pretty-printed Debug 格式

// 若要使你的自定义类型可打印,需要 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:?}");                     // 同上,使用内联变量语法
}

快速参考

PythonRust说明
print(x)println!("{}", x) 或 println!("{x}")Display 格式
print(repr(x))println!("{:?}", x)Debug 格式
f"Hello {name}"format!("Hello {name}")返回 String
print(x, end="")print!("{x}")不带换行 (print! vs println!)
print(x, file=sys.stderr)eprintln!("{x}")打印到标准错误 (stderr)
sys.stdout.write(s)print!("{s}")无换行

类型注解:可选还是必须

Python 类型提示 (可选,且不具备强制性)

# Python — 类型提示更多作为文档,而非强制约束
def add(a: int, b: int) -> int:
    return a + b

add(1, 2)         # ✅
add("a", "b")     # ✅ Python 并不在意 — 返回 "ab"
add(1, "2")       # ✅ 直到运行时崩溃:TypeError

Rust 类型声明 (必须提供,且由编译器强制执行)

#![allow(unused)]
fn main() {
// Rust — 类型是强制的。始终、没有任何例外。
fn add(a: i32, b: i32) -> i32 {
    a + b
}

add(1, 2);         // ✅
// add("a", "b");  // ❌ 编译错误:期望 i32,却是 &str

// 可空值必须显式使用 Option<T>
fn find(key: &str) -> Option<i32> {
    // 返回 Some(value) 或 None
    Some(42)
}

// 泛型类型
fn first(items: &[i32]) -> Option<i32> {
    items.first().copied()
}

// 类型别名
type UserId = i64;
type Mapping = HashMap<String, Vec<i32>>;
}

核心洞见:在 Python 中,类型提示虽然能帮助 IDE 和 mypy,但并不影响程序的实际运行。在 Rust 中,类型就是程序 —— 编译器利用类型来保证内存安全、防止数据竞争,并彻底消除空指针错误。

📌 延伸阅读: 第六章:枚举与模式匹配 展示了 Rust 的类型系统如何取代 Python 的 Union 类型以及 isinstance() 检查。


练习

🏋️ 练习:温度转换器(点击展开)

挑战:编写一个函数 celsius_to_fahrenheit(c: f64) -> f64 和一个函数 classify(temp_f: f64) -> &'static str,后者根据阈值返回 “cold”、“mild” 或 “hot”。为 0、20 和 35 摄氏度并打印出结果,并使用字符串格式化展示。

🔑 答案
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));
    }
}

核心要点: Rust 要求显式的 f64 类型(没有隐式的 int 到 float 转换),for 可以直接迭代数组(不需要 range()),且 if/else 代码块是表达式。


4. 控制流

English Original

条件语句

你将学到: 不需要括号(但需要花括号)的 if/else、loop/while/for 与 Python 迭代模型的对比、表达式块(一切皆可返回一个值),以及带有强制返回类型的函数签名。

难度: 🟢 初级

if/else

# Python
if temperature > 100:
    print("太热了!")
elif temperature < 0:
    print("太冷了!")
else:
    print("刚好")

# 三元表达式
status = "热" if temperature > 100 else "正常"
#![allow(unused)]
fn main() {
// Rust — 需要花括号,不需要冒号,使用 `else if` 而非 `elif`
if temperature > 100 {
    println!("太热了!");
} else if temperature < 0 {
    println!("太冷了!");
} else {
    println!("刚好");
}

// if 是一个表达式 (EXPRESSION) — 会返回一个值 (类似 Python 三元表达式,但更强大)
let status = if temperature > 100 { "热" } else { "正常" };
}

重要差异

#![allow(unused)]
fn main() {
// 1. 条件必须是 bool 类型 — 不存在“真值/假值” (truthy/falsy) 概念
let x = 42;
// if x { }          // ❌ 错误:期望 bool,却是整数
if x != 0 { }        // ✅ 必须进行显式比较

// 在 Python 中,这些都被视为真值/假值:
// if []:      → False    (空列表)
// if "":      → False    (空字符串)
// if 0:       → False    (零)
// if None:    → False

// 在 Rust 中,条件中仅限使用 bool:
let items: Vec<i32> = vec![];
// if items { }           // ❌ 错误
if !items.is_empty() { }  // ✅ 显式检查

let name = "";
// if name { }             // ❌ 错误
if !name.is_empty() { }    // ✅ 显式检查
}

循环与迭代

for 循环

# 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"] {             // 直接迭代
    println!("{}", item);
}

for (i, item) in ["a", "b", "c"].iter().enumerate() {  // 使用 enumerate()
    println!("{}: {}", i, item);
}

// HashMap 迭代
use std::collections::HashMap;
let map = HashMap::from([("x", 1), ("y", 2)]);
for (key, value) in &map {                // & 符号用于借用 map
    println!("{} = {}", key, value);
}
}

范围语法 (Range Syntax)

#![allow(unused)]
fn main() {
Python:              Rust:               说明:
range(5)             0..5                左闭右开 (不含终点)
range(1, 10)         1..10               左闭右开
range(1, 11)         1..=10              全闭 (包含终点)
range(0, 10, 2)      (0..10).step_by(2)  步长 (是方法而非语法)
}

while 循环

# Python
count = 0
while count < 5:
    print(count)
    count += 1

# 无限循环
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;
}

// 无限循环 — 使用 `loop`,而非 `while true`
loop {
    let data = get_input();
    if data == "quit" {
        break;
    }
}

// loop 可以返回一个值! (这是 Rust 独有的特性)
let result = loop {
    let input = get_input();
    if let Ok(num) = input.parse::<i32>() {
        break num;  // `break` 后面带一个值 — 类似于循环的 return
    }
    println!("不是数字,请重试");
};
}

列表推导式 vs 迭代器链

# 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 — 迭代器链 (.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();

// 这些是惰性的 (LAZY) — 在调用 .collect() 之前没有任何动作
// Python 的推导式是及早求值的 (EAGER,立即运行)
// Rust 迭代器在处理大数据集时通常更高效
}

表达式块 (Expression Blocks)

在 Rust 中,几乎一切也是表达式(或者说,可以被当作表达式)。而 Python 的 if/for 都是语句,这体现了两者的巨大差异。

# Python — if 是语句 (除了三元表达式)
if condition:
    result = "yes"
else:
    result = "no"

# 或者三元表达式 (限制为只能由一个表达式组成):
result = "yes" if condition else "no"
#![allow(unused)]
fn main() {
// Rust — if 是一个表达式 (返回一个值)
let result = if condition { "yes" } else { "no" };

// 块也是表达式 — 没有分号的最后一行即为返回值 
let value = {
    let x = 5;
    let y = 10;
    x + y    // 没有分号 → 这就是这个代码块的值 (15)
};

// match 也是表达式 
let description = match temperature {
    t if t > 100 => "开水",
    t if t > 50 => "烫手",
    t if t > 20 => "温水",
    _ => "凉水",
};
}

下图展示了 Python 以“语句”为中心与 Rust 以“表达式”为中心的控制流核心区别:

flowchart LR
    subgraph Python ["Python — 语句"]
        P1["if condition:"] --> P2["result = 'yes'"]
        P1 --> P3["result = 'no'"]
        P2 --> P4["后续使用 result"]
        P3 --> P4
    end
    subgraph Rust ["Rust — 表达式"]
        R1["let result = if cond"] --> R2["{ 'yes' }"]
        R1 --> R3["{ 'no' }"]
        R2 --> R4["值被直接返回"]
        R3 --> R4
    end
    style Python fill:#ffeeba
    style Rust fill:#d4edda

分号规则:在 Rust 块中,没有分号 的最后一行表达式会被当作该块的返回值。加上分号则使其变成了一个语句(其返回值是 (),即单元类型)。刚开始接触时这点可能会让 Python 开发者感到困惑 —— 它可以理解为是一种隐含的 return。


函数与函数签名

Python 函数

# Python — 类型是可选的且支持动态派发
def greet(name: str, greeting: str = "Hello") -> str:
    return f"{greeting}, {name}!"

# 支持变长位置参数 *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 函数

#![allow(unused)]
fn main() {
// Rust — 必须在签名中提供类型,不支持默认参数
fn greet(name: &str, greeting: &str) -> String {
    format!("{}, {}!", greeting, name)
}

// 不支持默认参数 — 此时可使用构建器模式或 Option
fn greet_with_default(name: &str, greeting: Option<&str>) -> String {
    let greeting = greeting.unwrap_or("Hello");
    format!("{}, {}!", greeting, name)
}

// 不支持 *args/**kwargs — 此时可使用切片或结构体代替
fn sum_all(numbers: &[i32]) -> i32 {
    numbers.iter().sum()
}

// 一等公民函数与闭包
fn apply(f: fn(i32) -> i32, x: i32) -> i32 {
    f(x)
}

let result = apply(|x| x * 2, 5);  // 10
}

返回值 (Return Values)

# Python — return 是显式的,默认返回 None
def divide(a, b):
    if b == 0:
        return None  # 或者抛出一个异常
    return a / b
#![allow(unused)]
fn main() {
// Rust — 最后一行表达式 (无分号) 即为返回值
fn divide(a: f64, b: f64) -> Option<f64> {
    if b == 0.0 {
        None              // 提前返回 (也可以显式写成 `return None;`)
    } else {
        Some(a / b)       // 这是代码块的最后一行 — 隐式返回
    }
}
}

多个返回值

# Python — 返回元组
def min_max(numbers):
    return min(numbers), max(numbers)

lo, hi = min_max([3, 1, 4, 1, 5])
#![allow(unused)]
fn main() {
// Rust — 通过元组实现多个返回值 (概念一致!)
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]);
}

方法:self vs &self vs &mut self

#![allow(unused)]
fn main() {
// 在 Python 中,`self` 永远是其所在对象的可变引用。
// 在 Rust 中,你可以根据需求选择:

impl MyStruct {
    fn new() -> Self { ... }                // 没有 self 参数 — 相当于“静态方法”
    fn read_only(&self) { ... }             // &self — 以不可变方式借用 (禁止修改)
    fn modify(&mut self) { ... }            // &mut self — 以可变方式借用 (允许修改)
    fn consume(self) { ... }                // self — 夺取所有权 (对象发生了 MOVED)
}

// Python 等效对照:
// class MyStruct:
//     @classmethod
//     def new(cls): ...                    # 不需要实例参与
//     def read_only(self): ...             # 下面这三者在 Python 中都是同一个意思:
//     def modify(self): ...                # Python self 总是可变的
//     def consume(self): ...               # Python 从不会对 self 进行“消耗”
}

练习

🏋️ 练习:使用表达式编写 FizzBuzz(点击展开)

挑战:使用 Rust 的表达式驱动 match 来实现 1..=30 的 FizzBuzz。每个数字应当打印 “Fizz”、“Buzz”、“FizzBuzz” 或数字本身。请使用 match (n % 3, n % 5) 作为表达式来处理逻辑。

🔑 答案
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}");
    }
}

核心要点: match 是一个能够返回结果值的表达式 — 无需编写多层冗余的 if/elif/else 链。通配符 _ 的用法等效于 Python 的 case _: 缺省分支。


5. 数据结构与集合

English Original

元组与解构

你将学到: Rust 元组与 Python 元组的对比、数组与切片、结构体 (Rust 中对类的一种替代实现)、Vec<T> 与 list、HashMap<K,V> 与 dict,以及用于领域建模的新类型模式 (newtype pattern)。

难度: 🟢 初级

Python 元组

# Python — 元组是不可变的序列
point = (3.0, 4.0)
x, y = point                    # 解包 (Unpacking)
print(f"x={x}, y={y}")

# 元组可以保存混合类型
record = ("Alice", 30, True)
name, age, active = record

# 为了清晰起见使用具名元组 (Named tuples)
from typing import NamedTuple

class Point(NamedTuple):
    x: float
    y: float

p = Point(3.0, 4.0)
print(p.x)                      # 通过名称访问

Rust 元组

#![allow(unused)]
fn main() {
// Rust — 元组是固定大小、强类型且可以保存混合类型的
let point: (f64, f64) = (3.0, 4.0);
let (x, y) = point;              // 解构 (等同于 Python 的解包)
println!("x={x}, y={y}");

// 混合类型
let record: (&str, i32, bool) = ("Alice", 30, true);
let (name, age, active) = record;

// 通过索引访问 (与 Python 不同,Rust 使用 .0 .1 .2 语法)
let first = record.0;            // "Alice"
let second = record.1;           // 30

// Python写法: record[0]
// Rust写法:   record.0      ← 注意是“点+索引”,而非“中括号+索引”
}

应该在何时使用元组 vs 结构体

#![allow(unused)]
fn main() {
// 元组:适用于快速组合、函数返回多个值、临时值
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]);

// 结构体:具名字段、意图明确、可关联方法
struct Point { x: f64, y: f64 }

// 经验法则:
// - 2 到 3 个相同类型的字段 → 元组即可
// - 需要具名字段来提高可读性 → 使用结构体
// - 需要关联方法 → 使用结构体
// (这与 Python 中选择 tuple vs namedtuple vs dataclass 的建议是一致的)
}

数组与切片

Python 列表 vs Rust 数组

# Python — 列表 (list) 是动态且异构的
numbers = [1, 2, 3, 4, 5]       # 可增长、缩小、保存混合类型
numbers.append(6)
mixed = [1, "two", 3.0]         # 允许混合类型
#![allow(unused)]
fn main() {
// Rust 在“固定大小”与“动态”之间定义了两个概念:

// 1. 数组 (Array) — 固定大小且在栈上分配 (Python 中无等价概念)
let numbers: [i32; 5] = [1, 2, 3, 4, 5]; // 长度也是类型的一部分!
// numbers.push(6);  // ❌ 数组不能增长

// 使用相同的值初始化所有元素:
let zeros = [0; 10];            // [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]

// 2. 切片 (Slice) — 数组或 Vec 的视图 (类似 Python 的切片,但是一种借用)
let slice: &[i32] = &numbers[1..4]; // [2, 3, 4] — 这是一个引用而非拷贝!

// Python: numbers[1:4] 会创建一个全新的列表 (发生了数据拷贝)
// Rust:   &numbers[1..4] 会创建一个视图 (无拷贝,无内存分配)
}

实践对比

# Python 切片 — 会创建拷贝
data = [10, 20, 30, 40, 50]
first_three = data[:3]          # 新列表: [10, 20, 30]
last_two = data[-2:]            # 新列表: [40, 50]
reversed_data = data[::-1]      # 新列表: [50, 40, 30, 20, 10]
#![allow(unused)]
fn main() {
// Rust 切片 — 会创建视图 (引用)
let data = [10, 20, 30, 40, 50];
let first_three = &data[..3];         // &[i32] 视图: [10, 20, 30]
let last_two = &data[3..];            // &[i32] 视图: [40, 50]

// 不支持负数索引 — 需要使用 .len()
let last_two = &data[data.len()-2..]; // &[i32] 视图: [40, 50]

// 反转:需要使用迭代器并进行 collect
let reversed: Vec<i32> = data.iter().rev().copied().collect();
}

结构体 vs 类 (Structs vs Classes)

Python 类

# Python — 带有 __init__、方法与各种特性的类
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 结构体

// Rust — 结构体 + impl 实现块 (不支持继承!)
#[derive(Debug, Clone)]
struct Rectangle {
    width: f64,
    height: f64,
}

impl Rectangle {
    // “构造函数” — 关联函数 (没有 self 参数)
    fn new(width: f64, height: f64) -> Self {
        Rectangle { width, height }   // 当名称一致时使用简写语法
    }

    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 的 __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 对象 (堆分配)"]
        PH["PyObject 头部\n(引用计数 + 类型指针)"] --> PW["width: float 对象"]
        PH --> PHT["height: float 对象"]
        PH --> PD["__dict__ 字典"]
    end
    subgraph Rust ["Rust 结构体 (栈分配)"]
        RW["width: f64\n(8 字节)"] --- RH["height: f64\n(8 字节)"]
    end
    style Python fill:#ffeeba
    style Rust fill:#d4edda

内存洞见:Python 的 Rectangle 对象包含一个 56 字节的头部信息 + 独立在堆上分配的浮点对对象。而 Rust 的 Rectangle 在栈上正好只占 16 字节 —— 没有间接寻址,也没有 GC (垃圾回收) 的压力。

📌 延伸阅读: 第十章:Trait 与泛型 涵盖了如何为结构体实现 Display、Debug 等 Trait,以及如何进行运算符重载。

关键映射:Python 魔术方法 → Rust Trait

PythonRust用途
__str__impl Display生成对人类可读的字符串
__repr__#[derive(Debug)]调试信息的展示
__eq__#[derive(PartialEq)]相等性比较
__hash__#[derive(Hash)]可生成哈希值 (作为 map 的键/用于 HashSet)
__lt__, __le__, 等#[derive(PartialOrd, Ord)]大小比较 (排序)
__add__impl Add运算符 +
__iter__impl Iterator迭代逻辑
__len__.len() 方法获取长度
__enter__/__exit__RAII + impl Drop自动化清理;在 Rust 中没有上下文管理器的直接等价物
__init__fn new() (惯用名称)构造逻辑
__getitem__impl Index使用 [] 进行索引访问
__contains__.contains() 方法in 运算符的等效逻辑

不支持继承 — 改为组合实现

# Python — 继承
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 — Trait + 组合 (不支持继承)
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)
    }
}

// 使用 Trait 对象来实现多态 (类似 Python 的鸭子类型):
fn animal_roll_call(animals: &[&dyn Animal]) {
    for a in animals {
        println!("{}", a.speak());
    }
}
}

思维模型:Python 说“继承行为”。Rust 说“实现契约”。 两者的效果相似,但 Rust 避免了多重继承带来的菱形继承问题和脆弱基类风险。


Vec vs list

Vec<T> 是 Rust 中可增长、堆分配的数组 —— 它是最接近 Python list 的概念。

创建 Vector

# 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! 宏 (类似列表字面量)
let empty: Vec<i32> = Vec::new();        // 空 Vec (需要类型注解)
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]
}

常用操作

# Python 列表操作
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()                     # 原地排序
sorted_copy = sorted(nums)     # 返回已排序的新列表
nums.reverse()                  # 原地反转
contains = 3 in nums           # True
index = nums.index(3)          # 第一个 3 的索引
#![allow(unused)]
fn main() {
// Rust Vec 操作
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();                           // 原地排序
let mut sorted_copy = nums.clone();
sorted_copy.sort();                    // 通过克隆来实现返回新列表
nums.reverse();                        // 原地反转
let contains = nums.contains(&3);      // true
let index = nums.iter().position(|&x| x == 3); // Some(index) 或 None
}

快速对照表

PythonRust说明
lst.append(x)vec.push(x)
lst.extend(other)vec.extend(other)
lst.pop()vec.pop()返回 Option<T>
lst.insert(i, x)vec.insert(i, x)
lst.remove(x)vec.iter().position(|v| v == &x).map(|i| vec.remove(i))指移除第一个匹配项
del lst[i]vec.remove(i)返回被移除的元素
len(lst)vec.len()
x in lstvec.contains(&x)
lst.sort()vec.sort()
sorted(lst)克隆后排序,或使用迭代器
lst[i]vec[i]如果索引越界会发生恐慌 (Panic)
lst.get(i, default)vec.get(i)返回 Option<&T>
lst[1:3]&vec[1..3]返回一个切片 (无拷贝)

HashMap vs dict

HashMap<K, V> 是 Rust 的哈希映射 —— 等同于 Python 的 dict。

创建 HashMap

# 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();
}

常用操作

# Python dict 操作
d = {"a": 1, "b": 2}
d["c"] = 3                      # 插入
val = d["a"]                     # 1 (如果缺失会触发 KeyError)
val = d.get("z", 0)             # 0 (缺失则返回默认值)
del d["b"]                       # 移除
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 操作
use std::collections::HashMap;

let mut d = HashMap::new();
d.insert("a", 1);
d.insert("b", 2);
d.insert("c", 3);                       // 插入或覆盖

let val = d["a"];                        // 1 (如果缺失则发生恐慌)
let val = d.get("z").copied().unwrap_or(0); // 0 (安全访问)
d.remove("b");                          // 移除
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 的 setdefault / defaultdict 模式
let mut word_count: HashMap<&str, i32> = HashMap::new();
for word in words {
    *word_count.entry(word).or_insert(0) += 1;
}
}

快速对照表

PythonRust说明
d[key] = vald.insert(key, val)返回 Option<V> (旧值)
d[key]d[&key]缺失则发生恐慌
d.get(key)d.get(&key)返回 Option<&V>
d.get(key, default)d.get(&key).unwrap_or(&default)
key in dd.contains_key(&key)
del d[key]d.remove(&key)返回 Option<V>
d.keys()d.keys()迭代器
d.values()d.values()迭代器
d.items()d.iter()(&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

其他集合

PythonRust说明
set()HashSet<T>use std::collections::HashSet;
collections.dequeVecDeque<T>use std::collections::VecDeque;
heapqBinaryHeap<T>默认是大顶堆 (Max-heap)
collections.OrderedDictIndexMap (crate)默认的 HashMap 不保证顺序
sortedcontainers.SortedListBTreeSet<T> / BTreeMap<K,V>基于树结构,已排序

练习

🏋️ 练习:词频统计器(点击展开)

挑战:编写一个函数,接受一段 &str 类型的句子,并返回一个存储词频的 HashMap<String, usize>(不区分大小写)。在 Python 中,这等效于 Counter(s.lower().split())。请用 Rust 实现它。

🔑 答案
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}");
    }
}

核心要点: HashMap::entry().or_insert() 相当于 Python 中的 defaultdict 或 Counter。由于 or_insert 返回的是 &mut usize,所以需要使用 * 进行解引用操作。


6. 枚举与模式匹配

English Original

代数数据类型 vs 联合类型

你将学到: 带有数据的 Rust 枚举 (enum) 与 Python Union 类型的对比、穷尽式匹配 match 与 match/case 的区别、作为 None 在编译期替代方案的 Option<T>,以及守卫模式 (guard patterns)。

难度: 🟡 中级

Python 3.10 引入了 match 语句和类型联合 (type unions)。而 Rust 的枚举则走得更远 —— 每个变体 (variant) 都可以携带不同的数据,且编译器会确保你处理了每一种情况。

Python 联合类型与 Match

# Python 3.10+ — 结构化模式匹配
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]  # 类型别名

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
        # 如果你漏掉了一个案例,编译器不会发出警告!
        # 如果增加了一个新形状?你只能在代码库中全局搜索并在心里默默祈祷能找全所有的 match 代码块。

Rust 枚举 — 携带数据的变体

#![allow(unused)]
fn main() {
// Rust — 枚举变体可以携带数据,编译器强制要求穷尽式匹配
enum Shape {
    Circle(f64),                // Circle 携带半径数据
    Rectangle(f64, f64),        // Rectangle 携带宽、高数据
    Triangle { base: f64, height: f64 }, // 也可以使用具名字段
}

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,
        // ❌ 如果你增加了 Shape::Pentagon 但在这里忘了处理它,
        //    编译器会拒绝构建。无需全局搜索,编译器会告诉你。
    }
}
}

关键洞见:Rust 的 match 是穷尽式 (Exhaustive) 的 —— 编译器会验证你是否处理了每一个变体。在枚举中增加一个新变体,编译器会准确地告诉你哪些 match 块需要更新。Python 的 match 则没有这种保障。

枚举替代了多种 Python 模式

# Python — 几种可以被 Rust 枚举替代的模式:

# 1. 字符串常量
STATUS_PENDING = "pending"
STATUS_ACTIVE = "active"
STATUS_CLOSED = "closed"

# 2. Python Enum (不带数据)
from enum import Enum
class Status(Enum):
    PENDING = "pending"
    ACTIVE = "active"
    CLOSED = "closed"

# 3. 标签联合 (类 + 类型字段)
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 — 一个枚举就能涵盖上述所有甚至更多场景

// 1. 简单枚举 (类似 Python 的 Enum)
enum Status {
    Pending,
    Active,
    Closed,
}

// 2. 携带数据的枚举 (标签联合 — 类型安全!)
enum Message {
    Text(String),
    Image { url: String, width: u32, height: u32 },
    Quit,                    // 不带数据
    Move { x: i32, y: i32 },
}
}
flowchart TD
    E["enum Message"] --> T["Text(String)\n🏷️ 标签=0 + 字符串数据"]
    E --> I["Image { url, width, height }\n🏷️ 标签=1 + 3 个字段"]
    E --> Q["Quit\n🏷️ 标签=2 + 无数据"]
    E --> M["Move { x, y }\n🏷️ 标签=3 + 2 个字段"]
    style E fill:#d4edda,stroke:#28a745
    style T fill:#fff3cd
    style I fill:#fff3cd
    style Q fill:#fff3cd
    style M fill:#fff3cd

内存洞见:Rust 的枚举是“标签联合 (tagged unions)” —— 编译器会存储一个判别标签 (discriminant tag) + 足够容纳最大变体的空间。Python 的等效实现 (Union[str, dict, None]) 则没有这种紧凑的表示方式。

📌 延伸阅读: 第九章:错误处理 大量使用了枚举 —— Result<T, E> 和 Option<T> 其实就是配合 match 使用的枚举。

#![allow(unused)]
fn main() {
fn process(msg: &Message) {
    match msg {
        Message::Text(content) => println!("文字内容: {content}"),
        Message::Image { url, width, height } => {
            println!("图片: {url} ({width}x{height})")
        }
        Message::Quit => println!("正在退出"),
        Message::Move { x, y } => println!("移动到 ({x}, {y})"),
    }
}
}

穷尽式模式匹配 (Exhaustive Pattern Matching)

Python 的 match — 并非穷尽

# Python — 通配符案例是可选的,编译器无法提供帮助
def describe(value):
    match value:
        case 0:
            return "zero"
        case 1:
            return "one"
        # 如果你忘了设置默认值,Python 会静默地返回 None。
        # 不会有任何警告或错误。

describe(42)  # 返回 None — 这是一个潜在的静默 bug

Rust 的 match — 由编译器强制要求

#![allow(unused)]
fn main() {
// Rust — 必须处理每一种可能的情况
fn describe(value: i32) -> &'static str {
    match value {
        0 => "zero",
        1 => "one",
        // ❌ 编译错误:非穷尽模式:未涵盖 `i32::MIN..=-1_i32` 
        //    与 `2_i32..=i32::MAX` 
        _ => "other",   // _ = 通配符 (对于数值类型是必需的)
    }
}

// 对于枚举,不需要通配符 —— 编译器了解其所有的变体:
enum Color { Red, Green, Blue }

fn color_hex(c: Color) -> &'static str {
    match c {
        Color::Red => "#ff0000",
        Color::Green => "#00ff00",
        Color::Blue => "#0000ff",
        // 无需 _ — 所有变体都已覆盖
        // 如果以后增加了 Color::Yellow → 编译器会在此处直接报错
    }
}
}

模式匹配特性

#![allow(unused)]
fn main() {
// 多个值 (类似 Python 的 case 1 | 2 | 3:)
match value {
    1 | 2 | 3 => println!("小"),
    4..=9 => println!("中"),    // 范围模式
    _ => println!("大"),
}

// 守卫 (类似 Python 的 case x if x > 0:)
match temperature {
    t if t > 100 => println!("沸腾中"),
    t if t < 0 => println!("结冰中"),
    t => println!("正常: {t}°"),
}

// 嵌套解构
let point = (3, (4, 5));
match point {
    (0, _) => println!("在 y 轴上"),
    (_, (0, _)) => println!("y=0"),
    (x, (y, z)) => println!("x={x}, y={y}, z={z}"),
}
}

用于“None 安全”的 Option

Option<T> 是针对 Python 开发者最重要的 Rust 枚举。它提供了一个类型安全的方式来替代 None。

Python 的 None

# Python — None 作为一个值可以出现在任何地方
def find_user(user_id: int) -> dict | None:
    users = {1: {"name": "Alice"}}
    return users.get(user_id)

user = find_user(999)
# user 是 None — 但没有什么强迫你一定要检查!
print(user["name"])  # 💥 运行时产生 TypeError

Rust Option

#![allow(unused)]
fn main() {
// Rust — Option<T> 会强制你处理 None 的情况
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 是 Option<User> — 如果不处理 None 你就无法使用它

// 方式 1: match
match find_user(999) {
    Some(user) => println!("找到: {}", user.name),
    None => println!("未找到"),
}

// 方式 2: if let (类似 Python 的 if (x := expr) is not None)
if let Some(user) = find_user(1) {
    println!("找到: {}", user.name);
}

// 方式 3: unwrap_or
let name = find_user(999)
    .map(|u| u.name)
    .unwrap_or_else(|| "未知".to_string());

// 方式 4: ? 运算符 (仅用于返回 Option 的函数内部)
fn get_user_name(id: i64) -> Option<String> {
    let user = find_user(id)?;     // 如果没找到则提前返回 None
    Some(user.name)
}
}

Option 常用方法 — Python 等效项

模式PythonRust
检查是否存在if x is not None:if let Some(x) = opt {
默认值x or defaultopt.unwrap_or(default)
延迟计算默认值x or compute()opt.unwrap_or_else(|| compute())
存在时执行转换f(x) if x else Noneopt.map(f)
链式查找x and x.attr and x.attr.method()opt.and_then(|x| x.method())
为 None 时直接崩溃无法事前预防opt.unwrap() (发生恐慌) 或 opt.expect("自定义信息")
获取或抛出错误x if x else raiseopt.ok_or(Error)?

练习

🏋️ 练习:几何形状面积计算器(点击展开)

挑战:定义一个枚举 Shape,包含三个变体:Circle(f64) (半径)、Rectangle(f64, f64) (宽、高) 和 Triangle(f64, f64) (底、高)。使用 match 为其实现一个 fn area(&self) -> f64 方法。分别创建这三种形状并打印它们的面积。

🔑 答案
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!("面积为: {:.2}", shape.area());
    }
}

核心要点: Rust 的枚举替代了 Python 的 Union[Circle, Rectangle, Triangle] 以及 isinstance() 检查。编译器会保证你处理了每一个变体 —— 如果增加了一个新形状但没有更新 area() 方法,代码将无法通过编译。


7. 所有权与借用

English Original

理解所有权

你将学到: 为什么 Rust 拥有所有权机制(没有 GC!)、移动语义与 Python 引用计数的对比、借用(& 和 &mut)、生命周期基础,以及智能指针(Box、Rc、Arc)。

难度: 🟡 中级

这是对 Python 开发者来说最具挑战性的概念。在 Python 中,你几乎从不需要考虑谁“拥有”数据 —— 垃圾回收器(GC)会处理好一切。而在 Rust 中,每个值都有且仅有一个所有者,编译器会在编译期对此进行追踪。

Python:处处皆是共享引用

# Python — 一切皆是引用,GC 负责清理
a = [1, 2, 3]
b = a              # b 和 a 指向同一个列表
b.append(4)
print(a)            # [1, 2, 3, 4] — 惊喜吗?a 也变了
# 谁拥有这个列表?a 和 b 都在引用它。
# 当没有引用剩余时,垃圾回收器会释放它。
# 你平时根本不用考虑这些。

Rust:单一所有权

#![allow(unused)]
fn main() {
// Rust — 每个值都有且仅有一个所有者
let a = vec![1, 2, 3];
let b = a;           // 所有权从 a “移动”(MOVE)到了 b
// println!("{:?}", a); // ❌ 编译错误:值在发生移动后被再次使用

// a 不再存在。b 是唯一的所有者。
println!("{:?}", b); // ✅ [1, 2, 3]

// 当 b 离开作用域时,Vec 会被释放。这是确定性的,且无需 GC。
}

所有权三条铁律

#![allow(unused)]
fn main() {
1. 每个值都有且仅有一个被称为“所有者”的变量。
2. 当所有者离开作用域,值就会被丢弃(释放)。
3. 所有权可以转移(移动),但不能被复制(除非显式使用 Clone)。
}

移动语义 — 给 Python 开发者带来的最大冲击

# Python — 赋值操作拷贝的是引用,而非数据
def process(data):
    data.append(42)
    # 原始列表被修改了!

my_list = [1, 2, 3]
process(my_list)
print(my_list)       # [1, 2, 3, 42] — 被 process 函数修改了!
#![allow(unused)]
fn main() {
// Rust — 传递给函数会“移动”所有权(对于非 Copy 类型)
fn process(mut data: Vec<i32>) -> Vec<i32> {
    data.push(42);
    data  // 必须将其返回,才能把所有权交还!
}

let my_vec = vec![1, 2, 3];
let my_vec = process(my_vec);  // 所有权移入函数,随后又移出
println!("{:?}", my_vec);      // [1, 2, 3, 42]

// 或者更优雅的做法 — 借用而非移动:
fn process_borrowed(data: &mut Vec<i32>) {
    data.push(42);
}

let mut my_vec = vec![1, 2, 3];
process_borrowed(&mut my_vec);  // 暂时“借”出去
println!("{:?}", my_vec);       // [1, 2, 3, 42] — 依然归我们所有
}

所有权可视化

Python:                              Rust:

  a ──────┐                           a ──→ [1, 2, 3]
           ├──→ [1, 2, 3]
  b ──────┘                           执行 let b = a; 后:

  (a 和 b 共享同一个对象)              a  (已失效,发生了移动)
  (引用计数 = 2)                       b ──→ [1, 2, 3]
                                       (仅由 b 拥有数据)

  del a → 引用计数 = 1                 drop(b) → 数据被释放
  del b → 引用计数 = 0 → 释放           (确定性,无 GC)
stateDiagram-v2
    state "Python (引用计数)" as PY {
        [*] --> a_owns: a = [1,2,3]
        a_owns --> shared: b = a
        shared --> b_only: del a (引用计数 2→1)
        b_only --> freed: del b (引用计数 1→0)
        note right of shared: a 和 b 指向\n同一个对象
    }
    state "Rust (所有权移动)" as RS {
        [*] --> a_owns2: let a = vec![1,2,3]
        a_owns2 --> b_owns: let b = a (移动)
        b_owns --> freed2: b 离开作用域
        note right of b_owns: 移动后 a 失效\n再次使用会导致编译错误
    }

移动语义 vs 引用计数

拷贝 vs 移动 (Copy vs Move)

#![allow(unused)]
fn main() {
// 简单数据类型(整数、浮点数、布尔值、字符)是发生了“拷贝”,而非“移动”
let x = 42;
let y = x;    // x 被拷贝给 y (两者均有效)
println!("{x} {y}");  // ✅ 42 42

// 堆分配类型 (String, Vec, HashMap) 则是发生了“移动”
let s1 = String::from("hello");
let s2 = s1;  // s1 被移动到了 s2
// println!("{s1}");  // ❌ 错误:数值在移动后被再次使用

// 如果显式拷贝堆上的数据,请使用 .clone()
let s1 = String::from("hello");
let s2 = s1.clone();  // 深拷贝
println!("{s1} {s2}");  // ✅ hello hello (两者均有效)
}

Python 开发者的思维模型

Python:                    Rust:
─────────                  ─────
int, float, bool           Copy 类型 (i32, f64, bool, char)
→ 对不可变对象的共享引用    → 在赋值时发生逐位拷贝 
  (并非真正的拷贝)          (始终是互相独立的数值)
                           (注意:Python 会缓存小整数;Rust 的拷贝则是始终可预测的)

list, dict, str            Move 类型 (Vec, HashMap, String)
→ 共享引用                  → 所有权转移 (行为不同!)
→ 由 GC 负责清理            → 由所有者丢弃数据 
→ 使用 list(x) 进行克隆     → 使用 x.clone() 进行克隆
   或使用 copy.deepcopy(x)

当 Python 的共享模型引发 Bug 时

# 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          # 别名,而非拷贝
unique = remove_duplicates(alias)
# original 依然是 [1, 2, 2, 3, 3, 3] — 仅仅是因为我们没有进行变动操作
# 如果 remove_duplicates 修改了输入,那么 original 也会被波及。
#![allow(unused)]
fn main() {
use std::collections::HashSet;

// Rust — 所有权机制防止了意外的别名错误
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); // 借用 — 无法修改原始数据
// 原始数据 original 保证不会改变 — 编译器通过 & 符号防止了意外的改动
}

借用与生命周期

借入 (Borrowing) = 借一本书

#![allow(unused)]
fn main() {
可以把所有权想象成一本实体书:

Python:  每个人都持有一份影印本(共享引用 + GC)
Rust:    只有一个人拥有原著。其他人可以:
         - &书     = 翻看 (不可变借用,允许多人同时观看)
         - &mut 书 = 在里面写字 (可变借用,具有排他性)
         - 书      = 把书送走 (移动)
}

借用规则

flowchart TD
    R["借用规则"] --> IMM["✅ 多个 &T\n(共享的/不可变的)"]
    R --> MUT["✅ 一个 &mut T\n(独占的/可变的)"]
    R --> CONFLICT["❌ &T + &mut T\n(严禁同时存在)"]
    IMM --> SAFE["多位读者,是安全的"]
    MUT --> SAFE2["一位作者,是安全的"]
    CONFLICT --> ERR["引发编译错误!"]
    style IMM fill:#d4edda
    style MUT fill:#d4edda
    style CONFLICT fill:#f8d7da
    style ERR fill:#f8d7da,stroke:#dc3545
#![allow(unused)]
fn main() {
// 规则 1:你可以拥有“多个不可变借用”或者“一个可变借用”(二者不可得兼)

let mut data = vec![1, 2, 3];

// 多个不可变借用 — 没问题
let a = &data;
let b = &data;
println!("{:?} {:?}", a, b);  // ✅

// 可变借用 — 必须具有排他性
let c = &mut data;
c.push(4);
// println!("{:?}", a);  // ❌ 错误:当存在可变借用时,无法再使用之前的不可变借用

// 这在编译期就防止了数据竞争!
// Python 则没有这样的对等规则 — 这也是为什么 Python 的“迭代期间修改字典”会导致运行时崩溃。
}

生命周期 (Lifetimes) — 简要引导

#![allow(unused)]
fn main() {
// 生命周期用于回答:“这个引用能存活多久?”
// 通常编译器会自动推导。你很少需要手动编写。

// 简单案例 — 编译器自动处理:
fn first_word(s: &str) -> &str {
    s.split_whitespace().next().unwrap_or("")
}
// 编译器了解:返回的 &str 会和输入的 &str 存活得一样久

// 当你需要显式生命周期(极少数情况):
fn longest<'a>(a: &'a str, b: &'a str) -> &'a str {
    if a.len() > b.len() { a } else { b }
}
// 'a 表示:“返回值的存活时间与这两个输入存活得一样久”
}

给 Python 开发者的建议:起初不用担心生命周期。当需要用到它们时,编译器会给予提示,而且 95% 的情况下编译器会自动推导。可以将生命周期标注视作你在编译器无法自行判定关系时给它的一些补全提示。


智能指针 (Smart Pointers)

在单一所有权过于受限的情况下,Rust 提供了“智能指针”。它们更接近 Python 的引用模型 —— 但需要你显式地选择使用。

#![allow(unused)]
fn main() {
// Box<T> — 堆分配,单一所有者 (类似 Python 的正常内存分配)
let boxed = Box::new(42);  // 在堆上分配的 i32

// Rc<T> — 引用计数 (类似 Python 的引用计数机制!)
use std::rc::Rc;
let shared = Rc::new(vec![1, 2, 3]);
let clone1 = Rc::clone(&shared);  // 增加引用计数
let clone2 = Rc::clone(&shared);  // 增加引用计数
// 这三个变量都指向同一个 Vec。当它们全部被丢弃时,Vec 会释放。
// 这类似于 Python 的引用计数,但 Rc 不处理循环引用 —— 
// 需要使用 Weak<T> 来打破循环(Python 的 GC 会自动处理循环引用)

// Arc<T> — 原子引用计数 (用于多线程场景下的 Rc)
use std::sync::Arc;
let thread_safe = Arc::new(vec![1, 2, 3]);
// 当跨线程共享数据时请使用 Arc (Rc 仅限单线程使用)

// RefCell<T> — 运行时借用检查 (类似 Python 的“万物皆可变”模型)
use std::cell::RefCell;
let cell = RefCell::new(42);
*cell.borrow_mut() = 99;  // 在运行时产生可变借用 (如果发生重复借用会崩溃)
}

应该在何时使用它们?

智能指针Python 类比使用场景
Box<T>正常分配大数据块、递归类型、Trait 对象
Rc<T>Python 默认引用计数单线程环境下的共享所有权
Arc<T>线程安全的引用计数多线程环境下的共享所有权
RefCell<T>Python 的“直接修改”模式内部可变性 (安全出口)
Rc<RefCell<T>>Python 的常规对象模型共享且可变的数据(如:图结构)

关键洞见:Rc<RefCell<T>> 赋予了你类似 Python 的语义(共享的、可变的数据),但你必须要显式地选择这种方式。Rust 默认的(拥有的、移动的)模式更快,且避开了引用计数的开销。对于包含循环引用的图状结构,请使用 Weak<T> 来打破引用环 —— 与 Python 不同,Rust 的 Rc 并不包含循环引用收集器 (Cycle Collector)。

📌 延伸阅读: 第 13 章:并发 涵盖了多线程共享状态所需的 Arc<Mutex<T>>。


练习

🏋️ 练习:找出借用检查器错误(点击展开)

挑战:以下代码中包含 3 处借用检查器错误。请找出每一处,并尝试在不使用 .clone() 的情况下修复它们:

fn main() {
    let mut names = vec!["Alice".to_string(), "Bob".to_string()];
    let first = &names[0];
    names.push("Charlie".to_string());
    println!("第一个名字: {first}");

    let greeting = make_greeting(names[0]);
    println!("{greeting}");
}

fn make_greeting(name: String) -> String {
    format!("你好, {name}!")
}
🔑 答案
fn main() {
    let mut names = vec!["Alice".to_string(), "Bob".to_string()];
    let first = &names[0];
    println!("第一个名字: {first}"); // 在发生变动前使用借用内容
    names.push("Charlie".to_string()); // 现在安全了 — 因为之前没有现存的不可变借用

    let greeting = make_greeting(&names[0]); // 传递引用,而非所有权
    println!("{greeting}");
}

fn make_greeting(name: &str) -> String { // 接收 &str,而非 String
    format!("你好, {name}!")
}

已修复的错误:

  1. 不可变借用 + 数据变动: first 借用了 names,紧接着 push 操作修改了它。修复方法:在执行 push 之前使用 first。
  2. 从 Vec 中移出数据: names[0] 试图从 Vec 中移出一个 String(这是不被允许的)。修复方法:使用 &names[0] 来进行借用。
  3. 函数夺取了所有权: 原本的 make_greeting(String) 会消耗该值。修复方法:改用接收 &str。

8. Crates 与模块

English Original

Rust 模块 vs Python 包

你将学到: mod 和 use 与 import 的对比、可见性 (pub) 与 Python 基于约定的私有化、Cargo.toml 与 pyproject.toml、crates.io 与 PyPI,以及工作空间 (workspaces) 与单仓库 (monorepos) 的管理。

难度: 🟢 初级

Python 模块系统

# Python — 文件即模块,带有 __init__.py 的目录即为包

# myproject/
# ├── __init__.py          # 使其成为一个包
# ├── main.py
# ├── utils/
# │   ├── __init__.py      # 使 utils 成为子包
# │   ├── helpers.py
# │   └── validators.py
# └── models/
#     ├── __init__.py
#     ├── user.py
#     └── product.py

# 导入方式:
from myproject.utils.helpers import format_name
from myproject.models.user import User
import myproject.utils.validators as validators

Rust 模块系统

#![allow(unused)]
fn main() {
// Rust — mod 声明创建了模块树,文件则提供了具体内容

// src/
// ├── main.rs             # Crate 根节点 — 用于声明模块
// ├── utils/
// │   ├── mod.rs           # 模块声明 (等效于 __init__.py)
// │   ├── helpers.rs
// │   └── validators.rs
// └── models/
//     ├── mod.rs
//     ├── user.rs
//     └── product.rs

// 在 src/main.rs 中:
mod utils;       // 告诉 Rust 去 src/utils/mod.rs 寻找
mod models;      // 告诉 Rust 去 src/models/mod.rs 寻找

use utils::helpers::format_name;
use models::user::User;

// 在 src/utils/mod.rs 中:
pub mod helpers;      // 声明并导出 helpers.rs
pub mod validators;   // 声明并导出 validators.rs
}
graph TD
    A["main.rs<br/>(Crate 根节点)"] --> 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 等效对照:可以将 mod.rs 视作 __init__.py —— 它负责声明该模块导出的内容。而 Crate 的根节点(main.rs / lib.rs)则相当于顶层包的 __init__.py。

核心差异

概念PythonRust
模块 = 文件✅ 自动生效必须通过 mod 声明
包 = 目录__init__.pymod.rs (或新版目录同名文件)
默认公开✅ 所有内容均为公开❌ 默认均为私有
标记公开使用 _前缀 约定使用 pub 关键字
导入语法from x import yuse x::y;
通配符导入from x import *use x::*; (不推荐使用)
相对导入from . import siblinguse super::sibling;
重新导出使用 __all__ 或显式操作pub use inner::Thing;

可见性 — 默认私有

# Python — “大家都是成年人”
class User:
    def __init__(self):
        self.name = "Alice"       # 公开 (按约定)
        self._age = 30            # “私有” (约定:单下划线)
        self.__secret = "shhh"    # 名字修饰 (并非真正的私有)

# 没什么能阻止你访问 _age 甚至是 __secret
print(user._age)                  # 运行正常
print(user._User__secret)        # 也能跑通 (名字修饰)
#![allow(unused)]
fn main() {
// Rust — 私有性由编译器强制执行
pub struct User {
    pub name: String,      // 公开 — 任何人都可以访问
    age: i32,              // 私有 — 仅限当前模块访问
}

impl User {
    pub fn new(name: &str, age: i32) -> Self {
        User { name: name.to_string(), age }
    }

    pub fn age(&self) -> i32 {   // 公开的 Getter
        self.age
    }

    fn validate(&self) -> bool { // 私有方法
        self.age > 0
    }
}

// 在模块外部:
let user = User::new("Alice", 30);
println!("{}", user.name);        // ✅ 公开
// println!("{}", user.age);      // ❌ 编译错误:字段是私有的
println!("{}", user.age());       // ✅ 公开方法 (Getter)
}

Crate vs PyPI 包

Python 包 (PyPI)

# Python
pip install requests           # 从 PyPI 安装
pip install "requests>=2.28"   # 使用版本约束
pip freeze > requirements.txt  # 锁定版本
pip install -r requirements.txt # 复现环境

Rust Crate (crates.io)

# Rust
cargo add reqwest              # 从 crates.io 安装(并添加到 Cargo.toml)
cargo add [email protected]         # 使用版本约束
# Cargo.lock 会自动生成 — 无需手动操作
cargo build                    # 下载并编译依赖

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"] }  # 支持特性标志 (Features)
reqwest = { version = "0.12", features = ["json"] }
tokio = { version = "1", features = ["full"] }
log = "0.4"

[dev-dependencies]
mockall = "0.13"

Python 开发者必备 Crate 对照表

Python 库Rust Crate用途
requestsreqwestHTTP 客户端
json (标准库)serde_jsonJSON 解析
pydanticserde序列化/验证
pathlibstd::path (标准库)路径处理
os / shutilstd::fs (标准库)文件操作
reregex正则表达式
loggingtracing / log日志记录
click / argparseclap命令行参数解析
asynciotokio异步运行时
datetimechrono日期与时间
pytest内置支持 + rstest测试
dataclasses#[derive(...)]数据结构
typing.ProtocolTraits结构化类型
subprocessstd::process (标准库)运行外部命令
sqlite3rusqliteSQLite
sqlalchemydiesel / sqlxORM / SQL 工具包
fastapiaxum / actix-webWeb 框架

工作空间 (Workspaces) vs 单仓库 (Monorepos)

Python 单仓库 (典型结构)

# Python 单仓库 (有多种方案,但缺乏标准)
myproject/
├── pyproject.toml           # 根项目配置
├── packages/
│   ├── core/
│   │   ├── pyproject.toml   # 每个包拥有各自的配置
│   │   └── src/core/...
│   ├── api/
│   │   ├── pyproject.toml
│   │   └── src/api/...
│   └── cli/
│       ├── pyproject.toml
│       └── src/cli/...
# 所用工具:poetry workspaces, pip -e ., uv workspaces — 尚无统一标准

Rust 工作空间 (Workspace)

# Rust — 位于根目录的 Cargo.toml
[workspace]
members = [
    "core",
    "api",
    "cli",
]

# 在整个工作空间共享的依赖项
[workspace.dependencies]
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
# Rust 工作空间结构 — 具有统一标准,且内置于 Cargo 中
myproject/
├── Cargo.toml               # 工作空间根配置
├── Cargo.lock               # 所有 Crate 共用同一个锁定文件
├── core/
│   ├── Cargo.toml            # [dependencies] serde.workspace = true
│   └── src/lib.rs
├── api/
│   ├── Cargo.toml
│   └── src/lib.rs
└── cli/
    ├── Cargo.toml
    └── src/main.rs
# 工作空间常用命令
cargo build                  # 构建所有内容
cargo test                   # 测试所有内容
cargo build -p core          # 仅构建 core crate
cargo test -p api            # 仅测试 api crate
cargo clippy --all           # 对所有内容进行 lint 检查

关键洞见:Rust 工作空间是核心功能,直接内置于 Cargo 中。而 Python 的单仓库则需要第三方工具(如 poetry、uv、pants)且支持程度各异。在 Rust 工作空间中,所有 Crate 共享同一个 Cargo.lock,确保了整个项目中依赖版本的一致性。


练习

🏋️ 练习:模块可见性(点击展开)

挑战:根据以下模块结构,预测哪些代码行可以编译通过,哪些不能:

mod kitchen {
    fn secret_recipe() -> &'static str { "42 种香料" }
    pub fn menu() -> &'static str { "今日特供" }

    pub mod staff {
        pub fn cook() -> String {
            format!("正在使用 {} 烹饪", super::secret_recipe())
        }
    }
}

fn main() {
    println!("{}", kitchen::menu());             // A 行
    println!("{}", kitchen::secret_recipe());     // B 行
    println!("{}", kitchen::staff::cook());       // C 行
}
🔑 答案
  • A 行: ✅ 编译通过 — menu() 是 pub 公开的
  • B 行: ❌ 编译错误 — secret_recipe() 对 kitchen 外部来说是私有的
  • C 行: ✅ 编译通过 — staff::cook() 是 pub 公开的,且 cook() 可以通过 super:: 访问 secret_recipe()(子模块可以访问其父模块的私有条目)

核心要点: 在 Rust 中,子模块可以看见父模块的私有内容(类似于 Python 的 _private 约定,但由编译器强制执行)。而外部人员则无法看见。这与 Python 不同,Python 的 _private 仅是一个提示建议。


9. 错误处理

English Original

异常 vs Result

你将学到: Result<T, E> 与 try/except 的对比、简洁传播错误的 ? 运算符、使用 thiserror 定义自定义错误类型、面向应用程序的 anyhow 库,以及为什么显式错误可以防止隐藏的 Bug。

难度: 🟡 中级

这是 Python 开发者面临的最大思维转变之一。Python 使用异常 (Exceptions) 来处理错误 —— 错误可以从任何地方抛出,并可以在任何地方被捕获(或者根本不被捕获)。而 Rust 使用 Result<T, E> —— 错误被视为必须显式处理的“值”。

Python 异常处理

# Python — 警告:异常可能从任何地方抛出
import json

def load_config(path: str) -> dict:
    try:
        with open(path) as f:
            data = json.load(f)     # 可能抛出 JSONDecodeError
            if "version" not in data:
                raise ValueError("缺少 version 字段")
            return data
    except FileNotFoundError:
        print(f"找不到配置文件: {path}")
        return {}
    except json.JSONDecodeError as e:
        print(f"无效的 JSON: {e}")
        return {}
    # 这段代码还可能抛出哪些异常?
    # IOError? PermissionError? UnicodeDecodeError?
    # 你无法从函数签名中看出来!

Rust 基于 Result 的错误处理

#![allow(unused)]
fn main() {
// Rust — 错误是返回值,在函数签名中清晰可见
use std::fs;
use serde_json::Value;

fn load_config(path: &str) -> Result<Value, ConfigError> {
    let contents = fs::read_to_string(path)    // 返回 Result
        .map_err(|e| ConfigError::FileError(e.to_string()))?;

    let data: Value = serde_json::from_str(&contents)  // 返回 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),
}
}

核心差异

Python:                                 Rust:
─────────                               ─────
- 错误是异常 (抛出制)                    - 错误是值 (返回制)
- 隐式控制流 (堆栈展开)                  - 显式控制流 (? 运算符)
- 签名无法体现错误类型                   - 必须在返回类型中体现错误
- 未捕获异常会在运行时导致崩溃           - 未处理的 Result 会产生编译警告(且必须处理)
- 使用 try/except 是可选的              - 处理 Result 是强制要求的
- 宽泛的 except 会捕获所有异常           - match 分支是穷尽式的

Result 的两种变体

#![allow(unused)]
fn main() {
// Result<T, E> 恰好有两个变体:
enum Result<T, E> {
    Ok(T),    // 成功 — 包含具体值 (类似 Python 的 return 返回值)
    Err(E),   // 失败 — 包含错误信息 (类似 Python 的 raise 抛出异常)
}

// 使用 Result:
fn divide(a: f64, b: f64) -> Result<f64, String> {
    if b == 0.0 {
        Err("除数不能为零".to_string())  // 类似: raise ValueError("...")
    } else {
        Ok(a / b)                       // 类似: return a / b
    }
}

// 处理 Result — 类似 try/except 但更显式:
match divide(10.0, 0.0) {
    Ok(result) => println!("结果: {result}"),
    Err(msg) => println!("错误: {msg}"),
}
}

? 运算符

? 运算符是 Rust 的一种机制,允许错误沿着调用堆栈向上传播,这与 Python 的异常抛出类似,但它是显式且清晰可见的。

Python — 隐式传播

# Python — 异常会在调用堆栈中静默传播
def read_username() -> str:
    with open("config.txt") as f:      # FileNotFoundError 向上冒泡
        return f.readline().strip()    # IOError 向上冒泡

def greet():
    name = read_username()             # 如果抛出错误,greet() 也会抛出
    print(f"你好, {name}!")           # 发生错误时跳过
# 错误传播是不可见的 — 你不得不通过阅读实现代码来了解到底会漏掉哪些异常。

Rust — 使用 ? 的显式传播

#![allow(unused)]
fn main() {
// Rust — ? 用于传播错误,且在代码和签名中均清晰可见
use std::fs;
use std::io;

fn read_username() -> Result<String, io::Error> {
    let contents = fs::read_to_string("config.txt")?;  // ? = 遇到 Err 则传播
    Ok(contents.lines().next().unwrap_or("").to_string())
}

fn greet() -> Result<(), io::Error> {
    let name = read_username()?;       // ? = 若为 Err 则直接返回该错误
    println!("你好, {name}!");        // 仅在 Ok 时执行
    Ok(())
}
}

? 的意思是:“如果这是个 Err,请立即从当前函数返回该错误。”这类似于 Python 的异常冒泡,但不同点在于:

  1. 它在代码中是可见的(你能看到 ?)。
  2. 它体现在返回类型中(Result<..., io::Error>)。
  3. 编译器会确保你在某处对其进行了处理。

使用 ? 构建链式调用

# Python — 多个可能失败的操作
def process_file(path: str) -> dict:
    with open(path) as f:                    # 可能失败
        text = f.read()                       # 可能失败
    data = json.loads(text)                   # 可能失败
    validate(data)                            # 可能失败
    return transform(data)                    # 可能失败
# 任何一环都可能抛出异常 — 且类型各异!
#![allow(unused)]
fn main() {
// Rust — 同样的链式调用,但每步都是显式的
fn process_file(path: &str) -> Result<Data, AppError> {
    let text = fs::read_to_string(path)?;     // ? 传播 io::Error
    let data: Value = serde_json::from_str(&text)?;  // ? 传播 serde 错误
    let validated = validate(&data)?;          // ? 传播验证错误
    let result = transform(&validated)?;       // ? 传播转换错误
    Ok(result)
}
// 每个 ? 都是一个潜在的提前返回点 — 且所有点都是可见的!
}
flowchart TD
    A["read_to_string(path)?"] -->|Ok| B["serde_json::from_str?"] 
    A -->|Err| X["返回 Err(io::Error)"]
    B -->|Ok| C["validate(&data)?"]
    B -->|Err| Y["返回 Err(serde::Error)"]
    C -->|Ok| D["transform(&validated)?"]
    C -->|Err| Z["返回 Err(ValidationError)"]
    D -->|Ok| E["Ok(result) ✅"]
    D -->|Err| W["返回 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

每个 ? 都是退出点 — 这与 Python 的 try/except 不同,你不需要阅读文档就能一眼看出哪一行可能会抛出错误。

📌 延伸阅读: 第 15 章:迁移模式 涉及了在实际代码库中将 Python 的 try/except 模式翻译至 Rust 的具体案例。


使用 thiserror 自定义错误类型

graph TD
    AE["AppError (枚举)"] --> NF["NotFound\n{ 实体, ID }"]
    AE --> VE["Validation\n{ 字段, 消息 }"]
    AE --> IO["Io(std::io::Error)\n#[from]"]
    AE --> JSON["Json(serde_json::Error)\n#[from]"]
    IO2["std::io::Error"] -->|"通过 From 自动转换"| IO
    JSON2["serde_json::Error"] -->|"通过 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

#[from] 属性会自动生成 impl From<io::Error> for AppError。因此,? 运算符会自动将库产生的错误转换为你应用程序自己的错误。

Python 自定义异常

# Python — 自定义异常类
class AppError(Exception):
    pass

class NotFoundError(AppError):
    def __init__(self, entity: str, id: int):
        self.entity = entity
        self.id = id
        super().__init__(f"未找到 id 为 {id} 的 {entity}")

class ValidationError(AppError):
    def __init__(self, field: str, message: str):
        self.field = field
        super().__init__(f"{field} 的验证错误: {message}")

# 使用方式:
def find_user(user_id: int) -> dict:
    if user_id not in users:
        raise NotFoundError("用户", user_id)
    return users[user_id]

使用 thiserror 的 Rust 自定义错误

#![allow(unused)]
fn main() {
// Rust — 使用 thiserror 库定义错误枚举 (最流行的方法)
// 在 Cargo.toml 中添加: thiserror = "2"

use thiserror::Error;

#[derive(Debug, Error)]
enum AppError {
    #[error("未找到 id 为 {id} 的 {entity}")]
    NotFound { entity: String, id: i64 },

    #[error("{field} 的验证错误: {message}")]
    Validation { field: String, message: String },

    #[error("IO 错误: {0}")]
    Io(#[from] std::io::Error),        // 自动将 io::Error 进行转换

    #[error("JSON 错误: {0}")]
    Json(#[from] serde_json::Error),   // 自动将 serde 错误进行转换
}

// 使用方式:
fn find_user(user_id: i64) -> Result<User, AppError> {
    users.get(&user_id)
        .cloned()
        .ok_or(AppError::NotFound {
            entity: "用户".to_string(),
            id: user_id,
        })
}

// 由于使用了 #[from] 属性,? 运算符会自动执行 io::Error → AppError::Io 的转换
fn load_users(path: &str) -> Result<Vec<User>, AppError> {
    let data = fs::read_to_string(path)?;  // io::Error 会自动转为 AppError::Io
    let users: Vec<User> = serde_json::from_str(&data)?;  // 自动转为 AppError::Json
    Ok(users)
}
}

错误处理快速参考表

PythonRust说明
raise ValueError("msg")return Err(AppError::Validation {...})显式返回
try: ... except:match result { Ok(v) => ..., Err(e) => ... }每一项都必须处理
except ValueError as e:Err(AppError::Validation { .. }) =>模式匹配
raise ... from e#[from] 属性或 .map_err()错误链式连接
finally:Drop trait (自动执行)确定性清理
with open(...):基于作用域的 Drop (自动执行)RAII 模式
异常会静默向上传递? 显式向上传播始终体现于返回类型中
isinstance(e, ValueError)matches!(e, AppError::Validation {..})类型检查

练习

🏋️ 练习:解析配置项中的端口号(点击展开)

挑战:编写一个函数 parse_port(s: &str) -> Result<u16, String>,要求:

  1. 拒绝空字符串,报错信息为 "输入内容不能为空"。
  2. 将字符串解析为 u16。如果解析失败,报错信息包含原始错误:"无效数字: {原始错误信息}"。
  3. 拒绝 1024 以下的端口(特权端口),报错信息为 "端口 {n} 是特权端口"。

使用 ""、"hello"、"80" 和 "8080" 作为输入调用该函数,并打印结果。

🔑 答案
fn parse_port(s: &str) -> Result<u16, String> {
    if s.is_empty() {
        return Err("输入内容不能为空".to_string());
    }
    let port: u16 = s.parse().map_err(|e| format!("无效数字: {e}"))?;
    if port < 1024 {
        return Err(format!("端口 {port} 是特权端口"));
    }
    Ok(port)
}

fn main() {
    for input in ["", "hello", "80", "8080"] {
        match parse_port(input) {
            Ok(port) => println!("✅ {input} → {port}"),
            Err(e) => println!("❌ {input:?} → {e}"),
        }
    }
}

核心要点: ? 配合 .map_err() 是 Rust 中对 try/except ValueError as e: raise ConfigError(...) from e 模式的替代。所有的错误路径在返回类型中是一目了然的。


10. Traits 与泛型

English Original

Trait vs 鸭子类型 (Duck Typing)

你将学到: 作为显式协议的 Trait(与 Python 鸭子类型的对比)、Protocol (PEP 544) ≈ Trait、带有 where 子句的泛型类型约束、Trait 对象 (dyn Trait) 与静态分发的对比,以及常见的标准库 Trait。

难度: 🟡 中级

这是 Rust 类型系统对 Python 开发者来说最为出彩的地方。Python 的“鸭子类型”认为:“如果它走起来像鸭子,叫起来也像鸭子,那它就是只鸭子。”而 Rust 的 Trait 则认为:“我会告诉你我在编译期到底需要哪些属于‘鸭子’的行为。”

Python 鸭子类型

# Python — 鸭子类型:任何拥有对应方法的对象都能运行
def total_area(shapes):
    """适用于任何带有 .area() 方法的对象。"""
    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

# 在运行时正常工作 — 无需集成!
shapes = [Circle(5), Rectangle(3, 4)]
print(total_area(shapes))  # 90.54

# 但如果某个对象没有 .area() 方法呢?
class Dog:
    def bark(self): return "汪汪!"

total_area([Dog()])  # 💥 AttributeError: 'Dog' 对象没有 'area' 属性
# 错误发生在“运行时”,而非定义时

Rust Trait — 显式的鸭子类型

#![allow(unused)]
fn main() {
// Rust — Trait 让“鸭子”协议变得显式化
trait HasArea {
    fn area(&self) -> f64;      // 任何实现该 Trait 的类型都拥有 .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
    }
}

// Trait 约束是显式的 — 编译器在编译期进行检查
fn total_area(shapes: &[&dyn HasArea]) -> f64 {
    shapes.iter().map(|s| s.area()).sum()
}

// 使用方式:
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 {}]);  // ❌ 编译错误:Dog 未实现 HasArea
}

关键洞见:Python 的鸭子类型将错误推迟到了运行时。Rust 的 Trait 则在编译期就能捕捉到它们。同样的灵活性,但能更早地发现错误。


Protocols (PEP 544) vs Traits

Python 3.8 引入了 Protocol (PEP 544) 用于结构化子类型(structural subtyping)—— 这是 Python 中最接近 Rust Trait 的概念。

Python Protocol

# Python — Protocol (结构化类型,类似 Rust Trait)
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())

# 运行正常,因为 User 和 Product 都有 to_string() 方法
print_all([User("Alice"), Product("Widget", 9.99)])

# 但是:mypy 会检查它,而 Python 运行时并不会强制执行
# print_all([42])  # mypy 会警告,但 Python 照样运行且随后崩溃

Rust Trait (等效,但具有强制性!)

#![allow(unused)]
fn main() {
// Rust — Trait 在编译期被强制执行
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]);  // ❌ 编译错误:i32 未实现 Printable
}

特性对比表

特性Python ProtocolRust Trait
结构化类型 (Structural typing)✅ (隐式的)❌ (显式的 impl)
检查时机运行时 (或 mypy)编译期 (始终执行)
默认实现❌✅
能够为外部类型添加❌✅ (在受限范围内)
多个协议✅✅ (多个 Trait)
关联类型❌✅
泛型约束✅ (配合 TypeVar)✅ (Trait Bounds)

泛型约束

Python 中的泛型

# Python — 使用 TypeVar 定义泛型函数
from typing import TypeVar, Sequence

T = TypeVar('T')

def first(items: Sequence[T]) -> T | None:
    return items[0] if items else None

# 带有约束的 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)

带有 Trait 约束的 Rust 泛型

#![allow(unused)]
fn main() {
// Rust — 泛型与 Trait 约束
fn first<T>(items: &[T]) -> Option<&T> {
    items.first()
}

// 带有 Trait 约束 — “T 必须实现这些 Trait”
fn average<T>(items: &[T]) -> f64
where
    T: Into<f64> + Copy,   // T 必须能转为 f64 且可拷贝
{
    let sum: f64 = items.iter().map(|&x| x.into()).sum();
    sum / items.len() as f64
}

// 多个约束 — “T 必须实现 Display 且实现 Debug 且实现 Clone”
fn log_and_clone<T: std::fmt::Display + std::fmt::Debug + Clone>(item: &T) -> T {
    println!("Display: {}", item);
    println!("Debug: {:?}", item);
    item.clone()
}

// 使用 impl Trait 简写 (适用于简单情况)
fn print_it(item: &impl std::fmt::Display) {
    println!("{}", item);
}
}

泛型快速参考

PythonRust说明
TypeVar('T')<T>无约束泛型
TypeVar('T', bound=X)<T: X>有约束泛型
Union[int, str]enum 或 Trait 对象Rust 无联合类型
Sequence[T]&[T] (切片)借用的序列
Callable[[A], R]Fn(A) -> R函数 Trait
Optional[T]Option<T>内置枚举

常见的标准库 Trait

这些 Trait 相当于 Rust 版本的 Python “魔术方法” (dunder methods) —— 它们定义了类型在常见场景下的行为。

Display 与 Debug (打印输出)

#![allow(unused)]
fn main() {
use std::fmt;

// Debug — 类似 __repr__ (可以通过 #[derive] 自动生成)
#[derive(Debug)]
struct Point { x: f64, y: f64 }
// 现在可以执行: println!("{:?}", point);

// Display — 类似 __str__ (必须手动实现)
impl fmt::Display for Point {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "({}, {})", self.x, self.y)
    }
}
// 现在可以执行: println!("{}", point);
}

比较类 Trait

#![allow(unused)]
fn main() {
// PartialEq — 类似 __eq__
// Eq — 全等 (f64 实现了 PartialEq 但不是 Eq,因为 NaN != NaN)
// PartialOrd — 类似 __lt__, __le__ 等
// Ord — 全序关系

#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)]
struct Student {
    name: String,
    grade: i32,
}

// 现在 Student 实例可以进行:比较、排序、作为 HashMap 的键、克隆
let mut students = vec![
    Student { name: "小明".into(), grade: 85 },
    Student { name: "阿强".into(), grade: 92 },
];
students.sort();  // 使用 Ord — 先按名字排序,再按成绩排序 (遵循字段顺序)
}

Iterator Trait

#![allow(unused)]
fn main() {
// 实现 Iterator — 类似 Python 的 __iter__/__next__
struct Countdown { value: i32 }

impl Iterator for Countdown {
    type Item = i32;       // 迭代器产生的类型

    fn next(&mut self) -> Option<Self::Item> {
        if self.value > 0 {
            self.value -= 1;
            Some(self.value + 1)
        } else {
            None             // 迭代结束
        }
    }
}

// 使用方式:
for n in (Countdown { value: 5 }) {
    println!("{n}");  // 5, 4, 3, 2, 1
}
}

常用 Trait 一览表

Rust TraitPython 等效项用途
Display__str__面向用户的字符串表示
Debug__repr__面向开发者的调试字符串
Clonecopy.deepcopy深拷贝
Copy(int/float 自动拷贝)简单类型的隐式拷贝
PartialEq / Eq__eq__相等性比较
PartialOrd / Ord__lt__ 等排序
Hash__hash__可哈希 (用于字典键)
Default默认 __init__提供默认值
From / Into__init__ 重载类型转换
Iterator__iter__ / __next__迭代行为
Drop__del__ / __exit__清理逻辑
Add, Sub, Mul__add__ 等运算符重载
Index__getitem__使用 [] 进行索引
Deref(无等效项)智能指针解引用
Send / Sync(无等效项)线程安全性标记
flowchart TB
    subgraph Static ["静态分发 (impl Trait)"]
        G["fn notify(item: &impl Summary)"] --> M1["编译后:notify_Article()"]
        G --> M2["编译后:notify_Tweet()"]
        M1 --> O1["内联化,零成本"]
        M2 --> O2["内联化,零成本"]
    end
    subgraph Dynamic ["动态分发 (dyn Trait)"]
        D["fn notify(item: &dyn Summary)"] --> VT["虚函数表 (vtable) 查找"]
        VT --> I1["Article::summarize()"]
        VT --> I2["Tweet::summarize()"]
    end
    style Static fill:#d4edda
    style Dynamic fill:#fff3cd

Python 对等说明:Python 始终 使用动态分发(运行时通过 getattr 查找)。Rust 默认使用静态分发(单态化 —— 编译器为每个具体类型生成专门的代码)。只有当你确定需要运行时多态时,才使用 dyn Trait。

📌 延伸阅读: 第 11 章:From/Into Trait 深入探讨了转换类 Trait (From, Into, TryFrom)。


关联类型 (Associated Types)

Rust 的 Trait 可以定义关联类型 —— 这是一种占位类型,由每个实现者具体填充。Python 中没有完全对应的概念:

#![allow(unused)]
fn main() {
// Iterator 定义了一个关联类型 'Item'
trait Iterator {
    type Item;
    fn next(&mut self) -> Option<Self::Item>;
}

struct Countdown { remaining: u32 }

impl Iterator for Countdown {
    type Item = u32;  // 该迭代器产生 u32 类型的值
    fn next(&mut self) -> Option<u32> {
        if self.remaining > 0 {
            self.remaining -= 1;
            Some(self.remaining)
        } else {
            None
        }
    }
}
}

在 Python 中,__iter__ / __next__ 返回的是 Any —— 无法强制声明“该迭代器产生 int”并由系统强制执行(使用 Iterator[int] 的类型提示仅具有建议性质)。

运算符重载:__add__ → impl Add

Python 使用魔术方法(如 __add__、__mul__)。Rust 则使用 Trait 实现 —— 思路一致,但在编译期会进行类型检查:

# 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)  # 不会对 '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;  // 关联类型:+ 运算后返回什么?
    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;  // 类型安全:仅允许 Vec2 + Vec2
}

核心差异:Python 的 __add__ 在运行时接受任何 other(你需要手动检查类型,否则会得到 TypeError)。Rust 的 Add Trait 在编译期强制要求操作数类型 —— 除非你显式地为 Vec2 实现 impl Add<i32>,否则 Vec2 + i32 会导致编译错误。


练习

🏋️ 练习:泛型 Summary Trait(点击展开)

挑战:定义一个 Summary Trait,其中包含一个 fn summarize(&self) -> String 方法。为两个结构体实现该 Trait:Article { title: String, body: String } 和 Tweet { username: String, content: String }。然后编写一个函数 fn notify(item: &impl Summary) 来打印摘要信息。

🔑 答案
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 真不错".into(),
        body: "在此我们将探讨为什么 Rust 在系统层面超越了 Python...".into(),
    };
    let tweet = Tweet {
        username: "rustacean".into(),
        content: "刚刚发布了我的第一个 crate!".into(),
    };
    notify(&article);
    notify(&tweet);
}

核心要点: &impl Summary 是 Rust 对 Python 中带有 summarize 方法的 Protocol 的对等实现。但 Rust 会在编译期进行检查 —— 传递一个未实现 Summary 的类型会导致编译错误,而不是运行时的 AttributeError。


11. From 与 Into Traits

English Original

Rust 中的类型转换

你将学到: 用于零成本类型转换的 From 与 Into Trait、用于易错转换的 TryFrom、impl From<A> for B 如何自动生成 Into,以及字符串转换的常见模式。

难度: 🟡 中级

Python 通过调用构造函数来处理类型转换(如 int("42")、str(42)、float("3.14"))。而 Rust 则使用 From 和 Into 这两个 Trait 来确保转换的类型安全性。

Python 的类型转换

# Python — 使用显式的构造函数进行转换
x = int("42")           # str → int (可能抛出 ValueError)
s = str(42)             # int → str
f = float("3.14")       # str → float
lst = list((1, 2, 3))   # tuple → list

# 通过 __init__ 或类方法进行自定义转换
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 定义转换规则
// 实现 From<T> 会自动为你生成对应的 Into<U>!

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)
    }
}

// 现在这两种方式都可以运行:
let c1 = Celsius::from(Fahrenheit(212.0));    // 显式调用 From
let c2: Celsius = Fahrenheit(212.0).into();   // 调用 Into (自动推导出的)

// 字符串转换:
let s: String = String::from("hello");         // &str → String
let s: String = "hello".to_string();           // 同上
let s: String = "hello".into();                // 同样有效 (因为实现了 From)

let num: i64 = 42i32.into();                   // i32 → i64 (无损转换,所以存在 From)
// let small: i32 = 42i64.into();              // ❌ i64 → i32 可能会丢失数据 — 故未实现 From

// 对于可能失败的转换,请使用 TryFrom:
let n: Result<i32, _> = "42".parse();          // str → i32 (可能失败)
let n: i32 = "42".parse().unwrap();            // 如果不是数字则发生恐慌 (Panic)
let n: i32 = "42".parse()?;                    // 使用 ? 运算符传播错误
}

From 与 Into 的关系

flowchart LR
    A["impl From&lt;A&gt; for B"] -->|"自动生成"| B["impl Into&lt;B&gt; for A"]
    C["Celsius::from(Fahrenheit(212.0))"] ---|"等效于"| D["Fahrenheit(212.0).into()"]
    style A fill:#d4edda
    style B fill:#d4edda

经验法则:始终只实现 From,永远不要直接去实现 Into。实现 From<A> for B 会让你免费获得 Into<B> for A 的能力。


应该在何时使用 From/Into

#![allow(unused)]
fn main() {
// 为你的类型实现 From<T> 可以让 API 设计变得更加符合人体工程学:

#[derive(Debug)]
struct UserId(i64);

impl From<i64> for UserId {
    fn from(id: i64) -> Self {
        UserId(id)
    }
}

// 现在该函数可以接收任何能够转为 UserId 的参数:
fn find_user(id: impl Into<UserId>) -> Option<String> {
    let user_id = id.into();
    // ... 查找逻辑
    Some(format!("用户 ID: {:?}", user_id))
}

find_user(42i64);              // ✅ i64 会自动转为 UserId
find_user(UserId(42));         // ✅ UserId 保持原样直传
}

TryFrom — 会失败的转换

并非所有的转换都能一定成功。在 Python 中,这会引发异常;在 Rust 中,这需要使用 TryFrom 显式地返回一个 Result:

# Python — 会失败的转换抛出异常
try:
    port = int("not_a_number")   # ValueError
except ValueError as e:
    print(f"无效输入: {e}")

# 在 __init__ 中进行自定义验证
class Port:
    def __init__(self, value: int):
        if not (1 <= value <= 65535):
            raise ValueError(f"无效的端口号: {value}")
        self.value = value

try:
    p = Port(99999)  # 运行时抛出 ValueError
except ValueError:
    pass
#![allow(unused)]
fn main() {
use std::num::ParseIntError;

// 内置类型的 TryFrom
let n: Result<i32, ParseIntError> = "42".try_into();   // Ok(42)
let n: Result<i32, ParseIntError> = "无效".try_into();  // Err(...)

// 自定义验证逻辑的 TryFrom
#[derive(Debug)]
struct Port(u16);

#[derive(Debug)]
enum PortError {
    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)),
        }
    }
}

impl std::fmt::Display for PortError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            PortError::Zero => write!(f, "端口号不能为零"),
        }
    }
}

// 使用方式:
let p: Result<Port, _> = 8080u16.try_into();   // Ok(Port(8080))
let p: Result<Port, _> = 0u16.try_into();       // Err(PortError::Zero)
}

Python → Rust 思维模型:TryFrom 相当于带有验证逻辑且可能会失败的 __init__。但由于它显式地返回 Result 而不是抛出异常,因此调用方必须显式处理错误路径。


字符串转换模式

字符串通常是 Python 开发者产生转换困扰的主要来源:

#![allow(unused)]
fn main() {
// String → &str (一种“借用”,它是几乎无开销的)
let s = String::from("hello");
let r: &str = &s;              // 自动通过 Deref 强制转换 (Coercion)
let r: &str = s.as_str();     // 显式手动转换

// &str → String (发生了内存分配,会占用额外的内存)
let r: &str = "hello";
let s1 = String::from(r);     // 通过 From Trait
let s2 = r.to_string();       // 通过 ToString Trait (间接通过 Display 获得)
let s3: String = r.into();    // 通过 Into Trait

// 数据类型 → String
let s = 42.to_string();       // "42" — 类似 Python 的 str(42)
let s = format!("{:.2}", 3.14); // "3.14" — 类似 f"{3.14:.2f}"

// String → 数字
let n: i32 = "42".parse().unwrap();       // 类似 Python 的 int("42")
let f: f64 = "3.14".parse().unwrap();     // 类似 Python 的 float("3.14")

// 自定义类型 → String (需要实现 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) — 类似 Python 的 __str__
let s = p.to_string();         // 同样有效!Display 会自动免费为你提供 ToString 功能。
}

转换快速对照表

PythonRust说明
str(x)x.to_string()需要实现 Display Trait
int("42")"42".parse::<i32>()返回 Result 类型
float("3.14")"3.14".parse::<f64>()返回 Result 类型
list(iter)iter.collect::<Vec<_>>()通常需要显式写出目标类型
dict(pairs)pairs.collect::<HashMap<_,_>>()同上
bool(x)无直接对等项必须进行显式的逻辑检查
MyClass(x)MyClass::from(x)需要实现 From<T>
MyClass(x) (带验证)MyClass::try_from(x)?需要实现 TryFrom<T>

转换链与错误处理

在实际代码中,我们经常需要串联多个转换操作。对比一下两种语言的处理方式:

# Python — 使用 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"错误的端口: {port}")
        return (host, port)
    except (ValueError, AttributeError) as e:
        raise ConfigError(f"无效配置: {e}") from e
fn parse_config(raw: &str) -> Result<(String, u16), String> {
    let (host, port_str) = raw
        .split_once(':')
        .ok_or_else(|| "缺少 ':' 分隔符".to_string())?;

    let port: u16 = port_str
        .parse()
        .map_err(|e| format!("无效端口: {e}"))?;

    if port == 0 {
        return Err("端口不能为零".to_string());
    }

    Ok((host.to_string(), port))
}

fn main() {
    match parse_config("localhost:8080") {
        Ok((host, port)) => println!("正在连接到 {host}:{port}"),
        Err(e) => eprintln!("配置解析错误: {e}"),
    }
}

关键洞见:每一个 ? 都是可见的退出点。在 Python 中,try 块内的任何一行都可能抛出异常;而在 Rust 中,只有带 ? 或显式返回 Err 的行才会导致失败。

📌 延伸阅读: 第 9 章:错误处理 深入探讨了 Result、? 以及使用 thiserror 库来实现自定义错误。


练习

🏋️ 练习:温度转换库(点击展开)

挑战:构建一个小型的温度转换库:

  1. 定义 Celsius(f64)、Fahrenheit(f64)、Kelvin(f64) 三种结构体。
  2. 为 Celsius 实现 From<Celsius> for Fahrenheit 和 From<Celsius> for Kelvin。
  3. 为 Kelvin 实现 TryFrom<f64> for Kelvin,拒绝绝对零度以下的值(-273.15°C ≈ 0K)。
  4. 为这三种类型实现 Display(例如输出为 "100.00°C")。
🔑 答案
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, "温度低于绝对零度")
    }
}

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!("错误: {e}"),
    }
}

核心要点: From 用于处理绝对不会失败的转换(摄氏度到华氏度的转换总是成功的)。TryFrom 则用于处理可能会失败的场景(负数开尔文温度是不存在的)。Python 在 __init__ 中将这两者混在了一起,而 Rust 在类型系统中对它们进行了明确区分。


12. 闭包与迭代器

English Original

Rust 闭包 vs Python Lambda

你将学到: 多行闭包(不仅仅是单表达式的 lambda)、Fn/FnMut/FnOnce 捕获语义、迭代器链与列表推导式的对比、map/filter/fold 的映射关系,以及 macro_rules! 宏的基础知识。

难度: 🟡 中级

Python 中的闭包与 Lambda

# Python — lambda 是单表达式的匿名函数
double = lambda x: x * 2
result = double(5)  # 10

# 完整的闭包从封闭作用域中捕获变量:
def make_adder(n):
    def adder(x):
        return x + n    # 从外部作用域捕获 `n`
    return adder

add_5 = make_adder(5)
print(add_5(10))  # 15

# 高阶函数:
numbers = [1, 2, 3, 4, 5]
doubled = list(map(lambda x: x * 2, numbers))
evens = list(filter(lambda x: x % 2 == 0, numbers))

Rust 中的闭包

#![allow(unused)]
fn main() {
// Rust — 闭包使用 |参数| 表达式 的语法
let double = |x: i32| x * 2;
let result = double(5);  // 10

// 闭包从封闭作用域中捕捉变量:
fn make_adder(n: i32) -> impl Fn(i32) -> i32 {
    move |x| x + n    // `move` 将 `n` 的所有权转移进闭包
}

let add_5 = make_adder(5);
println!("{}", add_5(10));  // 15

// 配合迭代器的高阶函数:
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();
}

闭包语法对比

Python:                              Rust:
─────────                            ─────
lambda x: x * 2                      |x| x * 2
lambda x, y: x + y                   |x, y| x + y
lambda: 42                           || 42

# 多行闭包
def f(x):                            |x| {
    y = x * 2                            let y = x * 2;
    return y + 1                         y + 1
                                       }

闭包捕获机制 — Rust 的不同之处

# Python — 闭包通过引用捕获 (延迟绑定!)
funcs = [lambda: i for i in range(3)]
print([f() for f in funcs])  # [2, 2, 2] — 惊讶吗?它们全部捕捉到了同一个 `i`

# 修复方法(使用默认参数的小技巧):
funcs = [lambda i=i: i for i in range(3)]
print([f() for f in funcs])  # [0, 1, 2]
#![allow(unused)]
fn main() {
// Rust — 闭包捕获是正确的 (不存在延迟绑定的陷阱)
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] — 正确!

// `move` 关键字为每个闭包捕获了 `i` 的一份副本 — 不会有任何延迟绑定的意外。
}

三种闭包 Trait

#![allow(unused)]
fn main() {
// Rust 闭包会实现以下一个或多个 Trait:

// Fn — 可多次调用,不修改捕获到的变量 (最常用)
fn apply(f: impl Fn(i32) -> i32, x: i32) -> i32 { f(x) }

// FnMut — 可多次调用,可能会修改捕获到的变量
fn apply_mut(mut f: impl FnMut(i32) -> i32, x: i32) -> i32 { f(x) }

// FnOnce — 只能调用“一次” (会消耗掉捕获的内容)
fn apply_once(f: impl FnOnce() -> String) -> String { f() }

// Python 中没有与之对应的概念 — 其闭包行为始终类似于 Fn。
// 在 Rust 中,编译器会自动确定使用哪种 Trait。
}

迭代器 vs 生成器

Python 生成器

# Python — 使用 yield 的生成器
def fibonacci():
    a, b = 0, 1
    while True:
        yield a
        a, b = b, a + b

# 惰性求值 — 按需计算数值
fib = fibonacci()
first_10 = [next(fib) for _ in range(10)]

# 生成器表达式 — 类似惰性的列表推导式
squares = (x ** 2 for x in range(1000000))  # 不分配内存
first_5 = [next(squares) for _ in range(5)]

Rust 迭代器

#![allow(unused)]
fn main() {
// Rust — Iterator Trait (概念相似,语法不同)
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)
    }
}

// 惰性求值 — 按需计算数值 (就像 Python 的生成器一样)
let first_10: Vec<u64> = Fibonacci::new().take(10).collect();

// 迭代器链 — 就像生成器表达式
let squares: Vec<u64> = (0..1_000_000u64).map(|x| x * x).take(5).collect();
}

推导式 vs 迭代器链

本节将 Python 的各种推导式语法映射到 Rust 的迭代器链上。

列表推导式 (List Comprehension) → map/filter/collect

# Python 推导式:
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["源数据\n[1,2,3,4,5]"] -->|.iter\(\)| B["迭代器"]
    B -->|.filter\(\|x\| x%2==0\)| C["[2, 4]"]
    C -->|.map\(\|x\| x*x\)| D["[4, 16]"]
    D -->|.collect\(\)| E["Vec&lt;i32&gt;\n[4, 16]"]
    style A fill:#ffeeba
    style E fill:#d4edda

关键洞见:Rust 的迭代器是惰性的 —— 只有在调用 .collect() 后才会触发计算。Python 的生成器也有同样的机制,但列表推导式则是会立即进行求值的。

#![allow(unused)]
fn main() {
// Rust 迭代器链:
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 为 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 为 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();
}

常用迭代器方法对比

PythonRust说明
map(f, iter).map(f)转换每一个元素
filter(f, iter).filter(f)保留匹配的元素
sum(iter).sum()求和
min(iter) / max(iter).min() / .max()返回 Option
any(f(x) for x in iter).any(f)是否有任何项匹配
all(f(x) for x in iter).all(f)是否全部匹配
enumerate(iter).enumerate()产生索引 + 值的元组
zip(a, b)a.zip(b)将两个迭代器项成对合并
len(list).count() (会消耗完!) 或 .len()计算项数
list(reversed(x)).rev()反向迭代
itertools.chain(a, b)a.chain(b)拼接两个迭代器
next(iter).next()获取下一项
next(iter, default).next().unwrap_or(default)且带默认值
list(iter).collect::<Vec<_>>()实体化为集合
sorted(iter)先 Collect, 随后再执行 .sort()无惰性的排序迭代器
functools.reduce(f, iter).fold(初始值, f) 或 .reduce(f)累加/折叠

核心差异

Python 迭代器:                        Rust 迭代器:
─────────────────                     ──────────────
- 默认惰性 (针对生成器)                - 默认惰性 (所有环节)
- yield 用于创建生成器                 - 实现 Iterator { fn next() }
- StopIteration 表示迭代完毕           - 返回 None 为终止
- 只能被消耗一次                       - 只能被消耗一次
- 缺乏类型安全性                       - 完全的类型安全性
- 稍慢 (解释器执行)                    - 零成本 (编译期消除)

为什么 Rust 中存在宏

Python 并没有宏系统 —— 它通过装饰器 (Decorators)、元类 (Metaclasses) 以及运行时内省 (Introspection) 来进行元编程。而 Rust 使用宏在编译期生成代码。

Python 元编程 vs Rust 宏

# Python — 使用装饰器和元类进行元编程
from dataclasses import dataclass
from functools import wraps

@dataclass              # 在导入时生成 __init__, __repr__, __eq__
class Point:
    x: float
    y: float

# 自定义装饰器
def log_calls(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print(f"正在调用 {func.__name__}")
        return func(*args, **kwargs)
    return wrapper

@log_calls
def process(data):
    return data.upper()
#![allow(unused)]
fn main() {
// Rust — 使用 derive 宏和声明式宏进行代码生成
#[derive(Debug, Clone, PartialEq)]  // 在“编译期”生成 Debug, Clone, PartialEq 的实现
struct Point {
    x: f64,
    y: f64,
}

// 声明式宏 (类似模板)
macro_rules! log_call {
    ($func_name:expr, $body:expr) => {
        println!("正在调用 {}", $func_name);
        $body
    };
}

fn process(data: &str) -> String {
    log_call!("process", data.to_uppercase())
}
}

常见的内置宏

#![allow(unused)]
fn main() {
// 这些宏在 Rust 中随处可见:

println!("你好, {}!", name);            // 格式化打印
format!("数值为: {}", x);               // 创建格式化 String
vec![1, 2, 3];                          // 创建 Vec
assert_eq!(2 + 2, 4);                  // 测试相等断言
assert!(value > 0, "必须为正数");        // 布尔值断言
dbg!(expression);                       // 调试打印:打印表达式及其值
todo!();                                // 占位符 — 可编译但运行到此处会 panic
unimplemented!();                       // 标记尚未实现的代码
panic!("出错了");                        // 带着消息崩溃 (类似 raise RuntimeError)

// 为什么这些是宏而不是函数?
// - println! 接收可变数量的参数 (Rust 函数做不到)
// - vec! 为任何类型和长度生成初始化代码
// - assert_eq! 知道你所比较对象的源代码信息
// - dbg! 知道文件名和行号
}

使用 macro_rules! 编写简单的宏

#![allow(unused)]
fn main() {
// 对标 Python 的 dict()
// 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! {
    "小明" => 100,
    "阿强" => 85,
    "阿珍" => 90,
};
}

Derive 宏 — 自动实现 Trait

#![allow(unused)]
fn main() {
// #[derive(...)] 是 Rust 中对 Python @dataclass 装饰器的对应实现

// 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,
}

// 常见的 derive 宏:
// Debug         → 提供 {:?} 的格式化支持 (类似 __repr__)
// Clone         → 提供 .clone() 的深拷贝支持
// Copy          → 提供隐式拷贝支持 (仅适用于简单类型)
// PartialEq, Eq → 提供 == 比较支持 (类似 __eq__)
// PartialOrd, Ord → 提供 <、> 以及排序支持 (类似 __lt__ 等)
// Hash          → 使其可作为 HashMap 的键使用 (类似 __hash__)
// Default       → 使其支持 MyType::default() (类似无参数的 __init__)

// 由外部 Crate 提供的常用 derive 宏:
// Serialize, Deserialize (serde 库) → JSON/YAML/TOML 序列化支持
//                                  (类似 Python 的 json.dumps/loads,但是类型安全的)
}

Python 装饰器与 Rust Derive 的映射

Python 装饰器Rust Derive用途
@dataclass#[derive(Debug, Clone, PartialEq)]数据类
@dataclass(frozen=True)默认即为不可变不可变性
@dataclass(order=True)#[derive(Ord, PartialOrd)]比较/排序
@total_ordering#[derive(PartialOrd, Ord)]完整排序支持
JSON json.dumps(obj.__dict__)#[derive(Serialize)]序列化
JSON MyClass(**json.loads(s))#[derive(Deserialize)]反序列化

练习

🏋️ 练习:Derive 与自定义 Debug 实现(点击展开)

挑战:创建一个包含 name: String、email: String 和 password_hash: String 字段的 User 结构体。为其通过派生 (derive) 方式实现 Clone 和 PartialEq,但需要手动实现 Debug,以便在打印时能输出姓名和邮箱,但要隐藏密码(显示为 "***")。

🔑 答案
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: "阿强".into(),
        email: "[email protected]".into(),
        password_hash: "a1b2c3d4e5f6".into(),
    };
    println!("{user:?}");
    // 输出: User { name: "阿强", email: "[email protected]", password_hash: "***" }
}

核心要点: 与 Python 的 __repr__ 不同,Rust 允许你免费通过派生获得 Debug 实现,但你仍保留了针对敏感字段进行重写的灵活性。相比 Python 这种容易在 print(user) 时不小心泄露私密信息的机制,Rust 的做法更具安全性。


13. 并发编程

English Original

无 GIL:真正的并行

你将学到: 为什么 GIL 限制了 Python 的并发性能、Rust 中用于编译期线程安全的 Send/Sync Trait、Arc<Mutex<T>> 与 Python threading.Lock 的对比、Channel 与 queue.Queue,以及 async/await 的差异。

难度: 🔴 高级

GIL (全局解释器锁) 是 Python 处理 CPU 密集型任务时的最大瓶颈。Rust 没有 GIL —— 线程可以真正地并行运行,且类型系统在编译期就能防止数据竞态。

gantt
    title CPU 密集型任务:Python GIL vs Rust 线程
    dateFormat X
    axisFormat %s
    section Python (GIL)
        线程 1 :a1, 0, 4
        线程 2 :a2, 4, 8
        线程 3 :a3, 8, 12
        线程 4 :a4, 12, 16
    section Rust (无 GIL)
        线程 1 :b1, 0, 4
        线程 2 :b2, 0, 4
        线程 3 :b3, 0, 4
        线程 4 :b4, 0, 4

关键洞见:Python 线程在执行 CPU 任务时是顺序运行的(GIL 将它们序列化了)。而 Rust 线程是真正的并行运行 —— 4 个线程可以带来约 4 倍的提速。

📌 先决条件:在学习本章之前,请确保你已经熟悉 第 7 章:所有权与借用。Arc、Mutex 和 move 闭包都建立在所有权概念之上。

Python 的 GIL 问题

# Python — 线程对 CPU 密集型任务没有帮助
import threading
import time

counter = 0

def increment(n):
    global counter
    for _ in range(n):
        counter += 1  # 并非线程安全!但 GIL “保护”了简单的操作

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}")    # 结果可能不是 4,000,000!
print(f"用时: {elapsed:.2f}s")  # 与单线程基本相同 (受限于 GIL)

# 为了实现真正的并行,Python 需要使用多进程 (multiprocessing):
from multiprocessing import Pool
with Pool(4) as pool:
    results = pool.map(cpu_work, data)  # 独立的进程,且存在 pickle 序列化开销

Rust — 真正的并行,编译期安全

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.load(Ordering::Relaxed)); // 始终为 4,000,000
    // 在所有核心上运行 — 真正的并行,没有 GIL
}

线程安全:类型系统保证

Python — 运行时错误

# Python — 数据竞争仅在运行时发现 (或者根本发现不了)
import threading

shared_list = []

def append_items(items):
    for item in items:
        shared_list.append(item)  # GIL 保证了 append 的“线程安全”
        # 但复杂的逻辑就不是安全的了:
        # if item not in shared_list:
        #     shared_list.append(item)  # 竞态条件!

# 使用锁 (Lock) 来保证安全
lock = threading.Lock()
def safe_append(items):
    for item in items:
        with lock:
            if item not in shared_list:
                shared_list.append(item)
# 如果忘了加锁?编译器不会警告。Bug 只会在生产环境中被发现。

Rust — 编译期错误

use std::sync::{Arc, Mutex};
use std::thread;

fn main() {
    // 试图在没有保护的情况下跨线程共享 Vec:
    // let shared = vec![];
    // thread::spawn(move || shared.push(1));
    // ❌ 编译错误:没有保护的情况下,Vec 不是 Send/Sync 的

    // 使用 Mutex (Rust 中对 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 才能访问
            data.push(i);
            // 当 `data` 离开作用域时,锁会自动释放
            // 不存在“忘了释放锁”的问题 — RAII 保证了这一点
        })
    }).collect();

    for h in handles {
        h.join().unwrap();
    }

    println!("{:?}", shared.lock().unwrap()); // [0, 1, 2, 3] (顺序可能不同)
}

Send 与 Sync Trait

#![allow(unused)]
fn main() {
// Rust 使用两个标记(Marker)Trait 来强制执行线程安全:

// Send — “该类型的所有权可以转移到另一个线程”
// 大多数类型都是 Send 的。Rc<T> 则不是 (跨线程请使用 Arc<T>)。

// Sync — “该类型的引用可以从多个线程同时访问”
// 大多数类型都是 Sync 的。Cell<T>/RefCell<T> 则不是 (跨线程请使用 Mutex<T>)。

// 编译器会自动检查这些项:
// thread::spawn(move || { ... })
//   ↑ 闭包捕获的变量必须是 Send 的
//   ↑ 共享的引用必须是 Sync 的
//   ↑ 如果不是 → 编译错误
}

并发原语对比表

PythonRust用途
threading.Lock()Mutex<T>互斥锁
threading.RLock()Mutex<T> (非重入)重入锁 (需采用不同的实现方式)
threading.RWLockRwLock<T>允许多个读取者或单个写入者
threading.Event()Condvar条件变量 (Condition variable)
queue.Queue()mpsc::channel()线程安全的通道
multiprocessing.Poolrayon::ThreadPool线程池
concurrent.futuresrayon / tokio::spawn基于任务的并行化
threading.local()thread_local!线程本地存储 (TLS)
(无内置项)Atomic* 类型无锁计数器和标记位

锁中毒 (Mutex Poisoning)

如果一个线程在持有 Mutex 锁时发生恐慌 (Panic),该锁就会变为“中毒”状态。Python 中没有对应的概念 —— 如果一个线程在持有 threading.Lock() 时崩溃,该锁就会一直被锁死。

#![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!("出错了!");  // 锁现在已中毒
}).join();

// 随后尝试加锁会返回 Err(PoisonError)
match data.lock() {
    Ok(guard) => println!("数据: {guard:?}"),
    Err(poisoned) => {
        println!("锁已中毒!正在恢复...");
        let guard = poisoned.into_inner();
        println!("已恢复: {guard:?}");  // [1, 2, 3, 4]
    }
}
}

原子性顺序 (Atomic Ordering) 说明

原子操作中的 Ordering 参数控制了内存可见性的保证:

顺序 (Ordering)适用场景
Relaxed简单的计数器,顺序无关紧要
Acquire/Release生产者-消费者模式:写入者用 Release,读取者用 Acquire
SeqCst如果你犹豫不决就选这个 — 最严谨的顺序,最符合直觉

Python 的 threading 模块将这些细节隐藏在了 GIL 之下。而在 Rust 中,你可以显式选择 —— 在性能剖析证明你需要更宽松的方案之前,请始终优先使用 SeqCst。


async/await 对比

Python 和 Rust 都有 async/await 语法,但它们的底层实现机制截然不同。

Python 的 async/await

# Python — 使用 asyncio 进行并发 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)} 字节")

asyncio.run(main())

# Python 的 async 是单线程的 (依然受限于 GIL)!
# 它只对 I/O 密集型任务 (等待网络/磁盘) 有帮助。
# async 中的 CPU 计算任务依然会阻塞整个事件循环。

Rust 的 async/await

// Rust — 使用 tokio 进行并发 I/O (以及 CPU 的并行处理!)
use reqwest;
use tokio;
use futures::future::join_all;  // 需要在 Cargo.toml 中添加 `futures`

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)))  // 不受 GIL 限制
        .collect();                                 // 可以利用所有 CPU 核心

    let results = futures::future::join_all(tasks).await;

    for (url, result) in urls.iter().zip(results) {
        match result {
            Ok(Ok(body)) => println!("{url}: {} 字节", body.len()),
            Ok(Err(e)) => println!("{url}: 出错 {e}"),
            Err(e) => println!("{url}: 任务失败 {e}"),
        }
    }

    Ok(())
}

核心差异

维度Python asyncioRust tokio
GIL依然适用无 GIL 限制
CPU 并行❌ 单线程✅ 多线程
运行时 (Runtime)内置 (asyncio)外部 Crate (tokio)
生态系统aiohttp, asyncpg 等reqwest, sqlx 等
性能适于 I/O极佳,兼顾 I/O 与 CPU
错误处理采用异常抛出采用 Result<T, E>
任务取消task.cancel()丢弃 (Drop) 对应的 Future
染色问题 (Color problem)同/异步边界限制同样存在此类问题

使用 Rayon 实现极简并行

# Python — 使用多进程多 CPU 并行
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 极其轻松地实现 CPU 并行 (仅需一行改动!)
use rayon::prelude::*;

// 串行执行:
let results: Vec<_> = items.iter().map(|item| heavy_computation(item)).collect();

// 并行执行 (只需将 .iter() 改为 .par_iter() —— 搞定!):
let results: Vec<_> = items.par_iter().map(|item| heavy_computation(item)).collect();

// 无需 pickle,无进程开销,无需序列化。
// Rayon 会自动在多个核心之间分配工作。
}

💼 案例研究:并行图像处理流水线

一个数据科学团队每晚需要处理 50,000 张卫星图像。他们的 Python 流水线使用了 multiprocessing.Pool:

# Python — 使用多进程进行 CPU 密集型的图像处理
import multiprocessing
from PIL import Image
import numpy as np

def process_image(path: str) -> dict:
    img = np.array(Image.open(path))
    # CPU 密集型任务:直方图均衡化、边缘检测、分类
    histogram = np.histogram(img, bins=256)[0]
    edges = detect_edges(img)       # 每张图约耗时 200ms
    label = classify(edges)          # 每张图约耗时 100ms
    return {"path": path, "label": label, "edge_count": len(edges)}

# 问题:每个子进程都会复制一份完整的 Python 解释器
# 内存:50MB/工人 × 16 个工人 = 800MB 额外开销
# 启动:Fork 和 Pickle 序列化参数需要 2-3 秒
with multiprocessing.Pool(16) as pool:
    results = pool.map(process_image, image_paths)  # 5 万张图处理完约需 4.5 小时

痛点:Fork 带来的 800MB 内存开销、参数与结果的 Pickle 序列化延迟、GIL 阻止了线程的使用、错误处理不透明(子进程中的异常很难调试)。

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 (没有 numpy 的开销)
    let edges = detect_edges(&img);                // 约 40ms (SIMD 优化)
    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 自动利用所有 CPU 核心 — 无需 Fork,无需 Pickle,没有 GIL
    let results: Vec<ImageResult> = paths
        .par_iter()                                // 并行迭代器
        .filter_map(|p| process_image(p).ok())     // 优雅地跳过错误
        .collect();                                // 并行收集结果

    println!("已处理 {} 张图像", results.len());
    Ok(())
}
// 5 万张图处理完约需 35 分钟 (Python 需要 4.5 小时)
// 内存:总计约 50MB (线程间共享内存,无需 Fork)

结果对比:

指标Python (multiprocessing)Rust (rayon)
时间 (5 万张图)约 4.5 小时约 35 分钟
内存开销800MB (16 个工人)约 50MB (共享)
错误处理模糊的 Pickle 错误每一步都有明确的 Result<T, E>
启动成本2–3s (Fork + Pickle)无 (原生线程)

核心教训:对于 CPU 密集型的并行任务,Rust 的线程 + Rayon 可以取代 Python 的 multiprocessing,且具有零序列化开销、内存共享以及编译期安全等显著优势。


练习

🏋️ 练习:线程安全的计数器(点击展开)

挑战:在 Python 中,你可能会使用 threading.Lock 来保护共享计数器。请将其转换为 Rust 代码:生成 10 个线程,每个线程将共享计数器递增 1000 次。最后打印最终值(应为 10000)。请使用 Arc<Mutex<u64>>。

🔑 答案
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!("最终计数: {}", *counter.lock().unwrap());
}

核心要点: Arc<Mutex<T>> 是 Rust 中对 Python 的 lock = threading.Lock() + shared variable 的对等实现 —— 但如果你忘了使用 Arc 或 Mutex,Rust 将无法通过编译。而在 Python 中,代码会带着竞态 Bug 运行,并静默地给出错误答案。


14. Unsafe Rust 与 FFI

English Original

何时以及为什么要使用 Unsafe

你将学到: unsafe 允许做什么及其存在的原因、使用 PyO3 编写 Python 扩展(Python 开发者的杀手级特性)、Rust 的测试框架与 pytest 的对比、使用 mockall 进行 Mock,以及基准测试。

难度: 🔴 高级

Rust 中的 unsafe 是一个逃生口 —— 它告诉编译器:“我正在做一些你无法验证的事情,但我保证它是正确的。” Python 中没有对应的概念,因为 Python 从不向你直接开放物理内存的访问权。

flowchart TB
    subgraph Safe ["安全 Rust (99% 的代码)"]
        S1["你的业务逻辑"]
        S2["pub fn safe_api\(&self\) -> Result"]
    end
    subgraph Unsafe ["unsafe 块 (极小量,经过审计)"]
        U1["裸指针解引用"]
        U2["调用 C/Python 接口 (FFI)"]
    end
    subgraph External ["外部 (C / Python / OS)"]
        E1["libc / PyO3 / 系统调用"]
    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

设计模式:使用安全的 API 包裹一个极小的 unsafe 块。调用方永远看不到 unsafe。而 Python 的 ctypes 则没有这种边界 —— 每一个 FFI 调用都是隐式不安全的。

📌 延伸阅读:第 13 章:并发 介绍了 Send/Sync Trait,它们是编译器用于检查线程安全性的 unsafe 自动 Trait。

unsafe 允许的操作

// unsafe 允许你做五个安全 Rust 禁止的操作:
// 1. 解引用裸指针
// 2. 调用不安全的函数或方法
// 3. 访问可变的静态变量
// 4. 实现不安全的 Trait
// 5. 访问 Union (联合体) 字段

// 示例:调用一个 C 函数
extern "C" {
    fn abs(input: i32) -> i32;
}

fn main() {
    // 安全说明:abs() 是一个定义明确的 C 标准库函数。
    let result = unsafe { abs(-42) };  // 安全 Rust 无法验证 C 代码
    println!("{result}");               // 42
}

何时使用 unsafe

#![allow(unused)]
fn main() {
// 1. FFI — 调用 C 语言库 (最常见的原因)
// 2. 对性能极其敏感的代码内循环 (罕见)
// 3. 借用检查器无法表达的数据结构 (罕见)

// 作为 Python 开发者,你主要会在以下场景遇到 unsafe:
// - PyO3 的内部机制 (Python ↔ Rust 桥接)
// - C 语言库的绑定
// - 底层系统调用

// 经验法则:如果你是在编写应用逻辑 (而非库代码),
// 你几乎永远不需要使用 unsafe。如果你觉得自己需要,请先咨询
// Rust 社区 —— 通常都会有安全的替代方案。
}

PyO3:用于 Python 的 Rust 扩展

PyO3 是 Python 和 Rust 之间的桥梁。它允许你编写可从 Python 调用的 Rust 函数和类 —— 非常适合替换慢速的 Python 热点代码。

使用 Rust 创建 Python 扩展

# 安装 maturin: 构建 Rust 编写的 Python 扩展的工具
pip install maturin
maturin init           # 初始化项目结构

# 项目结构如下:
# 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"]    # 用于 Python 的动态链接库

[dependencies]
pyo3 = { version = "0.22", features = ["extension-module"] }
#![allow(unused)]
fn main() {
// src/lib.rs — 可从 Python 调用的 Rust 函数
use pyo3::prelude::*;

#[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
}

#[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()
}

#[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)
    }
}

#[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(())
}
}

在 Python 中使用

# 构建并安装
maturin develop --release   # 构建并将扩展安装到当前的虚拟环境中
# Python — 像任何普通 Python 模块一样使用此扩展
import my_extension

# 调用 Rust 函数
result = my_extension.fibonacci(50)
print(result)  # 12586269025 — 以微秒级速度计算出的结果

# 使用 Rust 类
counter = my_extension.Counter(0)
counter.increment()
counter.increment()
print(counter.get_value())  # 2
print(counter)              # Counter(value=2)

# 性能对比:
import time

# Python 版本
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 — 提速 70 倍!
print(f"结果一致: {py_result == rs_result}")  # True

PyO3 快速对照表

Python 概念PyO3 属性标记说明
函数#[pyfunction]暴露给 Python 的函数
类#[pyclass]暴露给 Python 的类
方法#[pymethods]类对应的方法实现
__init__#[new]构造函数
__repr__fn __repr__()对象字符串表示
__str__fn __str__()显示字符串
__len__fn __len__()长度
__getitem__fn __getitem__()索引访问
属性#[getter] / #[setter]属性访问器
静态方法#[staticmethod]无 self 参数
类方法#[classmethod]接收 cls 参数

FFI 安全规范

将 Rust 暴露给 Python (无论是通过 PyO3 还是裸 C FFI) 时,这些规则可防止大多数常见的 Bug:

  1. 永远不要让恐慌 (Panic) 跨越 FFI 边界 — 如果 Rust 恐慌未能被捕获并解算 (Unwind) 到了 Python 或 C 代码中,将导致 未定义行为。PyO3 对 #[pyfunction] 自动处理了这一点,但对于裸 FFI 函数,需要手动进行显式保护。

  2. 为共享结构体使用 #[repr(C)] —— 如果 Python/C 需直接读取结构体字段,必须使用 #[repr(C)] 以保证内存布局与 C 兼容。如果是传递不透明指针 (PyO3 对 #[pyclass] 的做法),则不需要。

  3. extern "C" — 裸 FFI 函数必须标注此项,以确保调用约定符合 C/Python 的预期。

PyO3 的优势:它为你包裹了绝大多数安全忧虑 —— 包括恐慌捕获、类型转换、以及对 GIL 锁的管理。除非有极特殊的理由,否则请优先选用 PyO3 而不是裸 FFI。


单元测试 vs pytest

使用 pytest 进行 Python 测试

# 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)

# 参数化测试
@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
# 运行测试
pytest                      # 运行所有测试
pytest test_calculator.py   # 运行指定文件
pytest -k "test_add"        # 运行匹配名称的测试
pytest -v                   # 详细输出

Rust 的内置测试机制

#![allow(unused)]
fn main() {
// src/calculator.rs — 测试代码可以直接写在同一个文件中!
fn add(a: i32, b: i32) -> i32 {
    a + b
}

fn divide(a: f64, b: f64) -> Result<f64, String> {
    if b == 0.0 {
        Err("不能除以零".to_string())
    } else {
        Ok(a / b)
    }
}

// 测试代码放在 #[cfg(test)] 模块中 — 仅在执行 `cargo test` 时编译
#[cfg(test)]
mod tests {
    use super::*;  // 导入父模块的所有内容

    #[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());
    }

    // 测试是否发生了恐慌 (类似 pytest.raises)
    #[test]
    #[should_panic(expected = "out of bounds")]
    fn test_out_of_bounds() {
        let v = vec![1, 2, 3];
        let _ = v[99];  // 触发恐慌
    }
}
}
# 运行测试
cargo test                         # 运行所有测试
cargo test test_add                # 运行匹配名称的测试
cargo test -- --nocapture          # 显示 println! 的输出
cargo test -p my_crate             # 测试工作区中的特定 Crate
cargo test -- --test-threads=1     # 串行运行(适用于带有副作用的测试)

测试对照快速索引

pytestRust说明
assert x == yassert_eq!(x, y)相等断言
assert x != yassert_ne!(x, y)不等断言
assert conditionassert!(condition)布尔断言
assert condition, "msg"assert!(condition, "msg")带自定义消息
pytest.raises(E)#[should_panic]期望发生恐慌
@pytest.fixture在测试或辅助函数中设置无内置的 Fixture 机制
@pytest.mark.parametrize需要使用 rstest 库参数化测试
conftest.pytests/common/mod.rs共享测试辅助工具
pytest.skip()#[ignore]跳过测试
tmp_path fixture需要使用 tempfile 库临时目录处理

使用 rstest 进行参数化测试

#![allow(unused)]
fn main() {
// Cargo.toml: rstest = "0.23"

use rstest::rstest;

// 类似 @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);
}

// 类似 @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);
}
}

使用 mockall 进行 Mock

# Python — 使用 unittest.mock 进行 Mock
from unittest.mock import Mock, patch

def test_fetch_user():
    mock_db = Mock()
    mock_db.get_user.return_value = {"name": "阿强"}

    result = fetch_user_name(mock_db, 1)
    assert result == "阿强"
    mock_db.get_user.assert_called_once_with(1)
#![allow(unused)]
fn main() {
// Rust — 使用 mockall 库进行 Mock
// Cargo.toml: mockall = "0.13"

use mockall::{automock, predicate::*};

#[automock]                          // 自动生成 MockDatabase
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: "阿强".into() }));

    let result = fetch_user_name(&mock, 1);
    assert_eq!(result, Some("阿强".to_string()));
}
}

练习

🏋️ 练习:围绕 Unsafe 编写安全包装层(点击展开)

挑战:编写一个安全的函数 split_at_mid,它接收一个 &mut [i32] 并返回两个在中心点切分的切片 (&mut [i32], &mut [i32])。在内部实现中,请使用裸指针和 unsafe(模拟 split_at_mut 的实现方式)。随后将其包装在安全的 API 中。

🔑 答案
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); // 在进入 unsafe 之前进行安全检查

    // 安全说明:mid <= len (由上方断言保证),且 ptr 源于一个有效的 &mut 切片,
    // 因此两个子切片都在边界内且互不重叠。
    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:?}, 右侧: {right:?}");
    // 输出: 左侧: [99, 2, 3], 右侧: [88, 5, 6]
}

核心要点: unsafe 块非常小,且受到了 assert! 的严密保护。暴露出的 API 是完全安全的 —— 调用方永远看不到 unsafe。这就是 Rust 的典型模式:内部使用不安全实现以换取性能或表达力,外部提供安全接口。Python 的 ctypes 则无法以此种方式提供安全保证。


15. 迁移模式

English Original

Rust 中的常用 Python 模式

你将学到: 如何将 dict 转换为 struct、将 class 转换为 struct+impl、将列表推导式转换为迭代器链、将装饰器转换为 Trait,以及将上下文管理器转换为 Drop/RAII。此外还将介绍核心 Crate 以及渐进式迁移策略。

难度: 🟡 中级

字典 (Dictionary) → 结构体 (Struct)

# Python — 使用 dict 作为数据容器 (非常常见)
user = {
    "name": "阿强",
    "age": 30,
    "email": "[email protected]",
    "active": True,
}
print(user["name"])
#![allow(unused)]
fn main() {
// Rust — 带有命名字段的结构体
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
struct User {
    name: String,
    age: i32,
    email: String,
    active: bool,
}

let user = User {
    name: "阿强".into(),
    age: 30,
    email: "[email protected]".into(),
    active: true,
};
println!("{}", user.name);
}

上下文管理器 → RAII (Drop)

# Python — 使用上下文管理器进行资源清理
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")
# 退出 `with` 块时文件会自动关闭
#![allow(unused)]
fn main() {
// Rust — RAII:当值离开作用域时调用 Drop Trait 执行清理
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` 离开作用域时文件会自动关闭
    // 不需要专门的 `with` 语句 — RAII 机制会处理好它!
}
}

装饰器 → 高阶函数或宏

# Python — 用于计时的装饰器
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__} 耗时 {elapsed:.4f}s")
        return result
    return wrapper

@timed
def slow_function():
    time.sleep(1)
#![allow(unused)]
fn main() {
// Rust — 不存在装饰器,需使用包装函数或宏来实现
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!("{} 耗时 {:.4?}", name, start.elapsed());
    result
}

// 使用方式:
let result = timed("slow_function", || {
    std::thread::sleep(std::time::Duration::from_secs(1));
    42
});
}

迭代器流水线 (数据处理)

# Python — 一连串的转换操作
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 — 带有强类型的迭代器链
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
}
}

全局配置 / 单例模式 (Singleton)

# Python — 模块级单例 (常用模式)
# 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()  # 模块级单例
#![allow(unused)]
fn main() {
// Rust — 使用 OnceLock 实现惰性静态初始化 (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("读取配置失败");
        serde_json::from_str(&data)
            .expect("解析配置失败")
    })
}

// 可以在任何地方使用:
let db_host = get_config()["database"]["host"].as_str().unwrap();
}

Python 开发者的 Rust 核心库 (Crates) 建议

数据处理与序列化

任务PythonRust Crate说明
JSONjsonserde_json类型安全的序列化
CSVcsv, pandascsv流式处理,极低内存
YAMLpyyamlserde_yaml用于配置文件
TOMLtomllibtoml常用配置格式
数据验证pydanticserde + 自定义编译期验证
日期/时间datetimechrono完善的时区支持
正则表达式reregex极其快速
UUIDuuiduuid功能相同

Web 与网络

任务PythonRust Crate说明
HTTP 客户端requestsreqwest异步优先
Web 框架FastAPI/Flaskaxum / actix-web性能极强
WebSocketwebsocketstokio-tungstenite异步支持
gRPCgrpciotonic完整支持
数据库 (SQL)sqlalchemysqlx / diesel编译期检查的 SQL
Redisredis-pyredis支持异步

CLI 与系统工具

任务PythonRust Crate说明
CLI 参数解析argparse/clickclap使用 derive 宏实现
终端彩色输出coloramacolored功能雷同
进度条tqdmindicatif体验一致
文件系统监控watchdognotify跨平台支持
日志记录loggingtracing结构化日志,异步友好
环境变量os.environstd::env + dotenvy支持 .env 文件
子进程subprocessstd::process::Command标准库内置
临时文件tempfiletempfile名字完全一样!

测试框架

任务PythonRust Crate说明
测试框架pytest内置测试 + rstest执行 cargo test
模拟对象 (Mock)unittest.mockmockall基于 Trait 实现
属性测试hypothesisproptest接口风格相似
快照测试syrupyinsta快照工作流
基准测试pytest-benchmarkcriterion统计级误差分析
代码覆盖率coverage.pycargo-tarpaulin基于 LLVM

渐进式落地策略

flowchart LR
    A["1️⃣ 对 Python 进行分析\n(寻找性能热点)"] --> B["2️⃣ 编写 Rust 扩展\n(PyO3 + maturin)"]
    B --> C["3️⃣ 替换 Python 函数调用\n(相同的 API)"]
    C --> D["4️⃣ 逐步扩大范围\n(增加更多函数)"]
    D --> E{"全量重写\n是否值得?"}
    E -->|是| F["纯 Rust蟹 🦀"]
    E -->|否| G["混用模式 🐍+🦀"]
    style A fill:#ffeeba
    style B fill:#fff3cd
    style C fill:#d4edda
    style D fill:#d4edda
    style F fill:#c3e6cb
    style G fill:#c3e6cb

📌 延伸阅读:第 14 章:Unsafe Rust 与 FFI 详细介绍了 PyO3 绑定底层所需的 FFI 细节。

步骤 1:识别性能热点

# 首先对你的 Python 代码进行性能分析 (Profiling)
import cProfile
cProfile.run('main()')  # 寻找耗时较长的 CPU 密集型函数

# 或者使用 py-spy 进行采样分析:
# py-spy top --pid <python-pid>
# py-spy record -o profile.svg -- python main.py

步骤 2:为性能热点编写 Rust 扩展

# 使用 maturin 创建 Rust 扩展项目
cd my_python_project
maturin init --bindings pyo3

# 在 Rust 中重写热点函数 (详见前文 PyO3 部分)
# 构建并安装:
maturin develop --release

步骤 3:用 Rust 调用替换 Python 调用

# 修改前:
result = python_hot_function(data)  # 慢

# 修改后:
import my_rust_extension
result = my_rust_extension.hot_function(data)  # 快!

# 使用同样的接口、同样的测试,却能获得 10-100 倍的提速

步骤 4:逐步扩大重写比例

第 1-2 周:将一个 CPU 密集型函数替换为 Rust
第 3-4 周:替换数据解析/校验层
第 2 个月:替换核心数据流水线逻辑
第 3 个月以上:如果收益足够大,考虑用全量 Rust 重写整个项目

核心原则:保留 Python 进行编排,利用 Rust 进行计算。

💼 案例研究:使用 PyO3 加速数据流水线

某金融科技初创公司有一条 Python 数据流水线,每天需要处理 2GB 的交易记录 CSV 文件。其关键瓶颈在于校验和转换步骤:

# Python — 慢速部分 (处理 2GB 数据约需 12 分钟)
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:
            # 解析并校验每一个字段
            amount = Decimal(row["amount"])
            if amount < 0:
                raise ValueError(f"金额为负数: {amount}")
            date = datetime.strptime(row["date"], "%Y-%m-%d")
            category = categorize(row["merchant"])  # 字符串匹配,约 50 条规则

            results.append({
                "amount_cents": int(amount * 100),
                "date": date.isoformat(),
                "category": category,
                "merchant": row["merchant"].strip().lower(),
            })
    return results
# 1500 万行数据需约 12 分钟。尝试过 pandas — 提速到 8 分钟但需 6GB 内存。

步骤 1:通过性能分析确定热点 (CSV 解析 + Decimal 转换 + 字符串匹配占用了 95% 的时间)。

步骤 2:编写 Rust 扩展:

#![allow(unused)]
fn main() {
// src/lib.rs — PyO3 扩展
use pyo3::prelude::*;
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 或简单规则 — 编译一次,极速运行
    if merchant.contains("amazon") { "购物" }
    else if merchant.contains("uber") || merchant.contains("lyft") { "交通" }
    else if merchant.contains("starbucks") { "餐饮" }
    else { "其他" }
}

#[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); // 预分配内存

    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)?;  // 自定义解析器 (无需 Decimal)
        let date = &record[1];  // 已经是 ISO 格式,只需校验
        let merchant = record[2].trim().to_lowercase();
        let category = categorize(&merchant).to_string();

        results.push((amount_cents, date.to_string(), category, merchant));
    }
    Ok(results)
}
}

步骤 3:在 Python 中替换调用行:

# 修改前:
results = validate_and_transform("transactions.csv")  # 12 分钟

# 修改后:
import fast_pipeline
results = fast_pipeline.process_transactions("transactions.csv")  # 45 秒

# 同样的 Python 编排,同样的测试,同样的部署流程

结果对比:

指标Python (csv + Decimal)Rust (PyO3 + csv crate)
时间 (2GB / 1500 万行)12 分钟45 秒
内存峰值6GB (pandas) / 2GB (csv)200MB
Python 代码改动量—1 行 (import + 调用)
编写的 Rust 代码量—约 60 行
测试通过率47/4747/47 (无变化)

核心教训:你不需要重写整个应用。找到那 5% 占据了 95% 运行时间的“瓶颈代码”,用 Rust 加 PyO3 重新实现它,而其余部分继续留在 Python 中。该团队从“我们需要增加服务器”转变为“一台服务器就足够了”。


练习

🏋️ 练习:迁移决策矩阵(点击展开)

挑战:你有一个包含以下组件的 Python Web 应用。对于每一个组件,请决定是:保留在 Python 中、用 Rust 重写,还是使用 PyO3 桥接。并说明理由。

  1. Flask 路由处理器 (请求解析,JSON 响应)
  2. 图像缩略图生成 (CPU 密集型,每天处理 1 万张图)
  3. 数据库 ORM 查询 (SQLAlchemy)
  4. 处理 2GB 财务文件的 CSV 解析器 (每晚运行一次)
  5. 管理后台面板 (Jinja2 模板)
🔑 答案
组件决策理由
Flask 路由处理器🐍 保留在 PythonI/O 密集型,重度依赖框架,改用 Rust 收益极低
图像缩略图生成🦀 PyO3 桥接CPU 密集型热点路径,保留 Python API,内部用 Rust 实现
数据库 ORM 查询🐍 保留在 PythonSQLAlchemy 已经很成熟,且查询属于 I/O 密集型
CSV 解析器 (2GB)🦀 PyO3 或全量 RustCPU 与内存双重瓶颈,Rust 的零拷贝解析极具优势
管理后台面板🐍 保留在 Python属于 UI/模板类代码,对性能没有特殊要求

核心要点: 迁移的最佳切入点是那些有着清晰边界、且对性能极其敏感的 CPU 密集型代码。不要去重写那些“胶水代码”或 I/O 密集型处理器 —— 它们的性能提升往往无法抵消重写的成本。


16. 最佳实践

English Original

面向 Python 开发者的地道 Rust 指南

你将学到: 应当培养的前 10 个习惯、常见误区及其修复方案、结构化的 3 个月学习路径、完整的 Python→Rust “罗塞塔石碑”对照表,以及推荐的学习资源。

难度: 🟡 中级

flowchart LR
    A["🟢 第 1-2 周\n基础阶段\n“为什么编译不通过?”"] --> B["🟡 第 3-4 周\n核心概念\n“原来它是在保护我”"] 
    B --> C["🟡 第 2 个月\n进阶阶段\n“我明白这为什么重要了”"] 
    C --> D["🔴 第 3 个月及以后\n高级阶段\n“在编译期就抓到了 Bug!”"] 
    D --> E["🏆 第 6 个月\n熟练阶段\n“在任何语言中都成了更出色的程序员”"]
    style A fill:#d4edda
    style B fill:#fff3cd
    style C fill:#fff3cd
    style D fill:#f8d7da
    style E fill:#c3e6cb,stroke:#28a745

应当培养的前 10 个习惯

  1. 对 Enum 使用 match 而非 if isinstance()

    # Python                              # Rust
    if isinstance(shape, Circle): ...     match shape { Shape::Circle(r) => ... }
    
  2. 听从编译器的引导 — 仔细阅读错误信息。Rust 的编译器是所有语言中最出色的,它不仅会告诉你哪里错了,还会告诉你如何修复。

  3. 在函数参数中优先使用 &str 而非 String — 接受最通用的类型。&str 既能接收 String 也能接收字符串字面量。

  4. 使用迭代器而非索引循环 — 迭代器链更符合 Rust 惯用法,且通常比 for i in 0..vec.len() 更快。

  5. 拥抱 Option 和 Result — 不要对所有东西都用 .unwrap()。学会使用 ?、map、and_then 以及 unwrap_or_else。

  6. 大胆派生 (Derive) Trait — 大多数结构体都应当标上 #[derive(Debug, Clone, PartialEq)]。这是免费的,且能极大地方便测试。

  7. 坚持使用 cargo clippy — 它能发现数百种风格和正确性问题。将其视为 Rust 版的 ruff。

  8. 不要与借用检查器对着干 — 如果你发现自己在苦苦挣扎,那很可能是数据结构设计得不合理。通过重构使所有权关系变得清晰。

  9. 使用 Enum 实现状态机 — 相比于字符串标记位或布尔值,Enum 更佳。编译器会确保你处理了每一种状态。

  10. 先 Clone,后优化 — 在学习阶段,可以大方地使用 .clone() 来规避复杂的所有权问题。只有当性能分析显示有必要时再进行优化。

Python 开发者的常见错误

错误原因修复方案
到处使用 .unwrap()导致运行时崩溃 (Panic)改用 ? 或 match
参数使用 String 而不使用 &str造成不必要的内存分配参数改用 &str
for i in 0..vec.len()不符合 Rust 习惯使用 for item in &vec
忽略 clippy 警告错失改进机会执行 cargo clippy
过多调用 .clone()带来性能开销重构所有权逻辑
过于庞大的 main() 函数难以进行测试提取到 lib.rs 中
不使用 #[derive()]在重复造轮子派生常用的 Trait
出错时直接触发恐慌错误不可恢复返回 Result<T, E>

性能对比

基准测试:常用操作对比

操作项目              Python 3.12    Rust (release)    提速倍数
─────────────────────  ────────────   ──────────────    ─────────
Fibonacci(40)          约 25s         约 0.3s           约 80x
排序 1000 万个整数      约 5.2s        约 0.6s           约 9x
解析 100MB JSON        约 8.5s        约 0.4s           约 21x
100 万次正则匹配       约 3.1s        约 0.3s           约 10x
HTTP 服务器 (req/s)    约 5,000       约 150,000        约 30x
1GB 文件 SHA-256 计算   约 12s         约 1.2s           约 10x
解析 100 万行 CSV      约 4.5s        约 0.2s           约 22x
字符串拼接             约 2.1s        约 0.05s          约 42x

注意:使用 C 语言编写的扩展库 (如 NumPy 等) 会极大地缩小 Python 处理数值运算的差距。上述测试是纯 Python 与纯 Rust 之间的对比。

内存占用对比

Python:                                 Rust:
─────────                               ─────
- 对象头: 每个对象 28 字节              - 无对象头
- int: 28 字节 (即使是 0)               - i32: 4 字节, i64: 8 字节
- str "hello": 54 字节                  - &str "hello": 16 字节 (指针 + 长度)
- 1000 个整数的列表: 约 36 KB           - Vec<i32>: 约 4 KB
  (8 KB 指针 + 28 KB 整数对象)
- 100 个项的 dict: 约 5.5 KB            - 100 个项的 HashMap: 约 2.4 KB

典型应用的基线内存占用:
- Python: 50-200 MB 基线                - Rust: 1-5 MB 基线

常见的陷阱与解决方案

陷阱 1:“借用检查器不让我这么做”

#![allow(unused)]
fn main() {
// 问题:试图在迭代时进行修改
let mut items = vec![1, 2, 3, 4, 5];
// for item in &items {
//     if *item > 3 { items.push(*item * 2); }  // ❌ 禁止在借用期间进行可变借用
// }

// 方案 1:先收集修改项,结束后统一应用
let additions: Vec<i32> = items.iter()
    .filter(|&&x| x > 3)
    .map(|&x| x * 2)
    .collect();
items.extend(additions);

// 方案 2:利用 retain 或 extend
items.retain(|&x| x <= 3);
}

陷阱 2:“字符串类型太多了”

#![allow(unused)]
fn main() {
// 犹豫不决时的经验法则:
// - 在函数参数中使用 &str
// - 在结构体字段和返回值中使用 String
// - 字符串字面量 ("hello") 在任何需要 &str 的地方都能正常工作

fn process(input: &str) -> String {    // 接收 &str,返回 String
    format!("已处理: {}", input)
}
}

陷阱 3:“我想念 Python 的简洁性”

#![allow(unused)]
fn main() {
// Python 单行代码:
// result = [x**2 for x in data if x > 0]

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

// 它虽然更显臃肿,但是:
// - 在编译期就实现了类型安全
// - 速度提快了 10 到 100 倍
// - 不可能出现运行时类型错误
// - 对内存分配有着显式的表达 (.collect())
}

陷阱 4:“我的交互式解释器 (REPL) 在哪?”

#![allow(unused)]
fn main() {
// Rust 没有 REPL。可以改用以下工具:
// 1. 将 `cargo test` 当作 REPL — 编写微型测试来尝试功能
// 2. 使用 Rust 演练场 (play.rust-lang.org) 进行快速原型验证
// 3. 利用 `dbg!()` 宏快速打印调试输出
// 4. 使用 `cargo watch -x test` 实现保存后的自动测试

#[test]
fn playground() {
    // 将其视为你的“REPL” — 通过 `cargo test playground` 运行
    let result = "hello world"
        .split_whitespace()
        .map(|w| w.to_uppercase())
        .collect::<Vec<_>>();
    dbg!(&result);  // 输出: [src/main.rs:5] &result = ["HELLO", "WORLD"]
}
}

学习路径与资源建议

第 1-2 周:夯实基础

  • 安装 Rust,配置带有 rust-analyzer 插件的 VS Code
  • 完成本指南的前 4 章 (类型、控制流)
  • 编写 5 个小程序,尝试将 Python 脚本转换为 Rust
  • 熟悉 cargo build、cargo test 和 cargo clippy 的使用

第 3-4 周:核心概念

  • 完成第 5-8 章 (结构体、枚举、所有权、模块)
  • 用 Rust 重写一个 Python 数据处理脚本
  • 练习使用 Option<T> 和 Result<T, E>,直至形成本能
  • 仔细阅读编译器报错信息 —— 它们就是最好的老师

第 2 个月:进阶阶段

  • 完成第 9-12 章 (错误处理、Trait、迭代器)
  • 利用 clap 和 serde 开发一个 CLI 工具
  • 为 Python 项目的性能热点编写一个 PyO3 扩展
  • 练习迭代器链,直到用起来像列表推导式一样顺手

第 3 个月:高级阶段

  • 完成第 13-16 章 (并发、Unsafe、测试)
  • 利用 axum 和 tokio 开发一个 Web 服务
  • 尝试向开源 Rust 项目贡献代码
  • 阅读《Programming Rust》(O’Reilly) 以深入理解底层原理

推荐资源

  • Rust 程序设计语言 (The Rust Book): https://doc.rust-lang.org/book/ (官方出品,必读经典)
  • 通过例子学 Rust (Rust by Example): https://doc.rust-lang.org/rust-by-example/ (在实践中学习)
  • Rustlings: https://github.com/rust-lang/rustlings (精选题库)
  • Rust 演练场 (Rust Playground): https://play.rust-lang.org/ (在线编译器)
  • Rust 周报 (This Week in Rust): https://this-week-in-rust.org/ (获取行业动态)
  • PyO3 指南: https://pyo3.rs/ (Python ↔ Rust 桥接技术)
  • Google 全面 Rust 教程: https://google.github.io/comprehensive-rust/

Python → Rust 罗塞塔石碑 (对照表)

Python 概念Rust 对等概念对应章节
listVec<T>5
dictHashMap<K,V>5
setHashSet<T>5
tuple(T1, T2, ...)5
classstruct + impl5
@dataclass#[derive(...)]5, 12
Enumenum6
NoneOption<T>6
raise/try/exceptResult<T,E> + ?9
Protocol (PEP 544)trait10
TypeVar泛型 <T>10
__dunder__ 魔术方法Traits (Display, Add, 等)10
lambda`args
生成器 yieldimpl Iterator12
列表推导式.map().filter().collect()12
@decorator 装饰器高阶函数或宏12, 15
asynciotokio13
threadingstd::thread13
multiprocessingrayon13
unittest.mockmockall14
pytestcargo test + rstest14
pip installcargo add8
requirements.txtCargo.lock8
pyproject.tomlCargo.toml8
with (上下文管理器)作用域触发的 Drop15
json.dumps/loadsserde_json15

给 Python 开发者的结语

你会怀念 Python 的地方:
- REPL 和交互式探索体验
- 极快的原型开发速度
- 极其丰富的 ML/AI 生态 (如 PyTorch 等)
- “运行正常”的动态类型系统
- pip install 后的即刻可用性

你将从 Rust 中获得的东西:
- “只要能编译通过,就能正常运行”的终极自信
- 10 到 100 倍的性能提升
- 再也不会出现运行时类型错误
- 再也不会出现 None/null 导致的崩溃
- 真正的并行处理 (没有 GIL!)
- 单一二进制文件的便捷部署
- 可预测的内存占用
- 任何编程语言中最出色的编译器错误提示

心路历程:
第 1 周:  “为什么编译器这么讨厌我?”
第 2 周:  “噢,原来它是在保护我不出 Bug”
第 1 个月: “我明白这为什么重要了”
第 2 个月: “我在编译期抓到了一个本会导致线上事故的 Bug”
第 3 个月: “我再也不想写没有类型约束的代码了”
第 6 个月: “Rust 让我成了更出色的程序员,无论我在写哪种语言”

练习

🏋️ 练习:代码审查清单(点击展开)

挑战:审查下面这段由 Python 开发者编写的 Rust 代码,并提出 5 项符合 Rust 惯用法的改进建议:

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);
}
🔑 答案

五项改进建议:

// 1. 使用 &[String] 而非 Vec<String> (不要获取整个 Vec 的所有权)
// 2. 使用 usize 处理索引 (索引永远不为负数)
// 3. 返回 Option<&str> 而非空字符串 (善用类型系统!)
// 4. 使用 .get() 代替手动边界检查
// 5. 在 main 中不要使用 clone() — 直接传递引用

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!("未找到"),
    }
}

核心要点: 在 Rust 中会让你感到吃力的 Python 习惯:到处进行 clone(应改用借用)、使用 "" 之类的哨兵值(应改用 Option)、在借用即可时却获取所有权,以及对索引使用有符号整数。


Python 程序员 Rust 训练指南(完)

17. 结业项目:CLI 任务管理器

English Original

结业项目:构建一个 CLI 任务管理器

你将学到: 通过构建一个完整的 Rust CLI 应用程序,将课程中的所有知识点融会贯通。这是一个 Python 开发者通常会用 argparse + json + pathlib 编写的典型项目。

难度: 🔴 高级

这个结业项目涵盖了之前每个主要章节的概念:

  • 第 3 章:类型与变量 (结构体、枚举)
  • 第 5 章:集合 (Vec, HashMap)
  • 第 6 章:枚举与模式匹配 (任务状态、命令)
  • 第 7 章:所有权与借用 (传递引用)
  • 第 9 章:错误处理 (Result, ?, 自定义错误)
  • 第 10 章:Trait (Display, FromStr)
  • 第 11 章:类型转换 (From, TryFrom)
  • 第 12 章:迭代器与闭包 (过滤、映射)
  • 第 8 章:模块 (有条理的项目结构)

项目目标:rustdo

一个命令行任务管理器(类似于 Python 的 todo.txt 系列工具),它将任务存储在一个 JSON 文件中。

Python 等效实现 (参考)

#!/usr/bin/env python3
"""一个简单的 CLI 任务管理器 — Python 版本。"""
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))

# 命令:add, list, done, remove, stats
# ... (你应该很熟悉 Python 的这部分写法)

你的 Rust 实现方案

我们将分步完成此项目。每一步都对应着相应章节的概念。


第一步:定义数据模型 (第 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;

/// 任务优先级 — 对应 Python 的 Priority(Enum)
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Priority {
    Low,
    Medium,
    High,
}

// Display Trait (对应 Python 的 __str__)
impl fmt::Display for Priority {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Priority::Low => write!(f, "低"),
            Priority::Medium => write!(f, "中"),
            Priority::High => write!(f, "高"),
        }
    }
}

// FromStr Trait (用于解析 "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!("未知优先级: '{other}' (请使用 low/medium/high)")),
        }
    }
}

/// 单个任务记录 — 对应 Python 的 Task 类
#[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 的对比:在 Python 中你会使用 @dataclass + Enum。而在 Rust 中,struct + enum 结合 derive 宏可以让你免费获得序列化、显示输出和解析能力。


第二步:存储层 (第 9, 7 章)

#![allow(unused)]
fn main() {
// src/storage.rs
use std::fs;
use std::path::PathBuf;
use crate::task::Task;

/// 获取任务文件路径 (~/.rustdo.json)
fn task_file_path() -> PathBuf {
    let home = dirs::home_dir().expect("无法确定主目录");
    home.join(".rustdo.json")
}

/// 从磁盘加载任务列表 — 如果文件不存在则返回空的 Vec
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)?;  // ? 用于传播 io::Error
    let tasks: Vec<Task> = serde_json::from_str(&content)?;  // ? 用于传播 serde 错误
    Ok(tasks)
}

/// 将任务列表存储到磁盘
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 的对比:Python 使用 Path.read_text() + json.loads()。而 Rust 则利用 fs::read_to_string() + serde_json::from_str()。请观察 ? 操作符 — 每一次错误处理都是显式声明并向上传播的。


第三步:命令枚举 (第 6 章)

#![allow(unused)]
fn main() {
// src/command.rs
use crate::task::Priority;

/// 所有可能的命令 — 每个动作对应一个枚举变体
pub enum Command {
    Add { title: String, priority: Priority },
    List { show_done: bool },
    Done { id: u32 },
    Remove { id: u32 },
    Stats,
    Help,
}

impl Command {
    /// 解析命令行参数
    /// (在实际生产中,你会使用 `clap` — 这里仅用于教学目的)
    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("用法: rustdo add <任务名称> [优先级]")?
                    .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("用法: rustdo done <ID>")?
                    .parse()
                    .map_err(|_| "ID 必须是数字")?;
                Ok(Command::Done { id })
            }
            Some("remove") => {
                let id: u32 = args.get(1)
                    .ok_or("用法: rustdo remove <ID>")?
                    .parse()
                    .map_err(|_| "ID 必须是数字")?;
                Ok(Command::Remove { id })
            }
            Some("stats") => Ok(Command::Stats),
            _ => Ok(Command::Help),
        }
    }
}
}

与 Python 的对比:Python 使用 argparse 或 click。这个手动编写的解析器展示了如何通过对 Enum 的模式匹配 (match) 来取代 Python 中的 if/elif 链。在真实项目中,应优先考虑使用 clap 这一 Crate。


第四步:业务逻辑 (第 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!("已添加: {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)   // 迭代器 + 闭包 (第 12 章)
        .collect();

    if filtered.is_empty() {
        println!("暂无任务!🎉");
        return Ok(());
    }

    for task in &filtered {
        println!("  {task}");   // 调用 Display Trait (第 10 章)
    }
    println!("\n当前显示了 {} 个任务", 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 (第 12 章)
        .ok_or(format!("未找到 ID 为 {id} 的任务"))?;
    task.done = true;
    println!("已完成: {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 (第 5 章)
    if tasks.len() == len_before {
        return Err(format!("未找到 ID 为 {id} 的任务").into());
    }
    println!("已删除任务 {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;

    // 使用迭代器进行分组统计 (第 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!("📊 任务统计");
    println!("   总数:     {total}");
    println!("   已完成:   {done} ✅");
    println!("   待办中:   {pending}");
    println!("   🔴 高优先级: {high}");
    println!("   🟡 中优先级: {medium}");
    println!("   🟢 低优先级: {low}");
    Ok(())
}
}

使用的 Rust 核心模式:iter().map().max()、iter().filter().collect()、iter_mut().find()、retain() 和 iter().filter().count()。这些方法取代了 Python 中的列表推导式、next(x for x in ...) 以及 Counter。


第五步:连点成线 (第 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!("错误: {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!("错误: {e}");
        std::process::exit(1);
    }
}

fn print_help() {
    println!("rustdo — 专为在学 Rust 的 Python 开发者打造的任务管理器\n");
    println!("用法:");
    println!("  rustdo add <任务名称> [low|medium|high]   添加任务");
    println!("  rustdo list [--all]                     列出待办任务");
    println!("  rustdo done <ID>                        标记任务为已完成");
    println!("  rustdo remove <ID>                      删除任务");
    println!("  rustdo stats                            显示统计信息");
}
graph TD
    CLI["main.rs<br/>(CLI 入口)"] --> CMD["command.rs<br/>(参数解析)"]
    CMD --> ACT["actions.rs<br/>(业务逻辑)"]
    ACT --> STORE["storage.rs<br/>(JSON 持久化)"]
    ACT --> TASK["task.rs<br/>(数据模型)"]
    STORE --> TASK
    style CLI fill:#d4edda
    style CMD fill:#fff3cd
    style ACT fill:#fff3cd
    style STORE fill:#ffeeba
    style TASK fill:#ffeeba

第六步:Cargo.toml 依赖配置

[package]
name = "rustdo"
version = "0.1.0"
edition = "2021"

[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"
chrono = "0.4"
dirs = "5"

与 Python 的对比:这相当于你 pyproject.toml 中的 [project.dependencies]。使用 cargo add serde serde_json chrono dirs 命令的作用就像 pip install。


第七步:编写测试 (第 14 章)

#![allow(unused)]
fn main() {
// src/task.rs — 将以下代码添加到文件末尾
#[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, "写 Rust".to_string(), Priority::High);
        let display = format!("{task}");
        assert!(display.contains("写 Rust"));
        assert!(display.contains("🔴"));
        assert!(display.contains("⬜")); // 此时尚未完成
    }

    #[test]
    fn task_serialization_roundtrip() {
        let task = Task::new(1, "测试".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, "测试");
        assert_eq!(recovered.priority, Priority::Low);
    }
}
}

与 Python 的对比:这对应 Python 中的 pytest 测试。请使用 cargo test 来运行它们。Rust 不需要特殊的测试发现机制 — #[test] 显式地标记了测试函数。


进阶目标

当你完成上述基础版本后,可以尝试以下改进方案:

  1. 引入 clap 进行参数解析 — 使用 clap 的派生宏替换手写的解析器:

    #![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,
    }
    }
  2. 添加彩色输出 — 使用 colored 库(类似于 Python 的 colorama)为终端输出添加颜色。

  3. 增加截止日期 — 添加一个 Option<NaiveDate> 字段,并过滤出过期的任务。

  4. 增加标签/分类 — 使用 Vec<String> 存储标签,并利用 .iter().any() 进行过滤。

  5. 将其拆分为库 + 二进制文件 — 采用 lib.rs + main.rs 的结构(第 8 章模块模式),使业务逻辑可重用。


知识点复盘

章节核心概念在本项目中的应用场景
第 3 章类型与变量Task 结构体字段、u32、String、bool
第 5 章集合Vec<Task>、retain()、push()
第 6 章枚举与模式匹配Priority、Command 及其详尽匹配
第 7 章所有权与借用&[Task] 与 Vec<Task> 的对比、完成任务时的 &mut
第 8 章模块化mod task; mod storage; mod command; mod actions;
第 9 章错误处理Result<T, E>、? 操作符、.ok_or()
第 10 章TraitDisplay、FromStr、Serialize、Deserialize
第 11 章From/IntoPriority 的 FromStr 实现、利用 .into() 进行错误转换
第 12 章迭代器filter、map、find、count、collect
第 14 章测试#[test]、#[cfg(test)]、断言宏

🎓 恭喜你! 如果你已经亲手构建了本项目,那么你已经掌握并运用了本书涵盖的每一个 Rust 核心概念。你不再是一个在学 Rust 的 Python 开发者,而是一个同时精通 Python 的 Rust 开发者了。


Rust for Python Programmers: Complete Training Guide

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.

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.

Pacing recommendations:

ChaptersTopicSuggested TimeCheckpoint
1–4Setup, types, control flow1 dayYou can write a CLI temperature converter in Rust
5–6Data structures, enums, pattern matching1–2 daysYou can define an enum with data and match exhaustively on it
7Ownership and borrowing1–2 daysYou can explain why let s2 = s1 invalidates s1
8–9Modules, error handling1 dayYou can create a multi-file project that propagates errors with ?
10–12Traits, generics, closures, iterators1–2 daysYou can translate a list comprehension to an iterator chain
13Concurrency1 dayYou can write a thread-safe counter with Arc<Mutex<T>>
14Unsafe, PyO3, testing1 dayYou can call a Rust function from Python via PyO3
15–16Migration, best practicesAt your own paceReference material — consult as you write real code
17Capstone project2–3 daysBuild a complete CLI app tying everything together

How to use the exercises:

  • Chapters include hands-on exercises in collapsible <details> blocks with solutions
  • 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
  • The Rust Playground lets you run code without a local install

Difficulty indicators:

  • 🟢 Beginner — Direct translation from Python concepts
  • 🟡 Intermediate — Requires understanding ownership or traits
  • 🔴 Advanced — Lifetimes, async internals, or unsafe code

When you hit a wall:

  • Read the compiler error message carefully — Rust’s errors are exceptionally helpful
  • 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
  • For deeper async patterns, see the companion Async Rust Training

Table of Contents

Part I — Foundations

1. Introduction and Motivation 🟢

2. Getting Started 🟢

3. Built-in Types and Variables 🟢

4. Control Flow 🟢

5. Data Structures and Collections 🟢

6. Enums and Pattern Matching 🟡

Part II — Core Concepts

7. Ownership and Borrowing 🟡

8. Crates and Modules 🟢

9. Error Handling 🟡

10. Traits and Generics 🟡

11. From and Into Traits 🟡

12. Closures and Iterators 🟡

Part III — Advanced Topics & Migration

13. Concurrency 🔴

14. Unsafe Rust, FFI, and Testing 🔴

15. Migration Patterns 🟡

16. Best Practices 🟡


Part IV — Capstone

17. Capstone Project: CLI Task Manager 🔴


Speaker Intro and General Approach

  • Speaker intro
    • Principal Firmware Architect in Microsoft SCHIE (Silicon and Cloud Hardware Infrastructure Engineering) team
    • Industry veteran with expertise in security, systems programming (firmware, operating systems, hypervisors), CPU and platform architecture, and C++ systems
    • Started programming in Rust in 2017 (@AWS EC2), and have been in love with the language ever since
  • This course is intended to be as interactive as possible
    • Assumption: You know Python and its ecosystem
    • Examples deliberately map Python concepts to Rust equivalents
    • Please feel free to ask clarifying questions at any point of time

The Case for Rust for Python Developers

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.

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 — ~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. Why the difference? Python dispatches every + through a dictionary lookup, unboxes integers from heap objects, and checks types at every operation. Rust compiles fibonacci directly to a handful of x86 add/mov instructions — the same code a C compiler would produce.

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 — 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.


Common Python Pain Points That Rust Addresses

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 — 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: int has no .upper()
process_user(None, "Alice")             # Silently stores None as id — bug hides until downstream code expects 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 can appear anywhere a value is expected. Python has no compile-time way to prevent 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

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 — 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 (Rust’s parallel iterator library), parallelism is even simpler:

#![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 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

Choose Rust When:

  • 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
  • Memory efficiency: Reduce cloud costs for memory-intensive services
  • Long-running services: Where predictable latency matters (no GC pauses)

Stay with Python When:

  • Rapid prototyping: Exploratory data analysis, scripts, one-off tools
  • ML/AI workflows: PyTorch, TensorFlow, scikit-learn ecosystem
  • Glue code: Connecting APIs, data transformation scripts
  • Team expertise: When Rust learning curve doesn’t justify benefits
  • Time to market: When development speed trumps execution speed
  • Interactive work: Jupyter notebooks, REPL-driven development
  • Scripting: Automation, sys-admin tasks, quick utilities

Consider Both (Hybrid Approach with PyO3):

  • Compute-heavy code in Rust: Called from Python via PyO3/maturin
  • Business logic and orchestration in Python: Familiar, productive
  • Gradual migration: Identify hotspots, replace with Rust extensions
  • Best of both: Python’s ecosystem + Rust’s performance

Real-World Impact: Why Companies Choose Rust

Dropbox: Storage Infrastructure

  • Before (Python): High CPU usage, memory overhead in sync engine
  • After (Rust): 10x performance improvement, 50% memory reduction
  • Result: Millions saved in infrastructure costs

Discord: Voice/Video Backend

  • Before (Python → Go): GC pauses causing audio drops
  • After (Rust): Consistent low-latency performance
  • Result: Better user experience, reduced server costs

Cloudflare: Edge Workers

  • Why Rust: WebAssembly compilation, predictable performance at edge
  • Result: Workers run with microsecond cold starts

Pydantic V2

  • Before: Pure Python validation — slow for large payloads
  • After: Rust core (via PyO3) — 5–50x faster validation
  • Result: Same Python API, dramatically faster execution

Why This Matters for Python Developers:

  1. Complementary skills: Rust and Python solve different problems
  2. PyO3 bridge: Write Rust extensions callable from Python
  3. Performance understanding: Learn why Python is slow and how to fix hotspots
  4. Career growth: Systems programming expertise increasingly valuable
  5. Cloud costs: 10x faster code = significantly lower infrastructure spend

Language Philosophy Comparison

Python Philosophy

  • 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

  • Performance without sacrifice: Zero-cost abstractions, no runtime overhead
  • Correctness first: If it compiles, entire categories of bugs are impossible
  • Explicit over implicit: No hidden behavior, no implicit conversions
  • Ownership: Resources have exactly one owner — memory, files, sockets
  • 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 — CPython VM"]
        PY_INTERP --> PY_GC["Garbage Collector — ref count + GC"]
        PY_GC --> PY_GIL["GIL — 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 — compile-time, zero-cost"]
        RS_OWN --> RS_THR["Native threads — 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

ConceptPythonRustKey Difference
TypingDynamic (duck typing)Static (compile-time)Errors caught before runtime
MemoryGarbage collected (ref counting + cycle GC)Ownership systemZero-cost, deterministic cleanup
None/nullNone anywhereOption<T>Compile-time None safety
Error handlingraise/try/exceptResult<T, E>Explicit, no hidden control flow
MutabilityEverything mutableImmutable by defaultOpt-in to mutation
SpeedInterpreted (~10–100x slower)Compiled (C/C++ speed)Orders of magnitude faster
ConcurrencyGIL limits threadsNo GIL, Send/Sync traitsTrue parallelism by default
Dependenciespip install / poetry addcargo addBuilt-in dependency management
Build systemsetuptools/poetry/hatchCargoSingle unified tool
Packagingpyproject.tomlCargo.tomlSimilar declarative config
REPLpython interactiveNo REPL (use tests/cargo run)Compile-first workflow
Type hintsOptional, not enforcedRequired, compiler-enforcedTypes 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.

  1. x = [1, 2, 3]; y = x; x.append(4) — What happens in Rust?
  2. data = None; print(data.upper()) — How does Rust prevent this?
  3. import threading; shared = []; threading.Thread(target=shared.append, args=(1,)).start() — What does Rust demand?
🔑 Solution
  1. Ownership move: let y = x; moves x — x.push(4) is a compile error. You’d need let y = x.clone(); or borrow with let y = &x;.
  2. No null: data can’t be None unless it’s Option<String>. You must match or use .unwrap() / if let — no surprise NoneType errors.
  3. Send + Sync: The compiler requires shared to be wrapped in Arc<Mutex<Vec<i32>>>. Forgetting the lock = compile error, not a race condition.

Key takeaway: Rust shifts runtime failures to compile-time errors. The “friction” you feel is the compiler catching real bugs.


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.

Difficulty: 🟢 Beginner

Installing 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

PurposePythonRust
Language runtimepython (interpreter)rustc (compiler, rarely called directly)
Package managerpip / poetry / uvcargo (built-in)
Project configpyproject.tomlCargo.toml
Lock filepoetry.lock / requirements.txtCargo.lock
Virtual envvenv / condaNot needed (deps are per-project)
Formatterblack / ruff formatrustfmt (built-in: cargo fmt)
Linterruff / flake8 / pylintclippy (built-in: cargo clippy)
Type checkermypy / pyrightBuilt into compiler (always on)
Test runnerpytestcargo test (built-in)
Docssphinx / mkdocscargo doc (built-in)
REPLpython / ipythonNone (use cargo test or Rust Playground)

IDE Setup

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

Python Hello World

# hello.py — just run it
print("Hello, World!")

# Run:
# python hello.py

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:                              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, no setup.py vs setup.cfg vs pyproject.toml confusion. Just Cargo.toml + src/.


Cargo vs 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

# 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

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 ref 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 (Pythonic: if (m := regex.match(s)):)
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 (click to expand)

Challenge: Create a new Rust project and write a program that:

  1. Declares a variable name with your name (type &str)
  2. Declares a mutable variable count starting at 0
  3. Uses a for loop from 1..=5 to increment count and print "Hello, {name}! (count: {count})"
  4. After the loop, print whether count is even or odd using a match expression
🔑 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:

  • let is immutable by default (you need mut to change count)
  • 1..=5 is inclusive range (Python’s range(1, 6))
  • match is an expression that returns a value
  • No self, no if __name__ == "__main__" — just fn main()

Variables and Mutability

What you’ll learn: Immutable-by-default variables, explicit mut, primitive numeric types vs Python’s arbitrary-precision int, String vs &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 self in the method signature tells you (and the compiler) that increment modifies 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

PythonRustNotes
int (arbitrary precision)i8, i16, i32, i64, i128, isizeRust integers have fixed size
int (unsigned: no separate type)u8, u16, u32, u64, u128, usizeExplicit unsigned types
float (64-bit IEEE 754)f32, f64Python only has 64-bit float
boolboolSame concept
complexNo 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

PythonRustNotes
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 Union types and isinstance() 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.


Conditional Statements

What you’ll learn: if/else without parentheses (but with braces), loop/while/for vs 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 implicit return.


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.


Tuples and Destructuring

What you’ll learn: Rust tuples vs Python tuples, arrays and slices, structs (Rust’s replacement for classes), Vec<T> vs list, HashMap<K,V> vs dict, 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 Rectangle object has a 56-byte header + separate heap-allocated float objects. A Rust Rectangle is 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

PythonRustPurpose
__str__impl DisplayHuman-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 IteratorIteration
__len__.len() methodLength
__enter__/__exit__RAII + impl DropAutomatic cleanup; no direct equivalent of context manager’s two-phase protocol
__init__fn new() (convention)Constructor
__getitem__impl IndexIndexing with []
__contains__.contains() methodin 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

PythonRustNotes
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.iter().position(|v| v == &x).map(|i| vec.remove(i))Removes first match only (use retain to remove all)
del lst[i]vec.remove(i)Returns the removed element
len(lst)vec.len()
x in lstvec.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

PythonRustNotes
d[key] = vald.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 dd.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

PythonRustNotes
set()HashSet<T>use std::collections::HashSet;
collections.dequeVecDeque<T>use std::collections::VecDeque;
heapqBinaryHeap<T>Max-heap by default
collections.OrderedDictIndexMap (crate)HashMap doesn’t preserve order
sortedcontainers.SortedListBTreeSet<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.


Algebraic Data Types vs Union Types

What you’ll learn: Rust enums with data vs Python Union types, exhaustive match vs match/case, Option<T> as a compile-time replacement for None, 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 match is exhaustive — the compiler verifies you handle every variant. Add a new variant to an enum and the compiler tells you exactly which match blocks need updating. Python’s match has 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> and Option<T> are just enums with match.

#![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

PatternPythonRust
Check if existsif x is not None:if let Some(x) = opt {
Default valuex or defaultopt.unwrap_or(default)
Default factoryx or compute()opt.unwrap_or_else(|| compute())
Transform if existsf(x) if x else Noneopt.map(f)
Chain lookupsx and x.attr and x.attr.method()opt.and_then(|x| x.method())
Crash if NoneNot possible to preventopt.unwrap() (panic) or opt.expect("msg")
Get or raisex if x else raiseopt.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.


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)
→ shared refs to immutable  → bitwise copied on assignment
  objects (no real copy)     (always independent values)
                           (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 PointerPython AnalogyUse Case
Box<T>Normal allocationLarge data, recursive types, trait objects
Rc<T>Python’s default refcountShared ownership, single-threaded
Arc<T>Thread-safe refcountShared ownership, multi-threaded
RefCell<T>Python’s “just mutate it”Interior mutability (escape hatch)
Rc<RefCell<T>>Python’s normal object modelShared + 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, use Weak<T> to break reference loops — unlike Python, Rust’s Rc has 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:

  1. Immutable borrow + mutation: first borrows names, then push mutates it. Fix: use first before pushing.
  2. Move out of Vec: names[0] tries to move a String out of Vec (not allowed). Fix: borrow with &names[0].
  3. Function takes ownership: make_greeting(String) consumes the value. Fix: take &str instead.

Rust Modules vs Python Packages

What you’ll learn: mod and use vs import, 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.rs as __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

ConceptPythonRust
Module = file✅ AutomaticMust declare with mod
Package = directory__init__.pymod.rs
Public by default✅ Everything❌ Private by default
Make public_prefix conventionpub keyword
Import syntaxfrom x import yuse x::y;
Wildcard importfrom x import *use x::*; (discouraged)
Relative importsfrom . import siblinguse super::sibling;
Re-export__all__ or explicitpub 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 [email protected]         # 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 LibraryRust CratePurpose
requestsreqwestHTTP client
json (stdlib)serde_jsonJSON parsing
pydanticserdeSerialization/validation
pathlibstd::path (stdlib)Path handling
os / shutilstd::fs (stdlib)File operations
reregexRegular expressions
loggingtracing / logLogging
click / argparseclapCLI argument parsing
asynciotokioAsync runtime
datetimechronoDate and time
pytestBuilt-in + rstestTesting
dataclasses#[derive(...)]Data structures
typing.ProtocolTraitsStructural typing
subprocessstd::process (stdlib)Run external commands
sqlite3rusqliteSQLite
sqlalchemydiesel / sqlxORM / SQL toolkit
fastapiaxum / actix-webWeb 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() is pub
  • Line B: ❌ Compile error — secret_recipe() is private to kitchen
  • Line C: ✅ Compiles — staff::cook() is pub, and cook() can access secret_recipe() via super:: (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.


Exceptions vs Result

What you’ll learn: Result<T, E> vs try/except, the ? operator for concise error propagation, custom error types with thiserror, anyhow for 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 produce compile warnings (always handle them)
- 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-generates impl 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

PythonRustNotes
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 visiblyAlways 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:

  1. Rejects empty strings with error "empty input"
  2. Parses the string to u16, mapping the parse error to "invalid number: {original_error}"
  3. 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.


Traits vs Duck Typing

What you’ll learn: Traits as explicit contracts (vs Python duck typing), Protocol (PEP 544) ≈ Trait, generic type bounds with where clauses, 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

FeaturePython ProtocolRust Trait
Structural typing✅ (implicit)❌ (explicit impl)
Checked atRuntime (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

PythonRustNotes
TypeVar('T')<T>Unbounded generic
TypeVar('T', bound=X)<T: X>Bounded generic
Union[int, str]enum or trait objectRust has no union types
Sequence[T]&[T] (slice)Borrowed sequence
Callable[[A], R]Fn(A) -> RFunction 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 TraitPython EquivalentPurpose
Display__str__Human-readable string
Debug__repr__Debug string (derivable)
Clonecopy.deepcopyDeep 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)
DefaultDefault __init__Default values
From / Into__init__ overloadsType 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 (getattr at runtime). Rust defaults to static dispatch (monomorphization — the compiler generates specialized code for each concrete type). Use dyn Trait only 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.


Type Conversions in Rust

What you’ll learn: From and Into traits for zero-cost type conversions, TryFrom for fallible conversions, how impl From<A> for B auto-generates Into, 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&lt;A&gt; for B"] -->|"auto-generates"| B["impl Into&lt;B&gt; 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 implement Into directly. Implementing From<A> for B gives you Into<B> for A for 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 {
    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)),
        }
    }
}

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"),
        }
    }
}

// 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 returns Result — 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

PythonRustNotes
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 equivalentUse 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 inside try could 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 with thiserror in depth.


Exercises

🏋️ Exercise: Temperature Conversion Library (click to expand)

Challenge: Build a mini temperature conversion library:

  1. Define Celsius(f64), Fahrenheit(f64), and Kelvin(f64) structs
  2. Implement From<Celsius> for Fahrenheit and From<Celsius> for Kelvin
  3. Implement TryFrom<f64> for Kelvin that rejects values below absolute zero (-273.15°C = 0K)
  4. Implement Display for 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.


Rust Closures vs Python Lambdas

What you’ll learn: Multi-line closures (not just one-expression lambdas), Fn/FnMut/FnOnce capture semantics, iterator chains vs list comprehensions, map/filter/fold, and macro_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&lt;i32&gt;\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

PythonRustNotes
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 DecoratorRust DerivePurpose
@dataclass#[derive(Debug, Clone, PartialEq)]Data class
@dataclass(frozen=True)Immutable by defaultImmutability
@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: "[email protected]".into(),
        password_hash: "a1b2c3d4e5f6".into(),
    };
    println!("{user:?}");
    // Output: User { name: "Alice", email: "[email protected]", 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.


No GIL: True Parallelism

What you’ll learn: Why the GIL limits Python concurrency, Rust’s Send/Sync traits for compile-time thread safety, Arc<Mutex<T>> vs Python threading.Lock, channels vs queue.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

PythonRustPurpose
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()CondvarCondition variable
queue.Queue()mpsc::channel()Thread-safe channel
multiprocessing.Poolrayon::ThreadPoolThread pool
concurrent.futuresrayon / tokio::spawnTask-based parallelism
threading.local()thread_local!Thread-local storage
N/AAtomic* typesLock-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:

OrderingWhen to use
RelaxedSimple counters where ordering doesn’t matter
Acquire/ReleaseProducer-consumer: writer uses Release, reader uses Acquire
SeqCstWhen 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;
use futures::future::join_all;  // add `futures` to Cargo.toml

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

AspectPython asyncioRust tokio
GILStill appliesNo GIL
CPU parallelism❌ Single-threaded✅ Multi-threaded
RuntimeBuilt-in (asyncio)External crate (tokio)
Ecosystemaiohttp, asyncpg, etc.reqwest, sqlx, etc.
PerformanceGood for I/OExcellent for I/O AND CPU
Error handlingExceptionsResult<T, E>
Cancellationtask.cancel()Drop the future
Color problemSync ↔ async boundarySame 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)?;
    // Application-specific functions (implement for your use case)
    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:

MetricPython (multiprocessing)Rust (rayon)
Time (50k images)~4.5 hours~35 minutes
Memory overhead800MB (16 workers)~50MB (shared)
Error handlingOpaque pickle errorsResult<T, E> at every step
Startup cost2–3s (fork + pickle)None (threads)

Key lesson: For CPU-bound parallel work, Rust’s threads + rayon replace Python’s multiprocessing with 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.


When and Why to Use Unsafe

What you’ll learn: What unsafe permits 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 unsafe block. Callers never see unsafe. Python’s ctypes has no such boundary — every FFI call is implicitly unsafe.

📌 See also: Ch. 13 — Concurrency covers Send/Sync traits which are unsafe auto-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 ConceptPyO3 AttributeNotes
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:

  1. 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 raw extern "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
        }
    }
    }
  2. #[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.

  3. 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

pytestRustNotes
assert x == yassert_eq!(x, y)Equality
assert x != yassert_ne!(x, y)Inequality
assert conditionassert!(condition)Boolean
assert condition, "msg"assert!(condition, "msg")With message
pytest.raises(E)#[should_panic]Expect panic
@pytest.fixtureSetup in test or helper fnNo built-in fixtures
@pytest.mark.parametrizerstest crateParameterized tests
conftest.pytests/common/mod.rsShared test helpers
pytest.skip()#[ignore]Skip a test
tmp_path fixturetempfile crateTemporary 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.


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": "[email protected]",
    "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: "[email protected]".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

TaskPythonRust CrateNotes
JSONjsonserde_jsonType-safe serialization
CSVcsv, pandascsvStreaming, low memory
YAMLpyyamlserde_yamlConfig files
TOMLtomllibtomlConfig files
Data validationpydanticserde + customCompile-time validation
Date/timedatetimechronoFull timezone support
RegexreregexVery fast
UUIDuuiduuidSame concept

Web & Network

TaskPythonRust CrateNotes
HTTP clientrequestsreqwestAsync-first
Web frameworkFastAPI/Flaskaxum / actix-webVery fast
WebSocketwebsocketstokio-tungsteniteAsync
gRPCgrpciotonicFull support
Database (SQL)sqlalchemysqlx / dieselCompile-time checked SQL
Redisredis-pyredisAsync support

CLI & System

TaskPythonRust CrateNotes
CLI argsargparse/clickclapDerive macros
Colored outputcoloramacoloredTerminal colors
Progress bartqdmindicatifSame UX
File watchingwatchdognotifyCross-platform
LoggingloggingtracingStructured, async-ready
Env varsos.environstd::env + dotenvy.env support
Subprocesssubprocessstd::process::CommandBuilt-in
Temp filestempfiletempfileSame name!

Testing

TaskPythonRust CrateNotes
Test frameworkpytestBuilt-in + rstestcargo test
Mockingunittest.mockmockallTrait-based
Property testinghypothesisproptestSimilar API
Snapshot testingsyrupyinstaSnapshot approval
Benchmarkingpytest-benchmarkcriterionStatistical
Code coveragecoverage.pycargo-tarpaulinLLVM-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)?;  // Your custom parser (no Decimal needed)
        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(m: &Bound<'_, 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:

MetricPython (csv + Decimal)Rust (PyO3 + csv crate)
Time (2GB / 15M rows)12 minutes45 seconds
Peak memory6GB (pandas) / 2GB (csv)200MB
Lines changed in Python—1 (import + call)
Rust code written—~60 lines
Tests passing47/4747/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.

  1. Flask route handlers (request parsing, JSON responses)
  2. Image thumbnail generation (CPU-bound, processes 10k images/day)
  3. Database ORM queries (SQLAlchemy)
  4. CSV parser for 2GB financial files (runs nightly)
  5. Admin dashboard (Jinja2 templates)
🔑 Solution
ComponentDecisionRationale
Flask route handlers🐍 Keep PythonI/O-bound, framework-heavy, low benefit from Rust
Image thumbnail generation🦀 PyO3 bridgeCPU-bound hot path, keep Python API, Rust internals
Database ORM queries🐍 Keep PythonSQLAlchemy is mature, queries are I/O-bound
CSV parser (2GB)🦀 PyO3 bridge or full RustCPU + memory bound, Rust’s zero-copy parsing shines
Admin dashboard🐍 Keep PythonUI/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.


Idiomatic Rust for Python Developers

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

Difficulty: 🟡 Intermediate

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

Top 10 Habits to Build

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

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

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

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

  5. Embrace Option and Result — Don’t .unwrap() everything. Use ?, map, and_then, unwrap_or_else.

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

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

  8. Don’t fight the borrow checker — If you’re fighting it, you’re probably structuring data wrong. Refactor to make ownership clear.

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

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

Common Mistakes from Python Developers

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

Performance Comparison

Benchmark: Common Operations

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

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

Memory Usage

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

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

Common Pitfalls and Solutions

Pitfall 1: “The Borrow Checker Won’t Let Me”

#![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> and Result<T, E> until natural
  • Read compiler error messages carefully — they’re teaching you

Month 2: Intermediate

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

Month 3: Advanced

  • Complete chapters 13-16 (concurrency, unsafe, testing)
  • Build a web service with axum and tokio
  • Contribute to an open-source Rust project
  • Read “Programming Rust” (O’Reilly) for deeper understanding
  • 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

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

Final Thoughts for Python Developers

#![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

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 + derive macros 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 uses fs::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 argparse or click. This hand-rolled parser shows how match on enum-like patterns replaces Python’s if/elif chains. For real projects, use the clap crate.


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 ...), and Counter.


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 dirs is like pip 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: pytest tests. Run with cargo test instead of pytest. No test discovery magic needed — #[test] marks test functions explicitly.


Stretch Goals

Once you have the basic version working, try these enhancements:

  1. Add clap for argument parsing — Replace the hand-rolled parser with clap’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,
    }
    }
  2. Add colored output — Use the colored crate for terminal colors (like Python’s colorama).

  3. Add due dates — Add an Option<NaiveDate> field and filter overdue tasks.

  4. Add tags/categories — Use Vec<String> for tags and filter with .iter().any().

  5. Make it a library + binary — Split into lib.rs + main.rs so the logic is reusable (Ch. 8 module pattern).


What You Practiced

ChapterConceptWhere It Appeared
Ch. 3Types and variablesTask struct fields, u32, String, bool
Ch. 5CollectionsVec<Task>, retain(), push()
Ch. 6Enums + matchPriority, Command, exhaustive matching
Ch. 7Ownership + borrowing&[Task] vs Vec<Task>, &mut for completion
Ch. 8Modulesmod task; mod storage; mod command; mod actions;
Ch. 9Error handlingResult<T, E>, ? operator, .ok_or()
Ch. 10TraitsDisplay, FromStr, Serialize, Deserialize
Ch. 11From/IntoFromStr for Priority, .into() for error conversion
Ch. 12Iteratorsfilter, map, find, count, collect
Ch. 14Testing#[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.