How to implement Eq and PartialEq

Implement `PartialEq` for types where equality might not be defined for all values (like floating-point numbers with NaN), and implement `Eq` only when your type guarantees a total ordering where `a == b` is always true, false, or undefined in a consistent way.

When == isn't enough

You write a struct to hold configuration data. You create two instances. You check if config1 == config2. The compiler rejects you with E0369 (binary operation == cannot be applied to type Config). You add #[derive(PartialEq)]. It compiles. You celebrate.

Then you try to put Config into a HashSet. The compiler screams again: E0277 (the trait Eq is not implemented for Config). You add Eq to the derive. It works.

Later, you add a floating-point field for a threshold. You derive Eq again. The code compiles. You run a test with NaN values. Your set silently drops items or returns wrong results. You didn't get a compiler error. You got a logic bomb.

Rust splits equality into two traits to prevent exactly this. PartialEq handles the comparison. Eq is a promise that the comparison follows strict mathematical rules. Most types can keep that promise. Some cannot. You need to know which is which, and you need to know how to lie to the compiler safely when your semantics demand it.

The two traits, one job

PartialEq defines how to compare two values. It has one method: fn eq(&self, other: &Self) -> bool. That's the workhorse. Every time you write a == b, Rust calls PartialEq::eq.

Eq is a marker trait. It has no methods. It contains no code. It exists solely to tell the compiler, "This type guarantees total equality." Total equality means the comparison is reflexive, symmetric, and transitive for every possible value.

Reflexive means a == a is always true. Symmetric means a == b implies b == a. Transitive means a == b and b == c implies a == c.

Eq extends PartialEq. You cannot implement Eq without implementing PartialEq first. The compiler enforces this hierarchy. If you try to implement Eq on a type that lacks PartialEq, you get a trait bound error. The compiler expects the foundation before the marker.

Why split them? Zero-cost abstraction. Floating-point numbers have NaN (Not a Number). NaN != NaN. This breaks reflexivity. f64 can implement PartialEq, but it cannot implement Eq without lying. By splitting the traits, Rust allows f64 to be compared while preventing you from accidentally using it in contexts that require total equality, like hash maps, without explicit handling.

Derive is the default. Fight it only when you have a reason.

Derive: the default path

For plain data structures, the derive macro generates correct, efficient code. It compares every field in order. If any field differs, the result is false. If all fields match, the result is true.

#[derive(PartialEq, Eq, Debug)]
/// A user account with a unique ID and display name.
struct User {
    id: u32,
    name: String,
}

fn main() {
    let u1 = User { id: 1, name: "Alice".to_string() };
    let u2 = User { id: 1, name: "Alice".to_string() };
    let u3 = User { id: 1, name: "Bob".to_string() };

    // PartialEq enables == and !=.
    assert_eq!(u1, u2);
    assert_ne!(u1, u3);
}

The derive macro checks each field's traits. If a field doesn't implement PartialEq, the derive fails. If a field doesn't implement Eq, you cannot derive Eq for the struct. This propagates constraints up the type hierarchy. A struct containing f64 cannot derive Eq because f64 lacks Eq.

Convention aside: Always derive Eq alongside PartialEq when possible. Implementing Eq unlocks Hash, which unlocks HashSet and HashMap. Leaving a type at PartialEq only limits what you can do with it. The community standard is #[derive(PartialEq, Eq, Hash)] for any type you intend to use as a key or in a set.

Trust the borrow checker. It usually has a point.

Custom logic and the marker trait

Derive compares fields literally. Sometimes you need semantic equality. Two strings might be equal even if their case differs. Two paths might be equal even if one has extra slashes. A config object might be equal even if a timestamp field changed.

You implement PartialEq manually to customize the logic. You implement Eq manually to assert that your custom logic still satisfies the mathematical contract.

#[derive(Debug)]
/// A string wrapper that treats case as irrelevant for equality.
struct CaseInsensitiveString(String);

impl PartialEq for CaseInsensitiveString {
    /// Compare the inner strings ignoring ASCII case.
    fn eq(&self, other: &Self) -> bool {
        self.0.eq_ignore_ascii_case(&other.0)
    }
}

// Eq is a marker trait. The empty body asserts that the custom
// PartialEq logic is reflexive, symmetric, and transitive.
impl Eq for CaseInsensitiveString {}

fn main() {
    let a = CaseInsensitiveString("Hello".to_string());
    let b = CaseInsensitiveString("HELLO".to_string());
    assert_eq!(a, b);
}

The impl Eq for CaseInsensitiveString {} block is empty. You don't write code there. You write the proof in your head. Case-insensitive comparison is reflexive ("hello" equals "hello"). It is symmetric ("hello" equals "HELLO" implies "HELLO" equals "hello"). It is transitive. The empty impl signals to the compiler that you have verified these properties.

If your custom logic breaks transitivity, you break Eq. Suppose you define equality as "first character matches". a = "ab", b = "ac", c = "bc". a == b is true. b == c is true. a == c is false. Transitivity fails. You cannot implement Eq for this type. If you do, HashSet will misbehave. Items may vanish. Lookups may fail. The compiler cannot check transitivity. You carry that burden.

Treat the Eq impl as a proof. If you can't write it, you don't have one.

Floats and the NaN trap

Floating-point types are the classic reason Eq exists. f32 and f64 implement PartialEq. They do not implement Eq. The culprit is NaN.

