Rust Modules vs Python Packages
What youβll learn:
modandusevsimport, visibility (pub) vs Pythonβs convention-based privacy, Cargo.toml vs pyproject.toml, crates.io vs PyPI, and workspaces vs monorepos.Difficulty: π’ Beginner
Python Module System
# Python β files are modules, directories with __init__.py are packages
# myproject/
# βββ __init__.py # Makes it a package
# βββ main.py
# βββ utils/
# β βββ __init__.py # Makes utils a sub-package
# β βββ helpers.py
# β βββ validators.py
# βββ models/
# βββ __init__.py
# βββ user.py
# βββ product.py
# Importing:
from myproject.utils.helpers import format_name
from myproject.models.user import User
import myproject.utils.validators as validators
Rust Module System
#![allow(unused)]
fn main() {
// Rust β mod declarations create the module tree, files provide content
// src/
// βββ main.rs # Crate root β declares modules
// βββ utils/
// β βββ mod.rs # Module declaration (like __init__.py)
// β βββ helpers.rs
// β βββ validators.rs
// βββ models/
// βββ mod.rs
// βββ user.rs
// βββ product.rs
// In src/main.rs:
mod utils; // Tells Rust to look for src/utils/mod.rs
mod models; // Tells Rust to look for src/models/mod.rs
use utils::helpers::format_name;
use models::user::User;
// In src/utils/mod.rs:
pub mod helpers; // Declares and re-exports helpers.rs
pub mod validators; // Declares and re-exports validators.rs
}
graph TD
A["main.rs<br/>(crate root)"] --> B["mod utils"]
A --> C["mod models"]
B --> D["utils/mod.rs"]
D --> E["helpers.rs"]
D --> F["validators.rs"]
C --> G["models/mod.rs"]
G --> H["user.rs"]
G --> I["product.rs"]
style A fill:#d4edda,stroke:#28a745
style D fill:#fff3cd,stroke:#ffc107
style G fill:#fff3cd,stroke:#ffc107
Python equivalent: Think of
mod.rsas__init__.pyβ it declares what the module exports. The crate root (main.rs/lib.rs) is like your top-level package__init__.py.
Key Differences
| Concept | Python | Rust |
|---|---|---|
| Module = file | β Automatic | Must declare with mod |
| Package = directory | __init__.py | mod.rs |
| Public by default | β Everything | β Private by default |
| Make public | _prefix convention | pub keyword |
| Import syntax | from x import y | use x::y; |
| Wildcard import | from x import * | use x::*; (discouraged) |
| Relative imports | from . import sibling | use super::sibling; |
| Re-export | __all__ or explicit | pub use inner::Thing; |
Visibility β Private by Default
# Python β "we're all adults here"
class User:
def __init__(self):
self.name = "Alice" # Public (by convention)
self._age = 30 # "Private" (convention: single underscore)
self.__secret = "shhh" # Name-mangled (not truly private)
# Nothing stops you from accessing _age or even __secret
print(user._age) # Works fine
print(user._User__secret) # Works too (name mangling)
#![allow(unused)]
fn main() {
// Rust β private is enforced by the compiler
pub struct User {
pub name: String, // Public β anyone can access
age: i32, // Private β only this module can access
}
impl User {
pub fn new(name: &str, age: i32) -> Self {
User { name: name.to_string(), age }
}
pub fn age(&self) -> i32 { // Public getter
self.age
}
fn validate(&self) -> bool { // Private method
self.age > 0
}
}
// Outside the module:
let user = User::new("Alice", 30);
println!("{}", user.name); // β
Public
// println!("{}", user.age); // β Compile error: field is private
println!("{}", user.age()); // β
Public method (getter)
}
Crates vs PyPI Packages
Python Packages (PyPI)
# Python
pip install requests # Install from PyPI
pip install "requests>=2.28" # Version constraint
pip freeze > requirements.txt # Lock versions
pip install -r requirements.txt # Reproduce environment
Rust Crates (crates.io)
# Rust
cargo add reqwest # Install from crates.io (adds to Cargo.toml)
cargo add reqwest@0.12 # Version constraint
# Cargo.lock is auto-generated β no manual step
cargo build # Downloads and compiles dependencies
Cargo.toml vs pyproject.toml
# Rust β Cargo.toml
[package]
name = "my-project"
version = "0.1.0"
edition = "2021"
[dependencies]
serde = { version = "1.0", features = ["derive"] } # With feature flags
reqwest = { version = "0.12", features = ["json"] }
tokio = { version = "1", features = ["full"] }
log = "0.4"
[dev-dependencies]
mockall = "0.13"
Essential Crates for Python Developers
| Python Library | Rust Crate | Purpose |
|---|---|---|
requests | reqwest | HTTP client |
json (stdlib) | serde_json | JSON parsing |
pydantic | serde | Serialization/validation |
pathlib | std::path (stdlib) | Path handling |
os / shutil | std::fs (stdlib) | File operations |
re | regex | Regular expressions |
logging | tracing / log | Logging |
click / argparse | clap | CLI argument parsing |
asyncio | tokio | Async runtime |
datetime | chrono | Date and time |
pytest | Built-in + rstest | Testing |
dataclasses | #[derive(...)] | Data structures |
typing.Protocol | Traits | Structural typing |
subprocess | std::process (stdlib) | Run external commands |
sqlite3 | rusqlite | SQLite |
sqlalchemy | diesel / sqlx | ORM / SQL toolkit |
fastapi | axum / actix-web | Web framework |
Workspaces vs Monorepos
Python Monorepo (typical)
# Python monorepo (various approaches, no standard)
myproject/
βββ pyproject.toml # Root project
βββ packages/
β βββ core/
β β βββ pyproject.toml # Each package has its own config
β β βββ src/core/...
β βββ api/
β β βββ pyproject.toml
β β βββ src/api/...
β βββ cli/
β βββ pyproject.toml
β βββ src/cli/...
# Tools: poetry workspaces, pip -e ., uv workspaces β no standard
Rust Workspace
# Rust β Cargo.toml at root
[workspace]
members = [
"core",
"api",
"cli",
]
# Shared dependencies across workspace
[workspace.dependencies]
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
# Rust workspace structure β standardized, built into Cargo
myproject/
βββ Cargo.toml # Workspace root
βββ Cargo.lock # Single lock file for all crates
βββ core/
β βββ Cargo.toml # [dependencies] serde.workspace = true
β βββ src/lib.rs
βββ api/
β βββ Cargo.toml
β βββ src/lib.rs
βββ cli/
βββ Cargo.toml
βββ src/main.rs
# Workspace commands
cargo build # Build everything
cargo test # Test everything
cargo build -p core # Build just the core crate
cargo test -p api # Test just the api crate
cargo clippy --all # Lint everything
Key insight: Rust workspaces are first-class, built into Cargo. Python monorepos require third-party tools (poetry, uv, pants) with varying levels of support. In a Rust workspace, all crates share a single
Cargo.lock, ensuring consistent dependency versions across the project.
Exercises
ποΈ Exercise: Module Visibility (click to expand)
Challenge: Given this module structure, predict which lines compile and which donβt:
mod kitchen {
fn secret_recipe() -> &'static str { "42 spices" }
pub fn menu() -> &'static str { "Today's special" }
pub mod staff {
pub fn cook() -> String {
format!("Cooking with {}", super::secret_recipe())
}
}
}
fn main() {
println!("{}", kitchen::menu()); // Line A
println!("{}", kitchen::secret_recipe()); // Line B
println!("{}", kitchen::staff::cook()); // Line C
}
π Solution
- Line A: β
Compiles β
menu()ispub - Line B: β Compile error β
secret_recipe()is private tokitchen - Line C: β
Compiles β
staff::cook()ispub, andcook()can accesssecret_recipe()viasuper::(child modules can access parentβs private items)
Key takeaway: In Rust, child modules can see parentβs privates (like Pythonβs _private convention, but enforced). Outsiders cannot. This is the opposite of Python where _private is just a hint.