Polyglot
Languages Rust scope
Rust § scope

Modules

Rust organises code at three levels: crates (the unit of compilation; produces a binary or library), modules (subdivisions within a crate), and paths (the qualified names that identify items). The module system has a substantial surface — module declarations (mod), the use directive, the pub visibility modifier, paths (crate::, self::, super::), and the file-system mapping. The system was substantially reworked in the Rust 2018 edition; modern conventions assume the post-2018 form. Beyond modules, Rust admits visibility control through pub, pub(crate), pub(super), and pub(in path), admitting fine-grained access boundaries.

This page covers the module mechanism, paths, the use directive, visibility, and the conventional patterns. The treatment of crates, dependencies, and Cargo is in Cargo and packaging.

Crates

A crate is the compilation unit:

  • Binary crate — produces an executable; has a main function.
  • Library crate — produces a library (.rlib or similar); other crates may depend on it.

A crate has a root file:

  • For binary crates: src/main.rs.
  • For library crates: src/lib.rs.

The root file declares the top-level module. Other modules are declared inline or in separate files.

The conventional Cargo project layout:

mycrate/
├── Cargo.toml
├── src/
│   ├── main.rs            # binary crate root
│   ├── lib.rs             # library crate root (optional)
│   ├── module1.rs         # a module
│   └── module2/
│       ├── mod.rs          # module2's root (Rust 2015) OR
│       ├── module2.rs      # module2's root (Rust 2018+)
│       └── submodule.rs
├── tests/                 # integration tests
└── examples/              # example binaries

Treated in Cargo and packaging.

Module declarations

The mod keyword declares a module:

// in src/main.rs:
mod greet;                  // declares: there is a module 'greet'
mod helper {                // declares an inline module
    pub fn assist() { /* ... */ }
}

fn main() {
    greet::hello();
    helper::assist();
}

The inline form mod foo { ... } defines the module’s contents inline. The non-inline form mod foo; declares the module and admits its contents in a separate file.

For non-inline modules, the compiler looks for the module’s contents at:

  • src/foo.rs (preferred since Rust 2018), or
  • src/foo/mod.rs (Rust 2015 style; still admitted).

The conventional contemporary form uses foo.rs for simple modules and foo/mod.rs (or foo.rs plus a foo/ directory) for modules with submodules:

src/
├── lib.rs
├── greet.rs               # mod greet; pub fn hello() {}
└── helper/
    ├── mod.rs             # mod helper; pub fn assist() {}
    └── inner.rs           # mod inner; (declared in helper/mod.rs)

Or with the modern convention:

src/
├── lib.rs
├── greet.rs               # the module
└── helper.rs              # the module (declares pub mod inner;)
└── helper/
    └── inner.rs           # the submodule

Paths

A path identifies an item. Paths take three principal forms:

crate::module::Item              // absolute; from the crate root
self::sibling                     // relative; from the current module
super::parent_item                // relative; from the parent module
module::Item                      // relative; without prefix

The crate:: prefix admits absolute paths from the crate root; the self:: prefix is relative; the super:: prefix navigates up.

mod outer {
    pub mod inner {
        pub fn helper() {}
    }

    pub fn use_helper() {
        self::inner::helper();      // self refers to outer
        inner::helper();             // equivalent (self:: is implicit)
        crate::outer::inner::helper();  // absolute
    }

    mod sibling {
        pub fn from_sibling() {
            super::inner::helper();   // up to outer, then down to inner
        }
    }
}

Most Rust code uses paths through use directives that bring items into scope.

use declarations

The use directive brings paths into scope:

use std::collections::HashMap;
use std::io::{self, Read, Write};       // multiple names from one module
use std::fmt::*;                          // glob (rare; usually bad)
use std::fmt::Debug as Dbg;               // alias
use crate::utils::helper;
use super::sibling::Item;

// Conventional imports at the top of a file:
use std::collections::HashMap;
use std::sync::Arc;
use std::time::Duration;

The conventional discipline:

  • One use per item or grouped form (use std::io::{Read, Write}).
  • Glob imports (use foo::*;) are rare; conventionally avoided outside the prelude.
  • Absolute paths (use crate::...) for items in the current crate.
  • The as keyword for renames; conventional when imported names would collide.

pub use re-exports

A pub use re-exports an item, making it part of the current module’s public interface:

mod internal {
    pub struct Inner;
}

pub use internal::Inner;            // re-export Inner from the parent module

// Now: parent_module::Inner is accessible from outside;
// internal::Inner is also accessible if internal is public.

The pattern admits clean public APIs without exposing the internal organisation:

// lib.rs
mod parser;
mod lexer;
mod ast;

pub use parser::Parser;
pub use ast::{Expression, Statement};
// Users see: my_crate::Parser, my_crate::Expression, etc.
// They don't see the parser/lexer/ast organisation.

Visibility

By default, items are private — visible only within the declaring module:

mod inner {
    fn private_helper() { /* ... */ }       // private; module-internal

    pub fn public_function() {                 // visible from outside
        private_helper();                       // OK; same module
    }
}

inner::public_function();                       // OK
inner::private_helper();                        // ERROR: private

The pub keyword admits visibility:

pub fn function() {}                            // visible everywhere
pub(crate) fn crate_internal() {}                // visible within the crate
pub(super) fn parent_only() {}                   // visible to the parent module
pub(in crate::module) fn specific_module() {}    // visible to a specific path

The granularity:

  • pub — visible to anyone with access to the enclosing module.
  • pub(crate) — visible throughout the crate; the conventional choice for “internal but used widely”.
  • pub(super) — visible to the parent module only.
  • pub(in path) — visible to a specific module path.

