Unlock Your Python Backend Career: Build 30 Projects in 30 Days. Join now for just $54

How to use the Default Trait in Rust

by Ugochukwu Chizaram Omumusinachi

.

Updated Tue May 13 2025

.
How to use the Default Trait in Rust

Introduction

When working with structs or enums in Rust, we often need a way to create a "standard" or "initial" instance without specifying all the fields every time. Manually setting each field can be repetitive, especially for complex types. How can we create a sensible default instance of a type conveniently?

Enter the std::default::Default trait. This trait provides a standardized way for types to offer a default value. If a type implements Default, we can easily get a default instance, simplifying initialization and making our code cleaner, particularly when dealing with configuration structs or optional fields.

The Need for Default Values

Imagine a configuration struct for an application:


<pre><code class="language-rust">
struct AppConfig {
    timeout_ms: u32,
    retries: u8,
    api_endpoint: String,
    enable_logging: bool,
}

fn main() {
    // Manually creating an instance feels verbose
    let config = AppConfig {
        timeout_ms: 5000,
        retries: 3,
        api_endpoint: "https://api.example.com".to_string(),
        enable_logging: false,
    };
    // ... use config ...
}
</code></pre>

If we often need a configuration with standard settings (say, a timeout of 5000ms, 3 retries, a default endpoint, and logging disabled), writing this out repeatedly is tedious. Furthermore, if we add new fields to AppConfig, we have to update every place where we manually create it. 

This is where the Default trait shines.

Introducing std::default::Default

The Default trait is remarkably simple. It's defined in the standard library (std::default::Default) and requires implementing just one method:


<pre><code class="language-rust">
pub trait Default: Sized {
    fn default() -> Self;
}
</code></pre>

The default() associated function takes no arguments and returns an instance of the type (Self) populated with default values. Many primitive types and standard library types already implement Default (e.g., numbers default to 0, bool to false, Option to None, String and Vec to empty ones).

Implementing Default

There are two main ways to make our types implement Default:

  1. Using #[derive(Default)]

If all the fields within our struct also implement Default, we can simply ask the compiler to generate the implementation for us using the derive attribute:


<pre><code class="language-rust">
#[derive(Default, Debug)] // We derive Default here!
struct AppConfig {
    timeout_ms: u32,       // u32 implements Default (defaults to 0)
    retries: u8,           // u8 implements Default (defaults to 0)
    api_endpoint: String,  // String implements Default (defaults to "")
    enable_logging: bool,  // bool implements Default (defaults to false)
}

fn main() {
    let default_config: AppConfig = AppConfig::default();
    println!("Default Config: {:?}", default_config);
    // Output: Default Config: AppConfig { timeout_ms: 0, retries: 0, api_endpoint: "", enable_logging: false }
}
</code></pre>

This is the easiest way, but the derived defaults might not always be what we consider "sensible" (like 0 for timeout_ms).

For enums, we can also derive Default, but we must explicitly mark one of the unit variants (a variant without data) with #[default] to tell the compiler which one is the default:


<pre><code class="language-rust">
#[derive(Default, Debug)]
enum LogLevel {
    Debug,
    Info,
    #[default] // Warning is the default level
    Warning,
    Error,
}

fn main() {
    let level: LogLevel = LogLevel::default();
    println!("Default log level: {:?}", level);
    // Output: Default log level: Warning
}

</code></pre>

2. Manual Implementation

If derive isn't suitable (e.g., some fields don't implement Default, or we need specific default values different from the derived ones), we can implement the trait manually:


<pre><code class="language-rust">
#[derive(Debug)] // Cannot derive Default if we implement manually
struct AppConfig {
    timeout_ms: u32,
    retries: u8,
    api_endpoint: String,
    enable_logging: bool,
}

// Manual implementation
impl Default for AppConfig {
    fn default() -> Self {
        AppConfig {
            timeout_ms: 5000, // Sensible default timeout
            retries: 3,       // Sensible default retries
            api_endpoint: "https://api.example.com".to_string(), // Default endpoint
            enable_logging: false, // Logging off by default
        }
    }
}

