What Is in the Rust Prelude and Why?

The Rust prelude automatically imports common types and functions like String and Vec into every program to reduce boilerplate.

The invisible import

You write let v = vec![1, 2, 3]; and the code compiles. You haven't typed use std::vec::Vec. You haven't typed use std::vec. The compiler just knows. Now you try let h = HashMap::new();. The compiler rejects you with E0412 (cannot find type HashMap in this scope). You need use std::collections::HashMap.

Why does Vec work and HashMap doesn't? The prelude.

The prelude is the invisible set of imports that Rust adds to every file in your project. It puts the tools you reach for every day right on your desk. It keeps the tools you use occasionally in the shed, where you have to walk over and grab them explicitly. This design reduces noise in common code while forcing you to declare your dependencies when you branch into specialized territory.

The prelude is a module, not magic

The prelude is a real module named std::prelude::v1. The compiler automatically runs use std::prelude::* for every scope in your program. You don't see the import. You don't write the import. It just happens.

The prelude contains a curated list of types, traits, and macros. The Rust team maintains this list. Items get added when they become ubiquitous enough that requiring an import would slow down development. Items stay out when they are domain-specific, rare, or when explicit imports improve readability.

// No imports needed. The prelude brought these to the party.
fn main() {
    // Vec is a core collection, used in almost every program.
    let numbers = vec![1, 2, 3];
    
    // Option handles absence without null pointers.
    let maybe = Option::Some(42);
    
    // Result handles errors as values.
    let outcome = Result::Ok("success");
    
    // Traits are also in the prelude.
    // Clone is needed to duplicate values.
    let copy = numbers.clone();
    
    // println! is a macro in the prelude.
    println!("{:?}", numbers);
}

The prelude is not a suggestion. It is a contract. If something is in the prelude, you can use it anywhere without qualification. If it is not, you must import it. The compiler enforces this boundary strictly.

What lives in the prelude

The prelude is small but dense. It contains five core types, a handful of macros, and a large set of traits.

Types:

  • String: The growable, heap-allocated text type.
  • Vec<T>: The growable, heap-allocated vector.
  • Box<T>: The simple heap pointer.
  • Option<T>: The type for values that might be missing.
  • Result<T, E>: The type for operations that can fail.

Macros:

  • println!, print!, format!: Formatting and output.
  • panic!, assert!, debug_assert!: Error handling and assertions.
  • write!, writeln!: Writing to streams.
  • vec!: Creating vectors with initial data.
  • format_args!: Low-level formatting arguments.

Traits:

  • Clone, Copy, Drop: Ownership and lifecycle.
  • PartialEq, PartialOrd, Eq, Ord: Comparison and ordering.
  • AsRef, AsMut: Cheap conversions.
  • Into, From: Conversions.
  • Default: Default values.
  • Iterator, Extend, IntoIterator: Iteration.
  • Borrow, BorrowMut, ToOwned: Borrowing semantics.
  • Sized, Send, Sync, Unpin: System-level guarantees.
  • Fn, FnMut, FnOnce: Closures.

The trait list is long because traits provide the methods you call on values. When you call .clone(), you are invoking a method from the Clone trait. When you call .into(), you are invoking Into::into. When you call .iter(), you are invoking Iterator::iter. The prelude puts these traits in scope so you can call the methods without qualifying the trait name.

The trait ecosystem

The prelude is heavy on traits. This is intentional. Traits are the glue that makes generic code work. The prelude includes the traits that appear in function signatures and method chains across the entire ecosystem.

Into and From are conversion traits. From is the one you implement. Into is auto-implemented whenever you implement From. Both are in the prelude. This means you can write let s: String = 42.into(); without importing anything, provided From<i32> is implemented for String.

AsRef and AsMut are for cheap conversions. AsRef is used in function arguments to accept multiple types without copying. A function that takes &str can instead take impl AsRef<str> to accept &str, String, and OsString without allocation. AsRef is in the prelude because this pattern is everywhere.

Borrow is similar to AsRef but with stricter guarantees. Borrow is used by collections like HashMap to allow lookups with a type that borrows from the key type. If your map stores String keys, you can look up with &str because String implements Borrow<str>. Borrow is in the prelude because it enables this interoperability.

Iterator is the backbone of data processing. Methods like .map(), .filter(), .find(), and .collect() come from the Iterator trait. The prelude includes Iterator, Extend, and IntoIterator. This allows you to iterate over collections and chain methods without importing anything.

