Equality Tests (PartialEq and Eq)

How to define equality for your own types in Rust using the PartialEq and Eq traits, including manual implementations, derive macros, and the meaning of partial versus total equality.

Rust does not hard-code what == means. When you write a == b, the compiler silently rewrites it into a method call: PartialEq::eq(&a, &b). The inequality operator != becomes PartialEq::ne(&a, &b). This is not a hidden detail you can ignore — it is the entire mechanism. If a type does not implement PartialEq, the == operator simply does not exist for it.

This design turns equality from a built-in assumption into a deliberate contract. It forces you to answer a question that many languages let you skip: what does it actually mean for two values of your type to be the same? And because Rust separates partial equality from total equality with a second trait, Eq, it also forces you to confront whether every value of your type is equal to itself — which, for floating-point numbers, is famously not true.

How == Reaches Your Code

The PartialEq trait lives in std::cmp. Its definition, simplified, looks like this:

pub trait PartialEq<Rhs = Self> {
    fn eq(&self, other: &Rhs) -> bool;
    fn ne(&self, other: &Rhs) -> bool {
        !self.eq(other)
    }
}

The generic parameter Rhs (right-hand side) defaults to Self, which is why you normally compare two values of the same type. When the compiler sees x == y, it finds the implementation of PartialEq for x’s type with y’s type as the Rhs, and calls eq. The ne method has a default implementation that simply negates eq, so you never need to write it unless you have a more efficient inequality check.

Because == is just a trait method, you can override it, read its source for any type, and even implement it for types that are completely unrelated to each other — a capability we will return to.

What “Partial” Actually Means

The word “partial” in PartialEq is not about incomplete implementations. It is a mathematical property: a partial equivalence relation does not require every value to be equal to itself. The standard example is floating-point NaN. According to the IEEE 754 floating-point specification, NaN is not equal to anything, including itself:

let x = f64::NAN;
assert_ne!(x, x); // This passes. NaN != NaN.

So f64 implements PartialEq, and f64::eq returns false when either operand is NaN. This breaks the reflexivity requirement of a total equivalence relation — the property that a == a must hold for all a. Because of that, f64 cannot implement the Eq trait, which is a marker trait that promises total equality.

A type that implements Eq guarantees three properties for all of its values:

  • Reflexive: a == a
  • Symmetric: if a == b, then b == a
  • Transitive: if a == b and b == c, then a == c

PartialEq is Almost Always Enough:

Most types you will write satisfy all three properties naturally. Rust still separates the two traits because a handful of important types — floats being the most common — genuinely cannot. When you derive or implement Eq, you are making a statement to the compiler and to other developers: every value of this type is equal to itself, and equality here works as a proper equivalence relation.

Deriving Equality

For the vast majority of structs and enums, equality is exactly what you would expect: two values are equal if all their fields are equal, recursively. Rust can generate this implementation automatically with the derive attribute:

#[derive(PartialEq, Eq, Debug)]
struct Point {
    x: i32,
    y: i32,
}
fn main() {
    let p1 = Point { x: 1, y: 2 };
    let p2 = Point { x: 1, y: 2 };
    let p3 = Point { x: 3, y: 4 };
    assert!(p1 == p2);
    assert_ne!(p1, p3);
}

The derived PartialEq compares every field in declaration order. If any field is not PartialEq, the compiler will refuse to derive it and tell you which field is the problem. The derived Eq simply declares that total equality holds, which is valid because all the field types (i32 in this case) are themselves Eq.

Deriving works for enums too: two enum values are equal if they are the same variant and all associated data is equal. For unit variants, equality is just variant identity.

You Almost Always Want Derive Here:

If your type is a plain collection of data with no unusual equality rules, start with #[derive(PartialEq, Eq)]. It is one line, it is correct, and it never goes out of sync when you add fields later. Only drop down to a manual implementation when you need custom logic.

Implementing PartialEq by Hand

