Magic Methods & the Python Data Model
Reviewed & published by Brayan K
Master Python's powerful magic methods (dunder methods) and understand how the Python data model works under the hood. Learn to create custom classes that behave like built-in types, implement operator overloading, and build professional-grade objects.
Part of the free Python course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
🔥 1. What Are Magic Methods?
Magic methods are methods Python calls automatically when certain operations happen.
| Operation | Magic Method |
|---|---|
| len(obj) | __len__ |
| obj + other | __add__ |
| obj[i] | __getitem__ |
| for x in obj | __iter__ |
| str(obj) | __str__ |
| with obj: | __enter__, __exit__ |
| obj() | __call__ |
They let you define how your objects behave in every situation.
⚙️ 2. Object Construction (Creation & Initialization)
class User:
def __new__(cls, *args, **kwargs):
print("Allocating memory...")
return super().__new__(cls)
def __init__(self, name):
print("Initializing...")
self.name = name
# Create a user
user = User("Alice")
print(f"User name: {user.name}")
# ✅ Expected output:
# Allocating memory...
# Initializing...
# User name: Alice- ✔ custom immutable types
- ✔ singleton patterns
- ✔ building objects from cached pools
🧱 3. Representation Methods
These control how an object looks when printed or displayed.
__repr__ — official representation (for developers)
class User:
def __init__(self, name):
self.name = name
def __repr__(self):
return f"User(name={self.name!r})"
user = User("Alice")
print(repr(user)) # User(name='Alice')
# ✅ Expected output:
# User(name='Alice')class User:
def __init__(self, name):
self.name = name
def __str__(self):
return self.name
user = User("Alice")
print(str(user)) # Alice
print(user) # Alice
# ✅ Expected output:
# Alice
# AliceUsed for f-strings, formatting currencies, dates, metrics.
🔢 4. Numeric Magic Methods (Act Like Numbers!)
Implementing these turns objects into custom numeric types:
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, other):
return Vector(self.x + other.x, self.y + other.y)
def __repr__(self):
return f"Vector({self.x}, {self.y})"
v1 = Vector(1, 2)
v2 = Vector(3, 4)
v3 = v1 + v2
print(v3) # Vector(4, 6)
# ✅ Expected output:
# Vector(4, 6)Also available: Subtraction, Multiplication, Division, Modulo, Power, Negation
- ✔ game engines
- ✔ simulation math
- ✔ ML tensor wrappers (PyTorch does this heavily)
🧠 5. Comparisons (Sorting, Ordering, Equality)
- __eq__ — equals
- __ne__ — not equal
- __lt__ — less than
- __gt__ — greater than
class Player:
def __init__(self, name, score):
self.name = name
self.score = score
def __lt__(self, other):
return self.score < other.score
def __repr__(self):
return f"{self.name}: {self.score}"
players = [Player("Alice", 100), Player("Bob", 150), Player("Charlie", 75)]
sorted_players = sorted(players)
for p in sorted_players:
print(p)
# ✅ Expected output:
# Charlie: 75
# Alice: 100
# Bob: 150- ✔ leaderboard systems
- ✔ sorting objects
- ✔ priority queues
- ✔ ranking algorithms
📦 6. Container Protocol (Behaving Like Lists & Dicts)
Controls what len(obj) returns.
class MyList:
def __init__(self, data):
self.data = data
def __len__(self):
return len(self.data)
def __getitem__(self, index):
return self.data[index]
my_list = MyList([1, 2, 3, 4, 5])
print(f"Length: {len(my_list)}")
print(f"First item: {my_list[0]}")
print(f"Slice: {my_list[1:3]}")
# ✅ Expected output:
# Length: 5
# First item: 1
# Slice: [2, 3]🔄 7. Iterable Protocol (for...in Loops)
To make an object iterable:
- __iter__ — return an iterator
- __next__ — steps through values
class Countdown:
def __init__(self, start):
self.start = start
def __iter__(self):
self._current = self.start
return self
def __next__(self):
if self._current <= 0:
raise StopIteration
self._current -= 1
return self._current + 1
for num in Countdown(5):
print(num)
# ✅ Expected output:
# 5
# 4
# 3
# 2
# 1- ✔ custom data streams
- ✔ tokenizers
- ✔ ML data loaders
- ✔ generators
🧪 Worked Example — One Class, Seven Magic Methods
You have met the pieces separately. Here they are working together on one small class, so you can see what "the data model" actually buys you: a Playlist that you can print, measure with len(), index, slice, search with in, loop over, and join with + — using nothing but ordinary Python syntax.
Read the comments before you run it. Each one names the method Python calls and what it hands back.
# A Playlist that behaves like a built-in Python container.
# Every method below is a hook: you never call it yourself —
# Python calls it for you when you use the matching syntax.
class Playlist:
def __init__(self, name, tracks):
# Runs straight after the object is created. Stores data, returns nothing.
self.name = name
self.tracks = list(tracks) # copy the list so the caller can't mutate ours
def __repr__(self):
# The DEVELOPER view. Aim for text you could paste back into Python.
# !r applies repr() to the value, so strings keep their quotes.
return f"Playlist(name={self.name!r}, tracks={self.tracks!r})"
def __str__(self):
# The USER view — what print() shows. Leave it out and print() falls
# back to __repr__, which is why __repr__ is the one you always write.
return f"{self.name} ({len(self.tracks)} tracks)"
def __len__(self):
# Called by len(playlist). Must return a non-negative whole number.
return len(self.tracks)
def __getitem__(self, index):
# Called by playlist[0] AND by playlist[0:2] (index is then a slice).
# Bonus: because this exists, `for track in playlist` works too.
return self.tracks[index]
def __contains__(self, track):
# Called by `track in playlist`.
return track in self.tracks
def __add__(self, other):
# Called by playlist_a + playlist_b. Return a NEW object — never edit
# self here, or `a + b` would secretly change a.
return Playlist(f"{self.name} + {other.name}", self.tracks + other.tracks)
chill = Playlist("Chill", ["Weightless", "Nightswimming"])
focus = Playlist("Focus", ["Clair de Lune"])
print(str(chill)) # __str__
print(repr(chill)) # __repr__
print(len(chill)) # __len__
print(chill[0]) # __getitem__ with an int
print(chill[0:2]) # __getitem__ with a slice -> a plain list
print("Clair de Lune" in chill) # __contains__
for track in focus: # iteration, powered by __getitem__
print("-", track)
both = chill + focus # __add__
print(both) # __str__ of the brand-new object
# ✅ Expected output:
# Chill (2 tracks)
# Playlist(name='Chill', tracks=['Weightless', 'Nightswimming'])
# 2
# Weightless
# ['Weightless', 'Nightswimming']
# False
# - Clair de Lune
# Chill + Focus (3 tracks)Two details worth noticing. chill[0:2] returns a plain list, not a Playlist — your __getitem__ passed the slice straight to self.tracks, so you get whatever a list gives back. And "Clair de Lune" in chill is False because that track lives in focus, not chill.
🎯 Your Turn — Make a Basket Act Like a Container
Everything except the three magic method bodies is written for you. Fill in the blanks marked ___, run it, and compare against the expected output at the bottom of the file.
Hint for the third blank: inside the class, len(self) calls your own __len__ — you do not need to reach for self.items again.
# 🎯 YOUR TURN — a shopping Basket that behaves like a real container
# Fill in the three blanks marked ___
class Basket:
def __init__(self, items):
self.items = list(items) # already written for you
def __len__(self):
# 👉 replace ___ so that len(basket) reports how many items are inside
return ___
def __getitem__(self, index):
# 👉 replace ___ so that basket[1] returns the second item
return ___
def __str__(self):
# 👉 replace ___ with the item count (len(self) calls your __len__)
return f"Basket of ___ items"
basket = Basket(["apple", "bread", "milk"])
print(len(basket)) # uses __len__
print(basket[1]) # uses __getitem__
print(basket) # uses __str__
for item in basket: # looping falls back to __getitem__
print("-", item)
# ✅ Expected output:
# 3
# bread
# Basket of 3 items
# - apple
# - bread
# - milk🧩 8. Callable Objects (Pretend to Be Functions)
class Adder:
def __init__(self, n):
self.n = n
def __call__(self, x):
return x + self.n
# Now:
add5 = Adder(5)
print(add5(10)) # 15
print(add5(20)) # 25
# ✅ Expected output:
# 15
# 25- ✔ ML models (PyTorch layers implement __call__)
- ✔ function wrappers
- ✔ on-the-fly factories
- ✔ middleware
🔐 9. Attribute Access Control
- __getattr__ — fallback for missing attributes
- __getattribute__ — intercept ALL attribute access
- __setattr__ — intercept setting attributes
- __delattr__ — intercept deletion
class FlexibleObject:
def __init__(self):
self.data = {}
def __getattr__(self, name):
return f"{name} is not defined"
obj = FlexibleObject()
print(obj.name) # name is not defined
print(obj.anything) # anything is not defined
# ✅ Expected output:
# name is not defined
# anything is not defined- ✔ lazy loading
- ✔ proxies (database lazy models in Django)
- ✔ dynamic API clients
- ✔ configuration wrappers
🧱 10. Boolean Value
class Container:
def __init__(self, items):
self.items = items
def __bool__(self):
return len(self.items) > 0
empty = Container([])
full = Container([1, 2, 3])
if full:
print("Container has items")
if not empty:
print("Container is empty")
# ✅ Expected output:
# Container has items
# Container is emptyNow objects behave logically in:
- if statements
- conditional checks
🧊 11. Context Managers
class Timer:
def __enter__(self):
import time
self.start = time.time()
print("Starting timer...")
return self
def __exit__(self, exc_type, exc, tb):
import time
elapsed = time.time() - self.start
print(f"Elapsed: {elapsed:.4f} seconds")
with Timer():
# Some operation
total = sum(range(1000000))
print(f"Sum: {total}")- ✔ file handling
- ✔ database sessions
- ✔ locking systems
- ✔ resource guards
🧱 12. Copy & Serialization Hooks
- __deepcopy__
- __getstate__
- __setstate__
- how objects are serialized
- custom caching behavior
- saving/loading ML models
- multiprocessing transfers
🔥 13. Operator Overloading (Make Objects Behave Like Built-ins)
Python lets you define how your objects react to operators.
- __add__(self, other) — +
- __sub__(self, other) — -
- __mul__(self, other) — *
- __truediv__(self, other) — /
- __floordiv__(self, other) — //
- __mod__(self, other) — %
- __pow__(self, other) — **
class Vector:
def __init__(self, x, y):
self.x, self.y = x, y
def __add__(self, other):
return Vector(self.x + other.x, self.y + other.y)
def __sub__(self, other):
return Vector(self.x - other.x, self.y - other.y)
def __mul__(self, scalar):
return Vector(self.x * scalar, self.y * scalar)
def __repr__(self):
return f"Vector({self.x}, {self.y})"
v1 = Vector(3, 4)
v2 = Vector(1, 2)
print(f"v1 + v2 = {v1 + v2}")
print(f"v1 - v2 = {v1 - v2}")
print(f"v1 * 2 = {v1 * 2}")
# ✅ Expected output:
# v1 + v2 = Vector(4, 6)
# v1 - v2 = Vector(2, 2)
# v1 * 2 = Vector(6, 8)Operator overloading is used everywhere:
- ✔ physics simulations
- ✔ ML tensors (PyTorch / TensorFlow)
- ✔ financial modeling
- ✔ vector graphics engines
🧱 14. Rich Comparisons — Smarter Sorting & Ranking
If you must support ordering, implement the following:
- __lt__(self, other) — <
- __le__(self, other) — <=
- __gt__(self, other) — >
- __ge__(self, other) — >=
- __eq__(self, other) — ==
- __ne__(self, other) — !=
Example: sortable players by score
class Player:
def __init__(self, name, score):
self.name = name
self.score = score
def __lt__(self, other):
return self.score < other.score
def __eq__(self, other):
return self.score == other.score
def __repr__(self):
return f"{self.name}({self.score})"
p1 = Player("Alice", 100)
p2 = Player("Bob", 150)
print(f"{p1} < {p2}: {p1 < p2}")
print(f"{p1} == {p2}: {p1 == p2}")
# ✅ Expected output:
# Alice(100) < Bob(150): True
# Alice(100) == Bob(150): False🧩 15. Sequence Protocol (Full Custom List Behavior)
To behave like a real sequence, implement:
- __len__(self)
- __getitem__(self, index)
- __setitem__(self, index, value)
- __delitem__(self, index)
- __contains__(self, item)
class ReadOnlySeq:
def __init__(self, data):
self._data = tuple(data)
def __len__(self):
return len(self._data)
def __getitem__(self, i):
return self._data[i]
def __contains__(self, item):
return item in self._data
seq = ReadOnlySeq([1, 2, 3, 4, 5])
print(f"Length: {len(seq)}")
print(f"First: {seq[0]}")
print(f"3 in seq: {3 in seq}")
# ✅ Expected output:
# Length: 5
# First: 1
# 3 in seq: True🧠 16. Iterable vs Iterator (Clear Distinction)
Has __iter__(), returns an iterator.
Has __iter__() and __next__().
- for loops depend on it
- generator pipelines rely on it
- async systems use async iterators
- ML dataloaders use custom iterables
🧱 17. Custom Iterators (Full Control of Data Streams)
class Countdown:
def __init__(self, n):
self.n = n
def __iter__(self):
return self
def __next__(self):
if self.n <= 0:
raise StopIteration
self.n -= 1
return self.n + 1
for num in Countdown(5):
print(num)
# ✅ Expected output:
# 5
# 4
# 3
# 2
# 1- ✔ streaming input
- ✔ chunked database queries
- ✔ on-the-fly data generation
- ✔ scraper crawls
- ✔ batching ML data
🔐 18. Attribute Access Magic
Python gives complete control over attribute access.
- __getattr__(self, name) — Occurs when attribute is missing
- __getattribute__(self, name) — Intercepts every attribute lookup
- __setattr__(self, name, value) — Intercepts setting attributes
- __delattr__(self, name) — Intercepts deletion
class DynamicObject:
def __getattr__(self, name):
return f"{name} does not exist"
obj = DynamicObject()
print(obj.foo) # foo does not exist
print(obj.bar) # bar does not exist
# ✅ Expected output:
# foo does not exist
# bar does not exist🧩 19. Emulating Functions With __call__
Anything can behave like a function.
class Multiply:
def __init__(self, factor):
self.factor = factor
def __call__(self, x):
return x * self.factor
# Usage:
double = Multiply(2)
triple = Multiply(3)
print(double(10)) # 20
print(triple(10)) # 30
# ✅ Expected output:
# 20
# 30- neural network layers
- preprocessing pipelines
- middleware wrappers
- job schedulers
- configurable callbacks
🧱 20. Context Manager Magic
- __enter__(self)
- __exit__(self, exc_type, exc, tb)
class DatabaseConnection:
def __enter__(self):
print("Opening connection...")
return self
def __exit__(self, exc, val, tb):
print("Closing connection...")
def query(self, sql):
print(f"Executing: {sql}")
with DatabaseConnection() as db:
db.query("SELECT * FROM users")
# ✅ Expected output:
# Opening connection...
# Executing: SELECT * FROM users
# Closing connection...🔥 21. Descriptors — The Hidden Power Behind @property
Descriptors define how attributes behave.
A descriptor is any object defining one or more of:
- __get__(self, instance, owner)
- __set__(self, instance, value)
- __delete__(self, instance)
class Logged:
def __set_name__(self, owner, name):
self.name = name
def __get__(self, instance, owner):
if instance is None:
return self
print(f"Accessed {self.name}")
return instance.__dict__.get(self.name)
def __set__(self, instance, value):
print(f"Setting {self.name} to {value}")
instance.__dict__[self.name] = value
class User:
name = Logged()
u = User()
u.name = "Alice"
print(u.name)
# ✅ Expected output:
# Setting name to Alice
# Accessed name
# Alice🧬 22. Properties Built on Top of Descriptors
property is just a wrapper around descriptors.
class User:
def __init__(self, name):
self._name = name
@property
def name(self):
return self._name
@name.setter
def name(self, value):
if not value:
raise ValueError("Name cannot be empty")
self._name = value
u = User("Alice")
print(u.name)
u.name = "Bob"
print(u.name)
# ✅ Expected output:
# Alice
# Bob⚡ 23. Slots — Memory-Efficient Objects
Use __slots__ to avoid dynamic dictionaries:
class Point:
__slots__ = ("x", "y")
def __init__(self, x, y):
self.x = x
self.y = y
p = Point(1, 2)
print(f"Point: ({p.x}, {p.y})")
# This would raise AttributeError:
# p.z = 3 # Can't add new attributes!
# ✅ Expected output:
# Point: (1, 2)- ✔ lower memory
- ✔ faster attribute access
- ✔ prevents accidental attributes
⚡ 24. Understanding the Python Object Lifecycle
Every Python object goes through:
- Allocation → __new__
- Initialization → __init__
- Destruction → __del__
class Singleton:
_instance = None
def __new__(cls):
if not cls._instance:
print("Creating new instance")
cls._instance = super().__new__(cls)
else:
print("Returning existing instance")
return cls._instance
s1 = Singleton()
s2 = Singleton()
print(f"Same object: {s1 is s2}")
# ✅ Expected output:
# Creating new instance
# Returning existing instance
# Same object: True🔥 25. Metaclasses — The Most Advanced Python Feature
A metaclass is the class of a class.
Classes create objects. Metaclasses create classes.
class Meta(type):
def __new__(mcls, name, bases, attrs):
print(f"Creating class {name}")
return super().__new__(mcls, name, bases, attrs)
# Usage:
class User(metaclass=Meta):
pass
class Admin(User):
pass
# ✅ Expected output:
# Creating class User
# Creating class AdminWhat metaclasses are used for IN REAL SYSTEMS
- ✔ Django ORM
- ✔ SQLAlchemy Models
- ✔ Pydantic / FastAPI Models
- ✔ TensorFlow Layers
- ✔ Enum internals
- ✔ Plugin systems
- ✔ Service registries
🧩 26. Class Decorators vs Metaclasses
Class decorators modify the class after it's created:
registry = []
def register(cls):
registry.append(cls)
return cls
@register
class User:
pass
@register
class Admin:
pass
print(f"Registered classes: {registry}")
# ✅ Expected output:
# Registered classes: [<class '__main__.User'>, <class '__main__.Admin'>]Metaclasses modify the class while being created.
When to use what:
- Class decorator → simple modification
- Metaclass → structural modification
🧠 27. Emulating Containers (Full Custom Collections)
class BoundedList:
def __init__(self, limit):
self.data = []
self.limit = limit
def __len__(self):
return len(self.data)
def __getitem__(self, i):
return self.data[i]
def append(self, val):
if len(self.data) >= self.limit:
raise ValueError("List full")
self.data.append(val)
def __repr__(self):
return f"BoundedList({self.data})"
bl = BoundedList(3)
bl.append(1)
bl.append(2)
bl.append(3)
print(bl)
# bl.append(4) # Would raise ValueError
# ✅ Expected output:
# BoundedList([1, 2, 3])🧱 28. Making Your Objects Hashable
For objects to be used as dictionary keys OR in sets:
class Point:
__slots__ = ("x", "y")
def __init__(self, x, y):
self.x = x
self.y = y
def __hash__(self):
return hash((self.x, self.y))
def __eq__(self, other):
return self.x == other.x and self.y == other.y
def __repr__(self):
return f"Point({self.x}, {self.y})"
# Now can use as dict keys
points = {Point(0, 0): "origin", Point(1, 1): "diagonal"}
print(points[Point(0, 0)])
# ✅ Expected output:
# origin🔍 29. Overriding Truthiness & Boolean Behavior
class Connection:
def __init__(self, status):
self.status = status
def __bool__(self):
return self.status == "ready"
conn1 = Connection("ready")
conn2 = Connection("disconnected")
if conn1:
print("Connection 1 is ready")
if not conn2:
print("Connection 2 is not ready")
# ✅ Expected output:
# Connection 1 is ready
# Connection 2 is not ready🧬 30. Controlling String Representations
class User:
def __init__(self, id, name):
self.id = id
self.name = name
def __repr__(self):
return f"User(id={self.id})"
def __str__(self):
return self.name
u = User(1, "Alice")
print(repr(u)) # User(id=1)
print(str(u)) # Alice
print(u) # Alice (uses __str__)
# ✅ Expected output:
# User(id=1)
# Alice
# Alice⚙️ 31-34. Advanced Topics
Additional advanced magic method patterns:
- Dynamic Attribute Computation - Using __getattr__ for lazy loading
- Proxy Objects - Forwarding magic methods
- Custom Number Types - Money, Temperature classes
- Full Domain Models - Complete custom types
class Money:
def __init__(self, amount):
self.amount = amount
def __add__(self, other):
return Money(self.amount + other.amount)
def __mul__(self, rate):
return Money(self.amount * rate)
def __repr__(self):
return "Money(" + str(self.amount) + ")"
m1 = Money(100)
m2 = Money(50)
print("m1 + m2 =", m1 + m2)
print("m1 * 1.5 =", m1 * 1.5)
# ✅ Expected output:
# m1 + m2 = Money(150)
# m1 * 1.5 = Money(150.0)🎓 35. Final Summary — You Now Understand the Entire Python Data Model
By mastering this lesson, you now understand:
- ✔ operator overloading
- ✔ full comparison system
- ✔ container emulation
- ✔ iterator/iterable protocols
- ✔ attribute access magic
- ✔ callable classes
- ✔ context manager internals
- ✔ descriptor protocol
- ✔ property internals
- ✔ slots memory optimization
- ✔ object lifecycle
- ✔ metaclass architecture
- ✔ custom domain-specific types
- ✔ proxy patterns
- ✔ advanced debugging behaviors
You now write Python the way framework authors, not beginners, write it.
This knowledge places you firmly at the top 1% of Python engineers.
📋 Quick Reference — Magic Methods
| Method | Triggered by |
|---|---|
| __init__(self) | Object creation: MyClass() |
| __repr__(self) | repr() and interactive shell display |
| __len__(self) | len(obj) |
| __getitem__(self, key) | obj[key] indexing |
| __enter__ / __exit__ | with obj: context manager |
🎯 Mini-Challenge: A Temperature Type That Sorts Itself
No filled-in logic this time — only an outline. Write a Temperature class that stores a reading in degrees Celsius, prints as Temperature(21.5), treats two readings of the same value as equal, and can be handed straight to sorted().
One thing that surprises people: sorted() only needs __lt__. It never calls __gt__ — it works out the whole ordering from "less than" alone.
# 🎯 MINI-CHALLENGE: a Temperature type that sorts itself
#
# 1. class Temperature with __init__(self, celsius) storing self.celsius
# 2. __repr__ returning text like Temperature(21.5)
# 3. __eq__ — True when the two readings hold the same celsius value
# 4. __lt__ — True when self.celsius is lower than other.celsius
# 5. Then run these three lines:
# readings = [Temperature(21.5), Temperature(-3), Temperature(8)]
# print(sorted(readings))
# print(Temperature(8) == Temperature(8))
#
# ✅ Expected output:
# [Temperature(-3), Temperature(8), Temperature(21.5)]
# True
# your code hereStuck on __repr__? It must return a string, not print one — returning None by accident is the most common slip here.
🎉 Great work! You've completed this lesson.
You now control how your objects respond to built-in Python operations — the same protocol powering NumPy, pandas, and SQLAlchemy.
Practice quiz
Which magic method is called when you do len(obj)?
- __size__
- __count__
- __len__
- __length__
Answer: __len__. Python calls __len__ to implement len(obj).
Which magic method implements obj + other?
- __add__
- __plus__
- __sum__
- __concat__
Answer: __add__. __add__ defines behavior for the + operator.
What does __repr__ provide?
- A user-friendly display for end users
- The length of the object
- A boolean value
- An official, developer-facing representation of the object
Answer: An official, developer-facing representation of the object. __repr__ is the official representation aimed at developers; __str__ is the user-friendly one.
Which two magic methods make an object work in a for...in loop as its own iterator?
- __loop__ and __step__
- __iter__ and __next__
- __getitem__ and __len__
- __enter__ and __exit__
Answer: __iter__ and __next__. An iterator implements __iter__ (returns self) and __next__ (yields values, raising StopIteration to end).
Which magic method lets an instance be called like a function, e.g. obj(10)?
- __call__
- __invoke__
- __run__
- __exec__
Answer: __call__. __call__ makes an instance callable, so obj(10) runs obj.__call__(10).
Which pair of magic methods implements the 'with obj:' context manager protocol?
- __open__ and __close__
- __start__ and __stop__
- __enter__ and __exit__
- __begin__ and __end__
Answer: __enter__ and __exit__. Context managers implement __enter__ (on entry) and __exit__ (on exit).
Which magic method controls the result of len() AND is part of the sequence protocol alongside __getitem__?
- __count__
- __len__
- __items__
- __size__
Answer: __len__. __len__ controls len(obj) and, with __getitem__, forms the core sequence protocol.
To use your objects as dictionary keys or in sets, which methods must you implement?
- __key__ and __set__
- __dict__ and __getitem__
- __index__ and __len__
- __hash__ and __eq__
Answer: __hash__ and __eq__. Hashable objects need __hash__ and a matching __eq__ so they behave correctly in dicts and sets.
Which method is the fallback called only when a normal attribute lookup fails?
- __getattribute__
- __getattr__
- __getitem__
- __get__
Answer: __getattr__. __getattr__ is the fallback for missing attributes; __getattribute__ intercepts EVERY attribute access.
What is a metaclass?
- A class with only static methods
- A class that cannot be instantiated
- The class of a class — it creates classes (default is type)
- A copy of a class
Answer: The class of a class — it creates classes (default is type). A metaclass is the class of a class; classes create objects, metaclasses create classes (default metaclass is type).
Continue this course
- Previous: Data Classes & Advanced Class Patterns
- Next: Operator Overloading & Custom Behaviours — Make your objects work with +, -, *, and comparison operators
- Quick reference: Python cheat sheet › Classes