13.3Visibility: pub
The previous lesson marked things pub and asked you to take it on faith. Now the faith is repaid. Visibility is the rule about what code can see what, and it's how modules stop being mere folders and become real boundaries: a module can hide its inner workings and expose only a deliberate, public surface. This is encapsulation, one of the most important ideas in structuring software, and Rust builds it from a single keyword.
Private by default
In Rust, everything is private to its module unless you say otherwise. A child module's items are hidden from the code outside it:
mod kitchen {
fn secret_recipe() {
println!("a pinch of salt");
}
}
fn main() {
kitchen::secret_recipe();
}error[E0603]: function `secret_recipe` is private
--> src/main.rs:8:14
|
8 | kitchen::secret_recipe();
| ^^^^^^^^^^^^^ private function
|
note: the function `secret_recipe` is defined here
--> src/main.rs:2:5
|
2 | fn secret_recipe() {
| ^^^^^^^^^^^^^^^^^^
secret_recipe exists, the path is correct, but the function is private to kitchen, so main can't call it (E0603). This is the opposite of many languages, where things are public unless hidden, and the default is deliberate: code is private until you make a conscious decision to expose it, so a module's public surface is always something you chose, never something that leaked out by accident.
Key insight
Private-by-default flips the burden in your favor. Instead of remembering to hide every internal helper (and paying for the one you forget), you expose only what you mean to. Everything you don't mention stays an implementation detail you're free to change later without breaking anyone, because no one outside could depend on it. The public surface is a promise; private-by-default keeps you from making promises by accident.
pub exposes an item
Add pub and the item becomes visible to the parent module (and, transitively, wherever the parent can reach):
mod kitchen {
pub fn order_food() {
let dish = prepare(); // private helper, callable from inside
println!("serving {dish}");
}
fn prepare() -> &'static str {
"soup"
}
}
fn main() {
kitchen::order_food(); // public: allowed
}serving soup
order_food is pub, so main can call it. prepare is private, so main cannot, but order_food can, because it lives inside kitchen and private items are visible within their own module. This is the encapsulation pattern in miniature: kitchen exposes what you can do (order_food) and hides how it does it (prepare). You could rewrite prepare entirely, and as long as order_food's behavior is unchanged, no caller notices.
One subtlety worth stating: making a module pub only makes the module itself reachable. The items inside it are still private unless they are also pub. Exposure is per-item, all the way down, which is why lesson 13.2's examples needed pub on both the module and its functions.
pub(crate): public within, private without
Between fully private and fully public sits a useful middle ground. pub(crate) makes an item visible everywhere in your crate but not to outside code that depends on your crate as a library (lesson 13.5). It's the right choice for things that many of your own modules share but that aren't part of the promise you make to external users:
mod helpers {
pub(crate) fn shared_utility() {
println!("used across the crate");
}
}
Any module in the same crate can call helpers::shared_utility(), but if someone adds your crate as a dependency, they can't. There are finer-grained forms (pub(super), pub(in some::path)), but pub, pub(crate), and private cover almost everything you'll write.
Struct fields are public one at a time
pub on a struct (lesson 10.2) exposes the type, but its fields stay private unless each is individually marked pub. This is the mechanism behind real encapsulation of data:
mod bank {
pub struct Account {
pub owner: String,
balance: u64, // private: only `bank` can touch it
}
impl Account {
pub fn new(owner: String) -> Account {
Account { owner, balance: 0 }
}
pub fn deposit(&mut self, amount: u64) {
self.balance += amount;
}
pub fn balance(&self) -> u64 {
self.balance
}
}
}
fn main() {
let mut acct = bank::Account::new(String::from("Ada"));
acct.deposit(100);
println!("{} has {}", acct.owner, acct.balance());
// acct.balance = 999; // refused: balance is private
}Ada has 100
owner is pub, so main reads it directly. balance is private, so main cannot set it to a bogus value; the only way to change it is through deposit, which bank controls. This is exactly the field-protection lesson 10.2 promised modules would provide. The struct enforces its own rules because outside code physically cannot reach past its methods. (Note that new must be pub and is necessary precisely because balance is private: outside code can't write the struct literal directly when a field is hidden, so it depends on the constructor.)
Best practice
Keep struct fields private and expose behavior through methods, unless the struct is a plain data bundle with no rules to enforce (like a Point whose fields are just coordinates). Private fields let you change a type's internal representation later, validate on every change, and maintain invariants. This is the same instinct as private-by-default modules: expose a deliberate surface, hide the rest.
Quiz time
Question #1
What's the default visibility of an item in a module, and why is that a good default?
Show solution
Private to its module. It's a good default because it makes the public surface something you opt into deliberately: nothing leaks out by accident, and anything you don't mark pub stays an internal detail you can change freely without breaking callers.
Question #2
This is refused. Why, and what's the fix?
mod shapes {
pub struct Circle {
radius: f64,
}
}
fn main() {
let c = shapes::Circle { radius: 2.0 };
}Show solution
Circle is pub, but its field radius is private, so main can't build the struct literal with that field (it would be an E0451-style privacy error on the field). Fixes: mark the field pub radius: f64, or (better, for encapsulation) keep it private and add a pub fn new(radius: f64) -> Circle associated function inside shapes for outside code to call.
Question #3
When would you choose pub(crate) over pub?
Show solution
When an item should be shared across your own crate's modules but should not be part of the public API that external code (depending on your crate as a library) can use. pub(crate) is visible everywhere inside your crate and invisible outside it, which is ideal for internal helpers that several of your modules need but that you don't want to promise to the outside world.
You can now organize code into modules with controlled visibility, all within one file. The next lesson does the thing every growing program needs: moves those modules into separate files. It's the lesson that replaces an entire C++ chapter, and it's shorter than this one.