Object Identity and Equality
This is the Layer 2 gate. Two Country instances can hold identical data yet not be "the same" โ understanding why is essential before we start tracking objects by identity in collections.
Same data is not the same thing โ identity and equality differ.
1. Learning Objective
By the end of this session you will be able to:
- Explain the difference between is (identity) and == (equality)
- Predict the default behaviour of == on a plain class with no __eq__ defined
- Implement __eq__ so two instances with the same data compare equal
- Explain why id() returns a different value for two distinct instances, even with identical attributes
- Recognise when identity comparison (is) is actually the correct tool, e.g. is None
2. Pre-Coding Quiz
Answer these before reading the concept explanation or writing any code. It is fine to get these wrong โ that is the point. You need 4/5 to proceed to the lab.
Given a = Country(name="Kenya", region="Africa", population=54000000) and b = Country(name="Kenya", region="Africa", population=54000000) with NO __eq__ defined, what does a == b return?
__eq__, Python's default equality check for objects is identical to is โ same object in memory, not same data. Two separately constructed instances are never == by default, no matter how identical their attributes look.What does a is b check, as opposed to a == b?
is is identity comparison โ are these literally the same object? == is equality comparison โ by default the same as is for custom classes, but overridable via __eq__ to compare by value instead.After adding def __eq__(self, other):\n return self.name == other.name and self.region == other.region and self.population == other.population to Country, what does a == b return for the a, b from question 1?
__eq__ overrides the default identity-based comparison. Python calls your method whenever == is used between two Country instances, and since every field matches, it returns True.After defining __eq__ as above, is a is b now True as well?
__eq__ only customizes ==. is always checks raw identity and cannot be overridden by a class โ a and b are still two separate objects in memory, regardless of how you define equality.Why is x is None considered more correct/idiomatic than x == None in Python?
None object in the entire program. Checking identity against it is both the idiomatic style and avoids any surprises from a class that might override __eq__ in an unexpected way.3. The Concept โ Identity vs Equality
a and b are separate objects with separate identities, even though their contents happen to match.
Two instances with identical data are not automatically equal
This surprises many people coming from other backgrounds. By default, comparing two custom-class instances with == checks whether they are the exact same object โ not whether their data matches.
class Country:
def __init__(self, name, region, population):
self.name = name
self.region = region
self.population = population
a = Country(name="Kenya", region="Africa", population=54000000)
b = Country(name="Kenya", region="Africa", population=54000000)
print(a == b) # False! Same data, but no __eq__ defined
print(a is b) # False โ definitely two separate objects
print(a is a) # True โ an object is always identical to itself
id() reveals the underlying memory identity
Every object has a unique identity while it exists, which id() reveals as a number (conceptually, its memory address). This is what is actually compares.
print(id(a)) # some large number, e.g. 140234...
print(id(b)) # a DIFFERENT large number
print(id(a) == id(b)) # False
print(a is b) # False โ is is really just id(a) == id(b)
Defining __eq__ to compare by value
If you want == to mean "same data" instead of "same object", define __eq__ yourself. This is a dunder method โ a special, double-underscore method Python calls automatically for a specific operator, similar in spirit to __init__.
class Country:
def __init__(self, name, region, population):
self.name = name
self.region = region
self.population = population
def __eq__(self, other):
return (
self.name == other.name
and self.region == other.region
and self.population == other.population
)
a = Country(name="Kenya", region="Africa", population=54000000)
b = Country(name="Kenya", region="Africa", population=54000000)
print(a == b) # True now โ __eq__ compares field by field
print(a is b) # Still False โ is is never affected by __eq__
When identity (is) is actually the right tool
is is not "wrong" โ it is the correct choice when you specifically care about object identity, most commonly when comparing against the None singleton.
capital = None
if capital is None: # idiomatic โ checking identity against the None singleton
print("No capital on file")
# vs. == which would also work here, but is not the conventional style
if capital == None: # works, but not idiomatic Python
print("No capital on file")
4. Lab
What you will build
A file called identity_lab.py.
Step-by-step instructions
Create the file with a Country class and NO __eq__ yet
# identity_lab.py
class Country:
def __init__(self, name, region, population):
self.name = name
self.region = region
self.population = population
Create two instances with identical data and compare them
Before running: predict what a == b will print, and why.
a = Country(name="Kenya", region="Africa", population=54000000)
b = Country(name="Kenya", region="Africa", population=54000000)
print("a == b:", a == b) # what do you expect?
print("a is b:", a is b)
print("id(a):", id(a))
print("id(b):", id(b))
Add __eq__ to compare by value and re-run the comparison
class Country:
def __init__(self, name, region, population):
self.name = name
self.region = region
self.population = population
def __eq__(self, other):
return (
self.name == other.name
and self.region == other.region
and self.population == other.population
)
a = Country(name="Kenya", region="Africa", population=54000000)
b = Country(name="Kenya", region="Africa", population=54000000)
print("a == b now:", a == b) # True
print("a is b still:", a is b) # still False
Prove __eq__ correctly returns False for genuinely different data
c = Country(name="Peru", region="Americas", population=33000000)
print("a == c:", a == c) # False โ different data, correctly not equal
Demonstrate is None as the idiomatic missing-value check
Give Country an optional capital and check it both ways, noting which is idiomatic.
capital = None
print(capital is None) # idiomatic
print(capital == None) # works, but not the conventional style โ explain why in a comment
5. Expected Files Changed
| File | Action | Why |
|---|---|---|
identity_lab.py |
Created | Demonstrates default identity-based equality, a custom __eq__, and is None. |
docs/sessions/session-19/index.html |
Created | This session document โ Layer 2 gate. |
6. Commit Checkpoint
Once the lab is complete and you can explain every line, make this exact commit:
git add identity_lab.py docs/sessions/session-19/index.html
git commit -m "session-19: implement value-based __eq__ and distinguish it from identity"
7. Code Review Checklist
Go through your code line by line and check each item:
8. Post-Coding Quiz
Same 5 questions. Take it again now that you have written and run the code. You need 4/5 to mark this session complete.
Given a = Country(name="Kenya", region="Africa", population=54000000) and b = Country(name="Kenya", region="Africa", population=54000000) with NO __eq__ defined, what does a == b return?
__eq__, Python's default equality check for objects is identical to is โ same object in memory, not same data. Two separately constructed instances are never == by default, no matter how identical their attributes look.What does a is b check, as opposed to a == b?
is is identity comparison โ are these literally the same object? == is equality comparison โ by default the same as is for custom classes, but overridable via __eq__ to compare by value instead.After adding def __eq__(self, other):\n return self.name == other.name and self.region == other.region and self.population == other.population to Country, what does a == b return for the a, b from question 1?
__eq__ overrides the default identity-based comparison. Python calls your method whenever == is used between two Country instances, and since every field matches, it returns True.After defining __eq__ as above, is a is b now True as well?
__eq__ only customizes ==. is always checks raw identity and cannot be overridden by a class โ a and b are still two separate objects in memory, regardless of how you define equality.Why is x is None considered more correct/idiomatic than x == None in Python?
None object in the entire program. Checking identity against it is both the idiomatic style and avoids any surprises from a class that might override __eq__ in an unexpected way.9. Reflection Questions
Think through these after the post-quiz. No right answer โ they are for discussion.
- Before running the lab, did you correctly predict that a == b would be False without __eq__? If not, what was your mental model, and how has it changed?
- Why does Python NOT give every class value-based equality by default? Can you think of a reason that would be a bad default?
- If you were writing a test in Layer 5 that checks "did this function return the right Country?", would you use == or is? Why?
- How does the id()-based explanation of is connect back to the shared-reference behaviour of dictionaries from Session 05?
10. What Breaks If This Knowledge Is Missing?
- False negatives in comparisons: Code that assumes two identically-built objects are automatically == (without a custom __eq__) will silently fail comparisons that should have succeeded โ a subtle bug that only appears when you actually try to compare two objects and get a surprising False.
- Testing assertions (Layer 5): Test frameworks in Session 33 rely heavily on == to check "did the function return what I expected?" If a class does not implement __eq__, every such test will need clumsy, error-prone field-by-field comparisons instead of a clean assertEqual.
- Deduplicating data (Layer 4): When we design data contracts and work with real data sources in Layer 4, detecting duplicate records requires exactly the value-based equality this session teaches โ the default identity comparison would treat every record as unique, even genuine duplicates.
11. What We Learned
Python concept mastered: Identity (is) vs equality (==) โ the default identity-based comparison for custom classes, overriding it with __eq__, and when is is still the right tool.
Unlocks: You can now reason precisely about whether two objects are literally the same or merely hold the same data โ required for every comparison-heavy session ahead.
Next session: Session 20 โ What Is State in a Program?. Layer 3 begins. We start tracking data that changes over the lifetime of a running program โ state.