In the previous tutorial, we learned error handling with Result and Option. Now we learn traits — Rust’s way to define shared behavior between types.
If you know interfaces in Java, Kotlin, or TypeScript, traits are similar. A trait says “any type that implements me must have these methods.” But Rust traits go further — they support default methods, operator overloading, and dynamic dispatch.
What Is a Trait?
A trait defines a set of methods that a type must implement. Think of it as a contract. Any type that signs the contract must provide the required methods.
trait Describable {
fn describe(&self) -> String;
}
This trait says: “Any type that is Describable must have a describe method that returns a String.”
Implementing a Trait
You implement a trait with the impl TraitName for TypeName syntax:
struct User {
name: String,
age: u32,
}
impl Describable for User {
fn describe(&self) -> String {
format!("{} (age {})", self.name, self.age)
}
}
struct Product {
name: String,
price: f64,
}
impl Describable for Product {
fn describe(&self) -> String {
format!("{} — ${:.2}", self.name, self.price)
}
}
Both User and Product implement Describable. Each provides its own version of describe. Now you can call .describe() on both:
fn main() {
let user = User { name: String::from("Alex"), age: 30 };
let product = Product { name: String::from("Laptop"), price: 999.99 };
println!("{}", user.describe()); // Alex (age 30)
println!("{}", product.describe()); // Laptop — $999.99
}
Default Methods
A trait can provide a default implementation. Types can use the default or override it:
trait Describable {
fn describe(&self) -> String;
// Default method — uses describe() internally
fn summary(&self) -> String {
format!("Summary: {}", self.describe())
}
}
User uses the default:
impl Describable for User {
fn describe(&self) -> String {
format!("{} (age {})", self.name, self.age)
}
// summary() uses the default: "Summary: Alex (age 30)"
}
Product overrides it:
impl Describable for Product {
fn describe(&self) -> String {
format!("{} — ${:.2}", self.name, self.price)
}
fn summary(&self) -> String {
format!("Product: {} costs ${:.2}", self.name, self.price)
}
}
let user = User { name: String::from("Alex"), age: 30 };
println!("{}", user.summary()); // Summary: Alex (age 30)
let product = Product { name: String::from("Laptop"), price: 999.99 };
println!("{}", product.summary()); // Product: Laptop costs $999.99
Default methods are useful when most types want the same behavior but some need something different.
Derive Macros
Rust has built-in traits you can implement automatically with #[derive]:
#[derive(Debug, Clone, PartialEq)]
struct Task {
title: String,
done: bool,
}
fn main() {
let task = Task { title: String::from("Write code"), done: false };
// Debug: format with {:?}
println!("{:?}", task); // Task { title: "Write code", done: false }
// Clone: create a copy
let task2 = task.clone();
// PartialEq: compare with ==
println!("{}", task == task2); // true
}
Common derive macros:
| Macro | What It Does |
|---|---|
Debug | Enables {:?} formatting |
Clone | Enables .clone() method |
Copy | Enables implicit copying (for small types) |
PartialEq | Enables == and != comparison |
Eq | Marks full equality (requires PartialEq) |
Hash | Enables use as HashMap key |
Default | Enables Type::default() |
You use derive for common traits. For custom behavior, you implement the trait manually.
The Display Trait
Debug gives you programmer-friendly output with {:?}. The Display trait gives you user-friendly output with {}:
use std::fmt;
impl fmt::Display for User {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{} (age {})", self.name, self.age)
}
}
fn main() {
let user = User { name: String::from("Alex"), age: 30 };
println!("{}", user); // Alex (age 30) — Display
println!("{:?}", user); // User { name: "Alex", age: 30 } — Debug
}
You cannot derive Display. You must always implement it manually. This is because Rust does not know how you want your type to look to users.
Operator Overloading
Traits let you define what operators like +, -, * do for your types. These operators are defined in the std::ops module:
use std::ops::Add;
#[derive(Debug, Clone, Copy, PartialEq)]
struct Point {
x: f64,
y: f64,
}
impl Add for Point {
type Output = Point;
fn add(self, other: Point) -> Point {
Point {
x: self.x + other.x,
y: self.y + other.y,
}
}
}
fn main() {
let p1 = Point { x: 1.0, y: 2.0 };
let p2 = Point { x: 3.0, y: 4.0 };
let p3 = p1 + p2;
println!("{:?}", p3); // Point { x: 4.0, y: 6.0 }
}
The type Output = Point; line is called an associated type. It tells Rust what type the + operator returns. You can also implement Display for nicer output:
impl fmt::Display for Point {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "({}, {})", self.x, self.y)
}
}
fn main() {
let p1 = Point { x: 1.0, y: 2.0 };
let p2 = Point { x: 3.0, y: 4.0 };
println!("{} + {} = {}", p1, p2, p1 + p2);
// (1, 2) + (3, 4) = (4, 6)
}
Common operator traits:
| Trait | Operator |
|---|---|
Add | + |
Sub | - |
Mul | * |
Div | / |
Neg | - (unary) |
Index | [] |
Trait Bounds
Trait bounds let you write generic functions that only accept types with certain traits:
fn print_description<T: Describable>(item: &T) {
println!("{}", item.describe());
}
This function accepts any type T that implements Describable. You can call it with User, Product, or any future type that implements the trait.
print_description(&user); // Works — User implements Describable
print_description(&product); // Works — Product implements Describable
// print_description(&42); // ERROR — i32 does not implement Describable
Multiple Bounds
You can require multiple traits:
fn print_details<T: Describable + fmt::Display>(item: &T) {
println!("Description: {}", item.describe());
println!("Display: {}", item);
}
Where Clauses
When trait bounds get long, use a where clause for readability:
fn longest_description<T>(a: &T, b: &T) -> String
where
T: Describable,
{
let desc_a = a.describe();
let desc_b = b.describe();
if desc_a.len() >= desc_b.len() {
desc_a
} else {
desc_b
}
}
The where clause does the same thing as <T: Describable> but is easier to read when you have many bounds or multiple generic parameters.
Trait Objects (dyn)
Sometimes you need a collection of different types that all implement the same trait. You cannot use generics for this because Vec<T> means all elements are the same type. Instead, use trait objects with dyn:
fn describe_all(items: &[&dyn Describable]) -> Vec<String> {
items.iter().map(|item| item.describe()).collect()
}
fn main() {
let user = User { name: String::from("Alex"), age: 30 };
let product = Product { name: String::from("Laptop"), price: 999.99 };
let items: Vec<&dyn Describable> = vec![&user, &product];
let descriptions = describe_all(&items);
for desc in &descriptions {
println!("{}", desc);
}
}
&dyn Describable means “a reference to any type that implements Describable.” The dyn keyword stands for dynamic dispatch — Rust decides which method to call at runtime, not at compile time.
Returning Trait Objects
You can also return trait objects using Box<dyn Trait>:
fn create_describable(use_user: bool) -> Box<dyn Describable> {
if use_user {
Box::new(User { name: String::from("Alex"), age: 30 })
} else {
Box::new(Product { name: String::from("Laptop"), price: 999.99 })
}
}
Box<dyn Describable> is a heap-allocated trait object. You need Box because the compiler does not know the size of the concrete type at compile time.
Static Dispatch vs Dynamic Dispatch
There are two ways to use traits:
Static dispatch (generics) — the compiler creates a separate version of the function for each type. Fast, but creates more code:
fn print_desc<T: Describable>(item: &T) {
println!("{}", item.describe());
}
Dynamic dispatch (trait objects) — the compiler uses a pointer to look up the method at runtime. Slightly slower, but more flexible:
fn print_desc(item: &dyn Describable) {
println!("{}", item.describe());
}
Use generics (static dispatch) by default. Use trait objects when you need a collection of different types or when you need to return different types from a function.
Multiple Traits on One Type
A type can implement as many traits as you want:
trait Printable {
fn print_info(&self) -> String;
}
impl Printable for User {
fn print_info(&self) -> String {
format!("User: {}", self.name)
}
}
// Now User implements both Describable and Printable
let user = User { name: String::from("Alex"), age: 30 };
println!("{}", user.describe()); // From Describable
println!("{}", user.print_info()); // From Printable
This is how Rust achieves polymorphism without inheritance. Instead of one parent class, a type implements many traits. This is called composition over inheritance.
The From and Into Traits
From and Into are the standard way to convert between types in Rust. They are used everywhere in the standard library and in most Rust crates.
From — Convert from Another Type
struct Celsius(f64);
struct Fahrenheit(f64);
impl From<Celsius> for Fahrenheit {
fn from(c: Celsius) -> Self {
Fahrenheit(c.0 * 9.0 / 5.0 + 32.0)
}
}
fn main() {
let boiling = Celsius(100.0);
let f = Fahrenheit::from(boiling);
println!("{}°F", f.0); // 212°F
}
When you implement From<A> for B, you get Into<B> for A for free. So you can also write:
let boiling = Celsius(100.0);
let f: Fahrenheit = boiling.into();
Why From/Into Matter
You see From and Into everywhere in Rust APIs. For example, any function that accepts impl Into<String> can take both &str and String:
fn greet(name: impl Into<String>) {
let name = name.into();
println!("Hello, {}!", name);
}
fn main() {
greet("Alex"); // &str works
greet(String::from("Sam")); // String works too
}
This pattern makes APIs flexible without requiring the caller to convert types manually.
The Newtype Pattern
Sometimes you want to implement a trait from the standard library on a type you did not define. The orphan rule prevents this. The fix is the newtype pattern — wrap the type in a single-field struct:
struct Email(String);
impl From<&str> for Email {
fn from(s: &str) -> Self {
Email(s.to_string())
}
}
fn send_email(to: Email) {
println!("Sending to: {}", to.0);
}
fn main() {
let email = Email::from("alex@example.com");
send_email(email);
}
The newtype pattern gives you type safety (you cannot accidentally pass a username where an email is expected) and lets you implement any trait on your wrapper type.
Supertraits
A trait can require another trait. This is called a supertrait:
trait Named {
fn name(&self) -> &str;
}
trait Greetable: Named {
fn greet(&self) -> String {
format!("Hello, {}!", self.name())
}
}
Any type that implements Greetable must also implement Named. The Greetable trait can use methods from Named in its default implementations.
struct Person {
name: String,
}
impl Named for Person {
fn name(&self) -> &str {
&self.name
}
}
impl Greetable for Person {} // Uses default greet()
fn main() {
let person = Person { name: String::from("Alex") };
println!("{}", person.greet()); // Hello, Alex!
}
Common Mistakes
Mistake: Orphan Rule
You cannot implement a foreign trait on a foreign type. Both the trait or the type must be defined in your crate:
// ERROR: Display and Vec are both from the standard library
impl fmt::Display for Vec<i32> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "...")
}
}
Fix: Create a wrapper type:
struct NumberList(Vec<i32>);
impl fmt::Display for NumberList {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let items: Vec<String> = self.0.iter().map(|n| n.to_string()).collect();
write!(f, "[{}]", items.join(", "))
}
}
This rule prevents two crates from conflicting by implementing the same trait for the same type.
Mistake: Forgetting to Import Traits
let v = vec![3, 1, 2];
// v.sort(); // Works — sort() is on Vec directly
use std::io::Write;
// Without this import, you cannot call .write() on file handles
If a method is defined in a trait, you must import the trait to use it.
Common Standard Library Traits
Here are the traits you will use most often:
| Trait | Purpose | Example |
|---|---|---|
Display | User-friendly text with {} | println!("{}", value) |
Debug | Developer text with {:?} | println!("{:?}", value) |
Clone | Explicit copy | let copy = value.clone() |
Copy | Implicit copy for small types | let x = y; (y still valid) |
PartialEq | Equality comparison | a == b |
PartialOrd | Ordering comparison | a < b |
Iterator | For loops and iterator methods | for item in collection |
From/Into | Type conversion | String::from("hello") |
Default | Default values | Vec::default() returns [] |
Drop | Custom cleanup | Called when value goes out of scope |
Summary
| Concept | Syntax | Purpose |
|---|---|---|
| Define trait | trait Name { fn method(); } | Declare shared behavior |
| Implement trait | impl Trait for Type { } | Provide behavior for a type |
| Default method | Body in trait definition | Shared default behavior |
| Derive | #[derive(Debug, Clone)] | Auto-implement common traits |
| Trait bounds | <T: Trait> | Restrict generics to types with trait |
| Where clause | where T: Trait | Cleaner trait bounds |
| Trait object | &dyn Trait, Box<dyn Trait> | Dynamic dispatch |
| Operator overload | impl Add for Type | Custom operator behavior |
Source Code
Related Tutorials
- Rust Tutorial #8: Error Handling — previous tutorial
- Rust Tutorial #10: Generics — next tutorial
What’s Next?
We now know how to define shared behavior with traits. Next, we learn generics — how to write functions and types that work with any type. Generics and traits work together: trait bounds tell the compiler which types are allowed. This is where Rust’s type system really shines.