NaN represents an undefined result, like 0.0 / 0.0. The IEEE 754 standard specifies that NaN != NaN. This breaks reflexivity. If NaN were equal to itself, you could not distinguish a valid zero from an invalid computation in some contexts. Rust follows the standard. f64::NAN == f64::NAN is false.

If you wrap a float in a struct, you cannot derive Eq. The compiler blocks you. You must handle NaN explicitly if you want Eq.

#[derive(Debug)]
/// A 2D point with floating-point coordinates.
struct Point {
    x: f64,
    y: f64,
}

impl PartialEq for Point {
    /// Compare coordinates, treating NaN as equal to NaN.
    fn eq(&self, other: &Self) -> bool {
        // Standard equality fails for NaN, so check NaN explicitly.
        let x_eq = self.x == other.x || (self.x.is_nan() && other.x.is_nan());
        let y_eq = self.y == other.y || (self.y.is_nan() && other.y.is_nan());
        x_eq && y_eq
    }
}

// Safe to implement Eq because the custom logic restores reflexivity.
// NaN is now equal to NaN, so all values satisfy a == a.
impl Eq for Point {}

This implementation treats all NaN values as equal. This restores reflexivity. Point { x: f64::NAN, y: 0.0 } now equals itself. You can safely implement Eq.

Be careful with the semantics. Treating NaN as equal is common for geometry, but it changes behavior. Two points that are both invalid become indistinguishable. If you need to distinguish different sources of NaN, you cannot use Eq. Stick to PartialEq.

Floats lie about equality. Wrap them or accept the partial truth.

Hash and the equality contract

Eq rarely stands alone. It almost always pairs with Hash. Hash collections like HashSet and HashMap use Hash to bucket items and Eq to resolve collisions. The contract is strict: if a == b, then hash(a) == hash(b).

If you implement Eq to ignore a field, you must implement Hash to ignore that field too. If you break this contract, the collection breaks. You might insert an item and fail to find it. You might find the wrong item. The compiler cannot verify the hash contract. You must maintain it manually.

use std::hash::{Hash, Hasher};

#[derive(Debug)]
/// A user where ID determines equality, ignoring the name.
struct UserId {
    id: u32,
    name: String,
}

impl PartialEq for UserId {
    /// Equality depends only on the ID.
    fn eq(&self, other: &Self) -> bool {
        self.id == other.id
    }
}

impl Eq for UserId {}

impl Hash for UserId {
    /// Hash must match the equality contract.
    /// Only hash the ID, since name is ignored in Eq.
    fn hash<H: Hasher>(&self, state: &mut H) {
        self.id.hash(state);
    }
}

If you derived Hash here, it would hash both id and name. Two users with the same ID but different names would be equal according to Eq, but have different hashes. HashSet would place them in different buckets. A lookup would fail. The contract is broken.

Convention aside: When you customize Eq, audit Hash immediately. They are twins. Change one, change the other. The community pattern is to implement Hash in the same block as Eq to keep the contract visible.

Break the hash contract and your collections will silently corrupt. Test it.

Pitfalls and compiler signals

The compiler helps, but it has limits. It checks trait bounds and signatures. It does not check mathematical properties.

If you forget to implement PartialEq before Eq, you get E0277 (trait bound not satisfied). The compiler requires the supertrait.

If you try to use == on a type without PartialEq, you get E0369. The operator is unavailable.

If you try to put a type in a HashSet without Eq + Hash, you get E0277. The collection requires the traits.

If you implement PartialEq and move a field in eq, you get E0507 (cannot move out of borrowed content). The signature is fn eq(&self, other: &Self) -> bool. You have shared references. You cannot move data out. You must compare by reference.

// BAD: This fails to compile.
// impl PartialEq for MyType {
//     fn eq(&self, other: &Self) -> bool {
//         self.value == other.value // E0507 if value doesn't implement Copy
//     }
// }

// GOOD: Compare references.
// impl PartialEq for MyType {
//     fn eq(&self, other: &Self) -> bool {
//         self.value == other.value // Works if value implements PartialEq
//     }
// }

The biggest pitfall is runtime. The compiler cannot catch broken transitivity or broken hash contracts. You must write tests. Test reflexivity with assert_eq!(x, x). Test symmetry with assert_eq!(a, b) and assert_eq!(b, a). Test transitivity with chains. Test hash consistency with assert_eq!(hash(&a), hash(&b)) for equal values.

Counter-intuitive but true: the more you customize equality, the harder it is to reason about collections. Stick to derive when you can.

Decision matrix

Use #[derive(PartialEq, Eq)] when your type is a plain data container with no floats and no semantic quirks. Derive is fast, correct, and unlocks the full ecosystem.

Use #[derive(PartialEq, Eq, Hash)] when you plan to use the type in HashSet or HashMap. The trio is the standard for keys and set members.

Use manual PartialEq when you need semantic equality like case-insensitive strings, ignoring metadata fields, or comparing paths. Implement Eq only if your logic satisfies reflexivity, symmetry, and transitivity.

Reach for Eq only when your equality is total. If your type contains f32 or f64 and you cannot guarantee NaN handling, avoid Eq. Stick to PartialEq.

Avoid Eq when equality is genuinely partial. If aliasing or undefined states exist, PartialEq is the correct choice. Forcing Eq invites subtle bugs.

Use Hash implementation that mirrors Eq exactly. If Eq ignores a field, Hash must ignore it too. If Eq normalizes data, Hash must normalize it too.

Pick the trait that matches your math, not your convenience.

Where to go next