Manual implementations become necessary when equality ignores some fields, transforms data before comparing, or follows domain rules that differ from structural equivalence.

Case-Insensitive String Wrapper

Consider a wrapper that compares strings without regard to case:

struct CaseInsensitive(String);
impl PartialEq for CaseInsensitive {
    fn eq(&self, other: &Self) -> bool {
        self.0.to_lowercase() == other.0.to_lowercase()
    }
}
fn main() {
    let a = CaseInsensitive("Hello".into());
    let b = CaseInsensitive("HELLO".into());
    let c = CaseInsensitive("World".into());
    assert!(a == b);
    assert_ne!(a, c);
}

We only wrote eq. The ne method automatically uses the default implementation. The conversion to lowercase on every comparison is not the cheapest approach (Unicode-aware case folding requires an allocation), but it is correct and straightforward. For performance-critical paths, you would likely store the normalized form once.

Equality Based on a Subset of Fields

Sometimes two records represent the same entity even if some metadata differs. In a user database, you might identify users by a unique ID and treat a display name update as the same logical user:

struct User {
    id: u64,
    name: String,
}
impl PartialEq for User {
    fn eq(&self, other: &Self) -> bool {
        self.id == other.id
    }
}
impl Eq for User {}
fn main() {
    let u1 = User { id: 42, name: "Alice".into() };
    let u2 = User { id: 42, name: "Alicia".into() };
    assert!(u1 == u2); // Same identity, different name — still equal.
}

A critical detail here: we also wrote impl Eq for User {}. Since the only field we compare, id, is a u64 (which is Eq), total equality holds. Rust does not automatically infer Eq from a manual PartialEq — you must add the empty implementation yourself.

Keep Equality Consistent with Hash:

If you use a type as a key in a HashMap, the standard library requires that a == b implies hash(a) == hash(b). When you implement PartialEq based on a field subset, your Hash implementation must hash only those same fields. Deriving Hash on the whole struct would break this contract, leading to keys that are equal but stored in different buckets — a logic bug the compiler cannot catch.

Comparing Two Different Types

The Rhs generic parameter on PartialEq allows you to implement equality between two completely different types. This is not possible with derive — it requires a manual impl with an explicit Rhs type.

A realistic scenario: a domain identifier type that should be comparable to its raw representation without extra wrapping:

#[derive(PartialEq)]
struct UserId(u64);
impl PartialEq<u64> for UserId {
    fn eq(&self, other: &u64) -> bool {
        self.0 == *other
    }
}
fn main() {
    let uid = UserId(7);
    assert!(uid == 7);
    // The reverse does not automatically work:
    // assert!(7 == uid); // error: PartialEq<UserId> not implemented for u64
}

The trait implementation is asymmetric: we implemented PartialEq<u64> for UserId, meaning UserId on the left and u64 on the right. For symmetry, you would also need to implement PartialEq<UserId> for u64, which the orphan rule usually prevents because u64 is a foreign type. This is why standard library types rarely offer cross-type equality — it is left to crate authors for their own domain types.

Asymmetric Equality Can Surprise Callers:

Having uid == 7 work while 7 == uid does not is counterintuitive. If you open the door to cross-type equality, document the direction clearly. In most cases, providing an explicit method like uid.equals_raw(7) is clearer and avoids confusion about which operand order is valid.

The Eq Marker Trait and Hash Consistency

Eq has no methods. Its definition is simply pub trait Eq: PartialEq<Self> {}. It is a marker trait that tells the compiler and standard library: the implementation of PartialEq for this type is a full equivalence relation. The practical consequence is that types implementing Eq can be used where total equality is required — most notably as keys in HashMap and HashSet.

The Hash trait is the partner of Eq in this role. When you use a type as a hash map key, the compiler requires K: Eq + Hash. The contract is:

  • If a == b, then hash(a) == hash(b) must hold.
  • The reverse is not required: two different values may hash to the same bucket, and the map resolves collisions with Eq.

