How does trait inheritance work

Rust replaces trait inheritance with trait objects and default method implementations to share behavior across types.

The inheritance trap

You're building a UI library. You have a Button and a Panel. Both need to render themselves on the screen. In Python, you'd create a Widget class, put the rendering logic there, and subclass it. Button inherits from Widget. Panel inherits from Widget. You get the rendering code for free.

In Rust, you reach for a trait. You write trait Renderable, and then you try to make a Drawable trait that "inherits" from Renderable. The compiler yells. Or you try to add fields to a trait to store shared state, and that fails too. Rust doesn't support trait inheritance. It doesn't support class inheritance at all.

This feels like a missing feature. It isn't. Rust replaces inheritance with a combination of default implementations, supertraits, and composition. The result is more flexible, avoids the diamond problem, and keeps your code flat. You just have to stop thinking in hierarchies and start thinking in capabilities.

Traits are contracts, not blueprints

A class in Python or JavaScript is a blueprint. It defines structure and behavior. You can extend the blueprint to create a new blueprint. A trait is a contract. It defines a set of methods a type must provide. It says, "If you implement this trait, you promise to support these operations."

You can't extend a contract. You can only make a new contract that references the old one. This distinction changes how you design code. Instead of asking "What is this a subclass of?", you ask "What can this do?" A Button isn't a subclass of Widget. A Button is a struct that implements Renderable, Focusable, and Serializable. It has multiple capabilities. There's no single parent. There's just a list of promises the type keeps.

Sharing code with default implementations

The closest thing to inheritance in Rust is a default implementation. A trait can contain actual method bodies. Any type that implements the trait gets those methods for free, unless it overrides them. This lets you share logic across unrelated types without repeating yourself.

trait Speak {
    // Default implementation: any type implementing Speak gets this code
    // unless it explicitly overrides it.
    fn say_hello(&self) {
        println!("Hello!");
    }
}

struct Cat;

// Cat implements Speak but provides no methods.
// It automatically gets the default say_hello.
impl Speak for Cat {}

struct Dog;

// Dog implements Speak and overrides say_hello.
// The default code is replaced by this implementation.
impl Speak for Dog {
    fn say_hello(&self) {
        println!("Woof!");
        println!("Woof!");
    }
}

fn main() {
    let cat = Cat;
    let dog = Dog;

    cat.say_hello(); // Prints "Hello!"
    dog.say_hello(); // Prints "Woof!" twice
}

This pattern replaces the "base class with shared methods" pattern. You write the shared logic once in the trait. Types opt in by implementing the trait. They can use the defaults or provide custom behavior.

How the compiler handles defaults

When you call a method on a type, the compiler resolves the call at compile time. It looks at the concrete type, finds the trait implementation, and checks for the method. If the type provides an override, the compiler uses that. If not, it uses the default from the trait.

This is static dispatch. The compiler generates a direct function call. There's no runtime lookup. No vtable. No performance penalty. The default implementation is just code that gets copied into the trait's definition. When the compiler sees cat.say_hello(), it generates code equivalent to calling the default function with Cat as self.

Default methods can call other methods in the trait. This is where the power lies. You can write a complex default method that relies on required methods. Types only need to implement the required parts, and the default method composes them.

trait Renderable {
    // Required method: every implementor must provide this.
    fn draw(&self, canvas: &mut Canvas);

    // Default method: uses the required draw method.
    // Types get this behavior automatically.
    fn render_to_string(&self) -> String {
        let mut canvas = Canvas::new();
        self.draw(&mut canvas);
        canvas.to_string()
    }
}

struct Canvas {
    content: String,
}

impl Canvas {
    fn new() -> Self { Self { content: String::new() } }
    fn to_string(&self) -> String { self.content.clone() }
}

struct TextLabel {
    text: String,
}

impl Renderable for TextLabel {
    fn draw(&self, canvas: &mut Canvas) {
        canvas.content.push_str(&self.text);
    }
}

fn main() {
    let label = TextLabel { text: "Click me".to_string() };
    
    // TextLabel didn't implement render_to_string.
    // It got it from the trait default, which called draw.
    let output = label.render_to_string();
    println!("{}", output);
}

Write the composition logic in the trait. Let the types provide the primitives.

Supertraits: Requiring other traits

Sometimes one capability logically requires another. A Loggable type probably needs to be Printable. A Serializable type might need to be Clone. Rust lets you express this dependency using supertraits. A supertrait is a trait that must be implemented before you can implement the current trait.

trait Printable {
    fn print(&self);
}

// Loggable requires Printable.
// The syntax `trait Loggable: Printable` means Printable is a supertrait.
// Any type implementing Loggable must also implement Printable.
trait Loggable: Printable {
    fn log(&self) {
        // Default method can call supertrait methods.
        self.print();
    }
}

struct ConsoleLogger;

impl Printable for ConsoleLogger {
    fn print(&self) {
        println!("Logging...");
    }
}

