| layout | default |
|---|---|
| title | Python Data Model |
| parent | Lessons |
| nav_order | 1 |
| permalink | /lessons/python-data-model/ |
| course_lesson | true |
| course_index | 01 |
| previous_page | /setup/ |
| previous_title | Setup |
| next_page | /lessons/advanced-oop-design/ |
| next_title | Advanced Object-Oriented Design |
The Python data model is the set of rules that lets your own objects work with familiar syntax such as len(value), value[index], for, in, +, and print().
Think of Python operations as questions on standard forms. len(box) asks, “How many items do you have?” A class can answer by implementing __len__. The caller does not need to know how the box stores its items.
len(value) -> value.__len__()
iter(value) -> value.__iter__()
value[key] -> value.__getitem__(key)
left + right -> left.__add__(right)
repr(value) -> value.__repr__()
Methods surrounded by two underscores are called special methods or dunder methods. Usually call the public operation, such as len(value), instead of calling value.__len__() yourself.
A protocol is a behavior an object promises to support. This playlist supports length, iteration, and membership:
class Playlist:
def __init__(self, songs):
self._songs = list(songs)
def __len__(self):
return len(self._songs)
def __iter__(self):
return iter(self._songs)
def __contains__(self, song):
return song in self._songs
playlist = Playlist(["Sunrise", "Rain Song"])
print(len(playlist))
print("Sunrise" in playlist)
for song in playlist:
print(song)An iterable does not need to inherit from a special parent class. If it supports the operation correctly, Python can use it. This behavior-based style is called duck typing.
Implement only protocols that make sense. A Student does not need + merely because Python allows __add__.
repr(value)is mainly for developers and debugging.str(value)is mainly for readable user-facing text.- If
__str__is missing,str()falls back to__repr__.
class Temperature:
def __init__(self, celsius):
self.celsius = celsius
def __repr__(self):
return f"Temperature(celsius={self.celsius!r})"
def __str__(self):
return f"{self.celsius} °C"
temperature = Temperature(24.5)
print(str(temperature))
print(repr(temperature))!r asks an f-string to use repr() for the embedded value. Never include passwords, tokens, or private personal data in a representation that may enter logs.
Identity asks whether two names point to the same object. Equality asks whether two values should be considered equal.
class Money:
def __init__(self, amount, currency):
self.amount = amount
self.currency = currency
def __eq__(self, other):
if not isinstance(other, Money):
return NotImplemented
return (self.amount, self.currency) == (
other.amount,
other.currency,
)
first = Money(50, "INR")
second = Money(50, "INR")
print(first is second) # False
print(first == second) # TrueReturning NotImplemented gives Python a chance to try the reflected comparison or produce the correct result. It is different from raising NotImplementedError.
A hashable object can be a dictionary key or set member. Its hash must not change while stored, and equal objects must have equal hashes. Therefore, mutable value objects usually should not define __hash__. A frozen dataclass is often a safer choice.
__lt__ handles <. Python does not automatically invent a meaningful total order from equality. functools.total_ordering can create the remaining comparison methods from __eq__ and one ordering method, but generated methods add call overhead and may make tracebacks less direct.
For left + right, Python first tries left.__add__(right). If that returns NotImplemented, it can try right.__radd__(left). Do not silently accept unrelated types.
Common protocol groups include:
| Operation | Main methods |
|---|---|
| Context manager | __enter__, __exit__ |
| Callable object | __call__ |
| Subscription | __getitem__, __setitem__ |
| Attribute fallback | __getattr__ |
| Truth test | __bool__, then __len__ |
__getattr__ runs only after normal attribute lookup fails. __getattribute__ runs for every attribute lookup and is easy to make recursive, so override it only with a strong reason.
In CPython, a name stores a reference to an object, not the object bytes directly. Most objects begin with runtime bookkeeping that includes a type reference and reference count. An operation such as len(value) asks the object's type for the relevant protocol implementation; special-method lookup is generally performed on the type, not through an arbitrary same-named instance attribute.
class Box:
pass
box = Box()
box.__len__ = lambda: 99
# len(box) still raises TypeError because Box itself has no __len__.This is one reason special methods belong on the class. CPython is an implementation of Python, so do not make application correctness depend on a particular reference count or memory address.
class Catalog:
def __init__(self, books):
self._books = {book["id"]: dict(book) for book in books}
def __len__(self):
return len(self._books)
def __iter__(self):
return iter(self._books.values())
def __getitem__(self, book_id):
return self._books[book_id]
def __contains__(self, book_id):
return book_id in self._books
def __repr__(self):
return f"Catalog(book_count={len(self)})"The catalog returns copies during construction, but catalog[id] still returns a mutable internal dictionary. Decide and document whether callers may mutate it. A read-only view, immutable record, or copied result may be safer at a public boundary.
class User:
def __eq__(self, other):
return self.email.lower() == other.email.lower()
def __hash__(self):
return hash(self.email)def __eq__(self, other):
if not isinstance(other, Money):
return Falsedef __iter__(self):
self.position = 0
return selfShow Bug Hunter fixes
- Hash exactly the immutable values used by equality, or make the mutable class unhashable.
- Return
NotImplementedfor an unsupported comparison type. - Return a fresh iterator, such as
iter(self._items), unless the object is intentionally a one-pass iterator.
- Create a
Bagthat supportslen(), iteration, and membership. - Make a memory-efficient
RangeStepiterable without building a list. - Add safe
__repr__and friendly__str__methods toTemperature. - Implement equality for an immutable
Coordinatevalue. - Create a
Catalogthat supportscatalog["book-id"]and raises normalKeyErrorfor a missing ID. - Make a
Scoresortable by numeric value while rejecting unrelated types. - Explain why mutable objects are dangerous dictionary keys.
- Create a callable
TaxCalculatorobject with a configurable rate. - Write a function that accepts any object with an
area()method. - Test every protocol in one class, including empty and unsupported cases.
Show hints
- Delegate to an internal collection. 2. Return an iterator or use
yield. 3. Use!rin the developer representation. 4. A frozen dataclass can help. 5. Delegate subscription to a dictionary. 6. ReturnNotImplementedfor an unrelated value. 7. Think about lookup after mutation. 8. Implement__call__. 9. Depend on behavior, not a concrete class. 10. Call public operations such aslen()in tests.
Show solution ideas
- Store a list and implement the three smallest methods. 2. A generator loop can yield current values until the stop boundary. 3. Keep secrets out and make
reprunambiguous. 4.@dataclass(frozen=True)supplies value equality and a compatible hash. 5. Let the internal dictionary preserve normal lookup behavior. 6. Compare numeric fields after a type check. 7. A changed hash can make the key impossible to find in its original bucket. 8. Returnamount * ratefrom__call__. 9. Callshape.area()using duck typing or document it with a protocol. 10. Include normal, empty, wrong-type, and repeated-iteration tests.
Build a ReadingQueue that supports length, repeated iteration, membership, indexing, and safe developer representation. Document which mutations are allowed and test each public protocol.
Explain how Python translates five familiar operations into protocols, why NotImplemented matters, and when equality makes hashing unsafe.