pub and Visibility
Every item you write in Rust — a function, a struct, a field, a module — is private by default. Nothing outside the module that defines an item can see it unless you explicitly mark it pub. This is Rust’s answer to the question “what should other parts of my program be allowed to touch?” and it is enforced by the compiler, not just a convention. Understanding pub and its variants is essential once your code grows past a single file, because it is how you design a clean API boundary between the pieces of your crate.
Overview: How Visibility Works
Think of a Rust crate as a tree of nested rooms, where each mod is a room. By default, whatever you put in a room stays in that room and its closets (its descendant modules) — nobody standing outside the room can see it. Adding pub to an item is like putting a door in the wall so the item becomes visible to code outside the module that defines it. But a single door only opens one wall: if the room itself is nested inside another private room, marking an item pub only gets you to the edge of the immediately enclosing module — every module along the path from the caller down to the item must also be reachable for the whole path to resolve.
Concretely, the rule is: an item is visible inside the module where it is defined, and inside every descendant of that module. A fn with no visibility modifier can be called by any code physically inside its own module (including code above it in the same file, like main, since main and a plain mod declaration written side by side both live in the same enclosing module) and by any nested submodule underneath it. It cannot be called from a sibling module, a parent module, or anywhere outside the crate. Adding pub widens that to “anywhere the path to it is reachable,” and Rust gives you finer-grained versions of pub for when “visible to literally everyone” is too permissive:
| Modifier | Meaning |
|---|---|
(none) |
Private — visible only in the defining module and its descendants (the default) |
pub |
Fully public — visible to any code that can reach the item’s path, including outside the crate |
pub(crate) |
Visible anywhere in the current crate, but not to external crates that depend on it |
pub(super) |
Visible only to the immediate parent module (and its descendants) |
pub(in path) |
Visible only within the given ancestor module path |
One more nuance matters a great deal in practice: a pub struct does not make its fields public. Struct fields keep their own individual visibility, separate from the struct itself. This lets you expose a type’s name and methods while keeping its internal data private — the same encapsulation idea you may know from classes in other languages, except Rust checks it at compile time with no runtime cost.
Syntax
// on a module
pub mod name { /* ... */ }
// on a function, struct, enum, const, or type alias
pub fn name() { /* ... */ }
// on an individual struct field (each field decided independently)
pub struct Name {
pub visible_field: Type,
hidden_field: Type,
}
// restricted visibility
pub(crate) fn name() { /* ... */ }
pub(super) fn name() { /* ... */ }
pub(in crate::some::path) fn name() { /* ... */ }
pubalone — the widest visibility; usable from outside the crate if the crate is a library.pub(crate)— a very common middle ground: shareable across your own crate’s modules, but not part of the public API you export to users of your library.pub(super)— scopes visibility to exactly one level up, useful when a submodule needs to hand something back to its parent without exposing it further.- No modifier — private; this is the correct default for implementation details.
Examples
Example 1: A basic module with a public and a private function
mod greetings {
pub fn hello() -> String {
String::from("Hello from the greetings module!")
}
fn secret_helper() -> String {
String::from("You can't see me from outside!")
}
pub fn hello_with_helper() -> String {
format!("{} {}", hello(), secret_helper())
}
}
fn main() {
println!("{}", greetings::hello());
println!("{}", greetings::hello_with_helper());
}
Output:
Hello from the greetings module!
Hello from the greetings module! You can't see me from outside!
hello and hello_with_helper are marked pub, so main can call them through the path greetings::hello. secret_helper has no modifier, so it is private to the greetings module — but hello_with_helper is defined inside that same module, so it is allowed to call secret_helper directly. This is the core pattern for hiding implementation details: expose a small public function, and let it freely use private helpers that only it needs to know about.
Example 2: A public struct with a private field and a constructor
mod library {
pub struct Book {
pub title: String,
author: String,
}
impl Book {
pub fn new(title: &str, author: &str) -> Book {
Book {
title: String::from(title),
author: String::from(author),
}
}
pub fn author(&self) -> &str {
&self.author
}
}
}
fn main() {
let book = library::Book::new("The Rust Book", "Steve & Carol");
println!("Title: {}", book.title);
println!("Author: {}", book.author());
}
Output:
Title: The Rust Book
Author: Steve & Carol
Book is pub, and its title field is pub too, so main reads book.title directly. The author field has no modifier, so it stays private to the library module — outside code cannot write book.author or build a Book literal that sets it. Instead, library exposes a pub fn new constructor and a pub fn author(&self) accessor, both of which run inside the module and are therefore allowed to touch the private field. This is exactly how Rust encapsulation works: hide the data, expose behavior.
Example 3: Nested modules and pub(crate)
mod store {
pub mod inventory {
pub(crate) fn total_items() -> u32 {
42
}
pub fn public_report() -> String {
format!("Total items in stock: {}", total_items())
}
}
pub fn crate_report() -> String {
format!("Crate-level check: {}", inventory::total_items())
}
}
fn main() {
println!("{}", store::inventory::public_report());
println!("{}", store::crate_report());
}
Output:
Total items in stock: 42
Crate-level check: 42
store itself has no pub, so it is private — but main can still reach store::inventory::public_report() because main is defined in the very same crate-root module as the mod store declaration, and private items are visible throughout the module that defines them. Inside store, the nested inventory module is marked pub, so it is reachable from outside store. Its function total_items is only pub(crate): that’s wide enough for store::crate_report (a sibling function elsewhere in the same crate) to call it, but it would not be exported if this were a library other crates depended on.
How It Works Step by Step
When the compiler sees a path like store::inventory::total_items(), it resolves it one segment at a time and checks visibility at each hop:
- It looks up
storefrom the caller’s location. Ifstoreis private, the caller must be inside the module that definesstore(or a descendant of it) for this step to succeed. - It then looks up
inventoryrelative tostore. Becauseinventoryispub, this succeeds for any caller that already resolved the first step. - Finally it looks up
total_itemsrelative toinventory. Itspub(crate)visibility is checked against the caller’s crate: same crate, so it succeeds.
If any single segment along that path fails its visibility check, the whole expression fails to compile with an error naming exactly which item is private and where it was defined. Crucially, this check happens entirely at compile time by walking the module tree recorded during name resolution — there is no runtime cost and no way to bypass it with a cast or a pointer trick, unlike access modifiers in some other languages that can sometimes be circumvented via reflection.
Common Mistakes
Mistake 1: Calling a private function from outside its module
mod math {
fn square(x: i32) -> i32 {
x * x
}
}
fn main() {
println!("{}", math::square(4));
}
This fails to compile with an error similar to function `square` is private, because square has no visibility modifier, so it is only reachable from inside math or one of its descendants — not from main, which sits outside math. The fix is to add pub to the declaration:
mod math {
pub fn square(x: i32) -> i32 {
x * x
}
}
fn main() {
println!("{}", math::square(4));
}
Output:
16
Mistake 2: Building a struct literal from outside when a field is private
mod shapes {
pub struct Circle {
pub radius: f64,
color: String,
}
}
fn main() {
let c = shapes::Circle {
radius: 2.0,
color: String::from("red"),
};
println!("{}", c.radius);
}
Even though Circle is pub, its color field is not, so this fails with an error such as field `color` of struct `shapes::Circle` is private. Marking the struct pub only controls whether the type name itself is visible; every field keeps its own visibility. Outside code cannot construct or read a struct literal that includes a private field. The idiomatic fix is a constructor method that lives inside the module and is allowed to set the private field directly:
mod shapes {
pub struct Circle {
pub radius: f64,
color: String,
}
impl Circle {
pub fn new(radius: f64, color: &str) -> Circle {
Circle {
radius,
color: String::from(color),
}
}
pub fn color(&self) -> &str {
&self.color
}
}
}
fn main() {
let c = shapes::Circle::new(2.0, "red");
println!("Radius: {}, Color: {}", c.radius, c.color());
}
Output:
Radius: 2, Color: red
Best Practices
- Keep items private by default and only add
pubwhen something genuinely needs to be called from outside its module — a smaller public surface is easier to refactor safely later. - Prefer
pub(crate)over fullpubfor helpers that other modules in your own crate need but that should never become part of a published library’s external API. - Keep struct fields private and expose a constructor (often named
new) plus accessor methods, so the struct can validate its data and change its internal representation later without breaking callers. - Group related public items behind one module (a “facade”) rather than sprinkling
pubacross many small internal modules; it gives users of your crate one clear entry point. - Use
pub(super)when a submodule needs to expose something back to its immediate parent only, instead of reaching for fullpubout of convenience.
Practice Exercises
- Write a module
bankcontaining apub struct Accountwith a privatebalance: f64field. Add apub fn new(starting_balance: f64) -> Accountconstructor and apub fn balance(&self) -> f64accessor, then print an account’s balance frommain. - Create a nested module
app::configinside moduleapp. Giveconfigapub(crate) fn default_timeout() -> u32and apub fn describe() -> Stringinappthat calls it. Confirm it compiles when called frommain. - Take the broken code in Mistake 1 above and, without adding
pubtosquare, make it compile a different way: add a newpub fninside the samemathmodule that callssquareinternally and returns its result. Call that new function frommaininstead.
Summary
- All items in Rust are private by default, visible only within the module that defines them and that module’s descendants.
pubwidens visibility outward along the path from a caller to the item; every segment of that path must also be reachable, or the whole lookup fails.pub(crate),pub(super), andpub(in path)let you grant visibility to a specific scope instead of the entire world.- Marking a
structorenumpubdoes not make its fields public — each field’s visibility is decided independently. - The idiomatic pattern for encapsulation is: private fields, a public constructor, and public accessor/mutator methods that enforce any invariants.
- All of this is checked entirely at compile time by resolving module paths, with zero runtime overhead.
