The use Keyword and Bringing Paths into Scope
Learn how use simplifies module paths, idiomatic conventions for functions and types, re-exporting with pub use, nested paths, and handling name conflicts.
Without the use keyword, every reference to an item in another module requires its full path—which can become long and tedious. use creates a local shortcut that brings a path into the current scope, so you can refer to the item by a shorter name. It’s purely a convenience: the code would still compile if you wrote out the full path each time, but use reduces repetition and makes the code easier to read. Understanding where use applies, how it interacts with module boundaries, and which idioms to follow will keep your imports clean and predictable.
How use Creates a Scope-Local Shortcut
A use statement establishes a name in the current module that points to a path. You can think of it as a symbolic link: after writing use crate::some::path, the final segment becomes directly available without writing the whole chain.
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}
use crate::front_of_house::hosting;
pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
}
Here, hosting is brought into the root module’s scope. Inside eat_at_restaurant, we only need hosting::add_to_waitlist() instead of the full crate::front_of_house::hosting::add_to_waitlist().
use only affects the module it appears in:
The shortcut lives exclusively in the module where use is written. It does not propagate into child modules.
To see why that matters, move the function into a child module:
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}
use crate::front_of_house::hosting;
mod customer {
pub fn eat_at_restaurant() {
hosting::add_to_waitlist(); // error[E0433]: unresolved module `hosting`
}
}
The compiler rejects this because the customer module is a separate scope from the root. The use statement in the root does nothing for code inside customer. The fix is either to move the use inside customer or to reference the shortcut with super::hosting from the child.
This behavior is different from variable scope: a variable defined in an enclosing block is visible in nested blocks. Module boundaries block all names—functions, variables, and use shortcuts alike—unless explicitly imported again.
Idiomatic use Paths for Functions vs. Types
There are two common ways to import a function, but one is considered idiomatic.
For functions: bring the parent module into scope, then call the function with module::function.
// Idiomatic: bring the containing module
use crate::front_of_house::hosting;
fn eat() { hosting::add_to_waitlist(); }
// Not idiomatic: bring the function itself
use crate::front_of_house::hosting::add_to_waitlist;
fn eat() { add_to_waitlist(); }
Bringing only the function makes it ambiguous where add_to_waitlist comes from—it looks like a local definition. The module::function style signals that the function is defined elsewhere and keeps the call site self-documenting, all while avoiding the full path.
For structs, enums, and other types: bring the full path directly. There is no risk of confusion because types are typically used as names on their own.
// Idiomatic for types
use std::collections::HashMap;
fn main() {
let mut map = HashMap::new();
map.insert("key", 42);
}
When you need multiple variants from an enum, you can import them individually:
enum TrafficLight { Red, Yellow, Green }
use TrafficLight::{Red, Yellow};
fn main() {
let stop = Red;
let caution = Yellow;
let go = TrafficLight::Green; // Green was not imported
}
Why these conventions exist:
These are community conventions that evolved for readability. They are not enforced by the compiler. Following them makes Rust codebases more consistent and easier for other developers to navigate.
Dealing with Name Collisions: as Aliases
Sometimes two items have the same name but come from different modules. Rust forbids importing both directly, because the name would become ambiguous.
use std::fmt;
use std::io;
// Both modules have a `Result` type — this would be a name collision if imported directly.
// Instead, qualify them:
fn do_io() -> io::Result<()> { Ok(()) }
fn format() -> fmt::Result { Ok(()) }
Alternatively, you can rename one of the imports with the as keyword:
use std::fmt::Result;
use std::io::Result as IoResult;
fn do_io() -> IoResult<()> { Ok(()) }
fn format() -> Result { Ok(()) }
Both approaches are idiomatic. Use as when the alias makes the code clearer; otherwise, qualify with the parent module.
Re-exporting with pub use
Normally, a name brought in with use is private to the scope that imports it. Outside code cannot see it. By combining pub and use, you can make that imported name part of the public API of the current module. This is called re-exporting.
Consider a library that organizes its internals into a private module but wants to expose a clean public path:
// lib.rs
mod api {
mod v1 {
pub fn fetch_data() -> String {
"data".to_string()
}
}
// Re-export fetch_data so users see api::fetch_data instead of api::v1::fetch_data
pub use v1::fetch_data;
}
// In downstream code:
use my_crate::api::fetch_data;
Without pub use, the user would need to write my_crate::api::v1::fetch_data(), which leaks an implementation detail (the v1 module). With pub use, the public surface matches how users think about the domain, while the internal structure remains free to change.
Re-exporting also works for entire modules:
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}
// Make hosting accessible as restaurant::hosting
pub use front_of_house::hosting;
Cleaner API for external callers:
External code can now call restaurant::hosting::add_to_waitlist() instead of the full internal path. The internal module layout stays hidden.
Using Items from External Crates and std
To use something from an external crate, you first add the crate to Cargo.toml. Then you bring items into scope with use—the path starts with the crate name.
# Cargo.toml
[dependencies]
rand = "0.8"
use rand::Rng;
fn main() {
let secret = rand::thread_rng().gen_range(1..=100);
}
The standard library std is also a crate, but one that ships with Rust and doesn’t require a Cargo.toml entry. You still need use to import from it:
use std::collections::HashMap;
The same idiomatic rules apply: for functions from std, bring the module; for types, bring the full path.
Nested Paths to Reduce Clutter
When you import several items from the same crate or module, separate use lines add vertical noise. Rust allows nested paths to combine them.
Instead of:
use std::cmp::Ordering;
use std::io;
write:
use std::{cmp::Ordering, io};
You can nest further:
use std::collections::{HashMap, HashSet, hash_map};
The common part before the :: is written once, and the differing segments go inside braces.
Don't over-nest at the cost of readability:
A single nested path can group related items cleanly. If you try to combine ten unrelated things into one massive nested block, the import line becomes hard to scan. Break it into logical groups.
Glob Imports with the * Operator
The glob operator * imports all public items from a namespace:
use std::collections::*;
This brings HashMap, HashSet, BTreeMap, and everything else in that module into scope. It’s convenient for quick experiments or for test modules where the extra names won’t cause confusion.
In production code, glob imports are discouraged because they make it unclear where each name comes from and increase the risk of accidental name collisions. If two glob-imported modules export a type with the same name, the compiler will reject the ambiguous usage.
Glob imports can hide name conflicts until usage:
A * import might compile silently if all names are unique at the time of writing. Later, if a new public item with a conflicting name is added to the imported module, your code breaks. Use explicit imports to make dependencies visible.
Common Pitfalls and Misunderstandings
A handful of recurring mistakes can make use seem unpredictable. Recognizing them early saves debugging time.
use is module-scoped, not block-scoped:
As shown earlier, a use written in a parent module is invisible to child modules. If you move code into a new module, the import does not follow it. Always place use statements inside the module that needs them.
Relative paths in use default to the crate root:
Without a leading self or super, a path in a use statement is relative to the crate root, not to the current module. If you want to refer to a sibling module, write use self::sibling_module::SomeType or use super::parent_item.
Another common oversight: use does not make an item visible through private modules on the path. Even if the item itself is pub, every module between the current scope and that item must also be accessible. If any link in the chain is private, the import fails.
mod outer {
mod inner {
pub fn secret() {}
}
// Error: `inner` is private, so `secret` cannot be reached from here
use crate::outer::inner::secret;
}
Finally, unused use statements generate compiler warnings (or errors in some configurations). This is a deliberate lint to keep imports tidy. If you intentionally import something only for documentation or trait methods, you can prefix the path with _ to silence the warning, but the cleaner approach is to import only what you actually use.
Summary
The use keyword is the primary tool for managing path noise in Rust. It creates shortcuts that are scoped to a module, respects privacy, and does not propagate automatically. The idiomatic conventions—parent module for functions, full path for types—help keep code readable and self-documenting. When names collide, as aliases and qualified paths give you precise control. Combining pub with use reshapes your crate’s public API without exposing internal structure, and nested paths keep import lists concise.