Identity, equality and value semantics
Tell apart same object, same value and same key, then write equals and hashCode as one contract so sets and dictionaries keep working.
Three different questions get asked with the same two characters. Is this the same object as that one? Does it carry the same value? And will a dictionary find it again tomorrow? Most equality bugs are one of those questions answered with another one's operator.
Same object, same value
Python separates them by keyword. a is b compares identity — the address, what
id() returns. a == b calls __eq__, which by default falls back to identity,
so a class with no __eq__ is equal only to itself.
a = "hello world"
b = "hello world"
a == b # True — same value
a is b # sometimes True, because CPython interns some literals
Java splits the same two ideas across == and .equals(): == on references
is identity, and Object.equals is identity too until you override it. So both
languages default to "two objects are different unless they are the same object",
and value semantics are something you opt into.
The interning cases are where people get burned. CPython caches small integers
and some strings, and Java caches boxed Integer values from −128 to 127, so
Integer.valueOf(100) == Integer.valueOf(100) is true and
Integer.valueOf(200) == Integer.valueOf(200) is false. Nothing about the
numbers changed; the cache ran out. Use .equals for values and reserve ==
and is for the question "is this literally the same object", which in practice
means comparing against None / null or checking aliasing.
Value objects and entities
Two 500-paise amounts in rupees are the same money; there is no second fact that distinguishes them. Two customers called Priya are two customers. That is the whole distinction:
- A value object has no life outside its fields. Equality is over all of
them.
Money,DateRange, a coordinate. - An entity has an identity that survives changes to its fields. A customer who moves house is the same customer. Equality is over the id alone.
Deciding which one you have comes before writing either method, and it is the same act as listing the fields a class owns in state and behaviour: the fields that define the object, against the fields it merely carries.
Both languages will write a value object for you:
from dataclasses import dataclass
@dataclass(frozen=True)
class Money:
paise: int
currency: str
public record Money(long paise, String currency) {}
frozen=True generates __eq__ and __hash__; a Java record generates
equals, hashCode and toString. Leave off frozen and the dataclass still
generates __eq__ but sets __hash__ to None, making instances unhashable —
a deliberate choice by the language, and the right one, for the reason below.
The contract, and what breaks without it
Equal objects must have equal hash codes. The converse is not required: unequal objects may collide, which is what a bucket is for. Break the rule in one direction and every hash-based container quietly stops working.
Python refuses to let you: defining __eq__ without __hash__ sets __hash__
to None, and the first attempt to put an instance in a set raises
TypeError: unhashable type. Java says nothing at all.
final class Money {
private final long paise;
Money(long paise) { this.paise = paise; }
@Override public boolean equals(Object o) {
return o instanceof Money m && m.paise == paise;
}
// no hashCode
}
Set<Money> seen = new HashSet<>();
seen.add(new Money(500));
seen.contains(new Money(500)); // false
Object.hashCode is derived from identity, so the two equal objects land in
different buckets and equals is never reached. No exception, no stack trace, a
wrong answer.
The size of what you gave up is worth stating. A hash set exists to replace a scan: 1 million entries at roughly one main memory reference each, 1,000,000 × 100 ns = 0.1 s per lookup, becomes a hash plus a probe at roughly 100 ns — a factor of a million, the same kind of jump as the one counted in complexity by counting. Break the contract and you keep paying for the hashing and lose the correctness.
Mutating a key that is already in a set
The second failure is worse, because the object is genuinely in the container:
class Tag:
def __init__(self, name): self.name = name
def __eq__(self, other):
if not isinstance(other, Tag):
return NotImplemented
return self.name == other.name
def __hash__(self): return hash(self.name)
t = Tag("draft")
s = {t}
t.name = "final"
t in s # False
len(s) # 1
[x.name for x in s] # ['final'] — it is right there
The guard is not decoration. Without it, self.name == other.name reaches for a
field the other object may not have, so Tag("draft") == None raises
AttributeError instead of answering False, and a single tag in mixed_list
takes the process down. Return NotImplemented, not False: it tells Python to
offer the comparison to the right-hand operand's __eq__ before falling back to
identity, which is what lets a Tag subclass answer for itself. Java's version
above gets this for free from o instanceof Money m, which is false for
null and for every unrelated type.
Java behaves identically with a HashSet, for the identical reason. C++ removes
the option: std::unordered_set hands out only const references to its keys, so
the standard makes modifying one either impossible or undefined rather than
silently wrong.
The rule that falls out of the diagram is short. Anything used as a key must be
immutable, or must hash on only the parts that never change. For a value
object that means @dataclass(frozen=True) or a record with final fields —
built once, checked once, in the manner of
constructors and invariants.
For an entity it means hashing the id and nothing else, so a customer can change
address without escaping the map she is stored in.
In an interview
You are being tested on two things: whether you know equals and hashCode are
one contract rather than two methods, and whether you classify the type before
writing either.
Say the classification out loud. "Money is a value object, so equality is over
all fields, the hash comes from the same fields, and the type is immutable, which
means the hash cannot go stale. Customer is an entity, so equality is the
customer id only and changing the address changes nothing about identity."
The mistake that loses points: writing equals over every field of a mutable
entity and then using it as a map key. It works in the test, works in the demo,
and loses a record the first time production edits a field. The related slip is
overriding equals and stopping there — if you write one, write both, and say
why while you do it.
Check yourself
A Java class overrides equals but not hashCode. You add an object to a
HashSet and call contains with an equal-but-distinct object. What comes back,
and why?
false.Object.hashCodeis identity-derived, so the two objects hash to different buckets, and theequalsyou wrote is never called. Nothing throws.
A nightly job checks 1 million Tag objects for membership against the 1
million already seen. You wrote __eq__ and no __hash__, so the tags sit in a
list and membership is in. Estimate the run, estimate it again after you add
__hash__ and switch to a set, and name the field you would hash.
A list
inis a scan: 1 million entries at roughly one main memory reference each, 1,000,000 × 100 ns = 0.1 s per lookup, and 1 million lookups is 1,000,000 × 0.1 s = 100,000 s ≈ 28 hours — the job cannot finish overnight. Asetmakes each lookup a hash plus a probe at roughly 100 ns, so the pass is 1,000,000 × 100 ns = 0.1 s. The container is the easy half. The decision is which field the hash reads: hashnameand the run is silently wrong the first time anything renames a tag mid-job, so hash an immutable id and letnamebe a field the object merely carries.
An Order holds an id, a customer, a status and a total, and its status
changes as it is fulfilled. What should equals and hashCode use?
The id alone.
Orderis an entity: it is the same order before and after shipping, and hashing anything mutable would strand it in any map it was put into. If you want "same contents" as well, that is a separate named method, notequals.