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

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 reqwest@0.12         # Version constraint
# Cargo.lock is auto-generated β€” no manual step
cargo build                    # Downloads and compiles dependencies

Cargo.toml vs pyproject.toml

# Rust β€” Cargo.toml
[package]
name = "my-project"
version = "0.1.0"
edition = "2021"

[dependencies]
serde = { version = "1.0", features = ["derive"] }  # With feature flags
reqwest = { version = "0.12", features = ["json"] }
tokio = { version = "1", features = ["full"] }
log = "0.4"

[dev-dependencies]
mockall = "0.13"

Essential Crates for Python Developers

Python 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.