What is it? #
An object is one concrete thing built from a class. The class is the plan; the object occupies memory and holds actual values.
Every object has three aspects: identity (which one it is), state (the values it currently holds), and behaviour (what it can do).
Identity and equality are different questions. Two orders can have identical contents and still be two separate orders — two rows in a table, two parcels to ship. Whether "same values" should mean "same thing" is a decision you make per class.
Objects are passed around by reference. Handing an object to a function does not copy it, so changes made inside are visible outside. That is powerful and, when forgotten, the source of surprising bugs.
Think of it like this #
Two identical cars roll off the production line. Same model, same colour, same specification. They are equal in every visible way, but they are not the same car — they have different registration numbers, and scratching one does not scratch the other.
Identity is the registration number. Equality is the specification.
Simple example #
A cart holds order lines. Two lines for the same product at the same price are equal in content but remain separate objects. Meanwhile, passing the cart to a discount function modifies the original cart, because there is only one cart.
Code #
class OrderLine:
def __init__(self, sku, quantity, unit_price):
self.sku = sku
self.quantity = quantity
self.unit_price = unit_price
def total(self):
return round(self.quantity * self.unit_price, 2)
def __eq__(self, other): # define what "equal" means
if not isinstance(other, OrderLine):
return NotImplemented
return (self.sku, self.quantity, self.unit_price) == (
other.sku, other.quantity, other.unit_price
)
def __repr__(self):
return f"OrderLine({self.sku!r}, {self.quantity}, {self.unit_price})"
a = OrderLine("P-1", 2, 250.0)
b = OrderLine("P-1", 2, 250.0)
print(a == b) # True — equal state, because we defined __eq__
print(a is b) # False — different objects in memory
print(id(a) == id(b)) # False
c = a
print(c is a) # True — same object, two names
c.quantity = 5
print(a.quantity) # 5 — changing c changed a
# Objects are passed by reference
def apply_bulk_discount(line):
if line.quantity >= 5:
line.unit_price = round(line.unit_price * 0.9, 2) # modifies the caller's object
apply_bulk_discount(a)
print(a.unit_price) # 225.0 — the original changed
# If you need independence, copy explicitly
import copy
snapshot = copy.deepcopy(a)
snapshot.quantity = 99
print(a.quantity, snapshot.quantity) # 5 99
How it works #
OrderLine("P-1", 2, 250.0) allocates a new object and runs __init__ on it. Two such calls produce two distinct objects, even with identical arguments.
__eq__ defines what equality means for this class. Without it, Python falls back to identity, so a == b would be False. With it, two lines holding the same values compare as equal.
is always compares identity — literally "the same object in memory". Use == for value comparison and is only for None and genuine identity checks.
c = a binds a second name to one object. There is no copy, which is why changing c.quantity changes a.quantity.
apply_bulk_discount(a) shows the same mechanism across a function boundary. The function receives the object itself, so its changes persist after the call. Whether that is desirable is a design decision — functions that quietly modify their arguments surprise people, so it is worth being explicit about which ones do.
copy.deepcopy creates a genuinely independent object, including nested ones. A shallow copy.copy would duplicate the outer object but share anything nested inside it.
One practical note: if you define __eq__, Python stops generating a default __hash__, so the object can no longer go in a set or be used as a dictionary key unless you define __hash__ too. Dataclasses with frozen=True handle both for you.
Real-world use #
Identity versus equality shows up immediately in any system with a database. Two rows with identical column values are still different records with different IDs. That is why entities are usually compared by ID, while value objects — money amounts, addresses, date ranges — are compared by content.
Pass-by-reference behaviour explains a whole category of bugs. A service modifies the order object it was given, and the caller's copy silently changes too. Some teams prefer immutable objects specifically to avoid this: instead of mutating, return a new object with the change applied.
Caching depends on these ideas as well. If objects are cached and handed to multiple callers, one caller mutating a shared object corrupts it for everyone. Returning copies, or using frozen objects, prevents that.
Common mistakes #
- Using
isto compare values. It checks identity, not equality. - Defining
__eq__without__hash__, then finding the object unusable in a set. - Assuming a function will not modify the object you pass it.
- Using a shallow copy when nested objects also need to be independent.
- Comparing database entities by their fields rather than by their ID.
Practice #
Create a Money class holding an amount and a currency. Define __eq__ so that two Money objects with the same amount and currency are equal, and __hash__ so it can be used in a set. Then show that == is True while is is False for two separately created instances, and prove that adding to a set deduplicates them.