Traits in Rust Explained as Shared Behavior With Examples
A trait is a named set of methods a type promises to provide. See how to define one, implement it, give it default methods and use it as a bound.

A trait is a named set of methods that a type can promise to provide. If you have used interfaces in Java, protocols in Swift or type classes in Haskell, the idea is familiar, and Rust's version has two features that change how you use it: traits are resolved at compile time by default, so calling through a trait costs nothing extra, and you can implement your own trait for types you did not write. This post builds a trait from nothing, shows the three ways it gets used, and shows the two compiler errors you will see most often while writing implementations. Everything ran on Rust 1.99, and the output is the program's own.
Defining a trait and implementing it
A trait lists method signatures. A type opts in with an impl Trait for Type block that provides every method:
use std::f64::consts::PI;
use std::fmt;
trait Shape {
fn area(&self) -> f64;
fn name(&self) -> String;
}
struct Circle(f64);
struct Rect {
w: f64,
h: f64,
}
impl Shape for Circle {
fn area(&self) -> f64 {
PI * self.0 * self.0
}
fn name(&self) -> String {
format!("circle r={}", self.0)
}
}
impl Shape for Rect {
fn area(&self) -> f64 {
self.w * self.h
}
fn name(&self) -> String {
format!("rect {}x{}", self.w, self.h)
}
}
// A function that works for anything implementing the trait.
fn describe<T: Shape>(shape: &T) -> String {
format!("{} has area {:.2}", shape.name(), shape.area())
}
// A trait can be implemented for a type you did not write, as long as you own the trait.
trait Shout {
fn shout(&self) -> String;
}
impl Shout for str {
fn shout(&self) -> String {
self.to_uppercase() + "!"
}
}
impl Shout for i32 {
fn shout(&self) -> String {
format!("{self}!!")
}
}
struct Wrapper(Vec<String>);
impl fmt::Display for Wrapper {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "[{}]", self.0.join(", "))
}
}
fn main() {
println!("{}", describe(&Circle(1.5)));
println!("{}", describe(&Rect { w: 2.0, h: 3.5 }));
println!("{}", "hello".shout());
println!("{}", 42.shout());
println!("{}", Wrapper(vec!["a".into(), "b".into()]));
}circle r=1.5 has area 7.07
rect 2x3.5 has area 7.00
HELLO!
42!!
[a, b]The program defines Shape with two methods, area and name, and implements it for Circle and Rect. Each implementation is separate and lives wherever you choose to write it. The function describe<T: Shape>(shape: &T) is written once and works for any type that implements Shape: T: Shape is a trait bound, which reads "T is any type that is a Shape". Calling describe(&Circle(1.5)) and describe(&Rect { ... }) runs the same function body against two different types, and the compiler checks at each call that the type really has the methods the body uses.
That is the main job of traits: writing code that is generic over behavior. describe does not care whether the thing is a circle or a rectangle, only that it can report an area and a name.
Implementing your trait for someone else's type
The Shout trait in the program is implemented for str and i32, both from the standard library. That is allowed because Shout is defined in this crate. The rule, called the orphan rule, is that you may implement a trait for a type if you own either the trait or the type. Here you own the trait, so you can attach shout() to str and i32, and "hello".shout() and 42.shout() work like any method call. This is how crates such as itertools add methods to every iterator: they define a trait and implement it for all iterators.
The opposite combination, a foreign trait on a foreign type, is rejected, because two crates could otherwise write conflicting implementations. If you need it, wrap the type in a struct of your own, the newtype pattern. The last type in the program, Wrapper(Vec<String>), does exactly that to implement the standard Display trait for something that holds a vector. The details are in the orphan rule.
Three ways to use a trait
A trait appears in code in three forms, and choosing between them is most of the skill.
As a bound on a generic parameter: fn describe<T: Shape>(s: &T). The compiler generates a specialized copy of the function for each concrete type, so the call has no indirection. This is static dispatch, covered in generics and monomorphization.
As an impl Trait argument or return type: fn describe(s: &impl Shape) is shorthand for the bound above, and fn make() -> impl Shape says "I return some type that is a Shape, and I will not tell you which". See impl Trait versus dyn Trait.
As a trait object: Box<dyn Shape> or &dyn Shape holds any value that implements Shape, with the method chosen at run time through a table of function pointers. That is how you build a Vec of different shapes. Trait objects explains the cost and the restrictions.
The two errors you will meet writing impls
When the implementation block does not match the trait, the compiler is exact. First, adding a method the trait does not declare:
impl Shape for Square {
fn area(&self) -> f64 {
self.0 * self.0
}
fn perimeter(&self) -> f64 {
4.0 * self.0
}
}error[E0407]: method `perimeter` is not a member of trait `Shape`
--> t162x.rs:11:5
|
11 | / fn perimeter(&self) -> f64 {
12 | | 4.0 * self.0
13 | | }
| |_____^ not a member of trait `Shape`An impl Trait for Type block may contain only the trait's methods. A method of your own goes in a separate impl Square { ... } block, or the trait gains a perimeter declaration. Second, leaving a required method out:
error[E0046]: not all trait items implemented, missing: `name`
--> t162y.rs:8:1
|
3 | fn name(&self) -> String;
| ------------------------- `name` from trait
...
8 | impl Shape for Square {
| ^^^^^^^^^^^^^^^^^^^^^ missing `name` in implementationThe message points at both the trait's declaration of the missing item and the incomplete impl. A method with a default body in the trait does not need to be implemented, which is how you add a method to an existing trait without breaking its implementers; see default methods.
Traits you already use
Much of everyday Rust is trait use you may not have noticed. #[derive(Debug, Clone, PartialEq)] asks the compiler to write impl Debug, impl Clone and impl PartialEq blocks for you. println!("{}", x) works because x: Display, and the ? operator works because the error type implements From. When a method call fails with "no method named ... found", the usual cause is a trait that is not in scope: methods from a trait are available only after use brings the trait name in, which is why you write use std::io::Write; before calling write_all. Learning to read an error as "this type is missing an impl" or "this trait is not imported" resolves most beginner trait trouble, and the compiler's help text usually names the exact trait to add.
What goes into a good trait
Keep traits small and focused on one capability. Iterator, Display and Read each describe one thing, and a type can implement many of them. A trait with fifteen methods forces implementers to provide behavior they may not have, and splits poorly when one type supports only some of it. Prefer composing traits with supertraits, covered in supertraits, over one large interface.
Traits can also carry associated types and constants, which let an implementation choose a type the trait's methods mention, as Iterator does with type Item. When a trait should have one natural output per implementing type, an associated type is better than a generic parameter; see associated types versus generics.
When a trait is more than you need
If you have exactly two cases and they live in one crate, an enum with a match is simpler: no trait, no dispatch decision, and the compiler checks that every case is handled, as in enum dispatch. Reach for a trait when the set of types is open, meaning other code or other crates will add their own, or when you want to write generic code over behavior you do not control, such as the standard library's own traits. And do not define a trait with a single implementer just in case; add it when the second implementer or the test double arrives.


