You wrote a struct. The compiler says no.
You define a struct for a game character. You want to print their stats to debug a crash. You write println!("{:?}", hero). The compiler stops you. You want to check if two inventory items are the same. item1 == item2. The compiler stops you again. You want to clone the item for a backup. let backup = item.clone(). The compiler stops you.
You haven't written bad logic. You just forgot to tell the compiler how your type behaves. Rust doesn't guess. It refuses to assume your type supports printing, comparison, or duplication. It requires you to opt into behaviors explicitly. That's where traits come in.
Traits are capabilities, not just interfaces
In other languages, a class might inherit methods from a base class automatically. In Rust, a type has no capabilities until you grant them. Traits are like certifications a type earns. Once a type holds the Debug certification, it can enter the println!("{:?}") club. Once it holds PartialEq, it can use the == operator.
The standard library provides a set of fundamental traits that cover the most common operations. You will use these every single day. The big six are Debug, PartialEq, Clone, Copy, PartialOrd, and Ord.
Most of the time, you don't write the implementation by hand. You use the #[derive(...)] attribute. This tells the compiler to generate the implementation automatically based on your fields. It's the compiler doing the paperwork for you.
#[derive(Debug, Clone, PartialEq)]
struct Point {
x: i32,
y: i32,
}
fn main() {
let p1 = Point { x: 1, y: 2 };
// Debug trait enables {:?} formatting.
println!("Point: {:?}", p1);
// Clone trait enables .clone() method.
let p2 = p1.clone();
// PartialEq enables == operator.
assert_eq!(p1, p2);
}
Derive is a macro that runs at compile time. It inspects your struct, checks that every field implements the requested traits, and generates the code. If a field doesn't implement the trait, the derive fails. This recursive check keeps your code consistent. You can't derive Debug on a struct if one of its fields is a black box that refuses to print.
Trust the derive chain. If the compiler rejects a derive, fix the field or implement the trait manually.
How the compiler generates the code
When you write #[derive(Debug)], the compiler generates an impl Debug for Point. The Debug trait has one method: fn fmt(&self, f: &mut Formatter) -> Result. The generated code walks through your fields and writes them to the formatter.
For Point, the generated code looks roughly like this:
impl std::fmt::Debug for Point {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
// Generated code formats fields in declaration order.
f.debug_struct("Point")
.field("x", &self.x)
.field("y", &self.y)
.finish()
}
}
The compiler calls fmt on each field. Since i32 implements Debug, this works. If you added a field of a custom type that doesn't implement Debug, the compiler would emit E0277 (the trait Debug is not implemented for CustomType).
This is why #[derive(Debug)] is the first attribute you should add to every new struct. It costs nothing at runtime and saves hours of debugging. Without it, you're flying blind.
Real-world types and the Clone vs Copy distinction
Real structs often hold heap data. Let's look at a user configuration.
#[derive(Debug, Clone, PartialEq)]
struct UserConfig {
username: String,
permissions: Vec<String>,
theme: Theme,
}
#[derive(Debug, Clone, PartialEq)]
enum Theme {
Light,
Dark,
}
Here we derive Clone, but not Copy. Why? String and Vec manage heap memory. When you clone a String, you must allocate new memory and copy the contents. That's a real cost. Rust forces you to acknowledge that cost by requiring an explicit .clone() call.
Copy is different. Copy is a marker trait. It tells the compiler that the type can be duplicated by just copying bits on the stack. No allocation, no destructor, no cost. If a type implements Copy, the compiler automatically implements Clone for it, and .clone() becomes a cheap bitwise copy.
You can derive Copy only if every field is Copy. String is not Copy. Vec is not Copy. u32 is Copy. bool is Copy.
// This compiles because i32 is Copy.
#[derive(Debug, Clone, Copy, PartialEq)]
struct Color {
r: i32,
g: i32,
b: i32,
}
// This fails to compile. String is not Copy.
// #[derive(Debug, Clone, Copy, PartialEq)]
// struct BadConfig {
// name: String,
// }
Convention aside: Always derive Clone and Copy together for simple aggregate types. If a type is Copy, it must be Clone. The compiler enforces this rule. Writing #[derive(Clone, Copy)] signals to readers that the type is cheap to duplicate and safe to pass by value.
Equality and ordering: Partial vs Total
Rust splits equality and ordering into two tiers: partial and total. This sounds academic, but it prevents subtle bugs.
PartialEq provides == and !=. It allows for weird cases where equality isn't reflexive. The classic example is floating-point NaN. In IEEE 754, NaN != NaN. If you derive PartialEq on a struct containing f64, the generated code respects this quirk.
Eq is a marker trait that asserts equality is reflexive. a == a must always be true. If your type has no NaN-like edge cases, you should derive Eq alongside PartialEq. Many standard library functions, like using a type as a key in a HashMap, require Eq.
// Eq requires PartialEq. Derive both.
#[derive(Debug, Clone, PartialEq, Eq)]
struct UserId(u64);
Ordering works the same way. PartialOrd provides <, <=, >, >=. It returns Option<Ordering> because the comparison might fail. Ord provides a total order. Every value can be compared to every other value. Ord is required for sorting collections and for BTreeMap keys.
// Ord requires PartialOrd and Eq.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
struct Score {
points: i32,
name: String,
}
If you derive Ord, the compiler generates a lexicographic comparison based on field order. Score compares points first, then name. This is usually what you want. If you need custom ordering, you skip the derive and implement Ord manually.
Derive Eq and PartialEq together. Derive Ord and PartialOrd together. The compiler won't stop you from deriving just one, but the ecosystem expects the pairs.
When derive isn't enough
Derive gives you the default behavior. Sometimes the default is wrong. You might want case-insensitive string comparison. You might want to sort by a specific field only. You might want Debug to hide sensitive data.
In these cases, you implement the trait manually. You drop the #[derive] and write the impl block.
struct CaseInsensitiveString(String);
impl PartialEq for CaseInsensitiveString {
fn eq(&self, other: &Self) -> bool {
// Custom logic: compare lowercase versions.
self.0.to_lowercase() == other.0.to_lowercase()
}
}
impl Eq for CaseInsensitiveString {}
fn main() {
let a = CaseInsensitiveString("Rust".to_string());
let b = CaseInsensitiveString("rust".to_string());
// This passes because of custom PartialEq.
assert_eq!(a, b);
}
Manual implementation is verbose. You have to satisfy all trait bounds. PartialEq requires eq. Eq is a marker, so the body is empty, but you must ensure your eq is reflexive, symmetric, and transitive. The compiler can't check those mathematical properties. You are responsible for the proof.
Treat the trait implementation as a contract. If you lie about Eq, the HashMap will break. If you lie about Ord, the BTreeMap will break. The compiler trusts you once you write the impl.
Pitfalls and compiler errors
You will hit errors when traits are missing. The compiler messages are precise.
If you try to use == on a type without PartialEq, you get E0369 (binary operation == cannot be applied to type MyType). The fix is to derive PartialEq or implement it.
If you try to print with {:?} on a type without Debug, you get E0277 (the trait Debug is not implemented for MyType). Derive Debug.
If you try to use a type as a HashMap key without Eq and Hash, you get E0277 again. HashMap requires Eq + Hash. Deriving Eq isn't enough. You also need #[derive(Hash)].
A common trap is deriving Ord but forgetting that BTreeMap also requires Eq. If you derive Ord, you get PartialOrd and PartialEq for free, but not Eq. You must derive Eq explicitly.
// This fails for BTreeMap key because Eq is missing.
// #[derive(Debug, Clone, PartialOrd, Ord)]
// struct Key { value: i32 }
// This works.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
struct Key { value: i32 }
Another trap is Copy. If you try to derive Copy on a struct with a String, the compiler rejects it. You can't force Copy on a type that drops resources. The compiler protects you from double-free bugs here. If you need Copy semantics, you must change the fields to Copy types, like &str or u32, or use a wrapper that implements Copy safely.
Don't fight the trait bounds. If the compiler asks for a trait, give it the trait. The error message tells you exactly what's missing.
Decision: when to use which trait
Use Debug when you need to print a type for logging or debugging. Derive it on every struct you write. It costs nothing and saves hours of troubleshooting.
Use PartialEq when you need to check if two values are equal. Combine it with Eq if your type has no weird edge cases like floating-point NaN.
Use Clone when your type holds heap data like String or Vec and you need a deep copy. The compiler will force you to call .clone() explicitly, which reminds you that a copy is happening.
Use Copy when your type is a simple aggregate of Copy fields like integers or booleans. The compiler copies the value implicitly on assignment, which makes the code cleaner.
Use PartialOrd when you need to compare values with < or >. Pair it with Ord if the comparison is total and transitive.
Use Ord when you need to sort a collection or use the type as a key in a BTreeMap. It guarantees a consistent ordering that PartialOrd alone cannot provide.
Use manual implementation when the derived behavior is incorrect. Derive is for defaults. Manual impl is for custom logic. Reach for manual impl only when you have a specific reason to deviate.
Derive liberally. Implement manually only when the default behavior lies.