How visibility works in Rust
You're writing a module to handle database connections. You put a helper function inside to parse the connection string. You try to call that helper from your main function, and the compiler throws a fit. The error says the function is private. You slap pub on it, the error vanishes, and you move on.
A week later, you realize that helper function is now part of your public API. Every user of your crate can call it. You didn't mean to expose the internals. You just wanted to share code within your own crate. Now you're stuck. If you change the helper's signature, you break everyone who depends on it.
Rust's visibility system prevents this leak. It gives you granular control over who can see what. You can expose a function to the whole world, only to your crate, only to the parent module, or keep it locked inside the current module. The compiler enforces these boundaries at compile time.
The default is private
Rust treats visibility like physical access control. Every itemβfunctions, structs, constants, modulesβlives inside a module. By default, an item is private to its immediate parent module. You can access it from inside that module, but nothing outside can touch it.
Think of a crate as a building. Modules are rooms. By default, everything in a room is behind a locked door. Only people inside that room can see it. pub turns the door into a window. Anyone outside can look in. But Rust gives you finer control. You can make a window that only lets light into the hallway, or only into the building, but not to the street.
This default privacy keeps your implementation details hidden. It lets you change the internals without breaking code that depends on you. If a function is private, you can rename it, change its arguments, or delete it without affecting anything outside the module. This freedom is essential for refactoring.
Minimal example
mod secrets {
// This function is private to the `secrets` module.
// Code outside `secrets` cannot call this.
fn whisper() {
println!("shh");
}
// This function is public.
// Parent modules and other crates can call this.
pub fn shout() {
println!("HELLO");
}
}
fn main() {
// This works. `shout` is public.
// The root module is the parent of `secrets`.
secrets::shout();
// This fails. `whisper` is private.
// secrets::whisper();
}
The whisper function stays inside secrets. The shout function escapes to the parent. main lives in the root module, which is the parent of secrets, so it can call shout. If main tried to call whisper, the compiler would reject it with E0603 (private function).
Visibility flows upward
When you mark an item pub, you are granting access to the parent module. That's it. The parent can now see the item. If the parent module itself is pub, the item becomes visible to the parent's parent, and so on, all the way up to the crate root.
Visibility flows upward through the module tree. You can't expose a child if the parent is locked. If a module is private, all its pub items remain hidden from anything beyond that module. The parent acts as a gatekeeper.
// This module is private.
mod internal {
// This function is public within `internal`.
pub fn helper() {
println!("helping");
}
}
fn main() {
// This fails. `internal` is private.
// Even though `helper` is `pub`, it's trapped inside `internal`.
// internal::helper();
}
To expose helper, you must also make internal public. Or you can use pub(crate) to expose it only within the crate. The compiler checks the entire path. Every module in the path must allow access for the item to be visible.
Default to private. Expose only what you must.
Granular control: pub(crate) and beyond
pub is not the only option. Rust provides scoped visibility modifiers that let you restrict access to specific levels of the module hierarchy. These are essential for building clean libraries where internal helpers are shared but not exposed.
pub(crate) makes an item visible to the entire crate, but not to external crates. This is the standard choice for internal utilities. It allows you to share code across modules without polluting the public API.
// lib.rs
pub mod config {
// `load` is the public API. Users call this.
pub fn load() -> String {
let raw = read_file();
parse(raw)
}
// `parse` is used by `load`, but users shouldn't call it directly.
// `pub(crate)` makes it visible only within this crate.
pub(crate) fn parse(raw: String) -> String {
raw.trim().to_string()
}
// `read_file` is private. Only `config` module uses it.
fn read_file() -> String {
"default".to_string()
}
}
Here, load is the entry point. parse is a helper shared within the crate. read_file is locked to the module. If an external crate tries to call config::parse, the compiler rejects it. The API surface stays small.
You can also use pub(super) to expose an item only to the immediate parent module. This blocks access to siblings and the crate root. It's useful for tightly coupled parent-child relationships where the parent manages the child's state.
pub(self) explicitly marks an item as private. Since items are private by default, this is rarely needed. It's mostly used for emphasis in generated code or specific style guides.
Convention aside: Rust developers use pub(crate) heavily in libraries. It signals "this is part of the implementation, not the API." It prevents accidental exposure. Another convention: use pub use in lib.rs to flatten the API. Instead of making users write my_crate::utils::helpers::do_thing(), you write pub use utils::helpers::do_thing; so they can just write my_crate::do_thing(). This re-export keeps the internal structure flexible while presenting a clean interface.
Treat pub as a contract. Treat pub(crate) as a whisper.
The cost of exposure
Exposing an item locks you into its interface. If you make a function pub, you can't rename it, change its arguments, or delete it without breaking downstream users. This forces a major version bump.
Private items are free to change. You can refactor them aggressively. You can split a function, merge two functions, or rewrite the algorithm without touching the API. This is why Rust defaults to private. It encourages small, stable public surfaces.
When you expose a struct, you also expose its fields unless you mark them private. A pub struct with private fields is a capsule. Users can create it and pass it around, but they can't inspect or modify the internals. You control access through methods.
pub struct Counter {
// This field is private.
// Users cannot read or write it directly.
value: u32,
}
impl Counter {
pub fn new() -> Self {
Counter { value: 0 }
}
pub fn increment(&mut self) {
self.value += 1;
}
// This method is public, but it returns a private type.
// This fails. You can't leak private types.
// pub fn internal_state(&self) -> u32 {
// self.value
// }
}
The Counter struct is public, but value is private. Users must use new and increment. This encapsulation protects the invariant that value is always valid. If you made value public, users could set it to any number, potentially breaking logic that depends on it.
Start private. Promote only when the compiler forces you.
Pitfalls and compiler errors
Visibility errors are common when you're learning Rust. The compiler catches them early, but the messages can be confusing if you don't know the rules.
Private fields in public structs: A struct can be pub, but its fields are private by default. You must mark each field pub to expose it.
pub struct User {
// This field is private, even though the struct is pub.
name: String,
}
fn main() {
let u = User { name: "Alice".to_string() };
// This fails. The field is private.
// println!("{}", u.name);
}
The compiler rejects this with E0616 (private field). To fix it, add pub to the field: pub name: String. Or provide a getter method.
Returning private types: You can't return a private type from a public function. The return type becomes part of the API, so it must be visible to callers.
mod internal {
struct Secret {
data: String,
}
// This fails. You can't return a private type from a pub function.
// pub fn get_secret() -> Secret {
// Secret { data: "oops".to_string() }
// }
}
The compiler rejects this with E0603 (private type). You must make Secret public, or return a public wrapper type.
Accessing private items through use: The use statement creates an alias. It does not change visibility. If you use a private item, you can only use it within the scope where it's visible.
mod inner {
pub(crate) fn helper() {}
}
mod outer {
// This imports `helper` into `outer`.
// `helper` is still `pub(crate)`, not public.
use super::inner::helper;
pub fn do_work() {
// This works. `helper` is visible here.
helper();
}
}
If outer tried to expose helper via pub use, it would fail because helper is not visible to outer's parent. use is for convenience, not exposure.
The compiler protects you from leaking internals. Listen to it.
Testing implications
Visibility affects how you write tests. Rust distinguishes between unit tests and integration tests based on where they live.
Unit tests live inside the module they test. They can see private items. This lets you test internal logic without exposing it.
mod math {
pub fn add(a: i32, b: i32) -> i32 {
validate(a) + validate(b)
}
fn validate(n: i32) -> i32 {
if n < 0 { 0 } else { n }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_validate() {
// This works. `validate` is private, but the test is in the same module.
assert_eq!(validate(-5), 0);
}
}
}
Integration tests live in the tests/ directory. They compile as a separate crate. They can only see pub items. This forces you to test the public API, not the internals.
// tests/integration.rs
use my_crate::math;
#[test]
fn test_add() {
// This works. `add` is public.
assert_eq!(math::add(2, 3), 5);
// This fails. `validate` is private.
// math::validate(-5);
}
This distinction matters. If you need to test a private function, write a unit test inside the module. If you want to test the public API, write an integration test. Don't make internals public just to test them. That pollutes the API.
Keep tests close to the code they test. Use integration tests to verify the API.
Decision matrix
Use private visibility when the item is an implementation detail that should never be accessed outside the current module. This is the default. Keep helpers, internal state, and utility functions private unless you have a reason to expose them.
Use pub(crate) when the item needs to be shared across multiple modules within the same crate, but should not be part of the public API. This is the standard choice for internal helpers, shared constants, and test utilities.
Use pub when the item is part of the public API that external crates must access. Reserve this for the entry points, core types, and functions that users of your library rely on.
Use pub(super) when you need to expose an item only to the immediate parent module, blocking access to siblings or the crate root. This is useful for tightly coupled parent-child relationships where the parent manages the child's state.
Use pub(self) when you want to explicitly document that an item is private, even though it is private by default. This is rare and mostly used for emphasis in generated code or specific style guides.
Start with private. Promote visibility only when the compiler forces you. Every pub item is a promise you must keep.