Defining Modules to Control Scope

Learn how to define modules with the mod keyword to organize Rust code into namespaces, control visibility through privacy rules, and build a clear internal structure for your crate.

When a program grows beyond a single file, you need a way to group related pieces of code together—functions, types, constants—so they don't all sit in one flat namespace. Rust gives you modules for exactly this purpose. A module is a named region of code that acts as its own namespace and, by default, keeps everything inside it private from the rest of the crate. This section covers how to create those boundaries and what rules they enforce.

What a Module Does

Think of a module as a labelled container you place around a set of items. Inside that container, names don't leak out unless you explicitly mark them with pub. Outside code can only look into the container if the module itself is public and each item along the path is public. A module can contain:

  • Functions
  • Structs, enums, unions
  • Traits and their implementations
  • Constants and statics
  • Other modules (nesting)
  • Type aliases Modules exist for two reasons: organising code so that related things live together, and controlling visibility so that implementation details stay hidden.

Declaring a Module with mod

You create a module using the mod keyword followed by a name and a pair of curly braces. Everything between the braces belongs to that module.

mod front_of_house {
    fn seat_at_table() {
        // implementation
    }

    fn take_order() {
        // implementation
    }
}

This front_of_house module is private to whatever scope it's declared in (here, the crate root). Functions seat_at_table and take_order are also private—they exist only inside front_of_house.

Modules can be declared in two ways:

The code above shows an inline module. Later, in the section on separating modules into different files, you’ll see how to write mod garden; to load a module from another file. For now, we’ll keep everything inline so the privacy rules are easy to follow.

A module doesn’t need to be public for its parent to see that the module exists. A parent can always name its direct child modules, even if the child is private. What the parent cannot do is access anything inside that child module unless the item is marked pub.

mod kitchen {
    fn wash_dishes() {}
    pub fn clean_floor() {}
}

pub fn restaurant_open() {
    kitchen::clean_floor();     // allowed: clean_floor is public
    // kitchen::wash_dishes();  // error: wash_dishes is private
}

The module kitchen itself is private, but restaurant_open (which lives in the same parent scope) can see kitchen and call its public items. Code in a totally different module would also need kitchen to be pub before it could even try to reach clean_floor.

Privacy: Everything Is Private by Default

Rust’s privacy model is simpler than many developers expect: all items are private unless you write pub. This includes modules, functions, struct fields, enum variants (with an exception we’ll mention shortly), and methods. The rule creates a strong pressure to expose only what callers genuinely need. A private item can be accessed from:

  • The module that defines it
  • Any descendant (child, grandchild, etc.) of that module A public item can be accessed from anywhere in the crate, as long as every module in the path from the access point to the item is also public.

A public item in a private module is still invisible from outside:

This trips up newcomers constantly. Marking a function pub does nothing if the module containing it remains private to the rest of the crate. The entire path must be public for an item to be reachable.

Exceptions to the Default

Two cases reverse the default and make items public automatically:

  1. Variants of a public enum – If you write pub enum Direction { North, South }, both North and South are public. A public enum with private variants would be unusable.
  2. Associated items in a public trait – All method signatures and associated types in a pub trait are public by default. For structs, however, making the struct pub does not make its fields public. You need to add pub to each field you want to expose.
mod ordering {
    pub struct Meal {
        pub main_course: String,   // anyone can read/write
        side_dish: String,         // private field
    }

    impl Meal {
        pub fn new(main: &str, side: &str) -> Self {
            Meal {
                main_course: main.to_string(),
                side_dish: side.to_string(),
            }
        }
    }
}

pub fn take_meal() {
    let mut meal = ordering::Meal::new("steak", "salad");
    meal.main_course = String::from("fish"); // allowed
    // meal.side_dish = String::from("fries"); // error: private field
}

Your API can hide construction details:

By keeping side_dish private and providing a constructor, you prevent outside code from creating a Meal with an arbitrary side dish. This is the same encapsulation technique used in countless Rust libraries.

How Child Modules Reach Upward

A child module can always access items—public or private—from its parent (and ancestors). No special keyword is needed, but you do need to navigate the path correctly. The super keyword means “start from the parent module.”

mod back_of_house {
    fn prepare_ingredients() {
        // private implementation
    }

