Two threads have been left dangling, on purpose, since early in this course. Back in the panic-vs-Result lesson, Guess had a field, value, with no pub in front of it — and the lesson promised that "exactly how Rust draws and enforces that boundary is its own lesson, a little further down the road." And all the way back in the Hello Cargo lesson, Cargo.toml opened with a section called [package] — a word that's been sitting there, unexplained, for twenty-eight lessons. Both threads end here, along with a third idea that ties them together: mod, the tool for grouping code into named units in the first place.
One module, this whole time
Here's the thing that made both of those threads invisible until now: privacy in Rust is a property of modules, not files or projects — and every single program in this course, so far, has been exactly one module. When everything lives in one module, every item can already see every other item, automatically, regardless of pub. So the pub in front of Guess, and in front of discounted_price back in the comments lesson, did nothing observable — there was no boundary yet for it to cross. The moment you introduce a second module, that changes completely, and pub stops being decoration.
mod — grouping related code
mod creates a module: a named box that groups related items together, with its own privacy boundary. Here's a small one, plus a main that uses it:
Walk through it slowly, because almost every piece is a callback:
mod inventory { ... }declares a module namedinventoryand gives it a body, right here in the same file.inventory::Item::new(...)reaches into that module using::— the exact same::you've been writing since your very firstString::from(...). It was never special syntax just for built-in types;::is a path separator, full stop, andType::functionhas been a path into a module the entire time — you just hadn't met a hand-written one yet.pub struct Item,pub fn new,pub fn quantity,pub fn restock— every one of these is invisible outsidemod inventorywithoutpub. Remove any single one, and the corresponding line inmainstops compiling.- And here's the field-level version of the exact same rule, twice, on one struct:
pub name: Stringcan be read directly asitem.namefrommain— butquantity: i32, with nopub, cannot. The only way to read it is through.quantity(), and the only way to change it is through.restock(). This is exactly the shape ofGuess's privatevaluefield and its.value()getter — and now you can see precisely why it worked:Guessand the code using it were always implicitly in the same module, so the boundary was invisible. PutGuessin its own module, and that same private field becomes truly unreachable from outside it — on purpose.
One more recognition, while you're here: the testing lesson's #[cfg(test)] mod tests { ... } was already a mod — a module nested inside the same file, exactly like inventory above. Everything in this lesson was already true there. #[cfg(test)] just adds one more rule on top: "and also, only compile this module when running tests."
use — shortening paths you already write
Typing inventory::Item::new(...) every time gets old fast. use brings a path into scope, so you can refer to its last segment alone:
use inventory::Item; doesn't copy or duplicate anything — Item and inventory::Item are the exact same type, both before and after this line. All use does, from this point in the file downward, is let you write the short name instead of the full path. Look back at every use you've already written, and they're all this same one idea:
use std::collections::HashMap;, since the hash-maps lesson —HashMaplives in the standard library'scollectionsmodule, isn't part of the prelude, so every file that wants the short nameHashMaphas to ask for it.use std::fs;, since the propagating-errors lesson — this brings the modulefsitself into scope, which is why the calls readfs::read_to_string(...)rather than justread_to_string(...). That's a common, deliberate choice: writing the module name at the call site makes it obvious the function isn't defined locally.use super::*;, inside#[cfg(test)] mod tests—superis a special path segment meaning "the module containing this one." Formod tests, that's the rest of the file. And*is a glob: "everything at this path," all at once, instead of naming each item individually. Put together,use super::*;means exactly what it always quietly meant: "bring everything from the file surrounding this test module into scope, by its short names."
One module, many files
Everything so far has kept inventory in the same file as main. That stops being realistic the moment a module grows — and the fix is almost suspiciously simple. Replace the body of mod inventory { ... } with a semicolon — mod inventory; — and Rust goes looking for the module's code in its own file instead:
src/inventory.rs contains exactly what used to sit between the braces of mod inventory { ... } — nothing more, nothing less. The file boundary becomes the module boundary, so the wrapper itself is no longer written:
And main.rs shrinks down to one declaration plus the code that actually does something:
Same program, same output, two files instead of one. mod inventory; says "there is a module called inventory; find its contents in inventory.rs." Everything about pub, privacy, and use from the last two sections carries over completely unchanged — modules behave identically whether their body sits between braces in the same file or in a file of their own.
Crates and packages
Zoom out one more level, and two words from the Hello Cargo lesson finally get precise definitions. A crate is the smallest amount of code the compiler considers at one time — either a binary crate (has a main function, produces an executable) or a library crate (no main, meant to be used by other code). A package is one or more crates plus the Cargo.toml that describes them. Every cargo new you've run in this course created a package containing exactly one binary crate, rooted at src/main.rs — which is why Cargo.toml's very first section has always been called [package]: it's naming the package, and the name and version it gives apply to the crate inside. The module tree you just built is rooted at that same file:
mod inventory; added inventory as a child of the crate root in this tree — and that tree is exactly what :: paths like inventory::Item are addresses into. A package can hold a library crate too, rooted at src/lib.rs, with its own module tree alongside the binary — the shape that lets one project both run as a program and be used as a dependency by someone else's Cargo.toml.
Quick exercise
- Go back to
Guess, from the panic-vs-Result lesson. Wrap the whole thing — the struct and itsimplblock — in a module of your own naming, saymod guessing_game { ... }. Inmain, either writeguessing_game::Guess::new(42)or adduse guessing_game::Guess;and writeGuess::new(42). Notice what you didn't have to change: not onepub, not the privatevaluefield, nothing insideimpl Guessat all.Guesswas written correctly for module boundaries twenty-eight lessons before you knew modules existed. - Pick any function from an earlier lesson —
largest, from the generics lesson, is a good choice. Move it into amodof your choosing, mark itpub, and call it frommainthrough the module path or ause. Then, just to feel the boundary for yourself, delete thepuband runcargo check— and watch it refuse the call frommainthat worked one second ago.
Step back and look at what just got renamed, not replaced. mod is the testing lesson's mod tests, seen in full. pub is the reason Guess's value field could be private at all — a promise made twenty-eight lessons ago, kept. use is use std::collections::HashMap, use std::fs, and use super::*, all explained at once. :: is every Type::function call you've written since String::from, revealed as a path through a module tree. And crate / package are Cargo.toml's [package] section, named precisely for the first time. Nothing here was new machinery — it was vocabulary for things you'd been doing correctly, by following examples, since the first lesson that gave you a struct with a pub fn new.
Every program in this course has read its data from somewhere inside the source code itself — a vec![...] written by hand, a hardcoded "count.txt". Real programs take their input from outside themselves: arguments typed on the command line, environment variables, files whose names aren't known until the moment the program runs. The next lesson is where that finally happens — reading real input with std::env and std::fs, using exactly the module-path fluency you now have, and the start of a new stretch of this course where "does it compile" gives way to "build something a person could actually run": files and I/O.