Updated Aug 5, 2026

12. Traits

📋 Jump to Takeaways

🎁 Two completely unrelated types, a tweet and a news article, and you want to call .summarize() on both. No shared parent class, no inheritance. How?

Traits are Rust's answer to interfaces, and they go further. A trait describes what a type can do, and any type can opt in. That gives you polymorphism without inheritance at zero runtime cost (the compiler generates a separate copy of the function for each concrete type, so there's no indirection at runtime).

Defining a Trait

How do you write code for a type you haven't even seen yet? You describe what it can do, not what it is. That's a trait: a contract. Any type that signs it gains that capability.

trait Summary {
    fn summarize(&self) -> String;
}

Any type that implements Summary now owes you a summarize method returning a String. The trait doesn't know or care what's inside the type. It only cares that the method exists.

Implementing Traits

A trait on its own does nothing. You bring it to life by implementing it for a concrete type with an impl Trait for Type block.

trait Summary {
    fn summarize(&self) -> String;
}

struct Article {
    title: String,
    author: String,
}

impl Summary for Article {
    fn summarize(&self) -> String {
        format!("{} by {}", self.title, self.author)
    }
}

fn main() {
    let post = Article {
        title: String::from("Rust is Fast"),
        author: String::from("Alice"),
    };
    println!("{}", post.summarize()); // Rust is Fast by Alice
}

Each type brings its own logic. Implement Summary for ten different structs and you get ten different summaries, all callable the same way.

Default Methods

What if most types would write the same method the same way? Give the trait a default. Types use it as-is or override it.

trait Summary {
    fn summarize_author(&self) -> String;

    fn summarize(&self) -> String {
        format!("(Read more from {}...)", self.summarize_author())
    }
}

struct Tweet {
    username: String,
    content: String,
}

impl Summary for Tweet {
    fn summarize_author(&self) -> String {
        format!("@{}", self.username)
    }
    // Uses default summarize()
}

fn main() {
    let tweet = Tweet {
        username: String::from("rustlang"),
        content: String::from("Exciting news!"),
    };
    println!("{}", tweet.summarize());
    // (Read more from @rustlang...)
}

Notice the default summarize calls summarize_author, which has no default. So Tweet implements one tiny method and gets the bigger one for free.

Traits as Parameters

Here's the payoff. You can write one function that accepts any type implementing a trait, using &impl Trait.

trait Summary {
    fn summarize(&self) -> String;
}

fn notify(item: &impl Summary) {
    println!("Breaking: {}", item.summarize());
}

Pass it an Article, a Tweet, anything that implements Summary. The compiler generates specialized code for each concrete type you use, so there's zero runtime cost.

Returning impl Trait

It works the other way too. You can return something that implements a trait without naming the concrete type.

trait Summary {
    fn summarize(&self) -> String;
}

struct Article { title: String }

impl Summary for Article {
    fn summarize(&self) -> String {
        self.title.clone()
    }
}

fn create_summary() -> impl Summary {
    Article { title: String::from("New Discovery") }
}

fn main() {
    let item = create_summary();
    println!("{}", item.summarize()); // New Discovery
}

You can't move a field out of a borrowed &self, so .clone() creates a new owned copy to return.

Handy when the real return type is long and ugly, or when you want to hide it. The caller only knows it gets "something that implements Summary."

The Derive Macro

Implementing Debug or Clone by hand every time would be miserable. So Rust writes them for you with #[derive], generating the implementations at compile time.

#[derive(Debug, Clone, PartialEq)]
struct Point {
    x: f64,
    y: f64,
}

fn main() {
    let p1 = Point { x: 1.0, y: 2.0 };
    let p2 = p1.clone();

    println!("{:?}", p1);       // Point { x: 1.0, y: 2.0 }
    println!("{}", p1 == p2);   // true
}

#[derive] works for traits with obvious implementations. You can derive Debug, Clone, Copy, PartialEq, Eq, Hash, Default, and more.

Common Standard Library Traits

You'll bump into these traits in almost every Rust program. Learn them once and the standard library stops feeling mysterious.

use std::fmt;

#[derive(Debug, Clone, Copy, PartialEq, Default)]
struct Color {
    r: u8,
    g: u8,
    b: u8,
}

