Avoiding Wildcard Imports
Understand why glob imports can silently break your code and how to use explicit imports to keep your Rust project safe and readable
A wildcard import — sometimes called a glob import — is a use statement that pulls every public name from a module into the current scope with a single *:
use std::collections::*;
After this line, HashMap, BTreeMap, HashSet, and every other public item from std::collections are available without qualification. It feels convenient, especially when you are starting a module and don’t yet know which names you will need. The compiler will even suggest it sometimes.
The trouble is not what wildcard imports do today. The trouble is what they allow to happen to your code later, without you touching a single line.
Breaking Changes Without Warning:
A dependency can add a new public item in a minor version update. If that item happens to conflict with a name you already use, a wildcard import turns the upgrade from safe to compilation-breaking — despite semver promising otherwise.
The Core Problem: Name Clashes from Upstream Changes
Rust’s semver compatibility rules state that a crate may add new public items in a minor version bump. This is considered a backward-compatible change because adding a name should not break existing downstream code that does not mention that name.
A wildcard import silently pulls every such new name into your namespace. If one of those new names collides with something you already have — a local type, a function, or even a method from another trait — the compiler can suddenly see an ambiguity it did not see before. The dependency author did nothing wrong. You did not change your code. Yet your build fails.
The collision that hurts most is not a simple name shadowing, because Rust resolves that predictably. The real danger lies in method resolution on types that implement multiple traits.
Imagine a dependency crate databuf that provides:
// Inside crate `databuf`
pub trait Buf {
fn remaining(&self) -> usize;
// ... other methods
}
You are working on a project that processes byte slices. You write your own trait for a utility method:
use databuf::*;
trait BytesLeft {
fn remaining(&self) -> usize;
}
impl BytesLeft for &[u8] {
fn remaining(&self) -> usize {
self.len()
}
}
impl Buf for &[u8] {
fn remaining(&self) -> usize {
// hypothetical implementation from databuf
self.len()
}
}
fn main() {
let arr = [1u8, 2u8, 3u8];
let v = &arr[1..];
assert_eq!(v.remaining(), 2);
}
Today this compiles because databuf does not have a Buf implementation for &[u8] — you are providing one. Tomorrow, databuf releases a minor version that adds exactly that implementation. Now both BytesLeft and Buf are in scope via the wildcard import, and both offer a method named remaining. The call v.remaining() becomes ambiguous.
The compiler error shows two candidates, neither obviously wrong, and the fix requires you to disambiguate every call site:
error[E0034]: multiple applicable items in scope
--> src/main.rs:22:18
|
22 | assert_eq!(v.remaining(), 2);
| ^^^^^^^^^ multiple `remaining` found
|
= note: candidate #1 is defined in an impl of the trait `BytesLeft` for the type `&[u8]`
= note: candidate #2 is defined in an impl of the trait `databuf::Buf` for the type `&[u8]`
help: disambiguate the method for candidate #1
|
22 | assert_eq!(BytesLeft::remaining(&v), 2);
| ~~~~~~~~~~~~~~~~~~~~~~~~
help: disambiguate the method for candidate #2
|
22 | assert_eq!(databuf::Buf::remaining(&v), 2);
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
This is not a theoretical edge case. Any crate that offers trait implementations on common types (&[u8], String, Vec<T>, etc.) and any codebase that defines its own extension traits on those same types are one wildcard import away from this collision.
Method Ambiguity Is Worse Than Name Clashes:
A conflicting type name is resolved by Rust's precedence rules: your local definition always wins over a wildcard-imported name. Method calls are different — if two traits in scope both define the same method for the same type, no automatic precedence saves you. The compiler demands an explicit qualification.
How Precedence Rules Work (And Why They Don’t Fully Protect You)
Rust does have a sensible rule for name conflicts: an item explicitly defined or imported in the current module takes priority over anything brought in by a wildcard. This means that a local struct Bytes will not clash with a wildcard-imported bytes::Bytes, even if both exist. The local one simply shadows the wildcard version.
use bytes::*;
// This is fine — local `Bytes` shadows `bytes::Bytes`
struct Bytes(Vec<u8>);
So far, so good. You might think that this protection makes wildcard imports safe. It does not, because method resolution does not work on names that you define at the module level — it works on the set of traits that are in scope for a given type. When two traits both provide the same method name and both are in scope, the compiler cannot choose, regardless of whether either or both arrived via a wildcard.
Modules can also be affected in subtler ways. A wildcard import does not re‑import a module with a name that already exists in the current module, even if that existing name is a module you defined. This is consistent with the precedence rule but can cause confusion when you expect use some_crate::* to bring in a whole submodule tree that you then try to access under a path you thought would be extended.
A Concrete Example of Method Ambiguity
Here is a minimal, compilable demonstration of the problem. The example uses an external crate bytes (a real, widely used buffer library) to show how the conflict emerges in practice.
// Cargo.toml
// [dependencies]
// bytes = "1"
use bytes::*;
// Our own trait with a method that also exists on `bytes::Buf`
trait BytesLeft {
fn remaining(&self) -> usize;
}
impl BytesLeft for &[u8] {
fn remaining(&self) -> usize {
self.len()
}
}
fn main() {
let data = [1u8, 2u8, 3u8];
let slice = &data[1..];
// If `bytes::Buf` has an impl of `remaining` for `&[u8]`,
// this call becomes ambiguous.
println!("{}", slice.remaining());
}
When the bytes crate adds impl Buf for &[u8] in a future version, the wildcard import will pull that trait implementation into scope and the call to .remaining() will stop compiling. The only robust way to prevent this is to not use wildcard imports from crates you do not control.
Readability and Maintenance Costs
Even if name clashes never happen, wildcard imports make code harder to read and maintain. When a reader encounters a type name that is not defined anywhere in the file, they must scan every wildcard import to discover which crate it comes from. Explicit imports act as a map: each use line tells you exactly where a name originates.
During refactoring, wildcard imports obscure which external items are actually used. Removing an unused dependency or upgrading a crate becomes a detective task. Tools like rust-analyzer can sometimes help, but the signal-to-noise ratio is lower with glob imports.
Explicit imports also enable finer-grained control when you rename items with as:
use tokio::sync::Mutex as TokioMutex;
use std::sync::Mutex as StdMutex;
With a wildcard import, you lose the ability to distinguish between two identically named items from different modules unless you already know which one the wildcard provides.
Wildcard Imports Hinder Grepability:
When you search for the definition of a type, an explicit use path points you directly to its crate and module. A wildcard import forces you to grep through all possible sources, which is slower and error-prone.
When Wildcard Imports Are Acceptable
Three situations make wildcard imports reasonable and widely accepted in the Rust ecosystem.
Test Modules
Inside a #[cfg(test)] mod tests { ... } block, writing use super::*; is idiomatic. Test modules are short-lived, isolated from production code, and benefit from having all of the parent module’s items in scope without repetitive imports. A name clash inside a test module is unlikely to cause version‑to‑version breakage because test code typically does not interact with external dependency evolution the same way.
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() {
// All parent items are available directly
assert!(do_something());
}
}
Clippy’s wildcard_imports lint explicitly skips test modules by default for this reason.
Prelude Modules
Some crates provide a prelude module that re‑exports the most commonly used types, traits, and functions. This module is designed to be wildcard‑imported:
use some_crate::prelude::*;
The crate maintainer curates the prelude carefully, understanding that downstream code will bring every name in at once. The convenience of having the crate’s core interface available everywhere is judged to outweigh the small risk of future conflicts. Because the prelude is an explicit contract, this is a deliberate trade‑off, not an accidental exposure.
Internal Module Re-exports
When a crate uses modules solely to organize internal code, it is common to re‑export the entire public surface of a module with a wildcard in the parent:
mod internal_details;
pub use internal_details::*;
Since you control the internal_details module, any future name additions are under your own release management. There is no external dependency surprise. This pattern is useful for flattening a deeply nested module hierarchy while keeping source files logically separated.
When You Control the Source:
Wildcard imports from modules you own do not carry the same risk as those from third-party crates. You control the timing and content of changes, so name clashes become a refactoring task you can schedule, not an unexpected build failure from a dependency upgrade.
Tooling Support: Clippy’s wildcard_imports Lint
Clippy provides a lint named wildcard_imports that warns on every wildcard use statement. By default, it is set to allow — meaning it does not produce warnings unless you opt in. You can enable it crate‑wide:
// At the top of lib.rs or main.rs
#![warn(clippy::wildcard_imports)]
Or you can run Clippy with the flag directly:
cargo clippy -- -W clippy::wildcard_imports
The lint skips test crates and modules by default (a behavior that some developers disagree with, but it reflects the community consensus that use super::* in tests is acceptable). If you want it to fire in test code as well, you need to configure it with #![warn(clippy::wildcard_imports)] inside the test module or use a tool that overrides the default.
Summary
Wildcard imports create a silent contract between your code and every public item in a dependency’s module. That contract breaks when a dependency adds a name that collides with yours, and the breakage often manifests as method‑resolution ambiguity that the compiler cannot resolve without your intervention. Explicit imports cost a few extra keystrokes and remove that entire class of risk.
When you control the source module, when you are writing tests, or when you are importing a curated prelude, wildcard imports are fine. For everything else — especially for any crate outside your direct control — prefer explicit, item‑by‑item imports. It keeps your code robust against safe upstream changes and makes the origin of every name immediately visible.