Modules and Crates
Rust's module system controls code organization, visibility, and namespacing. Understanding it is essential for any project beyond a single file.
For Python developers: Think of modules as Python's module system, but with explicit visibility control (public/private). Crates are like Python packages.
Key Terminology
| Term | Meaning | Python Equivalent |
|---|---|---|
| Crate | A compilation unit - either a binary or a library | Package on PyPI |
| Package | One or more crates bundled by Cargo (has Cargo.toml) | Project with setup.py/pyproject.toml |
| Module | A namespace for organizing code within a crate | Python module (.py file) |
| Path | How you name an item (function, struct, etc.) | Import path |
Packages and Crates
Creating a Package
# Binary crate (has main.rs)
cargo new myapp
# Library crate (has lib.rs)
cargo new --lib mylib
Package Structure
A package can contain multiple crates:
mypackage/
├── Cargo.toml
├── src/
│ ├── main.rs # Binary crate root (optional)
│ ├── lib.rs # Library crate root (optional)
│ └── bin/
│ ├── tool1.rs # Additional binary crate
│ └── tool2.rs # Another binary crate
# Run the default binary
cargo run
# Run a specific binary
cargo run --bin tool1
Modules
Defining Modules
// src/lib.rs
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {
println!("Added to waitlist");
}
fn seat_at_table() {
println!("Seated");
}
}
mod serving {
fn take_order() {}
fn serve_order() {}
}
}
pub fn eat_at_restaurant() {
// Absolute path
crate::front_of_house::hosting::add_to_waitlist();
// Relative path
front_of_house::hosting::add_to_waitlist();
}
Module Tree
The code above creates this hierarchy:
crate (lib.rs)
└── front_of_house
├── hosting
│ ├── add_to_waitlist (pub)
│ └── seat_at_table (private)
└── serving (private)
├── take_order
└── serve_order
Visibility (pub)
Everything in Rust is private by default. Use pub to make items public.
Visibility Rules
mod outer {
pub mod inner {
pub fn public_function() {
println!("public");
private_function(); // OK: same module
}
fn private_function() {
println!("private");
}
}
fn outer_function() {
// OK: child module is accessible from parent
inner::public_function();
// inner::private_function(); // ERROR: private
}
}
fn main() {
outer::inner::public_function(); // OK
// outer::outer_function(); // ERROR: private
}
Pub for Structs
Fields of a public struct are still private by default:
mod restaurant {
pub struct Breakfast {
pub toast: String, // Public field
seasonal_fruit: String, // Private field
}
impl Breakfast {
// Need a constructor since seasonal_fruit is private
pub fn summer(toast: &str) -> Breakfast {
Breakfast {
toast: toast.to_string(),
seasonal_fruit: String::from("peaches"),
}
}
}
}
fn main() {
let mut meal = restaurant::Breakfast::summer("Rye");
meal.toast = String::from("Wheat"); // OK: public field
// meal.seasonal_fruit = "blueberries"; // ERROR: private field
println!("Toast: {}", meal.toast);
}
Pub for Enums
A public enum has all its variants public:
mod menu {
pub enum Appetizer {
Soup,
Salad,
}
}
fn main() {
let order = menu::Appetizer::Soup; // All variants are public
}
Visibility Modifiers
| Modifier | Visible To |
|---|---|
| (none) | Current module only |
pub | Everything |
pub(crate) | Current crate only |
pub(super) | Parent module |
pub(in path) | Specified module |
mod outer {
pub(crate) fn crate_only() {
println!("visible within crate");
}
mod inner {
pub(super) fn parent_only() {
println!("visible to outer");
}
}
fn demo() {
inner::parent_only(); // OK
}
}
The use Keyword
Bringing Paths into Scope
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}
// Bring the module into scope
use front_of_house::hosting;
pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
}
Idiomatic use Paths
// For functions: bring the parent module (shows where it comes from)
use std::collections::HashMap;
// Then call: HashMap::new()
// For structs/enums: bring the item directly
use std::collections::HashMap;
// Then use: HashMap::new()
// Exception: when two types have the same name
use std::fmt;
use std::io;
// Then: fmt::Result and io::Result
Renaming with as
use std::fmt::Result;
use std::io::Result as IoResult;
fn function1() -> Result {
Ok(())
}
fn function2() -> IoResult<()> {
Ok(())
}
Re-exporting with pub use
mod internal {
pub fn helper() {
println!("helping");
}
}
// Re-export so users of this module can access it directly
pub use internal::helper;
// External code can now call:
// mycrate::helper() instead of mycrate::internal::helper()
Nested use Paths
// Instead of:
use std::collections::HashMap;
use std::collections::HashSet;
use std::collections::BTreeMap;
// Write:
use std::collections::{HashMap, HashSet, BTreeMap};
// Or import everything:
use std::collections::*;
// Nested with self
use std::io::{self, Write};
// Brings in both std::io and std::io::Write
Separating Modules into Files
Single File Per Module
src/
├── main.rs
├── front_of_house.rs # Module contents
└── front_of_house/
└── hosting.rs # Submodule contents
// src/main.rs
mod front_of_house;
fn main() {
front_of_house::hosting::add_to_waitlist();
}
// src/front_of_house.rs
pub mod hosting;
// src/front_of_house/hosting.rs
pub fn add_to_waitlist() {
println!("Added to waitlist");
}
Practical Project Layout
myapp/
├── Cargo.toml
├── src/
│ ├── main.rs # Entry point, minimal code
│ ├── lib.rs # Library root (optional)
│ ├── config.rs # Config module
│ ├── database.rs # Database module
│ ├── models/
│ │ ├── mod.rs # models module root
│ │ ├── user.rs
│ │ └── product.rs
│ └── handlers/
│ ├── mod.rs # handlers module root
│ ├── auth.rs
│ └── api.rs
// src/main.rs
mod config;
mod database;
mod models;
mod handlers;
fn main() {
let cfg = config::load();
let db = database::connect(&cfg);
// ...
}
// src/models/mod.rs
pub mod user;
pub mod product;
// Re-export for convenience
pub use user::User;
pub use product::Product;
// src/models/user.rs
#[derive(Debug)]
pub struct User {
pub name: String,
pub email: String,
}
impl User {
pub fn new(name: &str, email: &str) -> Self {
User {
name: name.to_string(),
email: email.to_string(),
}
}
}
Now from main.rs you can write:
use crate::models::User;
let user = User::new("Alice", "alice@example.com");
External Crates
Adding Dependencies
# Cargo.toml
[dependencies]
serde = "1.0"
serde_json = "1.0"
rand = "0.8"
Using External Crates
use rand::Rng;
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize, Debug)]
struct Config {
name: String,
port: u16,
}
fn main() {
let mut rng = rand::thread_rng();
let n: u32 = rng.gen_range(1..=100);
println!("Random: {}", n);
let config = Config {
name: "myapp".to_string(),
port: 8080,
};
let json = serde_json::to_string(&config).unwrap();
println!("JSON: {}", json);
}
Version Specification
| Spec | Meaning |
|---|---|
"1.0" | Compatible with 1.0 (same as ^1.0) |
"^1.0" | >= 1.0.0, < 2.0.0 |
"~1.0" | >= 1.0.0, < 1.1.0 |
"=1.0.5" | Exactly 1.0.5 |
">=1.0, <2.0" | Range |
Features
[dependencies]
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
# Optional dependencies
reqwest = { version = "0.11", optional = true }
[features]
default = []
networking = ["reqwest"]
Workspaces
For larger projects with multiple related crates:
# Root Cargo.toml
[workspace]
members = [
"core",
"cli",
"server",
]
my-workspace/
├── Cargo.toml # Workspace root
├── core/
│ ├── Cargo.toml
│ └── src/lib.rs
├── cli/
│ ├── Cargo.toml
│ └── src/main.rs
└── server/
├── Cargo.toml
└── src/main.rs
# cli/Cargo.toml
[dependencies]
core = { path = "../core" }
Practice Exercises
Exercise 1: Library Organization
Create a library with this structure:
- A
mathmodule withadd,subtract,multiply,divide - A
string_utilsmodule withcapitalize,reverse,word_count - Re-export the most common functions at the crate root
Exercise 2: Multi-File Project
Refactor a single-file program into multiple modules:
modelsmodule with data structuresservicesmodule with business logicutilsmodule with helper functions
Exercise 3: Visibility Design
Design a module where:
- Internal implementation details are private
- Only a clean public API is exposed
- A constructor is required because fields are private
Common Mistakes
1. Forgetting pub
mod mymod {
fn helper() {} // Private!
}
mymod::helper(); // ERROR
Fix: Add pub to items you need from outside.
2. Wrong File Structure
src/
├── models.rs
└── models/
└── user.rs // ERROR: can't have both models.rs and models/
Fix: Use models/mod.rs instead of models.rs, or use only models.rs without the directory.
3. Circular Dependencies
Module A depends on B, and B depends on A. Rust doesn't allow circular crate dependencies.
Fix: Extract shared code into a third module.
Key Takeaways
- Crates are the unit of compilation; packages are managed by Cargo
- Modules organize code into namespaces
- Everything is private by default - use
pubto expose usebrings paths into scope;pub usere-exports them- Split modules into files as projects grow
pub(crate)limits visibility to the current crate- Workspaces manage multiple related crates
Next Steps
In the next chapter, we'll explore Rust's concurrency features - threads, channels, and shared state.
Quick Reference
// Define a module
mod mymod {
pub fn public_fn() {}
fn private_fn() {}
}
// Use a module
use mymod::public_fn;
use std::collections::HashMap;
// Rename
use std::io::Result as IoResult;
// Re-export
pub use internal::helper;
// Nested imports
use std::collections::{HashMap, HashSet};
// File-based modules
// src/mymod.rs or src/mymod/mod.rs
mod mymod; // in main.rs or lib.rs
// External crate (after adding to Cargo.toml)
use serde::Serialize;
// Visibility
pub fn public() {}
pub(crate) fn crate_only() {}
pub(super) fn parent_only() {}