// ConsoleLogger implements Loggable.
// It must also implement Printable, which it does above.
impl Loggable for ConsoleLogger {}

fn main() {
    let logger = ConsoleLogger;
    logger.log(); // Calls the default, which calls print.
}

If you try to implement Loggable for a type that doesn't implement Printable, the compiler rejects you with E0277 (trait bound not satisfied). The error tells you exactly which trait is missing. Supertraits enforce dependencies at the type level. You can't accidentally use a Loggable type that can't print.

Blanket implementations: The hidden inheritance

Rust has a feature that feels like automatic inheritance: blanket implementations. You can implement a trait for all types that implement another trait. The standard library uses this heavily.

The ToString trait is the classic example. It has a single method to_string. Instead of making every type implement ToString, the standard library implements ToString for every type that implements Display.

// Simplified view of std::string::ToString
trait Display {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result;
}

trait ToString {
    fn to_string(&self) -> String;
}

// Blanket implementation: any type with Display gets ToString.
// This is how "inheritance" works in the standard library.
impl<T: Display> ToString for T {
    fn to_string(&self) -> String {
        format!("{}", self)
    }
}

When you implement Display for your type, you automatically get to_string. You don't need to write impl ToString for MyType. The compiler finds the blanket impl and uses it. This is explicit, type-safe, and avoids boilerplate. It's the Rust way of saying "If you can display, you can convert to string."

Convention aside: When you see Rc::clone(&data) in Rust code, it looks like a method call. It is. But Rc implements Clone. The blanket impl pattern applies here too. Clone is a trait. Types implement it. The convention is to call Rc::clone explicitly to signal that you're cloning a reference count, not the data. data.clone() works, but Rc::clone is clearer. This is a community norm that pays off in readability.

Multiple behaviors without the mess

Inheritance hierarchies get messy fast. The diamond problem happens when a class inherits from two classes that both inherit from a third class. Which base class method do you use? Rust avoids this entirely.

A struct can implement as many traits as you want. There's no hierarchy. Just a flat set of capabilities.

struct Button;

impl Renderable for Button { /* ... */ }
impl Focusable for Button { /* ... */ }
impl Serializable for Button { /* ... */ }
impl Clone for Button { /* ... */ }
impl Debug for Button { /* ... */ }

Button has five capabilities. None of them are parents. They're just promises Button keeps. You can add new traits later without touching the struct. You can't have conflicts because traits don't share a common ancestor. If two traits define a method with the same name, you call them with the full trait path: Renderable::draw(&button) vs Focusable::draw(&button). The compiler resolves the ambiguity.

Traits have no memory. You can't write trait Counter { count: u32 }. That's a syntax error. Traits define behavior, not state. If you need shared state, use composition. Create a struct that holds the state, and put that struct inside your types.

Pitfalls and compiler errors

Default implementations are powerful, but they have traps.

You can't call a default method on a trait object if the default method uses generics that the trait object can't resolve. Trait objects use dynamic dispatch. They erase the concrete type. If a default method returns Self, you can't call it on &dyn Trait because the compiler doesn't know what Self is at runtime.

trait Cloneable {
    fn clone_me(&self) -> Self;
}

struct S;
impl Cloneable for S {
    fn clone_me(&self) -> Self { S }
}

fn main() {
    let s = S;
    let s2 = s.clone_me(); // Works: compiler knows Self is S.

    let dyn_s: &dyn Cloneable = &s;
    // let dyn_s2 = dyn_s.clone_me(); // Error: E0038
    // The trait Cloneable cannot be made into an object because
    // it requires Self as a return type.
}

The compiler rejects this with E0038 (trait cannot be made into an object). The error explains that Self in the return type prevents object safety. If you need dynamic dispatch, avoid Self in method signatures. Use Box<dyn Trait> or return a concrete type.

Another pitfall: default methods can't access private fields of the implementing type. They only see the trait interface. If a default method needs data from the struct, the struct must expose it through a required method.

trait Greetable {
    fn name(&self) -> &str;

    fn greet(&self) {
        // Default method uses name() to get data.
        // It can't access fields directly.
        println!("Hello, {}!", self.name());
    }
}

Trust the trait boundary. Defaults work with what the trait exposes.

Decision: When to use what

Use default trait implementations when multiple types share the same method logic but differ in required methods. This replaces the base class pattern and keeps code DRY.

Use supertraits when a trait's methods depend on methods from another trait. This enforces that prerequisite behavior exists and lets defaults call supertrait methods.

Use composition with a helper struct when you need to share mutable state or complex initialization logic across types. Traits cannot hold fields. Put the state in a struct and embed it.

Use generics with trait bounds when writing functions that accept any type implementing a trait. This keeps the code flexible and allows static dispatch.

Use blanket implementations when you want to automatically grant a capability to all types that already have a related capability. This reduces boilerplate and mirrors inheritance without the hierarchy.

Where to go next