What Is the Prelude in Rust?

The Rust Prelude is an automatically imported module containing common traits and types like Option and Result to reduce boilerplate.

The invisible imports

You write a function that returns Option<i32>. You compile it. It passes. You look at the top of the file. There are no use statements. No imports. Yet Option is right there, waiting to be used. You type vec![1, 2, 3]. It works. You call .map() on a vector iterator. It works.

It feels like the compiler is reading your mind, but it's just following a rule called the Prelude. The Prelude is a module that Rust automatically imports into every single module in your program. It contains the types, traits, and macros you use so frequently that typing use std::... every time would be tedious. The prelude is the set of tools that are always within reach.

The kitchen counter analogy

Imagine walking into a fully equipped kitchen. You don't need to file a request to grab a knife, a spoon, or a cutting board. They're sitting on the counter, ready to use. You just pick them up.

The Rust Prelude is that counter. It holds the items the language designers decided you'll use in almost every function. The rest of the standard library is the pantry. The pantry has everything else: HashMap, File, Path, TcpStream. Those items are useful, but you don't need them in every single function. To get them, you have to open the pantry door and reach in. In Rust, opening the pantry door means writing a use statement.

Treat the prelude like the counter. If you need something and it's not on the counter, check the pantry. Don't assume everything is there.

What the compiler actually does

When the compiler processes your code, it silently injects use std::prelude::v1::*; at the top of every module. You don't see this line. It's not in your source file. But the compiler treats it as if it's there. This happens before any of your own code runs. The items in the prelude become part of the namespace for that module.

The path is std::prelude::v1. The v1 is there for future-proofing. If Rust ever needs to change the prelude in a breaking way, they can add a v2. You can then opt into the new version explicitly. Right now, v1 is the only version. The compiler always uses v1 unless you tell it otherwise.

Convention aside: You'll sometimes see use std::prelude::v1; in old tutorials or generated code. That line does nothing. The compiler adds the import automatically. It's redundant. You can delete it. It won't change behavior. It just clutters the file.

The items on the counter

The prelude contains a specific set of items. Knowing what's in the prelude helps you write code faster and understand what you need to import.

The prelude includes fundamental types like Option, Result, String, Vec, and Box. It includes core traits like Clone, Copy, Drop, PartialEq, PartialOrd, Eq, Ord, AsRef, AsMut, Into, From, Default, Borrow, BorrowMut, ToOwned, ToString, and Iterator. It also includes the Sized marker trait, which is implicit on almost every type.

Macros are also in the prelude. println!, format!, vec!, assert!, debug_assert!, panic!, unreachable!, write!, writeln!, print!, eprint!, eprintln!, include!, concat!, stringify!, env!, option_env!, cfg!, assert_eq!, assert_ne!, todo!, unimplemented!, and format_args! are all available without imports.

// No imports needed. The prelude brings these into scope automatically.
fn main() {
    // Option and Result are in the prelude.
    let maybe_number: Option<i32> = Some(42);
    let calculation: Result<i32, &str> = Ok(100);

    // Vec and String are in the prelude.
    let names = vec!["Alice", "Bob"];
    let greeting = String::from("Hello");

    // Iterator trait is in the prelude, so .map() works.
    let doubled: Vec<i32> = names.iter().map(|n| n.len() * 2).collect();

    // println! is a macro in the prelude.
    println!("{:?}", doubled);
}

The philosophy behind the prelude is frequency and friction. If you use an item in 90% of your functions, it belongs in the prelude. If you use it in 10%, it belongs in the pantry. Clone is in the prelude because you clone values constantly. PartialEq is in the prelude because you compare values constantly. Iterator is in the prelude because you iterate constantly. HashMap is not in the prelude because you don't use hash maps in every function.

The I/O prelude

There's a twist. The standard library has sub-modules, and some of them have their own preludes. std::io::prelude is the most famous one.

