Organizing Code with Modules
As a Rust program grows past a single file, you need a way to group related code together, hide implementation details, and avoid naming collisions. Rust’s module system does exactly this: it lets you organize functions, structs, enums, traits, and constants into a tree of named namespaces called modules, and lets you control exactly which parts of that tree are visible from the outside. Privacy is not just a convention here — the compiler enforces the boundaries you draw, so a well-designed module layout is also how you build safe, encapsulated APIs in Rust. This lesson covers how the module tree works, the syntax for declaring and nesting modules, how visibility (pub) and paths work, and how modules map onto files as a project grows.
Overview: How Modules Work
Think of a Rust crate (a compiled unit — a binary or a library) as a filesystem. The crate root (src/main.rs for a binary, src/lib.rs for a library) is the top-level directory. Every mod declaration you write creates a new “subdirectory” — a nested namespace that can hold its own functions, structs, enums, traits, constants, and even further nested modules. The result is a tree, and every item in your program has an address in that tree called a path, written with :: as the separator, such as shapes::circle::Circle.
The critical rule that makes this tree useful is privacy: every item — a function, a struct, a struct’s fields, a module itself — is private by default. A private item is visible only inside the module that defines it and that module’s descendants. To make an item reachable from outside its module, you mark it pub. This means the module tree isn’t just an organizational nicety; it is literally how you decide what counts as your crate’s public API versus its private internals. Code outside a module cannot reach into it and touch something you didn’t explicitly expose, no exceptions, checked entirely at compile time with zero runtime cost.
There are two equivalent ways to write a module. You can write it inline, with the module’s contents in a brace block right where you declare it — mod shapes { ... }. Or, once a module grows large, you can write mod shapes; (note the semicolon, no braces) and the compiler will look for the module’s contents in a separate file, either src/shapes.rs or src/shapes/mod.rs. Both forms produce the exact same module tree; splitting into files is purely an organizational choice for humans, not something the compiler treats differently. Because this lesson’s examples must compile as single self-contained files, every example below uses the inline form — but everything about paths, nesting, and privacy applies identically once you split modules across files, which the Syntax section shows.
Finally, paths can be written two ways: absolute, starting from the crate root with the keyword crate (e.g. crate::shapes::circle::Circle), or relative, starting from the current module using self (this module) or super (the parent module). Typing a full path every time you use something is tedious, so a use declaration brings a path into scope once, letting you refer to the item by its short name afterward.
Syntax
The general forms for declaring and using modules:
// Declaring a module inline
mod module_name {
// items: fn, struct, enum, trait, const, mod ...
}
// Declaring a module that lives in another file
mod module_name; // looks for module_name.rs or module_name/mod.rs
// Visibility modifiers
pub fn visible_everywhere() {}
pub(crate) fn visible_in_this_crate_only() {}
pub(super) fn visible_to_parent_module() {}
fn private_by_default() {}
// Bringing paths into scope
use crate::module_name::item;
use crate::module_name::{item_a, item_b};
use crate::module_name::item as alias;
pub use crate::module_name::item; // re-export
| Modifier | Visible from |
|---|---|
(nothing) |
Current module and its descendants only |
pub |
Anywhere the module itself is reachable from |
pub(crate) |
Anywhere within the current crate, not outside it |
pub(super) |
The parent module only |
pub(in path) |
A specific ancestor module and its descendants |
When a project splits modules into files, the directory layout mirrors the module tree exactly:
my_project/
├── Cargo.toml
└── src/
├── main.rs // crate root: declares `mod shapes;`
├── shapes.rs // the `shapes` module: declares `pub mod circle;`
└── shapes/
└── circle.rs // the `shapes::circle` module
Examples
Example 1: A basic module
The simplest module just groups a function under a name. You call it through its path.
mod greetings {
pub fn hello() {
println!("Hello from the greetings module!");
}
}
fn main() {
greetings::hello();
}
Output:
Hello from the greetings module!
The function hello lives at the path greetings::hello. Because it’s marked pub, code outside the greetings module — here, main, which lives in the crate root — can call it. Drop the pub and this line fails to compile, because main is outside greetings.
Example 2: Nested modules and use
Modules can nest arbitrarily deep, and use lets you avoid typing the full path every time.
mod shapes {
pub struct Rectangle {
pub width: f64,
pub height: f64,
}
impl Rectangle {
pub fn new(width: f64, height: f64) -> Rectangle {
Rectangle { width, height }
}
pub fn area(&self) -> f64 {
self.width * self.height
}
}
pub mod circle {
pub struct Circle {
pub radius: f64,
}
impl Circle {
pub fn area(&self) -> f64 {
std::f64::consts::PI * self.radius * self.radius
}
}
}
}
use shapes::Rectangle;
use shapes::circle::Circle;
fn main() {
let rect = Rectangle::new(3.0, 4.0);
let circ = Circle { radius: 2.0 };
println!("Rectangle area: {}", rect.area());
println!("Circle area: {:.2}", circ.area());
}
Output:
Rectangle area: 12
Circle area: 12.57
circle is a module nested inside shapes, and it must itself be marked pub mod circle — every module on the path from the caller down to the item must be public, not just the final item. The two use lines bring Rectangle and Circle into scope by their short names, so main can write Rectangle::new(...) instead of the longer shapes::Rectangle::new(...).
Example 3: Encapsulation with a realistic example
Modules are how you hide internal state. Here, Account‘s balance field stays private — outside code can only change it through methods that enforce the rules.
mod bank {
pub struct Account {
owner: String,
balance: i64,
}
impl Account {
pub fn new(owner: &str) -> Account {
Account {
owner: owner.to_string(),
balance: 0,
}
}
pub fn deposit(&mut self, amount: i64) {
self.balance += amount;
}
pub fn withdraw(&mut self, amount: i64) -> Result<(), String> {
if amount > self.balance {
return Err(format!("{} has insufficient funds", self.owner));
}
self.balance -= amount;
Ok(())
}
pub fn balance(&self) -> i64 {
self.balance
}
}
}
use bank::Account;
fn main() {
let mut acct = Account::new("Priya");
acct.deposit(500);
match acct.withdraw(700) {
Ok(()) => println!("Withdrawal succeeded"),
Err(e) => println!("Withdrawal failed: {}", e),
}
acct.deposit(300);
match acct.withdraw(700) {
Ok(()) => println!("Withdrawal succeeded, balance is now {}", acct.balance()),
Err(e) => println!("Withdrawal failed: {}", e),
}
}
Output:
Withdrawal failed: Priya has insufficient funds
Withdrawal succeeded, balance is now 100
Notice owner and balance have no pub keyword, even though Account itself is pub. A struct being public does not make its fields public — each field’s visibility is independent. Outside code can only interact with the balance through deposit, withdraw, and balance, so it’s impossible to set a negative balance or bypass the insufficient-funds check by poking the field directly.
How It Works Step by Step
When rustc compiles your crate, module resolution happens roughly like this:
- 1. Discover the tree. Starting at the crate root, the compiler reads every
mod name { ... }ormod name;declaration. For the semicolon form, it loadsname.rs(orname/mod.rs) and recurses into it, discovering any furthermoddeclarations there. - 2. Attach a visibility to every item. Each function, struct, field, and module gets a privacy marker: private (the default) or one of the
pubvariants. - 3. Resolve every path. For each path like
shapes::circle::Circle, the compiler walks the tree edge by edge from the path’s starting point (the crate root for absolute paths, or the current module for relative ones). At each step, it checks: is this edge visible from where the path is being used? An item is reachable only if every module along the way, and the final item itself, is visible from the call site. - 4. Apply
useas a shortcut, not a copy. Ausedeclaration doesn’t duplicate code or change what’s compiled — it just registers a short alias for a path in the current scope, resolved once at compile time. - 5. Reject violations. If any path in your program reaches for something that isn’t visible from the call site, compilation stops with an error (commonly
E0603for a private item orE0616for a private field) — there is no way to work around this at runtime, because the check never happens at runtime in the first place.
Common Mistakes
Mistake 1: Forgetting pub, then being surprised the item is private
Everything is private by default, including struct fields even when the struct itself is public. Trying to construct or read fields from outside the module fails:
mod shapes {
struct Square {
side: f64,
}
impl Square {
fn area(&self) -> f64 {
self.side * self.side
}
}
}
fn main() {
let sq = shapes::Square { side: 4.0 }; // error: struct `Square` is private
println!("{}", sq.area()); // error: method `area` is private
}
Neither Square, its field side, nor the method area carry pub, so nothing about them is reachable from main. The fix is to mark exactly the pieces you intend to expose:
mod shapes {
pub struct Square {
pub side: f64,
}
impl Square {
pub fn area(&self) -> f64 {
self.side * self.side
}
}
}
fn main() {
let sq = shapes::Square { side: 4.0 };
println!("{}", sq.area());
}
Output:
16
Mistake 2: Using a short name without the correct path
A nested module’s items are not automatically in scope just because they’re marked pub — you still need to write (or use) the full path from where you are:
mod outer {
pub mod inner {
pub fn greet() {
println!("Hi from inner");
}
}
}
fn main() {
inner::greet(); // error[E0433]: failed to resolve: use of undeclared crate or module `inner`
}
From main‘s point of view, inner doesn’t exist on its own — it only exists as outer::inner. Either spell out the full path, or bring it into scope with use:
mod outer {
pub mod inner {
pub fn greet() {
println!("Hi from inner");
}
}
}
fn main() {
outer::inner::greet();
}
Output:
Hi from inner
Best Practices
- Start every item private and add
pubonly when something outside the module genuinely needs it — it’s much easier to widen an API later than to narrow one that’s already public. - Prefer
pub(crate)over fullpubfor items that other parts of your own crate need but that shouldn’t be part of the crate’s external API. - Use
useto shorten paths you type often, but avoid glob imports (use module::*;) in regular code — they hide where a name comes from; they’re fine for test modules or well-known preludes. - Use
pub useto re-export a deeply nested item at a shallower, more convenient path, giving users of your crate a clean public API that doesn’t mirror your internal file layout. - Once a module’s inline block grows past a screenful of code, split it into its own file with
mod name;— the module tree and privacy rules stay identical. - Keep struct fields private and expose behavior through methods, as in the bank account example, so invariants (like “balance never goes negative”) can’t be bypassed.
- Put unit tests in a
#[cfg(test)] mod testssubmodule inside the file they test — tests can see private items because they’re nested inside the same module.
Practice Exercises
- Exercise 1: Write a module named
mathcontaining public functionsadd(a: i32, b: i32) -> i32andmultiply(a: i32, b: i32) -> i32. Call both frommainand print the results. Expected output foradd(2, 3)andmultiply(2, 3):5and6. - Exercise 2: Create nested modules
library::fictionandlibrary::nonfiction, each with a public functiondescribe()that prints a short sentence. Bring both into scope with two separateusestatements and call them frommain. - Exercise 3: Inside a module
temperature, write a private helper functioncelsius_to_fahrenheitand a public functionreport(c: f64)that calls the helper and prints the converted value. Then, as a thought experiment (don’t submit code that fails to compile), predict what error you’d get ifmaintried to calltemperature::celsius_to_fahrenheitdirectly.
Summary
- A crate’s items form a tree of modules rooted at the crate root (
main.rsorlib.rs); every item has an address in that tree called a path. - Everything is private by default — visible only inside its own module and that module’s descendants — until marked
pub(or a narrower variant likepub(crate)). - A struct being
pubdoes not make its fields public; each field’s visibility is set independently. - Modules can be written inline (
mod name { ... }) or in a separate file (mod name;, looking forname.rsorname/mod.rs) — both produce an identical module tree. - Paths are absolute (from
crate) or relative (self,super); every module along a path must be visible from the call site, not just the final item. usebrings a path into scope under a short name; it’s a compile-time alias, not a copy of code.- Privacy is enforced entirely at compile time with no runtime cost, making modules Rust’s primary tool for building safe, encapsulated APIs.