For a struct, the struct itself and its fields have separate visibility:

pub struct Person {
    pub name: String,                            // public field
    age: i32,                                    // private field
}

The Person is publicly constructible (with pub fn new); the age is hidden.

For enums and tuple structs, all variants/fields share the enum/struct’s visibility:

pub enum Status {
    Active,                                       // public
    Inactive,                                     // public
    // (no per-variant visibility for enums)
}

pub struct Wrapper(i32);                         // pub field; equivalent to:
pub struct Wrapper {
    pub field: i32,                               // explicit
}

For tuple structs with mixed visibility, the conventional Rust pattern uses constructors:

pub struct UserId(u32);    // u32 is private (no pub on the field)

impl UserId {
    pub fn new(id: u32) -> Self {
        UserId(id)
    }
}

Without pub on the field, callers cannot construct UserId(42) directly; they must go through UserId::new.

The four scopes

Rust distinguishes:

  • Module scope — items declared at module level (functions, types, constants).
  • Function scope — parameters and locals within a function.
  • Block scope — locals within an inner { ... } block.
  • Lifetime scope — the scope of a reference; treated separately in Lifetimes.

Variables in a block are dropped at the end of the block:

fn main() {
    let s = String::from("hello");

    {
        let s2 = String::from("world");
        // s and s2 both in scope
    }
    // s2 has been dropped; only s in scope

    // s is dropped at the end of main
}

The drop order is the reverse of declaration order — the most-recently-declared is dropped first.

Common patterns

Module-per-file

src/
├── lib.rs
├── parser.rs
├── lexer.rs
└── ast.rs
// lib.rs
pub mod parser;
pub mod lexer;
pub mod ast;

The conventional Rust 2018+ form: each module is one file, declared at the parent’s level.

Module with submodules

src/
├── lib.rs
├── network.rs                    # the module's root
└── network/
    ├── tcp.rs
    └── udp.rs
// lib.rs:
pub mod network;

// network.rs:
pub mod tcp;
pub mod udp;

pub use tcp::Connection;          // re-export from tcp

The pattern admits substantial nesting; each subdirectory has a mod.rs (Rust 2015) or a sibling file at the parent level (Rust 2018+).

Internal-implementation modules

mod internal {
    pub(crate) fn helper() { /* ... */ }
    pub(crate) struct Implementation;
}

pub use internal::PublicAPI;        // expose only the public surface

The pattern admits encapsulation: implementation details live in the internal module; consumers see only the re-exported public surface.

Workspace-internal pub(crate)

// In a multi-module crate:
mod parser {
    pub(crate) fn parse(input: &str) -> Ast { /* ... */ }
}

mod compiler {
    use crate::parser;

    pub fn compile(input: &str) -> Binary {
        let ast = parser::parse(input);     // OK: pub(crate) is visible here
        // ...
    }
}

pub(crate) admits sharing between modules of the same crate without exposing to consumers.

Re-exports through pub use

// In lib.rs:
mod core;
mod io;
mod net;

pub use core::*;           // re-export everything from core
pub use io::Reader;
pub use net::Client;

The pattern admits a flat public API regardless of internal organisation.

Test modules

// In any module:
fn add(a: i32, b: i32) -> i32 {
    a + b
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_add() {
        assert_eq!(add(2, 3), 5);
    }
}

The #[cfg(test)] admits the module to be compiled only during testing. The super::* brings the parent module’s items into scope.

A note on the prelude

The std::prelude is automatically imported into every Rust module; it includes:

use std::marker::{Copy, Send, Sized, Sync, Unpin};
use std::ops::{Drop, Fn, FnMut, FnOnce};
use std::mem::drop;
use std::boxed::Box;
use std::borrow::ToOwned;
use std::clone::Clone;
use std::cmp::{PartialEq, PartialOrd, Eq, Ord};
use std::convert::{AsRef, AsMut, Into, From};
use std::default::Default;
use std::iter::{Iterator, Extend, IntoIterator, DoubleEndedIterator, ExactSizeIterator};
use std::option::Option::{self, Some, None};
use std::result::Result::{self, Ok, Err};
use std::string::{String, ToString};
use std::vec::Vec;

The prelude’s contents are available without explicit use. Consequences:

  • Option, Some, None, Result, Ok, Err are available without import.
  • Vec, String, Box are available.
  • The conventional traits (Clone, Copy, Debug, Default, From, Into) are available.

The 2024 edition (and the unstable Rust 2024 edition’s prelude) extends this slightly. The conventional Rust code uses the prelude implicitly; explicit imports are needed only for items outside it.

For library crates that want a custom prelude:

// In lib.rs:
pub mod prelude {
    pub use crate::Reader;
    pub use crate::Writer;
    pub use crate::Connection;
}

Users use my_crate::prelude::*; to bring the curated set into scope.

A note on the discipline

The contemporary Rust module advice:

  • Use Rust 2018+ — the foo.rs form for non-inline modules; mod.rs is legacy.
  • Default to private — make items pub only when they are part of the public API.
  • Use pub(crate) for items shared across the crate but not exported.
  • Use pub use for re-exports that flatten the public API.
  • Use crate:: prefix for absolute paths to the crate’s root.
  • Group related items in modules; each module is one file or one folder.

The combination — explicit module declarations, fine-grained visibility, the use directive, the prelude — is the substance of Rust’s namespace management. The system is more elaborate than Python’s import but admits stronger encapsulation guarantees.