Layer 2 Session 19 โ€” Gate Object-Oriented Basics

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.

x == y True / False

Same data is not the same thing โ€” identity and equality differ.

Estimated time: 35โ€“40 minutes  ยท  Pre-quiz โ†’ Concept โ†’ Lab โ†’ Commit โ†’ Post-quiz

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.

Question 1 of 5

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?

Without a custom __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.
Question 2 of 5

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.
Question 3 of 5

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?

Defining __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.
Question 4 of 5

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.
Question 5 of 5

Why is x is None considered more correct/idiomatic than x == None in Python?

Python guarantees there is exactly one 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

Aid: 1001name: "Kenya"Bid: 1002name: "Kenya"

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")
Layer 2 gate: This is the last Layer 2 session. Every remaining layer assumes you can correctly reason about whether two objects are "the same" versus merely "equal" โ€” this distinction underlies testing assertions in Layer 5 and data deduplication in Layer 4.

4. Lab

Lab objective: Prove the default identity-based equality, implement a custom __eq__, and demonstrate is None as the idiomatic check.

What you will build

A file called identity_lab.py.

Step-by-step instructions

1

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
2

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))
3

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
4

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
5

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

FileActionWhy
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.
If you find yourself editing any other file, stop. This session touches exactly 2 files.

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"
Do not commit until you can answer out loud: "Why does a is b stay False even after I defined __eq__ to make a == b return True?"

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.

Question 1 of 5

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?

Without a custom __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.
Question 2 of 5

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.
Question 3 of 5

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?

Defining __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.
Question 4 of 5

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.
Question 5 of 5

Why is x is None considered more correct/idiomatic than x == None in Python?

Python guarantees there is exactly one 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.

  1. 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?
  2. Why does Python NOT give every class value-based equality by default? Can you think of a reason that would be a bad default?
  3. If you were writing a test in Layer 5 that checks "did this function return the right Country?", would you use == or is? Why?
  4. 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.