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() { /* ... */ }
  • pub alone — 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 store from the caller’s location. If store is private, the caller must be inside the module that defines store (or a descendant of it) for this step to succeed.
  • It then looks up inventory relative to store. Because inventory is pub, this succeeds for any caller that already resolved the first step.
  • Finally it looks up total_items relative to inventory. Its pub(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 pub when something genuinely needs to be called from outside its module — a smaller public surface is easier to refactor safely later.
  • Prefer pub(crate) over full pub for 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 pub across 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 full pub out of convenience.

Practice Exercises

  • Write a module bank containing a pub struct Account with a private balance: f64 field. Add a pub fn new(starting_balance: f64) -> Account constructor and a pub fn balance(&self) -> f64 accessor, then print an account’s balance from main.
  • Create a nested module app::config inside module app. Give config a pub(crate) fn default_timeout() -> u32 and a pub fn describe() -> String in app that calls it. Confirm it compiles when called from main.
  • Take the broken code in Mistake 1 above and, without adding pub to square, make it compile a different way: add a new pub fn inside the same math module that calls square internally and returns its result. Call that new function from main instead.

Summary

  • All items in Rust are private by default, visible only within the module that defines them and that module’s descendants.
  • pub widens 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), and pub(in path) let you grant visibility to a specific scope instead of the entire world.
  • Marking a struct or enum pub does 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.