How to Use Nested Paths (use std

:{io, fs}) in Rust

Rust's nested-path use syntax compresses repetitive imports. Learn the brace-and-self mechanics, when nesting helps readability, and how it interacts with rustfmt.

A wall of use statements

Open a real Rust file and the top often looks like this:

use std::io;
use std::io::Read;
use std::io::Write;
use std::io::BufReader;
use std::io::BufWriter;
use std::fs;
use std::fs::File;
use std::path::Path;
use std::path::PathBuf;

Nine lines, all starting with std::. It works, but it's noisy. The compiler doesn't care about repetition; humans do. Every time you scroll past this block, your eyes have to filter out the redundant prefixes to find the actual imported names.

Rust's nested-path syntax exists to compress that block. The same imports written compactly:

use std::{
    io::{self, Read, Write, BufReader, BufWriter},
    fs::{self, File},
    path::{Path, PathBuf},
};

Same imports, far less visual noise. Once you've seen this style a few times, the unfolded version starts looking like an unsorted closet.

How the syntax actually works

The rule is straightforward: anywhere you'd write a path in use, you can replace a single name with {...} containing several names, and each one continues the path from that point.

use std::{io, fs} desugars to:

  • use std::io;
  • use std::fs;

use std::io::{Read, Write} desugars to:

  • use std::io::Read;
  • use std::io::Write;

You can nest more than one level:

use std::collections::{HashMap, HashSet, BTreeMap};

That's std::collections::HashMap, std::collections::HashSet, and std::collections::BTreeMap brought into scope.

You can mix levels too:

use std::{io, collections::HashMap};

That's std::io and std::collections::HashMap. The first item under the brace is one level deep; the second is two levels deep. Each item continues the path from where the brace opened.

The self keyword inside braces

The trickiest piece is self inside a nested path. It means "this path itself," letting you import a module and items inside it in the same use.

// Brings io (the module) and Read, Write (items inside io) into scope.
use std::io::{self, Read, Write};

fn main() -> io::Result<()> {
    // We can use `io::Result` here because the `self` brought `io` itself.
    let mut input = String::new();
    io::stdin().read_to_string(&mut input)?;
    print!("got: {}", input);
    Ok(())
}

Without the self, you could call read_to_string (because Read is in scope as a trait), but you couldn't write io::Result or io::stdin(), because the io module name itself wouldn't be in scope. The self adds it.

The desugared equivalent is:

use std::io;
use std::io::Read;
use std::io::Write;

Same effect. The nested form is shorter once you have more than two items.

A realistic example

Here's a chunk of imports from a small CLI that reads files and writes JSON:

use std::{
    // self brings the io module in; the rest are traits and types.
    io::{self, BufRead, BufReader, Write},

    // self for fs; File for opening; ErrorKind to distinguish "not found".
    fs::{self, File},

    // Path is a borrowed reference; PathBuf is the owned version.
    path::{Path, PathBuf},

    // Single deeply-nested item: the duration type used for timeouts.
    time::Duration,
};

// External crates can also use nested paths.
use serde::{Serialize, Deserialize};
use serde_json::{json, Value};

#[derive(Serialize, Deserialize)]
struct Config {
    timeout: Duration,
    output: PathBuf,
}

// A small function that uses several of the imported names.
fn load_config(path: &Path) -> io::Result<Config> {
    let file = File::open(path)?;
    let reader = BufReader::new(file);
    let cfg: Config = serde_json::from_reader(reader)
        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
    Ok(cfg)
}

Each name in the use block is doing a job. Reading from top to bottom, you get a quick picture of what this module talks to: stdin/stdout (io), the filesystem (fs, Path, PathBuf), time (Duration), and JSON via Serde. That kind of at-a-glance summary is the real reason to organise imports this way.

Renaming with as

A useful sibling feature: as lets you rename anything you import, and it works inside braces too.

// Bring in two iterators with the same name from different crates,
// renaming one to disambiguate.
use std::collections::{HashMap as StdMap};
use ahash::{HashMap as FastMap};

fn main() {
    let a: StdMap<&str, u32> = StdMap::new();
    let b: FastMap<&str, u32> = FastMap::default();
    println!("{} {}", a.len(), b.len());
}

Without as, the second use would clash with the first and you'd get an error like "the name HashMap is defined multiple times." Renaming sidesteps the clash without writing the full path everywhere.

The leading crate::, super::, and self::

Inside your own crate, you can root nested paths anywhere, not just at std:

// From inside src/main.rs, with modules `parser` and `output` in your crate.
use crate::{
    parser::{tokenize, Token},
    output::{write_json, write_yaml},
};

crate:: always refers to the root of the current crate. super:: refers to the parent module. self:: refers to the current module. They follow the same nesting rules as std:: does.

What the compiler does

use statements are pure scoping; they don't generate code. The compiler reads them, builds a name table for the current module, and forgets the imports as soon as name resolution is done. Nested paths are syntactic sugar; they desugar to flat use statements before anything else happens. That means there's zero runtime cost, zero binary-size cost, and no semantic difference from writing each use separately. Choose whichever style reads better.

Common pitfalls

You wrote use std::{io::Read, io::Write} instead of use std::io::{Read, Write}. The first compiles, but the duplicated io defeats the purpose; rustfmt will rewrite it for you.

You wrote use std::io::{Read, Write, self::Result} thinking self:: worked anywhere. It doesn't; self inside a nested path is the path-itself, written as bare self, not self::Something. Use use std::io::{self, Read, Write, Result} and then write io::Result or just Result.

You imported self accidentally and got a confusing error about ambiguous names:

error[E0252]: the name `Result` is defined multiple times

That happens when both std::io::Result and the prelude's Result collide. Either rename with as, or just write io::Result at the use site.

You used a glob (use std::io::*;) and now you have no idea which names are coming from where. Globs work but they make code harder to read and audit. Prefer explicit lists. See How to Use Glob Imports (use module::*) in Rust (and Why to Avoid Them).

When to keep imports flat anyway

Nested paths win when you have three or more items from the same parent. For one or two, flat is often clearer:

// Two flat lines: easy to read.
use std::collections::HashMap;
use std::time::Duration;

vs

// Same imports, nested. Slightly more eye-work for two items.
use std::{collections::HashMap, time::Duration};

Most projects use rustfmt with the imports_granularity = "Crate" or "Module" setting to make this consistent across a codebase. Pick one style, configure rustfmt, move on.

Where to go next

How to Use the use Statement to Import Items in Rust

What is the difference between mod and use

How to Use Glob Imports (use module::*) in Rust (and Why to Avoid Them)