Course progress Course outline 24 of 24 lessons available
Python Foundations
Data and Collections
Building Reliable Programs
Modeling with Objects
Professional Python
Advanced Python
Python syntax sends protocol requests
Special methods are not magic decorations. They are hooks with exact contracts. Normal syntax asks an object for a behavior.
-
len(box)
requests
__len__ -
a == b
negotiates with
__eq__ - for item in box requests an iterator
-
box[0]
requests
__getitem__
Implement only protocols your domain can honestly support. For example, __len__ must return a non-negative integer—not the text "2".
Give values two faces and fair comparisons
repr(value) is a stable diagnostic face for developers. str(value) is a readable face for users. If __str__ is missing, Python falls back to __repr__.
class ReadingItem:
def __init__(self, title, minutes):
self.title = title
self.minutes = minutes
def __repr__(self):
return f"ReadingItem({self.title!r}, {self.minutes!r})"
def __str__(self):
return f"{self.title} ({self.minutes} min)"
!r keeps quote boundaries visible. Never place secrets in repr; representations reach logs and tracebacks.
Equality is a negotiation. For an unsupported type, return the singleton NotImplemented, not False and not the exception NotImplementedError:
def __eq__(self, other):
if type(other) is not type(self):
return NotImplemented
return (self.title, self.minutes) == (other.title, other.minutes)
- Ask left can you compare this type?
- NotImplemented let Python ask the other side
- Final result equality may be False; order may error
Define < only when the domain has one meaningful order. A reading priority of (minutes, title) can be honest; a phone number “less than” another phone number usually is not. When contexts need different orders, callers should pass a sorting key.
For ordinary values, equality should remain reflexive, symmetric, and transitive. Choose its fields as part of the domain contract, not merely because they are available.
Hash only stable equality state
Hash containers require one law:
If
a == b, thenhash(a) == hash(b).
Hash exactly the stable fields and type used by equality:
def __hash__(self):
return hash((type(self), self.title, self.minutes))
Those fields cannot change while the object is a dictionary key or set member. A class that defines value equality but remains mutable should be unhashable. When __eq__ is defined without __hash__, Python normally sets __hash__ = None; keep that safe default. Do not restore identity hashing beside field equality, because equal objects could then have different hashes.
Make a container answer without leaking its box
- len / bool size; zero is false
- in clear membership meaning
- iter a fresh iterator every time
- [0] and [1:] items and safe slices
If __bool__ is absent, Python uses __len__: zero is false, positive is true. __contains__ powers in. Each __iter__ call must return a new iterator so nested traversals do not share position. __getitem__ should support integer indexes, negative indexes, slices, and normal IndexError behavior. A slice may return a tuple or a new container, but never the internal mutable list itself.
Build a Pythonic reading queue
Save this complete standard-library project as reading_queue.py:
class ReadingItem:
__slots__ = ("_title", "_minutes", "_sealed")
def __init__(self, title, minutes):
title = title.strip()
if not title:
raise ValueError("title must not be empty")
if isinstance(minutes, bool) or not isinstance(minutes, int) or minutes < 0:
raise ValueError("minutes must be a non-negative integer")
object.__setattr__(self, "_title", title)
object.__setattr__(self, "_minutes", minutes)
object.__setattr__(self, "_sealed", True)
def __setattr__(self, name, value):
if getattr(self, "_sealed", False):
raise AttributeError("ReadingItem is immutable")
object.__setattr__(self, name, value)
@property
def title(self):
return self._title
@property
def minutes(self):
return self._minutes
def __repr__(self):
return f"ReadingItem({self.title!r}, {self.minutes!r})"
def __str__(self):
return f"{self.title} ({self.minutes} min)"
def __eq__(self, other):
if type(other) is not type(self):
return NotImplemented
return (self.title, self.minutes) == (other.title, other.minutes)
def __lt__(self, other):
if type(other) is not type(self):
return NotImplemented
return (self.minutes, self.title) < (other.minutes, other.title)
def __hash__(self):
return hash((type(self), self.title, self.minutes))
class ReadingQueue:
__hash__ = None
def __init__(self, items=()):
self._items = []
for item in items:
self.add(item)
def add(self, item):
if not isinstance(item, ReadingItem):
raise TypeError("item must be a ReadingItem")
self._items.append(item)
def __len__(self):
return len(self._items)
def __contains__(self, item):
return item in self._items
def __iter__(self):
return iter(tuple(self._items))
def __getitem__(self, index):
if isinstance(index, slice):
return tuple(self._items[index])
return self._items[index]
def main():
python = ReadingItem(" Python data model ", 30)
same = ReadingItem("Python data model", 30)
async_item = ReadingItem("Async I/O", 45)
assert repr(python) == "ReadingItem('Python data model', 30)"
assert python == same and hash(python) == hash(same)
assert len({python, same}) == 1
assert python != ("Python data model", 30)
assert sorted([async_item, python]) == [python, async_item]
queue = ReadingQueue([python, async_item])
left, right = iter(queue), iter(queue)
assert next(left) == next(right) == python
assert len(queue) == 2 and bool(queue) and same in queue
assert queue[-1] == async_item and queue[1:] == (async_item,)
try:
hash(queue)
except TypeError:
pass
else:
raise AssertionError("mutable queue was hashable")
print("Tests passed.")
print(f"Queue: {len(queue)} items")
for item in queue:
print(f"- {item}")
if __name__ == "__main__":
main()
Output:
Tests passed.
Queue: 2 items
- Python data model (30 min)
- Async I/O (45 min)
The queue deliberately has no hash, iteration uses a fresh snapshot iterator, and slicing returns a tuple rather than its private list.
Three tiny missions and the handoff
- Two faces. Build
Bookmark(title, page)with a diagnosticreprand reader-facingstr; test a title containing quotes. - Comparison handshake. Make
Duration.__eq__and__lt__returnNotImplementedfor integers. VerifyDuration(5) == 5is false whileDuration(5) < 5raisesTypeError. - Independent shelf. Support length, membership, two simultaneous iterators, negative indexing, and tuple slices over private storage.
Sharp corners: direct special-method calls bypass public intent, NotImplementedError is not the comparison sentinel, invented ordering lies about the domain, mutable hash state breaks lookup, and returning storage leaks mutation.
- I can map public syntax to protocol requests.
- I can separate developer
reprfrom readerstr. - I return
NotImplementedfor unsupported comparison types. - I order only a domain with a real ordering rule.
- I preserve the equality/hash law or leave mutable values unhashable.
- I implement size, truth, membership, fresh iteration, indexing, and slicing.
- My queue passes every assertion and exposes no storage list.
- I completed the three tiny missions.
Next, you will separate concurrency from parallelism and choose threads or processes for the work each can perform safely.