Default Implementations
How default methods in Rust traits reduce boilerplate by providing shared behavior you can override or accept as-is
A trait method is not always an empty contract. When you define a trait, you can give any method a body right there in the trait definition. That body becomes the default implementation: every type that implements the trait inherits that behavior unless the type explicitly provides its own version.
This sounds like a small syntactic convenience. It is actually one of the most important design tools in Rust, because it lets a trait author supply rich, ready-to-use functionality while demanding only a handful of manually written methods from each implementor. The standard library uses this pattern extensively—Iterator is the most prominent example.
Terminology:
A method with a body in a trait definition is called a default method.
The body itself is the default implementation.
What a Default Implementation Looks Like
Start with a trait that has only a signature. That forces every type to write its own body.
pub trait Summary {
fn summarize(&self) -> String; // semicolon, no body
}
To make summarize default, replace the semicolon with a block.
pub trait Summary {
fn summarize(&self) -> String {
String::from("(Read more...)")
}
}
The trait now offers a built-in answer. Any type that doesn’t care about customizing the summary can simply declare that it implements Summary and move on. Types that do care can still write their own summarize—the syntax for overriding a default is identical to implementing a required method.
The compiler still enforces that the method exists. It just doesn’t force you to write the body yourself if a default is available.
Why Default Implementations Exist
There are two practical pain points that default methods solve.
First, code duplication across types. If ten structs all need the same save_to_disk logic, you could write ten identical method bodies. A default method lets you write it once in the trait and share it automatically, without inheritance or macros.
Second, incremental trait design. A library author can ship a trait with a dozen methods, provide defaults for all but one or two, and let users implement only the essential pieces. Later, new methods can be added to the trait with a default body without breaking existing implementations. This is how Iterator grew its vast API without forcing every implementor to revisit their code.
For a beginner, think of a default as a “pre-filled” answer. The trait asks each type, “Do you want to handle this yourself?” If the type stays silent (no override), the pre-filled answer is used. If the type speaks up (provides its own impl), the custom answer wins.
Using a Default Without Overriding
A type that accepts every default still needs an impl block—an empty one. This tells the compiler “yes, this type implements the trait, and I’m happy with all the defaults.”
pub struct NewsArticle {
pub headline: String,
pub location: String,
pub author: String,
pub content: String,
}
impl Summary for NewsArticle {} // empty block: uses the default
Callers can now invoke summarize on a NewsArticle as long as the trait is in scope.
use aggregator::{NewsArticle, Summary};
fn main() {
let article = NewsArticle {
headline: String::from("Penguins win the Stanley Cup Championship!"),
location: String::from("Pittsburgh, PA, USA"),
author: String::from("Iceburgh"),
content: String::from(
"The Pittsburgh Penguins once again are the best hockey team in the NHL.",
),
};
println!("New article available! {}", article.summarize());
}
The output is New article available! (Read more...)—the default string.
Missing impl block:
If you forget impl Summary for NewsArticle entirely, the compiler will not see NewsArticle as implementing Summary. Calling article.summarize() then produces an error like “no method named summarize found.” Even though a default body exists, Rust requires an explicit declaration that the type opts into the trait.
Why does Rust require the empty block? Because the impl block is the only place where coherence checks happen. It confirms that either the trait or the type is local to your crate (the orphan rule), which prevents conflicting implementations across crates.
Overriding a Default
Overriding looks exactly like providing a body for a required method. You write the method again inside the impl block.
pub struct Tweet {
pub username: String,
pub content: String,
pub reply: bool,
pub retweet: bool,
}
impl Summary for Tweet {
fn summarize(&self) -> String {
format!("{}: {}", self.username, self.content)
}
}
Calling tweet.summarize() now uses the custom body, not the default.
let tweet = Tweet {
username: String::from("horse_ebooks"),
content: String::from("of course, as you probably already know, people"),
reply: false,
retweet: false,
};
println!("1 new tweet: {}", tweet.summarize());
Output: 1 new tweet: horse_ebooks: of course, as you probably already know, people.
The important insight: a type can override some default methods and keep others. Each method decision is independent.
Overriding cannot call the default version:
There is no built-in syntax to invoke the default implementation from within an override, like self.default_summarize(). If you want both the default behavior and extra logic, you must either copy the default body or refactor the shared logic into a private helper function that both the default and the override call.
Required Methods That Power Default Methods
The most useful pattern flips the relationship: you require the implementor to define a small, focused method, and then you build several rich default methods on top of it. The implementor does the minimum work, but all callers get a complete API.
Consider a Summary trait where the only required method is summarize_author. The summarize method is a default that calls it.
pub trait Summary {
fn summarize_author(&self) -> String;
fn summarize(&self) -> String {
format!("(Read more from {}...)", self.summarize_author())
}
}
Now a Tweet only needs to supply summarize_author. It gets a working summarize for free.
impl Summary for Tweet {
fn summarize_author(&self) -> String {
format!("@{}", self.username)
}
}
A caller uses summarize without knowing it was assembled from a default.
let tweet = Tweet {
username: String::from("horse_ebooks"),
content: String::from("of course, as you probably already know, people"),
reply: false,
retweet: false,
};
println!("1 new tweet: {}", tweet.summarize());
Output: 1 new tweet: (Read more from @horse_ebooks...).
This is the “template method” pattern, but without inheritance. The trait owns the orchestration; types provide the building blocks.
No direct field access in defaults:
A default method body only has access to self through trait methods. It cannot reach into struct fields like self.username unless there is a trait method that exposes it. In the example, summarize never touches self.content because Summary says nothing about content. The default is written against the trait's interface, not any particular struct's internals.
Iterator – A Standard Library Example
Iterator is the canonical demonstration of default methods at scale. The trait has exactly one required method: fn next(&mut self) -> Option<Self::Item>. Everything else—map, filter, collect, sum, count, find, and dozens more—is a default implementation that calls next in some loop.
This means that by writing perhaps five lines of next logic for your custom collection, you instantly gain a full-featured iterator with combinators, adapters, and consumers.
struct Countdown {
remaining: u32,
}
impl Iterator for Countdown {
type Item = u32;
fn next(&mut self) -> Option<Self::Item> {
if self.remaining == 0 {
None
} else {
self.remaining -= 1;
Some(self.remaining + 1) // return the value before decrementing
}
}
}
Now you can use any Iterator method on a Countdown:
let countdown = Countdown { remaining: 5 };
let evens: Vec<u32> = countdown
.filter(|n| n % 2 == 0)
.map(|n| n * 10)
.collect();
println!("{:?}", evens); // [40, 20]
filter, map, and collect are all default methods that never had to be written for Countdown. They compose on top of next.
Huge leverage from one required method:
Implementing a single next method gives you roughly 70+ methods from Iterator. This is the payoff of the “required method powers defaults” pattern. When you encounter a new trait that asks for a handful of methods but promises many more, you are seeing this same design.
A beginner can internalize this by asking: “What is the absolute minimum information this trait needs from my type?” Once you identify that, the rest can often be built generically in defaults.
When Defaults Do Not Work
Defaults are resolved at compile time through static dispatch. A call to a default method is monomorphized into the concrete type's implementation—either the default body or the override. This has two consequences that sometimes surprise people.
First, no dynamic dispatch for non‑object‑safe traits. If a trait has methods that return Self or use generic parameters, it cannot be turned into a trait object (dyn Trait). Default methods do not change that. The trait must still be object-safe for dynamic dispatch to work, regardless of defaults.
Second, a default cannot call another default in a way that relies on overriding in a subclass-like chain. Default methods are not virtual in the sense of class inheritance. If method A calls method B, and a type overrides B, the call to A will still use the default’s body unless A itself is overridden. Rust resolves each method body independently at the definition site. This is unlike C++ virtual dispatch where a base-class method calling a virtual method will resolve to the overridden version. If you need that behavior, you must restructure the trait so that the orchestrating method is also overridden, or use a design where the type provides all the pieces.
Method resolution is not like OOP virtual dispatch:
In a trait, a default method summarize that calls self.summarize_author() will always call the type's summarize_author because dispatch goes through the vtable or monomorphized impl. That part works fine. The subtlety is when you have a default that calls another default—overriding only the inner method will work, but the caller still runs the default's body. The confusion arises when you expect the outer default to be “smart” and pick up overrides in a class-like hierarchy. Rust has no class hierarchy; each method is its own unit.
Summary
Default implementations turn a trait from a simple contract into a half-finished toolkit. The trait author writes the orchestration once; each type supplies only what is genuinely unique. This reduces duplication, keeps the public API stable across new method additions, and makes library traits like Iterator possible.
The key insight is the inversion of responsibility: instead of each type carrying the entire burden of behavior, the trait carries the shared parts and types contribute the custom pieces. When you design a trait, ask which methods can be built from others and make those the defaults. When you implement a trait someone else designed, look for the few required methods—the rest is already done for you.