Modules in Rust
Rust's module system lets you split a program into logical namespaces, control what code is visible to the outside world, and build large projects without everything ending up in one enormous file. Modules are the primary tool for organisation and privacy in Rust.
What is a Module?
A module is a named container for items: functions, structs, enums, constants,
traits, and even other modules. Every Rust file starts inside an implicit module
named after the file, and the entry point of a crate is either src/main.rs or
src/lib.rs.
Modules serve two purposes:
- Organisation — group related code together under a common name.
- Privacy — control which items are visible outside the module.
Defining an Inline Module
The simplest way to define a module is inline with the mod keyword followed by
a block:
mod greetings {
pub fn hello(name: &str) {
println!("Hello, {}!", name);
}
pub fn goodbye(name: &str) {
println!("Goodbye, {}!", name);
}
fn internal_helper() {
// private — only usable inside this module
println!("I am a private helper");
}
}
fn main() {
greetings::hello("Alice"); // Hello, Alice!
greetings::goodbye("Alice"); // Goodbye, Alice!
// greetings::internal_helper(); // ERROR: function is private
}Hello, Alice! Goodbye, Alice!
pub can be accessed from outside the module.Loading a Module from a File
For larger modules, writing everything inline becomes unwieldy. You can move the
module body into its own file and use a bare mod name; declaration (no block).
The compiler then looks for the module body in a corresponding file.
// src/main.rs
mod garden; // tells the compiler: look for src/garden.rs or src/garden/mod.rs
fn main() {
garden::plant("tomato");
}// src/garden.rs
pub fn plant(name: &str) {
println!("Planting a {}", name);
}
pub fn harvest(name: &str) {
println!("Harvesting {}", name);
}Planting a tomato
File System Conventions
When you write mod garden;, the compiler looks for the module body in one of
two places. The single-file convention is preferred in modern Rust because it
avoids having many files all named mod.rs open in your editor at once.
Declaration location | Module file path (pick one) |
|---|---|
src/main.rs or src/lib.rs | src/garden.rs OR src/garden/mod.rs |
src/garden.rs (submodule) | src/garden/vegetables.rs |
src/garden/mod.rs (submodule) | src/garden/vegetables.rs |
// Project layout
// src/
// main.rs
// garden.rs ← the garden module
// garden/
// vegetables.rs ← submodule: garden::vegetables
// src/garden.rs
pub mod vegetables; // loads src/garden/vegetables.rs
pub fn plant(name: &str) {
println!("Planting: {}", name);
}
// src/garden/vegetables.rs
pub struct Carrot;
pub struct Tomato;
pub fn describe(v: &str) {
println!("{} is a vegetable", v);
}
// src/main.rs
mod garden;
fn main() {
garden::plant("spinach");
garden::vegetables::describe("carrot");
}Planting: spinach carrot is a vegetable
Privacy: pub, pub(crate), pub(super)
Rust gives you fine-grained control over visibility with several modifiers:
Visibility modifier | Accessible from |
|---|---|
(nothing — private) | Only within the current module and its descendants |
pub | Anywhere — other modules, other crates |
pub(crate) | Anywhere within the same crate, not from external crates |
pub(super) | The parent module only |
pub(in path) | A specific ancestor module path |
mod outer {
pub fn public_fn() { println!("visible everywhere"); }
pub(crate) fn crate_fn() { println!("visible within this crate only"); }
pub(super) fn super_fn() { println!("visible to parent of outer"); }
fn private_fn() { println!("visible only inside outer"); }
mod inner {
pub fn inner_public() {
// Can call outer's private fn because inner is a child of outer
super::private_fn();
}
}
}
fn main() {
outer::public_fn(); // OK
outer::crate_fn(); // OK — we are in the same crate
// outer::super_fn(); // ERROR — main is not the parent module of outer
}pub only when you genuinely need something accessible from outside. This keeps your public API surface small and easy to maintain.Paths to Items
To refer to an item in a module, write a path — a sequence of module names
separated by ::. Rust supports three kinds of path roots:
crate::— starts from the crate root (absolute path, always unambiguous)super::— starts from the parent module (one level up)self::— starts from the current module (explicit, rarely required)
mod config {
pub const MAX_RETRIES: u32 = 3;
pub mod network {
pub fn connect(url: &str) {
// absolute path from the crate root
let retries = crate::config::MAX_RETRIES;
println!("Connecting to {} (max {} retries)", url, retries);
}
}
pub mod database {
pub fn init() {
// relative path: super goes up to config, then into network
super::network::connect("db://localhost");
}
}
}
fn main() {
config::network::connect("https://api.example.com");
config::database::init();
}Connecting to https://api.example.com (max 3 retries) Connecting to db://localhost (max 3 retries)
use Declarations
Typing the full path every time is verbose. The use keyword brings an item into
scope so you can refer to it by its short name:
use std::collections::HashMap;
fn main() {
// HashMap is now in scope — no need to write std::collections::HashMap
let mut scores: HashMap<&str, u32> = HashMap::new();
scores.insert("Alice", 95);
scores.insert("Bob", 87);
for (name, score) in &scores {
println!("{}: {}", name, score);
}
}Nested use Paths
When importing multiple items from the same parent path, collapse them into a
single use with curly braces. Use self to also bring the parent module itself
into scope at the same time:
// Instead of three separate lines:
// use std::io::Read;
// use std::io::Write;
// use std::io::BufReader;
// Write it as one:
use std::io::{Read, Write, BufReader};
// Bring both io and io::Write into scope using self:
use std::io::{self, Write};
// Glob import — brings in everything public (use sparingly)
use std::collections::*;
fn main() {
// HashMap comes from the glob import above
let mut map: HashMap<&str, &str> = HashMap::new();
map.insert("lang", "Rust");
println!("{:?}", map);
}use module::*) make it hard to tell where a name came from. Prefer explicit imports in application code. Glob imports are mainly used in test modules (e.g., use super::*) or prelude-style modules designed specifically to be glob-imported.Renaming with as
When two items from different modules share a name, or when you want a shorter
alias, rename the import with as:
use std::collections::HashMap as HMap;
use std::fmt::Result as FmtResult;
use std::io::Result as IoResult;
// Without renaming, bringing both Results into scope would be ambiguous.
fn format_data() -> FmtResult {
Ok(())
}
fn read_data() -> IoResult<()> {
Ok(())
}
fn main() {
let mut map: HMap<&str, i32> = HMap::new();
map.insert("x", 42);
println!("{:?}", map); // {"x": 42}
}Re-exporting with pub use
pub use re-exports an item: it brings it into scope and makes it part of the
current module's public API. This is the standard technique for shaping a clean
public interface regardless of how the code is internally organised:
// src/lib.rs — a library crate
mod auth;
mod user;
// Re-export the types callers actually need at the crate root.
// External users import from my_lib::User, not my_lib::user::User.
pub use auth::Session;
pub use user::User;
// src/auth.rs
pub struct Session {
pub token: String,
}
// src/user.rs
pub struct User {
pub name: String,
}
// External caller (another crate):
// use my_lib::User; // works thanks to pub use
// use my_lib::Session; // works thanks to pub usepub use in their root to expose a clean, flat API. Users should not need to know your internal folder structure.The Standard Prelude
Some items are so commonly used that Rust automatically imports them into every
module. This set of implicit imports is called the prelude. You can use these
without any use declaration:
Option<T>and its variantsSome/NoneResult<T, E>and its variantsOk/ErrString,Vec,BoxDrop,Clone,Copy,Send,SyncIteratorandIntoIteratorprintln!,eprintln!,format!,vec!,panic!
std::prelude::v1. You can opt into the Rust 2021 edition prelude (which adds TryInto, TryFrom, and FromIterator) by setting edition = "2021" in your Cargo.toml.Tests Module with #[cfg(test)]
The convention in Rust is to place unit tests in a tests submodule inside the
same file as the code being tested, guarded by #[cfg(test)]. This attribute
tells the compiler to include that module only during test builds, so test code
never ends up in production binaries:
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
pub fn is_even(n: i32) -> bool {
n % 2 == 0
}
#[cfg(test)]
mod tests {
use super::*; // bring everything from the parent module into scope
#[test]
fn test_add() {
assert_eq!(add(2, 3), 5);
assert_eq!(add(-1, 1), 0);
}
#[test]
fn test_is_even() {
assert!(is_even(4));
assert!(!is_even(7));
}
#[test]
#[should_panic]
fn test_overflow() {
let _ = i32::MAX + 1; // panics in debug builds
}
}$ cargo test running 3 tests test tests::test_add ... ok test tests::test_is_even ... ok test tests::test_overflow ... ok test result: ok. 3 passed; 0 failed
use super::* inside a test module is the standard idiom. It imports everything from the parent module, including private items. Tests are allowed to access private code because they live inside the same module tree.Module Structure Best Practices
One module per file — keep files focused; a very long file is a sign to split
Group by domain, not by type — put all authentication code together rather than all structs in one file and all functions in another
Expose a clean public API with
pub useat the crate root so callers have a stable import pathStart private, go public — default to private, promote to
pubonly when neededpub(crate)is useful — share code across modules without leaking it to external usersName modules in snake_case —
my_module, notMyModuleKeep tests next to code — use
#[cfg(test)]submodules in the same file for unit tests
Complete Module Example
pub mod geometry {
pub mod shapes {
#[derive(Debug)]
pub struct Circle {
pub radius: f64,
}
#[derive(Debug)]
pub struct Rectangle {
pub width: f64,
pub height: f64,
}
impl Circle {
pub fn area(&self) -> f64 {
std::f64::consts::PI * self.radius * self.radius
}
}
impl Rectangle {
pub fn area(&self) -> f64 {
self.width * self.height
}
}
}
pub mod utils {
use super::shapes::{Circle, Rectangle};
pub fn largest_area(c: &Circle, r: &Rectangle) -> f64 {
c.area().max(r.area())
}
}
}
// Re-export the most-used types at the library root
pub use geometry::shapes::{Circle, Rectangle};
pub use geometry::utils::largest_area;
fn main() {
let c = Circle { radius: 3.0 };
let r = Rectangle { width: 4.0, height: 5.0 };
println!("Circle area: {:.2}", c.area());
println!("Rectangle area: {:.2}", r.area());
println!("Largest: {:.2}", largest_area(&c, &r));
}Circle area: 28.27 Rectangle area: 20.00 Largest: 28.27
pub opt-in; paths navigate the module tree with crate::, super::, and self::; use brings items into scope for convenient short-name access; and pub use re-exports items to build a clean public API regardless of how your code is structured internally.