05open 25 min

Traits and generics: abstraction without Python-style inheritance

Learn how Rust models shared behavior and reusable logic through traits and generics instead of class inheritance or pure duck typing.

by the end of this lesson you can

  • Defines the behavior contract explicitly as a trait
  • Uses a generic function or trait object intentionally
  • Avoids mapping Python inheritance ideas directly onto Rust

Overview

Python developers are used to duck typing and class-based abstraction. Rust uses traits and generics to express shared behavior explicitly, with the compiler enforcing the contract.

In Python, you often

trust any object with the right methods or protocols to participate in an API.

In Rust, the common pattern is

to define behavior through traits and then use generics or trait objects when a function needs that capability.

why this difference matters

This keeps abstraction explicit and machine-checkable while still allowing reusable, expressive code.

Python

def render(item):
    return item.render()

Rust

fn render<T: Renderable>(item: &T) -> String {
    item.render()
}

Deeper comparison

Python version

class JsonWriter:
    def write(self, value):
        ...

def save(writer, value):
    writer.write(value)

Rust version

trait Writer {
    fn write(&self, value: &str);
}

fn save<W: Writer>(writer: &W, value: &str) {
    writer.write(value);
}

Reflect

What do you gain when shared behavior is declared as a trait instead of assumed implicitly from an object's shape?

what a strong answer notices

A strong answer mentions explicit contracts, compiler-checked usage, and clearer API expectations at the call site.

Rewrite

Rewrite this duck-typed Python helper into a Rust function with an explicit trait-bound contract.

Rewrite this Python

def export(item):
    return item.to_bytes()

what good looks like

  • Defines the behavior contract explicitly as a trait
  • Uses a generic function or trait object intentionally
  • Avoids mapping Python inheritance ideas directly onto Rust

Practice

Design a Rust API that can send notifications through different backends without building an inheritance tree.

success criteria

  • Uses a trait to define required behavior
  • Explains where a generic type parameter or trait object fits
  • Keeps the abstraction narrow and focused on the use case

Common mistakes

  • Assuming traits are just classes renamed.
  • Creating overly broad traits too early.
  • Using generics without first deciding what behavior the code actually needs.

takeaways

  • This keeps abstraction explicit and machine-checkable while still allowing reusable, expressive code.
  • A strong answer mentions explicit contracts, compiler-checked usage, and clearer API expectations at the call site.
  • Uses a trait to define required behavior