The module tree lives in code, not files
You've written a Python script that started as fifty lines and ballooned to two thousand. You've got helper functions, data structures, and business logic all tangled together. You try to split it into files, but suddenly imports break, names clash, and you're not sure what's public and what's internal. Rust handles this with a module system that feels different from the file-based imports you're used to. In Rust, the module tree is the source of truth, not the file system.
Rust's module system builds a tree of namespaces. This tree defines what code can see what other code. The key insight is that the module tree lives in your code, not on your disk. Files are just storage. You can have a module config that lives inside main.rs, or you can have config live in config.rs. The compiler doesn't care about the file structure as much as it cares about the mod declarations.
Think of modules like entries in a library catalog. The catalog tells you where things are and who can check them out. The physical shelves are the files. You can reorganize the shelves without changing the catalog, and the catalog structure doesn't force you to move books around. The module system is the catalog. The files are just where the compiler finds the text.
Minimal module
A module creates a new scope. Everything inside that scope is private by default. Only items marked pub are visible to code outside the module.
// main.rs
// Declare a module named utils. This creates a namespace.
mod utils {
// Functions are private by default.
// internal_helper is only accessible within the utils module.
fn internal_helper() {
println!("Internal work.");
}
// pub makes the function visible to code outside utils.
pub fn public_tool() {
internal_helper();
println!("Tool running.");
}
}
fn main() {
// Access public members via the module path.
utils::public_tool();
}
Privacy is the default. Expose only what the interface requires.
Visibility is relative
pub does not mean "public to the world". It means "public to the parent module". This distinction matters when you nest modules. If a module is private, its public children are still hidden from the outside world.
mod outer {
// This module is private to outer.
mod inner {
// This function is public to inner's parent (outer).
pub fn secret() {
println!("Found me.");
}
}
// outer can see inner::secret because inner is a child.
fn reveal() {
inner::secret();
}
}
fn main() {
// This fails. inner is private to outer.
// outer::inner::secret();
}
If you try to access a private item, the compiler rejects it with E0603 (function is private). The error tells you exactly which item is hidden and where it's defined. Rust's visibility rules are strict. There are no workarounds. If the path isn't fully public, the code doesn't compile.
Splitting across files
In a real project, you split modules across files. The convention is simple: mod name; in a parent file tells the compiler to look for name.rs in the same directory.
// src/lib.rs
// Declare the module. The compiler looks for src/utils.rs.
mod utils;
/// Run the main logic of the crate.
pub fn run() {
// Use the public function from utils.
utils::format_output();
}
// src/utils.rs
// This file defines the contents of the utils module.
// No mod keyword needed here.
/// Parse raw input into structured data.
fn parse_data(raw: &str) -> Vec<&str> {
raw.split_whitespace().collect()
}
/// Format and print the output.
pub fn format_output() {
let data = parse_data("hello world");
println!("Parsed: {:?}", data);
}
Convention aside: The community prefers name.rs over name/mod.rs for top-level modules. mod.rs exists for legacy reasons and for modules that contain submodules, but name.rs keeps the file system flat and readable. Stick to name.rs unless you have a specific reason to nest directories.
The file system is a detail. The module tree is the law.
Paths and imports
Rust uses paths to refer to items. Paths can be absolute or relative. Absolute paths start with crate (for the current crate) or an external crate name. Relative paths start with self, super, or an identifier.
mod alpha {
/// A tool provided by alpha.
pub fn tool() {}
mod beta {
// Absolute path from crate root.
// Robust against refactoring.
use crate::alpha::tool;
// Relative path going up one level.
// Fragile if beta moves.
// use super::tool;
/// Use the tool.
pub fn work() {
tool();
}
}
}
Convention aside: Prefer absolute paths like crate::utils over relative paths like super::utils. Absolute paths survive refactoring. If you move a module, relative paths break. Absolute paths stay correct. The compiler resolves both at compile time, so there's no performance difference. Clarity wins.
use creates a local alias. It does not change visibility. If you import a private item, you still can't use it. If you want to expose an item through a module, use pub use.
Re-exporting
Libraries often flatten their API by re-exporting items from submodules. This lets users call my_crate::function() instead of my_crate::utils::function().
// src/lib.rs
mod utils;
// Re-export to flatten the API.
// Users can call my_crate::format_output() instead of my_crate::utils::format_output().
pub use utils::format_output;
pub use makes the item part of the module's public API. The item retains its original definition. You're just adding a new public path to it. This is how standard library crates like std expose items from internal modules.
Treat pub use as a curation tool. Only re-export what belongs in the public interface.
Pitfalls
A common trap is thinking pub makes something public to the outside world. It only makes it public to the parent. If you have mod a { pub mod b { fn foo() {} } }, calling a::b::foo from outside a fails because b is private to a. You need pub mod b.
Another trap is confusing use with visibility. use creates a local alias. It doesn't re-export. If you want to expose an item through a module, use pub use.
If you misspell a path or forget a mod declaration, you get E0432 (unresolved import). The compiler lists what it found and what it expected. Read the suggestions. They're usually accurate.
If you try to import a private item, you get E0603. The error tells you the item is private. Check the pub modifiers up the tree.
Decision matrix
Use mod to define a new namespace and group related code. Use pub to expose an item to its parent module. Use pub(crate) to restrict visibility to the current crate only, hiding implementation details from downstream users. Use pub(super) to expose an item only to the immediate parent, useful for internal helpers that shouldn't leak further up. Use use to create a local alias for a path, reducing verbosity in the current scope. Use pub use to re-export an item, making it part of the module's public API without changing its original definition.
Start private. Make things public only when the interface demands it.