Trait Inheritance and Supertraits
Traits in Rust don't inherit the way classes do in object-oriented languages, there's no base class, no overriding, no super.method() dispatch up a hierarchy. What Rust calls a "supertrait" is something narrower and more precise: a requirement that any type implementing trait B must also implement trait A. This tutorial covers how supertrait bounds work, how default methods build richer APIs on top of a few required ones, and how to compose small traits without recreating the brittleness of a class hierarchy.
Supertraits: Requiring One Trait to Implement Another
A supertrait bound is written like a bound on the trait itself: trait Sub: Super. It declares that you cannot implement Sub for a type unless that type also implements Super. In exchange, Sub's methods, and its callers, can rely on Super's methods being available.
use std::fmt::Display;
trait Describe: Display {
fn describe(&self) -> String {
// because Describe requires Display, we can use {} formatting here
format!("This is: {self}")
}
}
struct Widget {
name: String,
}
impl Display for Widget {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.name)
}
}
impl Describe for Widget {} // requires Display, which Widget has
Describe: Display means the describe default method can freely use {self} formatting, the supertrait guarantees Display is implemented, so the compiler permits calling Display's methods inside Describe. Try to impl Describe for SomeType where SomeType doesn't implement Display, and it's a compile error pointing right at the missing supertrait.
This is a constraint, not inheritance: Describe doesn't get Display's methods as its own, and Widget implements the two traits separately. The supertrait just lets one trait depend on another being present.
Default Methods: Build a Wide API on a Narrow Core
A trait method can have a default body, an implementation provided by the trait itself, that implementors get for free unless they override it. The powerful pattern is defining a small number of required methods (no body, must be implemented) and then layering many provided methods on top, written in terms of the required ones.
trait Shape {
// the one thing every shape must define
fn area(&self) -> f64;
// everything below is derived from area(), for free
fn is_larger_than(&self, other: &dyn Shape) -> bool {
self.area() > other.area()
}
fn summary(&self) -> String {
format!("shape with area {:.2}", self.area())
}
}
struct Circle { radius: f64 }
impl Shape for Circle {
fn area(&self) -> f64 {
std::f64::consts::PI * self.radius * self.radius
}
// is_larger_than and summary come for free
}
An implementor only has to write area; is_larger_than and summary arrive automatically. This is exactly how Iterator works, from Implementing Your Own Iterator: you implement only next, and dozens of provided methods (map, filter, take, ...) are all default methods built on top of it. The required/provided split is the central technique for designing an ergonomic trait, keep the required surface tiny, derive everything else.
An implementor can still override a default when it has a better version, the same way iterators sometimes override size_hint or count for efficiency. The default is a fallback, not a ceiling.
Composing Small Traits Instead of Building Hierarchies
Because supertraits are just requirements, you can compose several small, focused traits rather than building one deep chain. A type that needs multiple capabilities lists them as separate bounds, and a trait that needs several can name them all as supertraits:
trait Serialize { fn serialize(&self) -> Vec<u8>; }
trait Deserialize { fn deserialize(bytes: &[u8]) -> Self; }
// a "persistable" thing needs both capabilities — composed, not inherited
trait Persist: Serialize + Deserialize {
fn save(&self, path: &str) {
let bytes = self.serialize();
// write bytes to path...
}
}
This mirrors how the standard library composes: Copy: Clone (you can't be Copy without Clone), Eq: PartialEq, Ord: Eq + PartialOrd. Each is a small trait with a precise contract, and the "richer" trait names the simpler ones as supertraits. The payoff over a class hierarchy is flexibility, a type opts into exactly the capabilities it has, in any combination, rather than being forced to accept everything an ancestor class carried.
The same caution from Generics and Trait Bounds Deep Dive applies: prefer many narrow traits over one wide one. A trait with a single clear responsibility is easy to implement, easy to bound on, and easy to compose; a sprawling trait that demands a dozen methods forces every implementor to satisfy all of them even when they need only a few.
Supertraits and Trait Objects
When you use a dyn Sub trait object, from Trait Objects vs Generics, the supertrait's methods are callable through it too, because the supertrait is guaranteed present:
fn print_described(item: &dyn Describe) {
println!("{}", item); // Display method — available via the supertrait
println!("{}", item.describe()); // Describe's own method
}
A &dyn Describe can be used wherever Display is needed for method calls, since every Describe implementor is also a Display implementor. Note this works for calling the methods; it does not mean &dyn Describe automatically coerces to &dyn Display as a type. The guarantee is about capability (the methods are there), which is usually exactly what you need.
Key Takeaways
- A supertrait (
trait Sub: Super) is a requirement, not inheritance: you can't implementSubwithout also implementingSuper, and in returnSub's methods may rely onSuper's being present. - Default (provided) methods let you build a wide API on a narrow required core, implement the few required methods and get the rest for free. This is how
Iteratorturns onenextinto dozens of adapters. - Implementors may override a default method when they have a more efficient version; the default is a fallback, not a hard ceiling.
- Compose capabilities from small, focused traits (
Persist: Serialize + Deserialize) rather than deep hierarchies, mirroringCopy: CloneandOrd: Eq + PartialOrd. A type then opts into exactly the traits it needs. - Through a
dyn Subtrait object, the supertrait's methods are callable because the supertrait is guaranteed implemented, capability is shared even though the object's type isn't automatically the supertrait's.