Skip to main content

Comparison: eq and Ordering

In Lesson 1, James gave his objects the ability to display themselves. Now he tries to compare them.

He creates two notes with identical content:

note1 = Note("Meeting Notes", "Discussed Q3 roadmap", 4)
note2 = Note("Meeting Notes", "Discussed Q3 roadmap", 4)
print(note1 == note2)

Output:

False

"But they have the same content!" James says. "Same title, same body, same word count. How is that not equal?"

"Because you never told Python what 'equal' means for a Note," Emma says. "Without __eq__, Python compares identity: are these the same object in memory? They are not. They are two different objects that happen to hold the same data."

TDG Still Applies

Same cycle, same tools. When you specify __eq__, you are specifying what "equal" means for your class. Tests verify both the equal case and the not-equal case.


If you're new to programming

Python has two ways to check if things are "the same." The is operator checks whether two variables point to the same object in memory (identity). The == operator checks whether two objects have equal value. By default, == does the same thing as is for custom classes. You change that by writing __eq__.

If you've coded before

The default __eq__ for custom classes falls back to is (identity comparison). Implementing __eq__ overrides this to compare value. Return NotImplemented (not NotImplementedError) for incompatible types so Python can try the reflected operation. Implementing __eq__ also makes instances unhashable by default (Python sets __hash__ to None), which we address in Lesson 5.


Identity vs Value: The Core Distinction

Create a file called equality_demo.py:

class Note:
def __init__(self, title: str, body: str, word_count: int) -> None:
self.title = title
self.body = body
self.word_count = word_count

def __repr__(self) -> str:
return f"Note(title={self.title!r}, word_count={self.word_count})"

note1 = Note("Meeting Notes", "Discussed Q3 roadmap", 4)
note2 = Note("Meeting Notes", "Discussed Q3 roadmap", 4)
note3 = note1 # same object, not a copy

print(f"note1 == note2: {note1 == note2}") # value?
print(f"note1 is note2: {note1 is note2}") # identity?
print(f"note1 == note3: {note1 == note3}") # value?
print(f"note1 is note3: {note1 is note3}") # identity?

Run it:

uv run python equality_demo.py

Output:

note1 == note2: False
note1 is note2: False
note1 == note3: True
note1 is note3: True

note1 and note2 are different objects with the same data. Without __eq__, == checks identity, so it returns False. note3 is the same object as note1 (just a different variable name pointing to the same memory), so both == and is return True.

James's warehouse analogy makes this concrete: two boxes with identical contents are the "same product" even if they are different physical boxes. The is operator asks "is this the same physical box?" The == operator should ask "do these boxes contain the same product?" Right now, == also asks about the physical box. __eq__ fixes that.


Implementing eq

Add __eq__ to the class:

class Note:
def __init__(self, title: str, body: str, word_count: int) -> None:
self.title = title
self.body = body
self.word_count = word_count

def __repr__(self) -> str:
return f"Note(title={self.title!r}, word_count={self.word_count})"

def __eq__(self, other: object) -> bool:
if not isinstance(other, Note):
return NotImplemented
return self.title == other.title and self.body == other.body

note1 = Note("Meeting Notes", "Discussed Q3 roadmap", 4)
note2 = Note("Meeting Notes", "Discussed Q3 roadmap", 4)
note3 = Note("Meeting Notes", "Different body", 3)

print(f"note1 == note2: {note1 == note2}")
print(f"note1 == note3: {note1 == note3}")
print(f"note1 == 'not a note': {note1 == 'not a note'}")

Output:

note1 == note2: True
note1 == note3: False
note1 == 'not a note': False

Three critical details in this implementation:

1. The other: object Type

The parameter type is object, not Note. This is the correct annotation because == can be called with any type on the right side. The isinstance check handles the type narrowing.

2. Returning NotImplemented

When other is not a Note, return NotImplemented (a special sentinel value, not an exception). This tells Python: "I do not know how to compare myself to this type. Try asking the other object." Python then calls other.__eq__(self), giving the other type a chance to handle the comparison. If neither side handles it, Python returns False.

Do not confuse NotImplemented with NotImplementedError. One is a return value; the other is an exception.

3. Choosing Equality Fields

This __eq__ compares title and body but ignores word_count. Why? Because two notes with the same title and body are the "same note" regardless of word count (which could be computed differently). Choosing which fields define equality is a design decision specific to your domain.

FieldInclude in eq?Reasoning
titleYesIdentifies the note
bodyYesThe content that makes it unique
word_countNoDerived from body (not independent)
is_draftNoState changes; same note in different states is still the same note
tagsDebatableDepends on your domain: are two notes with different tags the same note?

Ordering with @functools.total_ordering

Once you have __eq__, you might want to sort your notes. Python supports six comparison operators: ==, !=, <, <=, >, >=. Writing all six methods is tedious. The @functools.total_ordering decorator generates the missing ones from just __eq__ and one ordering method (usually __lt__).

import functools

@functools.total_ordering
class Note:
def __init__(self, title: str, body: str, word_count: int) -> None:
self.title = title
self.body = body
self.word_count = word_count

def __repr__(self) -> str:
return f"Note(title={self.title!r}, word_count={self.word_count})"

def __eq__(self, other: object) -> bool:
if not isinstance(other, Note):
return NotImplemented
return self.title == other.title and self.body == other.body