/// Filters names starting with 'A' and collects them.
/// Uses Iterator, Option, and String from the prelude.
fn find_names(names: &[String]) -> Vec<String> {
    // .iter() comes from IntoIterator.
    // .filter() comes from Iterator.
    // .cloned() comes from Iterator and Clone.
    // .collect() comes from Iterator and FromIterator.
    names.iter()
        .filter(|name| name.starts_with('A'))
        .cloned()
        .collect()
}

fn main() {
    let names = vec!["Bob".to_string(), "Alice".to_string(), "Ann".to_string()];
    let result = find_names(&names);
    println!("{:?}", result);
}

The prelude makes this code concise. Every method call relies on a trait in the prelude. If Iterator were not in the prelude, you would need use std::iter::Iterator; in every file that uses method chains.

Why HashMap is missing

HashMap is not in the prelude. BTreeMap is not. HashSet is not. LinkedList is not. Vec is the only sequence collection in the prelude.

This choice reflects usage frequency and design philosophy. Vec is the default collection. Almost every program uses vectors. Maps and sets are common, but not universal. Some programs never use a map. Requiring an import for HashMap forces you to declare that you are using a map. This declaration is a feature. It tells the reader that the code depends on a map. It also keeps the prelude small.

The prelude is not a dumping ground for popular types. It is a curated list. The Rust team evaluates additions carefully. Adding a type to the prelude increases the risk of name collisions. It also increases the cognitive load of learning the language. The bar for inclusion is high.

String is in the prelude because text is ubiquitous. Option and Result are in the prelude because error handling and absence are fundamental to Rust. Box is in the prelude because heap allocation is common. Vec is in the prelude because dynamic arrays are the default container. Everything else requires an import.

Pitfalls and shadowing

The prelude is powerful, but it has pitfalls.

Shadowing: If you define a type or function with the same name as a prelude item, you shadow the prelude item. The compiler uses your definition instead. This can break code that expects the prelude type.

// This shadows the prelude Vec.
struct Vec;

fn main() {
    // Error: E0412 cannot find type `Vec` in this scope.
    // The local `Vec` shadows the prelude `Vec`.
    let v = vec![1, 2, 3];
}

The vec! macro creates a std::vec::Vec. If you shadow Vec, the macro still works, but you cannot assign the result to a variable of type Vec because Vec now refers to your empty struct. Avoid shadowing prelude names.

Redundant imports: Writing use std::prelude::* is redundant. The compiler does it for you. Writing use std::vec::Vec; is allowed but unnecessary. It shadows the prelude Vec with the same Vec. This is harmless but adds noise. The community convention is to omit imports for prelude items.

to_string vs String::from: The ToString trait is not in the prelude. ToString is auto-implemented for types that implement Display. Display is also not in the prelude. This means you cannot call .to_string() on a value unless you import ToString or Display.

fn main() {
    let n = 42;
    
    // Error: E0599 no method named `to_string` found.
    // ToString is not in the prelude.
    // let s = n.to_string();
    
    // Solution: Use String::from, which relies on From.
    // From is in the prelude.
    let s = String::from(n);
    
    // Solution: Use format!, which is in the prelude.
    let s2 = format!("{}", n);
}

This is a common stumbling block. New Rust programmers expect .to_string() to work everywhere. It doesn't. Use String::from(value) or format!("{}", value) to avoid the import. String::from is preferred when you have a single value. format! is preferred when you are building a string from multiple parts.

Convention aside: cargo fmt removes use std::prelude::* if you accidentally write it. The formatter knows the import is redundant. It also removes use std::vec::Vec; if Vec is only used via the prelude. Trust the formatter. It keeps your imports clean.

Decision: prelude vs explicit imports

Use prelude items implicitly for Vec, String, Option, Result, and Box. These types are in the prelude. You don't need imports. You don't need to qualify them. Just use them.

Use prelude traits implicitly for method calls. When you call .clone(), .into(), .iter(), or .map(), the traits are already in scope. You don't need imports.

Use explicit imports for HashMap, BTreeMap, HashSet, LinkedList, and other collections. These types are not in the prelude. You must import them.

Use explicit imports for File, Path, TcpStream, and other I/O types. These types are domain-specific. You must import them.

Use explicit imports for external crates. The prelude only covers std. Crates like serde, tokio, and reqwest require imports.

Use explicit imports when you want to document the source of a type in a complex module. If a module uses Result from a custom crate, import it explicitly to avoid confusion with std::result::Result.

Use String::from instead of .to_string() to avoid importing ToString. String::from relies on From, which is in the prelude.

Don't fight the prelude. If the compiler can't find a name, you need an import. If it finds the name, you're good. The prelude is your default workspace. If a tool isn't there, you have to walk to the shed.

Where to go next