fn main() {
    let sensible_defaults: AppConfig = AppConfig::default();
    println!("Sensible Defaults: {:?}", sensible_defaults);
    // Output: Sensible Defaults: AppConfig { timeout_ms: 5000, retries: 3, api_endpoint: "https://api.example.com", enable_logging: false }
}
</code></pre>

Here, we provide the exact default values we want inside the default() function. Remember, we cannot both derive and manually implement Default for the same type.

Using Default

Once a type implements Default, we can get its default value using TypeName::default() or the fully qualified path std::default::Default::default().

A particularly useful pattern is combining Default::default() with struct update syntax. This lets us specify only the fields we want to change, while the rest take their default values:


<pre><code class="language-rust">
#[derive(Default, Debug)]
struct AppConfig {
    timeout_ms: u32,
    retries: u8,
    api_endpoint: String,
    enable_logging: bool,
}

impl AppConfig {
 fn default() -> Self { // Custom sensible defaults
        AppConfig {
            timeout_ms: 5000,
            retries: 3,
            api_endpoint: "https://api.example.com".to_string(),
            enable_logging: false,
        }
    }
}


fn main() {
    // Override only timeout and logging, keep other defaults
    let custom_config = AppConfig {
        timeout_ms: 10000,
        enable_logging: true,
        ..AppConfig::default() // Fill the rest with defaults
    };

    println!("Custom Config: {:?}", custom_config);
    // Output: Custom Config: AppConfig { timeout_ms: 10000, retries: 3, api_endpoint: "https://api.example.com", enable_logging: true }
}
</code></pre>

This makes creating slightly modified instances very easy.

Common Use Cases

  • Configuration Structs: Providing sensible defaults for application settings.

  • Builder Pattern: Often, the Default implementation provides the starting point for a builder.

  • Initializing Collections: Getting an empty Vec, HashMap, or String.

  • Generic Code: In generic functions, T: Default allows creating a default instance of T.

  • Testing: Quickly creating instances with default data for tests.

Considerations

  • Derive Limitations: #[derive(Default)] only works if all fields implement Default. If even one field doesn't, we must implement it manually.

  • Meaningful Defaults: Ensure the default values make sense for the type's purpose. Sometimes the derived defaults (like 0 or empty strings) aren't appropriate.

  • Initialization Cost: While usually cheap, the default() function can perform non-trivial work (like allocating memory for a String or Vec, or even more complex setup). Be aware if performance in initialization is critical.

Summary

The std::default::Default trait provides a clean and standard way to create default instances of Rust types. Whether through the convenient #[derive(Default)] attribute or a manual implementation for more control, it helps reduce boilerplate code and makes initializing structs and enums more ergonomic. 

With Default::default() and struct update syntax, we can create instances with little fuss, focusing only on the values that differ from the standard defaults. If you want to learn more about rust you can always check out our in-depth course on the language

Course image
Become a Rust Backend Engineeer today

All-in-one Rust course for learning backend engineering with Rust. This comprehensive course is designed for Rust developers seeking proficiency in Rust.

Start Learning Now

Whenever you're ready

There are 4 ways we can help you become a great backend engineer:

The MB Platform

Join 1000+ backend engineers learning backend engineering. Build real-world backend projects, learn from expert-vetted courses and roadmaps, track your learnings and set schedules, and solve backend engineering tasks, exercises, and challenges.

The MB Academy

The “MB Academy” is a 6-month intensive Advanced Backend Engineering BootCamp to produce great backend engineers.

Join Backend Weekly

If you like post like this, you will absolutely enjoy our exclusive weekly newsletter, Sharing exclusive backend engineering resources to help you become a great Backend Engineer.

Get Backend Jobs

Find over 2,000+ Tailored International Remote Backend Jobs or Reach 50,000+ backend engineers on the #1 Backend Engineering Job Board

Backend Tips, Every week

Backend Tips, Every week