Modules and Visibility in Rust Without the Confusion
Modules organize Rust code and control what is public. Learn mod, pub, use, super, self, file layout and why privacy is by module and not by type.

Modules are where newcomers meet their first confusing compiler errors outside of ownership, and the confusion has one root: a module tree is something you declare, not something the compiler discovers from your directory. Once you see that the file system only matters after you write mod, the rules fit on one page. This post builds a small shop crate with three modules, shows the files, and then breaks it four different ways so you can recognise each error. Everything compiled and ran on Rust 1.99, edition 2024.
The project
The crate has an inventory module, a billing module with a child module for invoices, and a main that wires them together. The tree on disk:
src/
main.rs
inventory.rs
billing/
mod.rs
invoice.rsmain.rs declares the two top-level modules and uses items from them:
mod billing;
mod inventory;
use billing::invoice::Invoice;
use inventory::Item;
fn main() {
let item = Item::new("keyboard", 4999, 3);
println!("{} costs {}", item.name, item.price_display());
let mut invoice = Invoice::new();
invoice.add(&item, 2);
println!("{}", invoice.summary());
println!("stock left: {}", item.stock());
}keyboard costs $49.99
subtotal 9998 cents + tax 799 cents = 10797 cents
stock left: 3The two mod lines are the declarations. mod inventory; tells the compiler: there is a module called inventory, load its body from inventory.rs or inventory/mod.rs. Without that line the file is ignored entirely, which surprises people who expect every file under src to be compiled. The use lines are separate and only shorten paths: use inventory::Item; lets main write Item instead of inventory::Item.
Privacy is the default
Here is inventory.rs:
pub struct Item {
pub name: String,
price_cents: u32, // private: only this module can read or write it directly
stock: u32,
}
impl Item {
pub fn new(name: &str, price_cents: u32, stock: u32) -> Item {
Item {
name: name.to_string(),
price_cents,
stock,
}
}
pub fn price_display(&self) -> String {
format_money(self.price_cents)
}
pub fn stock(&self) -> u32 {
self.stock
}
// Visible to the rest of the crate, but not to users of a library built from it.
pub(crate) fn price_cents(&self) -> u32 {
self.price_cents
}
}
// Private helper: nothing outside `inventory` may call it.
fn format_money(cents: u32) -> String {
format!("${}.{:02}", cents / 100, cents % 100)
}Item is pub, and so are its name field and the functions new, price_display and stock. The two fields price_cents and stock have no pub, so only code inside the inventory module can touch them directly; outside code reads the stock through the stock() method. This is the module system doing the job that private fields do in a class: the type's author decides which parts of its representation are part of the contract. Marking a struct pub does not make its fields public. A struct whose fields are all private cannot even be built with a literal outside the module, which is why Item::new exists.
The function format_money is private too. It is an implementation detail of price_display, and nothing outside inventory may call it. Try anyway:
println!("{}", inventory::format_money(5)); // added to mainerror[E0603]: function `format_money` is private
--> src/main.rs:14:31
|
14 | println!("{}", inventory::format_money(5));
| ^^^^^^^^^^^^ private function
|
note: the function `format_money` is defined here
--> src/inventory.rs:31:1
|E0603, function format_money is private. The compiler points at the call and at the definition, and the fix is a decision rather than a keyword: either the function should be part of the API (add pub) or the caller should use price_display instead. Making things pub to silence an error is the habit to avoid; a pub function is a promise that you will keep it working.
Fields work the same way. Reading item.price_cents from main instead of calling the method:
println!("{}", item.price_cents); // field, not methoderror[E0616]: field `price_cents` of struct `Item` is private
--> src/main.rs:14:25
|
14 | println!("{}", item.price_cents);
| ^^^^^^^^^^^ private field
|
help: a method `price_cents` also exists, call it with parentheses
|
14 | println!("{}", item.price_cents());
| ++E0616, field price_cents of struct Item is private. The help notes that a method with that name exists and suggests calling it. That method is pub(crate), which brings us to the middle level of visibility.
Visibility levels
| Written as | Visible to |
|---|---|
| nothing | the current module and its children |
pub(self) | same as nothing |
pub(super) | the parent module and everything below it |
pub(in path) | a named ancestor module |
pub(crate) | everything in this crate, and nobody outside it |
pub | anyone who can reach the module |
pub(crate) is the level most application code needs. price_cents() is read by the billing module, which lives elsewhere in the same crate, but it is not something a dependent crate should see if this code is ever published as a library. When you are writing a library, pub is your public API and every pub(crate) is room to change without breaking users; designing a public API develops that idea.
One rule runs in only one direction. A child module can use its parent's private items, because it is "inside" the parent. The parent cannot use the child's private items. billing/mod.rs shows both:
pub mod invoice;
// A sibling module can reach up with `super::`, and down with a relative path.
fn tax_rate_percent() -> u32 {
8
}
pub(super) fn tax_for(subtotal: u32) -> u32 {
subtotal * tax_rate_percent() / 100
}tax_rate_percent is private to billing, and tax_for is pub(super), visible to billing's parent and below. The child module invoice calls super::tax_for, and it could equally have called the private super::tax_rate_percent(); the compiler accepts that, because a child may see its ancestors' private items. Here is the child:
use crate::inventory::Item;
pub struct Invoice {
lines: Vec<(String, u32, u32)>,
}
impl Invoice {
pub fn new() -> Invoice {
Invoice { lines: Vec::new() }
}
pub fn add(&mut self, item: &Item, qty: u32) {
self.lines
.push((item.name.clone(), item.price_cents(), qty));
}
pub fn summary(&self) -> String {
let subtotal: u32 = self.lines.iter().map(|(_, price, qty)| price * qty).sum();
let tax = super::tax_for(subtotal);
format!(
"subtotal {} cents + tax {} cents = {} cents",
subtotal,
tax,
subtotal + tax
)
}
}Two path words appear. crate::inventory::Item starts at the root of the crate, so it works from anywhere. super::tax_for starts one module up. A path with neither is relative to the current module. Absolute crate:: paths survive moving a file; super:: paths survive moving the whole subtree. Choose crate:: for the cross-module imports and super:: for talking to your parent.
Two path errors
A use with a mistake in the path fails with a different code than a privacy error. Changing the import in main to use billing::invoices::Invoice; gives:
error[E0432]: unresolved import `billing::invoices`
--> src/main.rs:4:14
|
4 | use billing::invoices::Invoice;
| ^^^^^^^^ could not find `invoices` in `billing`
For more information about this error, try `rustc --explain E0432`.
error: could not compile `shop` (bin "shop") due to 1 previous errorE0432, unresolved import: billing has no module invoices. The module is invoice, singular. Typos like this are the most common cause, followed by forgetting the mod declaration, in which case the compiler says the module does not exist at all. The second kind of error is a name used without being brought into scope. Delete the use inventory::Item; line and keep the rest:
error[E0433]: cannot find type `Item` in this scope
--> src/main.rs:8:16
|
8 | let item = Item::new("keyboard", 4999, 3);
| ^^^^ use of undeclared type `Item`
|
help: consider importing this struct
|
4 + use crate::inventory::Item;
|With the use inventory::Item; line removed, Item::new has nothing to refer to, and E0433 explains: use of undeclared type Item, with a suggested import. The suggestion even includes the full crate::inventory::Item. Rust's diagnostics for missing imports are good enough that you can usually accept the suggestion as written.
What mod and use do, side by side
mod name; | use path::Item; | |
|---|---|---|
| Loads a file | yes | no |
| Creates a module | yes | no |
| Shortens a path | no | yes |
| Appears once per module | yes | as often as needed |
Can be pub | yes, to export the module | yes, pub use re-exports an item |
pub use is the tool for presenting a clean public API: define things deep in the tree for your own convenience and re-export the ones users need at the top, so crate::Item works even though the type lives in crate::inventory. Crates, packages and modules puts modules in context with crates and packages.
Habits
- Start with everything private and add the narrowest
pubthat compiles. - One file per module is fine until a file passes a few hundred lines; then split it.
- Prefer
mod.rs-free naming (billing.rsbeside abilling/folder) in new code if your team agrees; both work, and mixing them in one crate is the thing to avoid. - Group imports at the top of the file and let the formatter sort them.
- Treat a
pubitem in a library as a commitment and apub(crate)item as an implementation detail.
When the module system is not the answer
Modules organise names inside one crate. If you want to compile parts separately, publish them separately or enforce a dependency direction between teams, you want multiple crates in a workspace, where the visibility boundary is enforced by the build instead of by convention. And if a single file starts with fifteen mod blocks nested inline, the structure is hiding in indentation instead of in the file tree; moving them to files makes the layout visible to anyone who opens the directory.