If you manually implement PartialEq (and Eq), you must also manually implement Hash to match the same fields and comparison logic. The standard library’s documentation warns that violating this invariant leads to undefined behavior in HashMap — operations may panic, return incorrect results, or cause subtle data corruption.

For the User struct from earlier, a correct Hash implementation would only hash the id:

use std::hash::{Hash, Hasher};
impl Hash for User {
    fn hash<H: Hasher>(&self, state: &mut H) {
        self.id.hash(state);
    }
}

If you derived Hash on the full struct, the hash would include name, and a renamed user would no longer match its previous entry in a HashMap, violating the contract.

Common Mistakes and Their Consequences

Deriving Eq for a Type that Contains f32 or f64

The derive macro for Eq requires all fields to be Eq. Since f32 and f64 are not Eq, the compiler will reject #[derive(Eq)] on any struct containing a float. That is the easy case. The dangerous case is when you manually implement PartialEq in a way that satisfies reflexivity for all values and then manually implement Eq for a type that contains a float — you are asserting that the float never becomes NaN. If a NaN slips in later, the type no longer satisfies the Eq contract, and using it as a HashMap key becomes unsound.

Breaking Symmetry or Transitivity in a Manual Implementation

A manual PartialEq implementation that is not symmetric causes logic errors in collection operations. For example, if you accidentally compare only one field that can be equal while another field differs, you might end up with a == b but b != a if the implementation is inconsistent. The standard library relies on these properties; breaking them leads to panics or infinite loops in sort and search algorithms.

Forgetting to Implement Hash Consistently with Eq

This is the most common mistake that compiles silently and fails at runtime. The compiler cannot check that your Hash implementation aligns with your Eq implementation. Always write a test that inserts a value into a HashMap, modifies a field that does not affect equality, and checks that the map still finds the key. If that test fails, your Hash implementation is wrong.

A Structured Approach to Implementing Equality

When you need to equip a new type with equality, follow this sequence. Each step depends on the previous one.

1

Step 1: Decide what equality means for your type

Before writing code, answer: When are two values of this type considered the same? Does equality include all fields? Is it based on an identifier? Do edge cases like case sensitivity or floating-point tolerance matter? Write this definition down — it will guide the implementation and prevent accidental breakage later when fields are added.

2

Step 2: Derive or implement PartialEq

If structural field-by-field comparison matches your definition, use #[derive(PartialEq)]. If you need custom logic (subset of fields, normalization, approximate comparison), write impl PartialEq for YourType with an eq method. Do not implement ne unless you have a genuine optimization; the default negation is correct.

impl PartialEq for YourType {
    fn eq(&self, other: &Self) -> bool {
        // your comparison logic
    }
}
3

Step 3: Determine whether Eq applies

Ask: Does every possible value of this type equal itself? If your type contains f32 or f64 and those floats might become NaN, the answer is no — do not implement Eq. If the answer is yes, add impl Eq for YourType {} after PartialEq. This marker trait is what allows your type to be used as a HashMap key.

4

Step 4: Implement Hash if you implemented Eq manually

If you wrote custom PartialEq logic, you must also implement Hash manually so that equal values produce equal hashes. Hash exactly the same fields (and in the same way) that eq compares. Deriving Hash while using custom PartialEq on a subset of fields is the most frequent source of HashMap bugs in Rust.

Summary

The PartialEq and Eq traits transform equality from a language primitive into an intentional contract between your type and its users. PartialEq is the workhorse: it lets you decide what == means, whether through automatic derivation or manual implementation. Eq is the promise that equality works the way everyone expects — no NaN-style surprises, no broken reflexivity.

The biggest insight from working with these traits is that equality is not a single concept. Two points with the same coordinates are structurally equal. Two user records with the same ID are equal even if their display names differ. Two strings that differ only in case might be treated as equal for searching purposes. Rust forces you to choose which meaning your type represents, and then it enforces that choice everywhere the type is used.