The root prelude is always imported. The I/O prelude is not. If you're doing I/O, you'll often see use std::io::prelude::*; at the top of a file. This brings Read, Write, BufRead, and Seek into scope. These traits define methods like read(), write(), lines(), and seek().

Without the I/O prelude, you have to write the full path or import the traits manually. With the I/O prelude, the methods are available on any type that implements the trait.

use std::io;
// Import the I/O prelude to get Read, Write, BufRead, and Seek.
use std::io::prelude::*;

fn read_first_line() -> io::Result<String> {
    // stdin is in std::io, not the root prelude.
    let mut input = String::new();
    
    // read_line is a method from the Read trait.
    // It's available because of the I/O prelude import.
    io::stdin().read_line(&mut input)?;
    
    Ok(input)
}

fn main() {
    match read_first_line() {
        Ok(line) => println!("You typed: {}", line),
        Err(e) => eprintln!("Error: {}", e),
    }
}

Convention aside: The community treats use std::io::prelude::*; as a standard import for any file that does I/O. You'll see it in almost every I/O-heavy crate. It's not automatic, but it's expected. If you're writing a function that reads or writes, add the import.

When the prelude bites back

The prelude can cause name collisions. If you define a type or function with the same name as something in the prelude, you shadow the prelude item. The compiler won't stop you. It just assumes you meant your version. This can lead to confusing errors later.

// This struct shadows std::option::Option.
struct Option;

fn main() {
    // This refers to your struct, not the standard Option enum.
    let x = Option;
    
    // Error: no method named `unwrap` found for struct `Option`.
    // The compiler sees your struct, which has no unwrap method.
    // x.unwrap();
}

If you try to use a method that only exists on the standard library item, you get E0599 (no method named found for type). This is the classic shadowing error. The compiler is doing exactly what you told it to do. You defined Option. It uses your Option. Your Option doesn't have unwrap. The error is correct.

Shadowing prelude names is a fast track to confusing errors. Name your types distinctively. Avoid names like Option, Result, Vec, String, Clone, Copy, Drop, Iterator, Some, None, Ok, Err. If you must use a similar name, add a prefix or suffix. MyOption is better than Option.

There's another pitfall. Don't assume common types are in the prelude. HashMap is not in the prelude. Path is not in the prelude. File is not in the prelude. TcpStream is not in the prelude. If you try to use them without an import, you get E0433 (failed to resolve). The prelude contains the tools you use in almost every function. It does not contain every tool in the box.

fn main() {
    // Error: failed to resolve. Use of undeclared type `HashMap`.
    // HashMap is not in the prelude.
    // let mut map = HashMap::new();
    
    // You must import it explicitly.
    use std::collections::HashMap;
    let mut map = HashMap::new();
    map.insert("key", "value");
}

Trust the prelude for the basics. Import everything else explicitly.

Deciding what to import

Use the root prelude implicitly when you write idiomatic Rust; you never need to import Option, Result, Vec, String, Box, Iterator, Clone, Copy, Drop, PartialEq, PartialOrd, Eq, Ord, AsRef, AsMut, Into, From, Default, Borrow, BorrowMut, ToOwned, ToString, or Sized manually.

Use use std::io::prelude::*; when you're working with I/O operations and want Read, Write, BufRead, and Seek methods available without typing the full path; this is a community convention for I/O-heavy code.

Use explicit use statements for types like HashMap, HashSet, BTreeMap, Path, PathBuf, File, TcpStream, UdpSocket, Thread, Mutex, Arc, and Rc that live outside the prelude; the compiler will force you to import them anyway.

Use fully qualified paths like std::option::Option only when you have a name collision and need to disambiguate; this is rare and usually a sign to rename your local type to avoid shadowing.

Use macro imports explicitly only if you're defining a macro with the same name as a prelude macro; otherwise, rely on the prelude for println!, format!, vec!, assert!, and the other standard macros.

Where to go next