def __lt__(self, other: object) -> bool:
if not isinstance(other, Note):
return NotImplemented
return self.word_count < other.word_count

short = Note("Quick Thought", "Brief", 1)
medium = Note("Meeting Notes", "Discussed roadmap", 4)
long_note = Note("Design Doc", "Full architecture plan with details", 8)

print(f"short < medium: {short < medium}")
print(f"medium >= short: {medium >= short}")
print(f"sorted: {sorted([long_note, short, medium])}")

Output:

short < medium: True
medium >= short: True
sorted: [Note(title='Quick Thought', word_count=1), Note(title='Meeting Notes', word_count=4), Note(title='Design Doc', word_count=8)]

You wrote __eq__ and __lt__. The decorator generated __le__, __gt__, __ge__, and __ne__ for free. sorted() uses __lt__ to order the notes by word count.


PRIMM-AI+ Practice: Equality

Predict [AI-FREE]

Press Shift+Tab to enter Plan Mode before predicting.

Given this class:

class Employee:
def __init__(self, emp_id: int, name: str, department: str) -> None:
self.emp_id = emp_id
self.name = name
self.department = department

def __eq__(self, other: object) -> bool:
if not isinstance(other, Employee):
return NotImplemented
return self.emp_id == other.emp_id

e1 = Employee(101, "Alice", "Engineering")
e2 = Employee(101, "Alice Johnson", "Product")
e3 = Employee(102, "Alice", "Engineering")

Predict the result (True or False) and write a confidence score from 1 to 5:

  1. e1 == e2
  2. e1 == e3
  3. e1 is e2
  4. e1 == 101
Check your predictions
  1. True. Same emp_id (101). Name and department are different, but __eq__ only checks emp_id.
  2. False. Different emp_id (101 vs 102). Same name and department do not matter.
  3. False. Different objects in memory. is checks identity, not value.
  4. False. isinstance(101, Employee) fails, so __eq__ returns NotImplemented. Python then asks int.__eq__(101, e1), which also returns NotImplemented. Python defaults to False.

Run

Press Shift+Tab to exit Plan Mode.

Create equality_practice.py with the Employee class above. Add all four comparisons as print statements and run:

uv run python equality_practice.py

Compare each output to your prediction. Pay special attention to case 4 (comparing with a non-Employee).

Investigate

In Claude Code, type:

/investigate @equality_practice.py

Ask: "What happens if I change __eq__ to compare emp_id and name instead of just emp_id? Which of my four test cases would change their result?"


Try With AI

Opening Claude Code

If Claude Code is not already running, open your terminal, navigate to your SmartNotes project folder, and type claude. If you need a refresher, Chapter 44 covers the setup.

Prompt 1: Design eq for SmartNotes

Here is my Note class. I need to implement __eq__.

class Note:
def __init__(self, title: str, body: str, word_count: int,
author: str = "Anonymous", is_draft: bool = True,
tags: list[str] | None = None) -> None:
self.title = title
self.body = body
self.word_count = word_count
self.author = author
self.is_draft = is_draft
self.tags = tags if tags is not None else []

Should I include tags in the equality check? What about
is_draft? Walk me through the trade-offs for each field.

Read the AI's analysis. It should discuss whether is_draft represents state (same note, different status) or identity (different notes). Evaluate whether you agree with its reasoning.

What you're learning: Choosing equality fields is a design decision, not a syntax question. The AI helps you think through the trade-offs, but you make the decision based on what "same note" means in your application.

Prompt 2: Fix a Broken eq

This __eq__ has a bug. Find it and explain the consequence:

class Point:
def __init__(self, x: float, y: float) -> None:
self.x = x
self.y = y

def __eq__(self, other: object) -> bool:
return self.x == other.x and self.y == other.y

What you're learning: The missing isinstance check means comparing a Point with any object that has x and y attributes (like a different class) would return True instead of NotImplemented. The AI explains the type safety contract and why returning NotImplemented is essential.

Prompt 3: Equality in Your Domain

I work in [describe your professional domain: logistics,
healthcare, education, finance, etc.]. Design a class
for an entity in my domain. Implement __eq__ and explain
which fields define "sameness" and which are state that
can change. Include a test showing two objects that are
equal despite having different state fields.

What you're learning: You are transferring the identity-vs-value distinction from Notes to your own domain. The AI generates a domain-relevant example, and you evaluate whether its equality design correctly separates identifying fields from state fields.


James nods slowly. "So == was comparing memory addresses this whole time. And now it compares title and body."

"Exactly. Same product, different boxes," Emma says. "In the warehouse, when two pallets arrive with the same SKU, you do not care that they are different physical pallets. The inventory system counts them as the same product. That is value equality."

"What about tags?" James asks. "Should two notes with the same title and body but different tags be equal?"

Emma hesitates. "That depends on your application. If tags are metadata that changes over time, then yes, same note with different tags is still the same note. If tags are part of the identity, like a product's color variant, then no. I genuinely do not know the right answer for SmartNotes. That is a design decision you make, not a syntax rule Python gives you."

James considers it. "Tags are metadata. I add and remove them over time. The note is the same note."

"Good reasoning. Now try len(collection). You will get a TypeError."

"Why?"

"Because your NoteCollection does not know how to count itself. That is the next lesson."