One crate, one tree
In Go, organisation is the package, and the package is the directory: every .go file in a folder shares one flat namespace, and the folder name on disk is the import path. It is a simple, file-system-shaped model, and most of the time it serves. In Python a module is a file and a package is a directory with an __init__.py; in Java the package is declared at the top of each file and is expected to mirror the directory. Each language ties its namespace, more or less tightly, to where the bytes sit on disk.
Rust does something a little different, and it is the first thing to get straight. A crate (one library or one binary, the unit the compiler builds in a single pass) is a single tree of modules, and that tree is declared in the source, not inferred from the directory layout. The root of the tree is the crate root file itself. Every other module hangs off it, named, nested, and arranged exactly as you write it. Here is a whole tree in one file. Read it for shape, then press Run:
// One file, a small module tree. Everything lives in the crate root (this file),
// which has one child module, `billing`, which has its own child, `tax`.
mod billing {
pub mod tax {
// A path into the tree: crate -> billing -> tax -> rate.
pub fn rate() -> f64 {
0.2
}
}
pub fn total(net: f64) -> f64 {
net + net * tax::rate()
}
}
fn main() {
// From the crate root, name the path down to the item you want.
let gross = billing::total(100.0);
println!("gross = {gross}");
println!("rate = {}", billing::tax::rate());
}The keyword mod declares a module. mod billing opens a named region of the tree, and inside it pub mod tax opens a child of billing. To reach an item you write its path: a sequence of module names joined by ::, like billing::tax::rate, naming the steps down from where you are to where the item lives. A path in Rust is to the module tree what a file path is to a directory tree: a route through the nesting to one named thing.
Notice that the whole structure lives in one file here. The Workbench compiles a single file, so every module in this chapter is an inline mod name { ... } block. That is genuine Rust, not a workaround: inline modules are a real and common way to write a tree. The other way, one module per file, builds the exact same tree from many files, and section 08 shows how the two map onto each other.
A crate is one tree of modules rooted at the crate root file. You build the tree by writing mod declarations, and you navigate it by writing paths (a::b::c). Unlike Go, the shape of the tree comes from your source, not from the directory layout; the files-and-folders version in section 08 is one encoding of the same tree, not the source of truth.
Private by default, pub to open a door
Here is the rule that surprises people coming from looser languages, so meet it head on: in Rust, every item is private to its module by default. A function, struct, enum, constant, or child module with no pub in front of it can be used by the module that defines it and by that module's descendants, and by nobody else. You do not opt out of exposure; you opt in. The keyword pub is the single door you cut into a module's wall, and you cut it deliberately, item by item.
This inverts the Go convention you may carry in. In Go, visibility rides on the case of the first letter: Total is exported because it is capitalised, total is package-private because it is not. The rule is real but implicit, woven into the name. Rust pulls the decision out of the name and into a keyword you can see: a thing is visible only if pub says so, right there in front of it. Run this, then read what is exposed and what is sealed:
// Every item is private to its module until you write `pub`. The struct is
// public, but you reach its fields only through functions the module exposes.
mod account {
pub struct Wallet {
// No `pub`: balance is private even though Wallet is public.
balance: u64,
}
impl Wallet {
pub fn opened_with(cents: u64) -> Wallet {
Wallet { balance: cents }
}
pub fn balance(&self) -> u64 {
self.balance
}
// A private helper: callers outside `account` cannot name it.
fn fee() -> u64 {
30
}
pub fn after_fee(&self) -> u64 {
self.balance.saturating_sub(Wallet::fee())
}
}
}
fn main() {
let w = account::Wallet::opened_with(500);
// This works: balance() is pub.
println!("balance = {}", w.balance());
println!("after fee = {}", w.after_fee());
// These would NOT compile (uncomment to see the privacy errors):
// let _ = w.balance; // field is private (E0616)
// let _ = account::Wallet::fee(); // method is private (E0624)
}The interesting part is the struct. Wallet is pub, so code outside account can name the type, hold one, and pass it around. But its balance field has no pub, so the field is private even though the struct is public. Outside the module you cannot read or write w.balance directly; you go through the methods the module chose to expose, balance() and after_fee(). Privacy is decided per field, not per type, which is exactly what lets a type keep an invariant: a Wallet can guarantee its balance is only ever changed through code it controls.
The same applies to the fee() method. It carries no pub, so it is an internal helper: after_fee() (inside the module) may call it, but nothing outside account can. That is how you build a type whose surface is small and whose internals stay free to change.
Making a struct pub exposes the type, not its insides. Each field needs its own pub to be reachable from outside the module. Touch a private field from elsewhere and the code does not compile (error E0616, field is private); call a private method and you get E0624. This is the feature, not a nuisance: private fields are how a type protects the rules it promises to keep.
Paths: crate, self, and super
Once a tree has more than one branch, you need to say where a path starts. A path can be absolute, beginning at the crate root with the keyword crate, or relative, beginning from the current module. Two more keywords name relative starting points: self is the current module, and super is its parent, one step up the tree. If you have used a shell, the analogy is exact: crate is the root /, self is ., and super is ... Run this:
// crate, self, and super name where a path starts.
// `crate` is the root of this file; `super` is the parent module;
// `self` is the current module.
fn version() -> &'static str {
"1.0"
}
mod server {
pub fn banner() -> String {
// `super` steps up to the parent (the crate root) to reach version().
format!("server {}", super::version())
}
pub mod http {
pub fn greeting() -> String {
// `super` reaches the parent module `server`; you could also
// write the absolute path `crate::server::banner()`.
let b = super::banner();
// `self` names this module; here it is just for illustration.
format!("{} :: {}", b, self::status())
}
fn status() -> &'static str {
"ok"
}
}
}
fn main() {
println!("{}", server::banner());
println!("{}", server::http::greeting());
}Inside server::banner, the code needs version(), which lives one level up in the crate root. super::version() climbs that one step and finds it. Inside server::http, greeting calls super::banner() to reach the parent module server, and it could just as well have written the absolute path crate::server::banner(); both name the same function. The self::status() call names a function in the current module, which you rarely need to spell out, but it makes the family of prefixes complete.
When should you prefer absolute over relative? The guidance is practical. Absolute paths from crate survive a move: if you relocate the calling code to a different module, a crate::server::banner reference still points at the same item. Relative paths with super keep related code reading naturally when a whole subtree moves together. The standard library and most crates lean on use (next section) so that the body of a function names short tails rather than long paths either way.
crate::a::b is an absolute path from the root. self::b starts in this module, super::b starts in the parent. They are the module-tree versions of /, ., and ... Choose absolute when you want the reference to survive moving the calling code; choose super when a subtree moves as a unit.
use, and as to rename
Writing geometry::circle::area(2.0) every time gets old, the same way it would in any language. Go solves this by importing a package and then naming members through its short name (fmt.Println); Python lets you write from math import sqrt and then say sqrt bare. Rust's tool is use: it brings a path into the current scope so you can name its tail (the last segment) directly, instead of repeating the whole route. A use does not move or copy anything; it just creates a local shortcut to a name that already exists somewhere in the tree.
Two functions can legitimately share a name when they live in different modules: circle::area and square::area are both called area. Import both as bare area and the names collide. The fix is as, which renames an import as it enters scope. Run this:
// `use` brings a path into scope so you can name its tail. `as` renames it
// when two names would collide or one is unwieldy.
mod geometry {
pub mod circle {
pub fn area(r: f64) -> f64 {
std::f64::consts::PI * r * r
}
}
pub mod square {
pub fn area(side: f64) -> f64 {
side * side
}
}
}
// Two functions both named `area`; rename on import to keep them apart.
use geometry::circle::area as circle_area;
use geometry::square::area as square_area;
fn main() {
println!("circle = {:.2}", circle_area(2.0));
println!("square = {:.2}", square_area(2.0));
}Each use ... as ... line takes one path and binds it to a new local name. geometry::circle::area becomes circle_area, geometry::square::area becomes square_area, and now both are in scope under names that do not clash. as earns its keep in two situations: when two imported names would otherwise collide, and when a name is long or generic and a local alias reads better. You will see it most often around error types, where many crates each export a type literally named Error.
A use does not relocate the item or change who can see it; it only adds a name to the current scope. The item still lives where it was defined, and its visibility (whether it is pub) is unchanged. If a path is private to another module, no amount of use will let you reach it: use shortens names you are already allowed to name, it does not grant access.
Scoped visibility: pub(crate) and pub(super)
Plain pub and plain private are the two ends of a spectrum, and real code often wants the middle. Go gives you exactly two levels, exported and package-private, and when you need something visible across a few files but not to the outside world, you reach for the awkward internal/ directory trick. Rust lets you name the audience directly. pub(crate) means visible everywhere in this crate but not part of the published public API; pub(super) means visible to the parent module and its descendants, and no farther. Run this:
// pub(crate) and pub(super) expose an item to a chosen scope, not the world.
mod engine {
// Visible anywhere in this crate, but it would not be part of a published
// public API. Think "internal, shared across the crate."
pub(crate) fn seed() -> u64 {
42
}
pub mod rng {
// Visible to the parent module `engine` and its descendants only.
pub(super) fn mix(x: u64) -> u64 {
x.wrapping_mul(6364136223846793005).wrapping_add(1)
}
pub fn next() -> u64 {
// mix is pub(super), so `rng` (a child of engine) can call it,
// and so can engine itself.
mix(super::seed())
}
}
}
fn main() {
// seed() is pub(crate), so main (same crate) may call it.
println!("seed = {}", engine::seed());
// next() is fully pub, so it crosses module lines freely.
println!("next = {}", engine::rng::next());
// engine::rng::mix(1) would NOT compile here: pub(super) keeps it inside.
}Read the two scoped markers against where they get used. seed() is pub(crate), so main, which lives in the same crate, may call engine::seed(). If this were a library you published, seed would not appear in its public interface; it is shared internally and no further. mix() is pub(super) inside engine::rng, so its visibility reaches up to engine and across engine's descendants, which is why rng::next can call it but main cannot. The fully pub function next() is the only thing here that would cross a crate boundary.
The reason these exist is one good idea: a public API is a promise, and promises are expensive to break. Anything pub in a library is something other people's code can depend on, so every pub item is a maintenance commitment. pub(crate) lets you share code widely within your crate without making that commitment to the outside world, which is exactly what you want for the helpers that knit a crate together but are nobody else's business.
pub exposes an item to the whole world (including other crates). pub(crate) exposes it to this crate only. pub(super) exposes it to the parent module and its subtree. No marker at all keeps it private to its own module. Reach for pub(crate) for the shared internals of a library: visible where you need them, invisible in the published API.
pub use: re-exporting to flatten an API
There is a tension in any growing codebase. You want the inside organised into many small modules, because that keeps each piece focused and easy to move. But you do not want to inflict that internal arrangement on every caller, making them type store::inventory::detail::Item for a type that, from the outside, is just Item. The internal shape should be free to change without rewriting every call site. Rust resolves this with pub use: a use that is itself public, so the name it brings in becomes part of this module's public surface.
This is called re-exporting. The item keeps living in its deep, private home, but a pub use higher up lifts its name to a convenient level, and callers reach it through the short path. Run this and note the path in main:
// `pub use` re-exports a name, so callers see a flat surface even though the
// code is organised in nested modules underneath.
mod store {
mod inventory {
pub struct Item {
pub name: &'static str,
pub qty: u32,
}
pub fn first() -> Item {
Item { name: "bolt", qty: 7 }
}
}
// Lift Item and first() up to `store`, so callers write store::Item,
// not store::inventory::Item. The inventory module stays private.
pub use inventory::{first, Item};
}
fn main() {
// Note: no `inventory` in this path. The re-export flattened it away.
let item: store::Item = store::first();
println!("{} x{}", item.name, item.qty);
}The inventory module is private: it has no pub, so callers outside store cannot name store::inventory at all. Yet main reaches Item and first through store::Item and store::first, with no inventory in the path. The line pub use inventory::{first, Item} did that: it re-exported those two names up to store, flattening the public surface even though the implementation stays nested and sealed. You could reorganise inventory tomorrow, and as long as the pub use still points at the right items, no caller would notice.
This is exactly the move behind the prelude pattern you have already met: a crate gathers its most-used names into one module and pub uses them there, so a single import brings the essentials into scope. The standard library's own std::prelude works this way, which is why Option, Result, and Vec are in scope without you importing them.
pub use path makes a name from elsewhere part of this module's public API. The definition stays where it lives, possibly inside a private module; the pub use gives callers a short, stable path to it. This lets the inside reorganise freely while the published surface stays flat and unchanged.
Nested groups and glob imports
When you import several names from the same module, a column of nearly identical use lines is noise. Rust lets you group them: one use with a brace-delimited list pulls several tails from one path at once. And when you genuinely want everything a module exposes, the glob * imports all of its public names in a single line. Run this:
// One `use` can pull several names from a module with a nested group, and a
// glob (`*`) brings in everything public at once.
mod palette {
pub const RED: &str = "#f07142";
pub const TEAL: &str = "#7fd1c2";
pub const GOLD: &str = "#e3b66b";
pub fn name_of(hex: &str) -> &'static str {
match hex {
"#f07142" => "rust",
"#7fd1c2" => "teal",
"#e3b66b" => "gold",
_ => "unknown",
}
}
}
// A nested group: two items from `palette` in one line.
use palette::{name_of, RED};
// A glob is best kept for tightly scoped spots, like a test module or a
// prelude; here it pulls in the rest of palette's public names.
use palette::*;
fn main() {
println!("{} is {}", RED, name_of(RED));
// TEAL and GOLD arrived through the glob.
println!("{} is {}", TEAL, name_of(TEAL));
println!("{} is {}", GOLD, name_of(GOLD));
}The line use palette::{name_of, RED} is a nested group: one path, palette, then a brace list of the tails you want, here a function and a constant in one statement. Groups can nest deeper and can include self to pull in the module itself alongside its members, but the common case is just this: a tidy list from one parent. The use palette::* line is a glob: it sweeps in every public name from palette at once, which is how TEAL and GOLD arrived without being named.
Globs are convenient and, used carelessly, a quiet menace. Because a glob brings in whatever a module happens to export, you cannot tell from the import which names it added, and a new export upstream can silently shadow or collide with a name you already had. The community convention is narrow: reach for a glob inside a test module (use super::* to test a module's private items) or when importing a crate's deliberately curated prelude. In ordinary code, name what you use; the explicit list is documentation the next reader gets for free.
use some_module::* is idiomatic in exactly two spots: a test module pulling in the code under test with use super::*, and a prelude built for the purpose. Everywhere else, a glob hides which names are in scope and lets an upstream change introduce surprises, so prefer an explicit (possibly grouped) list. The compiler will not stop you globbing; the discipline is yours.
One tree, many files
Everything so far put the whole tree in one file, because the Workbench compiles one file. Real crates spread the same tree across many files, and the mapping is mechanical once you see it. When you write mod billing; with a semicolon instead of a brace block, you are telling the compiler: the body of this module lives in another file, go find it. The module tree is identical either way; only the encoding on disk changes.
Rust looks for the file in one of two places. For a module named billing declared in the crate root, the body is either src/billing.rs (the modern, preferred layout) or src/billing/mod.rs (the older layout, still fully valid). A child module tax declared inside billing then lives at src/billing/tax.rs. So the inline tree from section 01 maps onto files like this:
src/
├── main.rs // crate root: declares mod billing;
└── billing/
├── mod.rs // (or billing.rs beside the dir): mod tax; + fn total
└── tax.rs // the body of billing::taxIn that layout, main.rs holds mod billing; (no body, just the declaration), billing/mod.rs holds mod tax; plus the total function, and billing/tax.rs holds the rate function. The paths you write in code (billing::tax::rate), the pub markers, the use statements: all of it is exactly the same as the single-file version. The file split is purely about where the bytes sit, never about the shape of the tree or what is visible.
Which layout should you pick? Prefer billing.rs beside a billing/ directory over billing/mod.rs. Both work on Rust 1.96.0, but the file-beside-directory form avoids a folder full of identically named mod.rs tabs in your editor, which is why it is now the common choice. You will meet older code using mod.rs everywhere, and it is not wrong, just the previous convention.
mod name { ... } defines the module's body right here. mod name; says the body lives in name.rs or name/mod.rs, and the compiler loads it. Same tree, same paths, same visibility rules; the only difference is whether the body is inline or in its own file.
Read it again
Scroll back to the first program and read it once more with every name in hand. mod billing and pub mod tax declared a small tree; the path billing::tax::rate walked from the root down to one function. Every item with no pub was private to its module, and every pub was a door cut on purpose. That is the whole machinery, and it composes upward: a crate is a tree of these modules, a library is a crate with a deliberately chosen pub surface, and a program is the same tree with a main at its root.
A crate is one tree of modules. mod builds the tree, paths (crate, self, super) navigate it, and everything is private until pub (or pub(crate), or pub(super)) opens a precise door. use shortens the names you are allowed to write, and pub use re-exports them to flatten the surface you hand to callers.
One thread runs ahead of this chapter, and it gets its own home. A crate is the unit the compiler builds, but turning a tree of modules into something you can publish, version, and depend on from another project is the job of Cargo, the build tool and package manager. That is a Part V story. And two language features remain after this one: macros, code that writes code, are next, and unsafe, the small door out of the safety guarantees, comes after. With modules in hand you can now organise everything the earlier chapters taught into a namespace and control exactly what it exposes.
The whole chapter in six lines
- A crate is one tree of modules, rooted at the crate root file;
moddeclares a branch, inline or in its own file. - A path navigates the tree: absolute from
crate, or relative withself(here) andsuper(parent). - Everything is private to its module by default;
pubopens a door, and apubstruct still has private fields unless each sayspub. pub(crate)andpub(super)expose an item to a chosen scope: the whole crate, or the parent subtree, instead of the whole world.usebrings a path into scope (withasto rename), andpub usere-exports a name to flatten the public surface.- Nested groups tidy several imports into one line; globs (
*) belong in tests and preludes, not ordinary code.