When paths get in the way
You are writing a network client. You need TcpStream, Read, Write, and Error. Every time you use them, you type std::net::TcpStream or std::io::Read. Your fingers hurt. Your screen is cluttered with path noise. You reach for use to clean it up, but then you hit a name collision with a local function, or you accidentally shadow a type and the compiler screams. Imports in Rust look like imports in other languages, but the rules around scope, re-exporting, and visibility are stricter. Understanding use isn't just about typing less; it's about controlling what your module exposes and how it connects to the rest of the crate.
Imports are aliases, not copies
The use statement creates a local alias for an item. It does not copy the item. It does not link the code at this point. It just tells the compiler, "When I say TcpStream, I mean std::net::TcpStream." Think of it like adding a shortcut to your desktop. The shortcut doesn't contain the application. It just points to it. If you delete the shortcut, the application stays. If you move the application, the shortcut breaks. In Rust, use is the shortcut. The compiler resolves the full path during name resolution, long before linking happens.
This distinction matters. When you write use std::collections::HashMap;, the compiler adds HashMap to the current scope's symbol table. If you write HashMap::new(), the compiler looks for HashMap in the current scope, finds the alias, and expands it to the full path. This happens during the name resolution phase. If you don't import it, the compiler looks for HashMap in the current scope, finds nothing, and emits an error. The error isn't "HashMap doesn't exist." It's "cannot find type HashMap in this scope." The type exists in the crate; you just haven't brought it into your local view.
Imports are aliases, not copies. The compiler resolves them before it cares about types.
The prelude saves you typing
You probably noticed you didn't have to import Vec, Option, or Result in your first Rust programs. That's the prelude. Rust automatically imports std::prelude::v1::* into every module. It contains the types and traits you use in almost every program. You never write use std::prelude::*;. The compiler does it for you. If you need something not in the prelude, like HashMap or Duration, you add the import.
The prelude is a convenience, not a magic wand. It reduces boilerplate for the most common items while keeping the namespace clean. You can check what's in the prelude by looking at the standard library documentation. If a type is in the prelude, you can use it without use. If it's not, you need to import it.
Minimal example
use std::collections::HashMap;
/// Demonstrates basic use statement.
fn main() {
// HashMap resolves to std::collections::HashMap here.
// No need to write the full path every time.
let mut map = HashMap::new();
map.insert("key", 1);
// Accessing the value returns an Option.
// Option is in the prelude, so no import needed.
let value = map.get("key");
println!("{:?}", value);
}
Grouping and renaming
When you need multiple items from the same parent module, group them with curly braces. This keeps the top of your file tidy and makes it obvious where the items come from.
use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
/// Analyzes file paths and counts occurrences.
fn analyze(paths: &[PathBuf]) -> HashMap<&Path, usize> {
let mut counts = HashMap::new();
for path in paths {
// PathBuf derefs to Path, so we can use Path methods.
let key = path.as_path();
*counts.entry(key).or_insert(0) += 1;
}
counts
}
You'll often see use std::io::{self, Read, Write};. The self keyword imports the module itself under its own name. This lets you write io::Result instead of std::io::Result. It's a convention to keep the module name available for associated types or constants while importing specific items.
Use as to rename an import when you have a naming conflict or want a shorter alias for a verbose type.
use std::collections::HashMap as Map;
/// Uses a shorter alias for HashMap.
fn create_map() -> Map<String, i32> {
// Map resolves to HashMap.
Map::new()
}
Renaming is also useful when you import from multiple crates that define the same name. If crate_a and crate_b both define Error, you can't import both without renaming.
use crate_a::Error as AError;
use crate_b::Error as BError;
Scope and visibility
use statements are scoped. An import inside a function doesn't leak to the module level. An import inside a block is only visible inside that block. This lets you keep the namespace clean by importing items only where they are needed.
fn process() {
// HashMap is only available inside this function.
use std::collections::HashMap;
let map = HashMap::new();
}
// Error: cannot find type `HashMap` in this scope.
// let map = HashMap::new();
Imports also control visibility. use is private by default. If you want to re-export an item as part of your module's public API, use pub use. This is how you curate your interface. You can import an internal item and expose it publicly without changing its definition.
// In lib.rs or a module file
mod internal {
pub fn helper() {
println!("Helper called");
}
}
// Re-export helper as part of the public API.
pub use internal::helper;
// Now users can call crate::helper().
You can also limit visibility with pub(crate) use. This makes the item visible within the crate but not to external users. It's useful for sharing utilities across modules without polluting the public interface.
mod utils {
pub fn internal_tool() {}
}
// Visible to all modules in the crate.
pub(crate) use utils::internal_tool;
Convention aside: group your imports. Standard library first, then external crates, then local modules. Blank lines between groups. cargo fmt won't enforce this grouping, but it keeps files readable. Some teams use tools to sort imports automatically. Consistency matters more than the exact order.
Pitfalls: Wildcards and collisions
Wildcard imports (use module::*;) bring every public item from a module into scope. This is convenient for quick scripts but dangerous in production code. Wildcards pollute the namespace and make it hard to track where an item comes from. If you use std::io::*; and use std::fs::*;, and both define a function with the same name, you get a collision. The compiler rejects this with E0252 (the name is defined multiple times).
// Avoid this in library code.
use std::io::*;
fn main() {
// Where does `read` come from?
// It's ambiguous if other modules also export `read`.
// read(&mut buffer);
}
Wildcards also hide dependencies. If you add a new item to a module and someone is using use module::*;, their code might break or change behavior without them realizing it. Explicit imports make dependencies clear.
There is one exception: tests. In test modules, use super::*; is common. It imports everything from the parent module so you can test private items. Tests are temporary and isolated, so namespace pollution doesn't matter. The community accepts globs in tests.
Another pitfall is shadowing. If you define a local type with the same name as an imported type, the local type wins. This can lead to subtle bugs if you expect the imported type but get the local one.
use std::string::String;
struct String {
data: Vec<u8>,
}
fn main() {
// This creates a local String, not std::string::String.
let s = String { data: vec![1, 2, 3] };
// Error: no method named `push_str` found for struct `String`.
// s.push_str("hello");
}
The compiler will warn you if you shadow a type, but it won't stop you. Be careful with common names like Result, Error, or String. If you see a type error that doesn't make sense, check for shadowing.
Trust the compiler on name collisions. If E0252 fires, you have a shadowing problem, not a Rust bug.
Decision matrix
Use use to shorten long paths when you reference an item multiple times in a module. Use grouped imports with curly braces when you need multiple items from the same parent module. Use as to rename an import when you have a naming conflict or want a shorter alias for a verbose type. Use pub use when you want to re-export an item as part of your module's public API. Use pub(crate) use when you need to share an item across the crate without exposing it to external users. Avoid wildcard imports in library code to prevent namespace pollution and maintain clear provenance. Reach for use inside a function scope when the import is only needed for a small block of code, keeping the module-level namespace clean. Use use super::*; in test modules to access private items from the parent module.
Keep your imports explicit. Future you will thank present you when tracking down where a type came from.