    mod cooking {
        pub fn start_cooking() {
            // Access a private function from the parent module
            super::prepare_ingredients();
        }
    }
}

Here cooking is a child of back_of_house, so it can call the private prepare_ingredients via super::prepare_ingredients(). This upward visibility means you can keep a private helper in a parent module and share it across several children without exposing it to the whole crate. Sibling modules, however, cannot see each other’s private items. If you need to share something between siblings, put it in the common parent.

mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {}
    }

    pub mod serving {
        pub fn take_order() {
            // sibling's public function is reachable if hosting is public
            super::hosting::add_to_waitlist();
        }
    }
}

Because hosting is pub and add_to_waitlist is pub, serving can call it. If either module or function were private, the call would fail.

Don't confuse visibility with scope:

Just because a child can see a parent’s private item doesn’t mean that item is in scope without a path. You still need to write super::something or use a use import (covered in a later section) to bring it into the local namespace.

Building a Module Tree with Nested Modules

When you nest modules, you build a tree that mirrors the structure of your application domain. The Rust compiler sees the entire tree as a single hierarchy starting from the implicit root module crate. Each mod declaration inserts a branch. Here’s a realistic structure for a restaurant library:

// lib.rs (crate root)

mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {}
        fn seat_at_table() {} // private helper
    }

    mod serving {
        fn take_order() {}
        fn serve_order() {}
    }
}

mod back_of_house {
    fn cook_order() {}
    pub fn fix_incorrect_order() {
        // can call sibling private function
        cook_order();
    }
}

The corresponding module tree looks like this:

crate
├── front_of_house
│   ├── hosting
│   └── serving
└── back_of_house

Key observations:

  • front_of_house and back_of_house are siblings under crate. They can see each other’s modules if those modules are public, but not each other’s private items.
  • hosting is public inside front_of_house, so code elsewhere in the crate can reach it with crate::front_of_house::hosting::add_to_waitlist()—provided front_of_house itself is visible (here it’s private, so only code in the same parent as front_of_house can access it).
  • serving is entirely private, so its functions are invisible outside front_of_house.
  • Inside back_of_house, fix_incorrect_order can call the private cook_order because they share the same module.

The crate root is a module too:

The file lib.rs (or main.rs) forms the root module named crate. Anything you write directly in that file, outside any mod block, lives in the crate module. That’s why you don’t need a mod crate; declaration—it’s implicit.

Common Beginner Mistakes

Several mistakes appear repeatedly when developers first use modules. Knowing them ahead of time saves frustration.

Forgetting to Make the Module Itself Public

mod inventory {
    pub fn check_stock() {}
}

pub fn do_something() {
    // error: module `inventory` is private
    inventory::check_stock();
}

Even though check_stock is pub, the module inventory is not. Code outside the module’s parent can’t even see inventory, let alone its contents.

Trying to Access a Sibling’s Private Items Directly

mod a {
    fn private_helper() {}
    pub fn public_api() {}
}

mod b {
    pub fn call_a() {
        // error: function `private_helper` is private
        super::a::private_helper();
        // OK
        super::a::public_api();
    }
}

You can only call public_api because it’s marked pub. The sibling boundary blocks everything else.

Marking a Struct pub but Not Its Fields

pub struct Table {
    seats: u32,   // private
}

External code can name Table but cannot construct it with Table { seats: 4 } nor read the field directly. This often surprises people who expect the struct to be usable just because it’s public. You must explicitly make the fields pub, or provide a constructor and getter methods.

Confusing a Module’s Name with a Path

A mod declaration introduces a name, not a filesystem path. mod foo; doesn’t mean “look for foo.rs”—it means “define a module named foo.” The compiler uses convention to find the code, but the module system itself is about names and visibility, not files.

Summary

Defining modules with mod is the first step toward controlling scope in Rust. Every item you put inside a module gets its own namespace, and the default privacy rule gives you encapsulation for free. Child modules can see upward into their parents; parents cannot peer into children without explicit pub annotations. Nested modules build a tree that mirrors the logical structure of your program, and the crate root sits at the top. The privacy system’s strength comes from its simplicity: if you never write pub, nothing leaks. When you do expose something, you must make the entire path public, which forces you to think about the API you’re presenting to the rest of the crate.