Subtraits
Understanding subtraits in Rust and how they enforce prerequisite trait implementations
A subtrait is a trait that declares another trait as a requirement. Any type that implements the subtrait must also implement the supertrait. The syntax trait Sub: Super tells the compiler: "you cannot implement Sub for a type unless Super is already implemented." This is how Rust builds trait hierarchies without the inheritance of data or method overriding found in object-oriented languages.
What the Syntax Does
The colon after the trait name introduces one or more supertraits:
trait Shape {
fn area(&self) -> f64;
}
trait Drawable: Shape {
fn draw(&self);
}
Drawable is a subtrait of Shape. Writing trait Drawable: Shape is syntactic sugar for a bound on the Self type:
trait Drawable where Self: Shape {
fn draw(&self);
}
Both forms mean exactly the same thing: if you implement Drawable for a type, that type must already have an implementation of Shape.
Compiler Guarantee:
Rust enforces this at compile time. If you write impl Drawable for Circle but haven't written impl Shape for Circle, the compiler refuses with an error telling you that Shape is not satisfied. This prevents you from accidentally using a type in a context where required behavior is missing.
Mental Model: A Prerequisite, Not an Extension
If you come from languages with class inheritance, the word "inheritance" suggests that the subtrait somehow absorbs the supertrait's methods. That is not what happens. Each trait is a completely independent namespace. Drawable does not "inherit" the area method; it simply cannot exist without Shape being implemented separately.
Think of it like a university course with a prerequisite. You cannot enroll in Advanced Physics without passing Introductory Physics first. But when you sit in the Advanced class, you don't automatically know all the material from the prerequisite — you are just guaranteed to have taken it. Similarly, a trait bound T: Drawable guarantees that T also implements Shape, and you can call shape_area methods on T without adding a separate Shape bound.
This separation avoids the fragile base class problem. A method from the supertrait and a method from the subtrait live in distinct namespaces. They can even share the same name without automatically overriding each other — you use fully qualified syntax to disambiguate.
Implementing a Subtrait on a Concrete Type
A type that fulfills both traits needs two separate impl blocks: one for the supertrait, one for the subtrait.
struct Circle {
radius: f64,
}
impl Shape for Circle {
fn area(&self) -> f64 {
std::f64::consts::PI * self.radius * self.radius
}
}
impl Drawable for Circle {
fn draw(&self) {
println!("Drawing a circle with radius {}", self.radius);
}
}
The order of the impl blocks does not matter, as long as both exist before the compiler sees code that relies on the Drawable bound. The key point is that impl Drawable for Circle does not include any area definition; that must stay in the Shape block.
Compiler Error: Method Not a Member:
A common mistake is trying to write the supertrait's method inside the subtrait's impl block:
impl Drawable for Circle {
fn draw(&self) { ... }
fn area(&self) -> f64 { ... } // Compiler error: method `area` is not a member of trait `Drawable`
}
This fails because area belongs to Shape, not to Drawable. Even though Drawable requires Shape, each trait defines its own set of methods. Rust does not merge them into a single block.
Using Supertrait Methods in Subtrait Default Implementations
A subtrait's default methods can rely on methods from the supertrait. Because the trait bound Self: Shape is in scope, self is guaranteed to respond to shape_area.
trait Drawable: Shape {
fn draw(&self) {
println!("Drawing shape with area {}", self.area());
}
}
Now any type that implements Drawable gets this default draw method for free, as long as it has provided an area method through its Shape implementation. If a type overrides draw, the default is simply ignored. There is no "super" call; if you want the default behavior inside your custom draw, you would need to call the trait's method explicitly via <Self as Drawable>::draw(self).
Supertrait Bounds on Generic Functions
When a function requires a subtrait, the supertrait bound is automatically satisfied. You do not need to repeat it.
fn render<T: Drawable>(item: &T) {
// `area()` is available because `Drawable: Shape` guarantees `T: Shape`.
println!("Rendering area: {}", item.area());
item.draw();
}
Without subtraits, you would have to write T: Shape + Drawable everywhere. The subtrait encodes the relationship once and then carries the supertrait bound wherever it goes.
Multiple Supertraits:
A subtrait can require more than one supertrait:
trait Number: Add<Output=Self> + PartialOrd + Display {
fn is_positive(&self) -> bool;
}
The same rule applies: an implementation of Number requires separate impl blocks for Add, PartialOrd, and Display. The compiler checks each one independently.
Method Name Conflicts Between Supertraits
It is legal for a supertrait and a subtrait to define methods with the same name. Because the traits are separate namespaces, there is no automatic override. You call the method you want using fully qualified syntax.
trait Greet {
fn say(&self) {
println!("Hello!");
}
}
trait FormalGreet: Greet {
fn say(&self) {
println!("Good day.");
}
}
struct Person;
impl Greet for Person {}
impl FormalGreet for Person {}
Now person.say() is ambiguous. Rust will not compile a bare method call if both traits are in scope. You must write:
<Person as Greet>::say(&person); // prints "Hello!"
<Person as FormalGreet>::say(&person); // prints "Good day."
If you only bring one trait into scope with use, the bare call works because there is no ambiguity. This design keeps trait hierarchies clean without hidden method overriding that can silently change behavior.
When Subtraits Are Useful
Use a subtrait when you have a set of behaviors that always go together. The standard library's std::error::Error trait is a subtrait of both Debug and Display:
pub trait Error: Debug + Display {
fn source(&self) -> Option<&(dyn Error + 'static)> { ... }
// other provided methods
}
This means any type that claims to be an error must be both printable and debuggable. You can write error-reporting code that formats an error without needing to add separate bounds — the Error bound already guarantees Display and Debug.
Subtrait Does Not Auto-Implement:
A common misunderstanding is that trait Foo: Bar automatically provides a blanket implementation of Bar for types that implement Foo. It does not. You must still implement both traits explicitly unless you write a separate blanket impl impl<T: Foo> Bar for T {}, which can cause coherence issues (the "middleware" problem described in RFC 3437). Subtrait syntax only adds a bound, never an implementation.
Summary
Subtraits let you express that one trait depends on another. They make APIs self-documenting by moving prerequisite bounds into the trait definition, so generic code that names the subtrait automatically picks up all the required capabilities. The key to using them correctly is remembering that each trait remains its own namespace: you must implement supertraits in separate blocks, and method resolution can require fully qualified syntax when names collide. When you see a trait with a colon followed by another trait name, read it as "this trait requires that trait" — not as inheritance, but as a checked prerequisite that keeps your abstractions coherent.