When == doesn't compile
You define a Point struct in Rust. You make two of them with the same coordinates and try assert_eq!(a, b). The compiler bites:
error[E0369]: binary operation `==` cannot be applied to type `Point`
--> src/main.rs:8:14
|
8 | assert_eq!(a, b);
| ^^^
|
= note: an implementation of `PartialEq` might be missing for `Point`
help: consider annotating `Point` with `#[derive(PartialEq)]`
This is one of the first surprises a Python or JS person runs into. In those languages, == works on anything. In Rust, == is an operator backed by a trait called PartialEq. If your type doesn't implement that trait, the operator simply isn't there. Rust doesn't guess what equality means for your data; you have to opt in.
Most of the time you opt in by writing #[derive(PartialEq)] above your struct, and the compiler generates a sensible implementation for you. But there's more depth here than the one-liner suggests, and it pays off to understand it.
Two traits, not one
The standard library splits equality into two traits: PartialEq and Eq.
PartialEq says: I have an eq(&self, other: &Self) -> bool method, and == and != work on me. It's called "partial" because it only requires symmetry (a == b implies b == a) and transitivity (a == b and b == c implies a == c). It does not require reflexivity (a == a). That sounds weird until you remember floating-point numbers exist: f64::NAN != f64::NAN. NaN is not equal to itself, by IEEE 754 design. So f64 implements PartialEq but not Eq.
Eq is a stricter promise. It's a marker trait with no methods of its own; you only get to implement it on top of PartialEq. It says: my equality is a true equivalence relation, including reflexivity (a == a always holds). For most types you write, this is true. For anything involving f32 or f64, it usually isn't.
The practical rule: if your type only contains "normal" equality-respecting fields (integers, booleans, strings, other Eq types), derive both. If it contains floats, derive only PartialEq and leave Eq off.
The basic case: derive both
// Derive both. Rust generates field-by-field comparison.
#[derive(PartialEq, Eq, Debug)]
struct Point {
x: i32,
y: i32,
}
fn main() {
let a = Point { x: 1, y: 2 };
let b = Point { x: 1, y: 2 };
let c = Point { x: 9, y: 9 };
// == compiles now because PartialEq is implemented.
assert_eq!(a, b);
// != is the negation; same trait gives both.
assert_ne!(a, c);
}
#[derive(PartialEq)] writes an impl PartialEq for Point block for you that compares each field with ==. So a == b becomes a.x == b.x && a.y == b.y. Adding Eq on top costs nothing at runtime; it's just a promise to the type system that the comparison is a real equivalence relation.
Debug is unrelated to equality, but I derived it because assert_eq! needs Debug to print a useful failure message when the assertion fails. Without it, you'd get a different error.
Floating-point: PartialEq only
// No Eq, because f64 doesn't implement Eq.
#[derive(PartialEq, Debug)]
struct Position {
x: f64,
y: f64,
}
fn main() {
let a = Position { x: 1.0, y: 2.0 };
let b = Position { x: 1.0, y: 2.0 };
assert_eq!(a, b);
// The classic trap: NaN is not equal to itself.
let nan = Position { x: f64::NAN, y: 0.0 };
let same_nan = Position { x: f64::NAN, y: 0.0 };
assert!(nan != same_nan);
}
If you tried to add Eq to Position, you'd get this:
error[E0277]: the trait bound `f64: Eq` is not satisfied
That's the type system protecting you from a lie. You're free to use == on Position, but the result might surprise you when NaN shows up. For game positions, statistics, or anywhere floats are arithmetic, that's usually fine. For deduplication, hashing, or use as a HashMap key (which requires Eq + Hash), it's a real problem and you'll need a different design (round to a fixed precision, use integer fixed-point, etc.).
Custom equality: skip the derive
Sometimes default field-by-field equality isn't what you want. Maybe you have an internal cache field that shouldn't affect whether two values are "equal." Maybe equality should be case-insensitive. In those cases, write the impl yourself.
#[derive(Debug)]
struct User {
id: u64,
// A cached display name. We don't want this to affect equality.
cached_label: String,
}
// Manual impl: only the id determines equality.
impl PartialEq for User {
fn eq(&self, other: &Self) -> bool {
self.id == other.id
}
}
// Eq is just a marker. Implementing it asserts that our PartialEq impl
// is a real equivalence relation. Since u64 equality is, this is fine.
impl Eq for User {}
fn main() {
let alice1 = User { id: 1, cached_label: "Alice".into() };
let alice2 = User { id: 1, cached_label: "ALICE".into() };
assert_eq!(alice1, alice2); // same id → equal
}
When you write a manual PartialEq and you also implement Hash, you have to be careful: the Hash and Eq impls must agree. If two values are equal, they must hash the same. The standard library and HashMap rely on this invariant; breaking it leads to silent bugs where a key you just inserted appears to be missing.
Comparing across types
PartialEq is generic. The full signature is PartialEq<Rhs = Self>, meaning by default you compare to your own type, but you can implement PartialEq<OtherType> if it makes sense. The standard library does this so you can write String == &str or Vec<u8> == [u8; 4] without manually converting.
// Compare a wrapper to a plain integer.
struct Money(i64);
impl PartialEq<i64> for Money {
fn eq(&self, other: &i64) -> bool {
self.0 == *other
}
}
fn main() {
let cost = Money(500);
assert!(cost == 500); // Money == i64 thanks to the impl above
}
This is rarely what you want at the start; reach for it when you have a strong reason, like a domain wrapper that should compare cleanly to its underlying numeric type.
Common pitfalls
The error you'll see most often:
error[E0369]: binary operation `==` cannot be applied to type `MyType`
help: consider annotating `MyType` with `#[derive(PartialEq)]`
Add the derive. If your fields don't implement PartialEq, you'll get a follow-up error about that, and you can derive it on those types too.
You derived PartialEq but a field is a function pointer or a closure. Closures don't implement PartialEq, so the derive fails. Either remove the closure from the struct or implement equality manually, ignoring that field.
You added Eq to a struct containing f64. Compile error: the trait bound f64: Eq is not satisfied. Drop Eq and stay with PartialEq.
You wrote a manual PartialEq that breaks transitivity (e.g. fuzzy-equality with a tolerance). The compiler can't catch this, but HashMap, HashSet, dedup, and sort algorithms that assume real equality will misbehave. Be careful.
You implemented Hash and PartialEq separately and they disagree on which fields matter. HashMap will silently lose entries. The fix: keep them aligned, or derive both with #[derive(Hash, PartialEq, Eq)].
When to derive vs implement manually
Derive both PartialEq and Eq for value-like structs whose fields are simple. This is the default and the right move 90% of the time.
Derive only PartialEq when any field is a float or otherwise non-Eq.
Implement PartialEq manually when default equality isn't what your domain calls for: identity-only comparisons, case-insensitive strings, ignoring caches.
Don't implement Eq on a type whose equality isn't truly reflexive. The marker is a promise; breaking it doesn't crash, but it makes downstream collections subtly wrong.
Where to go next
Equality is a small piece of a bigger story about traits and type-driven design.