// Display must be implemented manually
impl fmt::Display for Color {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "#{:02x}{:02x}{:02x}", self.r, self.g, self.b) // :02x = 2-digit lowercase hex, zero-padded
    }
}

fn main() {
    let red = Color { r: 255, g: 0, b: 0 };
    let default = Color::default();

    println!("{}", red);        // #ff0000 (Display)
    println!("{:?}", red);      // Color { r: 255, g: 0, b: 0 } (Debug)
    println!("{:?}", default);  // Color { r: 0, g: 0, b: 0 } (Default)

    let copy = red; // Copy — no move!
    println!("{} == {}: {}", red, copy, red == copy); // #ff0000 == #ff0000: true
}

The write! macro writes formatted text into the buffer f that Rust provides, similar to how Go's fmt.Fprintf writes to a writer instead of returning a string.

  • Display, user-facing formatting ({})
  • Debug, developer-facing formatting ({:?})
  • Clone, explicit deep copy (.clone())
  • Copy, implicit bitwise copy (small, stack-only types). A type can only be Copy if all its fields are Copy. Anything containing a String, Vec, or heap data can't be.
  • PartialEq, equality comparison (==, !=)
  • Default, provides a zero/empty value (like Go's automatic zero values, but opt-in)

Default in Practice

In Go, Config{} gives you zero values automatically. In Rust, you derive Default and call .default():

#[derive(Debug, Default)]
struct Config {
    port: u16,       // default: 0
    debug: bool,     // default: false
    name: String,    // default: ""
}

fn main() {
    let c = Config::default();
    println!("{:?}", c); // Config { port: 0, debug: false, name: "" }

    // Override specific fields, default the rest
    let c2 = Config {
        port: 8080,
        ..Default::default()
    };
    println!("{:?}", c2); // Config { port: 8080, debug: false, name: "" }
}

..Default::default() fills remaining fields with their defaults. It's Rust's equivalent of Go's partial struct literal (Config{Port: 8080} where unset fields are zero).

PartialEq vs Eq

PartialEq and Eq both let you use ==, but they promise different things.

  • PartialEq — gives you == and !=. It only requires that you implement an eq() method. It does NOT promise that a == a is true for every value.
  • Eq — a marker trait (no extra methods). It extends PartialEq and adds one guarantee: reflexivity, meaning a == a is always true, no exceptions.

Why would a == a ever be false? Floats.

#[derive(Debug, PartialEq)]
struct Temperature {
    celsius: f64,
}

fn main() {
    let freezing = Temperature { celsius: 0.0 };
    let boiling = Temperature { celsius: 100.0 };
    println!("{}", freezing == boiling); // false

    let nan = f64::NAN;
    println!("{}", nan == nan); // false — breaks reflexivity, so f64 can't derive Eq
}

f64 implements PartialEq but not Eq, because NaN != NaN. That's why Temperature above can only derive PartialEq, adding Eq to the derive list would fail to compile since one of its fields (f64) doesn't implement Eq.

Types built from comparable fields (integers, strings, bools) can derive both:

#[derive(Debug, PartialEq, Eq)]
struct Point {
    x: i32,
    y: i32,
}

This matters in practice because HashMap and HashSet require their key type to implement Eq (plus Hash), since their correctness depends on that reflexivity guarantee. That's part of why you can't use f64 directly as a HashMap key.

Key Takeaways

  • Traits define shared behavior, they're Rust's answer to interfaces
  • impl Trait for Type provides the concrete implementation
  • Default methods let a trait supply behavior with minimal required implementation
  • &impl Trait as a parameter accepts any type that implements the trait
  • -> impl Trait hides the concrete return type behind a trait contract
  • #[derive] auto-generates common trait implementations at compile time
  • Display, Debug, Clone, Copy, PartialEq, Default are std traits you'll reach for constantly
  • PartialEq vs Eq, Eq promises a == a is always true (reflexivity); types with f64 fields can't derive it

🎁 Traits say what a type can do. But what if you want your own Pair struct to hold two ints, or two strings, from a single definition, and pay nothing at runtime for that flexibility? Next up: generics, and the trait bounds that keep them safe.

📝 Ready to test your knowledge?

Answer the quiz below to mark this lesson complete.

Spot something off? Report an issue
© 2026 ByteLearn.dev. Free courses for developers. · Privacy