🎓 Velnov

🐍 Python · Topic 16 of 20

Magic Methods & Advanced OOP — Python Questions with Answers

Start with the study notes on Magic Methods & Advanced OOP (definitions, examples and a quick revision), then work through 30 practice questions, each with the correct answer and a clear explanation of why it is right. When you are ready, take this topic as an interactive quiz to test yourself.

4 easy · 14 medium · 12 hard · Part of Python: Master Python from basics to advanced - 20 topics.

Topic overview

Magic Methods and Advanced OOP – making objects feel built-in

1. What are magic methods?

Magic methods (or dunder methods, for “double underscore”) have names like __str__ and __len__. Python calls them automatically: print(obj) calls __str__, len(obj) calls __len__, and a + b calls __add__.

2. __str__ and __repr__

class Book:
    def __init__(self, title):
        self.title = title
    def __str__(self):
        return f"Book: {self.title}"
    def __repr__(self):
        return f"Book({self.title!r})"

b = Book("Dune")
print(b)
print([b])
OutputBook: Dune
[Book('Dune')]

Remember: If you define only one, define __repr__; str() falls back to it.

3. Operators: __add__, __eq__, __lt__

Defining these lets your objects work with +, == and <. This is called operator overloading. A single __lt__ also makes sorted() work.

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 __eq__(self, other):
        return (self.x, self.y) == (other.x, other.y)
    def __lt__(self, other):
        return self.x < other.x
    def __repr__(self):
        return f"Vector({self.x}, {self.y})"

print(Vector(1, 2) + Vector(3, 4))
print(Vector(1, 2) == Vector(1, 2))
print(sorted([Vector(5, 0), Vector(2, 0)]))
OutputVector(4, 6)
True
[Vector(2, 0), Vector(5, 0)]

Example: __rmul__ handles your object on the right side, so that 3 * v works as well as v * 3.

4. Containers: __len__, __getitem__, __contains__

class Playlist:
    def __init__(self, songs):
        self.songs = songs
    def __len__(self):
        return len(self.songs)
    def __getitem__(self, i):
        return self.songs[i]
    def __contains__(self, song):
        return song in self.songs

p = Playlist(["a", "b", "c"])
print(len(p), p[1], "b" in p)
print(bool(Playlist([])))     # empty, because __len__ is 0
Output3 b True
False

5. Callable objects: __call__

__call__ lets you call an object like a function.

class Greeter:
    def __init__(self, greeting):
        self.greeting = greeting
    def __call__(self, name):
        return f"{self.greeting} {name}"

hello = Greeter("Hi")
print(hello("Ana"))
OutputHi Ana

6. Private by convention

class Account:
    def __init__(self):
        self.__pin = 1234

a = Account()
print(hasattr(a, "__pin"), a._Account__pin)
OutputFalse 1234

7. @property and setters

@property turns a method into a computed attribute, read without brackets. A matching setter can check values when they are assigned.

class Circle:
    def __init__(self, r):
        self.r = r
    @property
    def area(self):
        return round(3.14159 * self.r ** 2, 2)

class Person:
    @property
    def age(self):
        return self._age
    @age.setter
    def age(self, value):
        if value < 0:
            raise ValueError("age can't be negative")
        self._age = value

print(Circle(2).area)
p = Person()
p.age = 30
print(p.age)
Output12.57
30

8. @staticmethod and @classmethod

class Date:
    def __init__(self, d, m, y):
        self.d, self.m, self.y = d, m, y
    @classmethod
    def from_string(cls, text):
        d, m, y = map(int, text.split("-"))
        return cls(d, m, y)
    @staticmethod
    def is_leap(year):
        return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)

date = Date.from_string("03-07-2026")
print(date.y, Date.is_leap(2024))
Output2026 True

9. __new__, singletons and dataclasses

from dataclasses import dataclass

@dataclass
class Point:
    x: int
    y: int

p = Point(1, 2)
print(p)
print(p == Point(1, 2))
OutputPoint(x=1, y=2)
True

Remember: __enter__ and __exit__ make a class work with the with statement (see Advanced Concepts).

10. Common misconceptions

MisconceptionCorrect understanding
print([obj]) uses __str__.Lists show their items with __repr__.
__pin makes an attribute truly private.It is only name-mangled; obj._Class__pin still reaches it.
A property needs brackets: c.area().A property is read like an attribute: c.area.
__init__ creates the object.__new__ creates it; __init__ fills in its attributes.
You must write getters for every attribute.Use plain attributes and add @property only when logic is needed.

11. Quick revision

Dunder method
Double-underscore method Python calls automatically.
__str__
Friendly display for users.
__repr__
Developer view; used inside lists.
__add__ / __eq__ / __lt__
+ / == / <.
__len__ / __getitem__
len(obj) / obj[i].
@property
A method read like an attribute.
@classmethod
Receives the class (cls); alternative constructors.
@dataclass
Writes __init__, __repr__ and __eq__ for you.

12. Test your understanding

  1. Which method controls what print(obj) shows?

    Show answer

    __str__

  2. Why does print([obj]) look different from print(obj)?

    Show answer

    Lists use __repr__ for their items.

  3. Which method makes v1 + v2 work?

    Show answer

    __add__

  4. Which method makes len(obj) work?

    Show answer

    __len__

  5. What does a single leading underscore (_x) mean?

    Show answer

    Internal by convention; not enforced.

  6. What does @property do?

    Show answer

    Lets you read a computed value without brackets, like an attribute.

  7. What does a classmethod receive as its first argument?

    Show answer

    cls, the class itself.

  8. What does @dataclass generate?

    Show answer

    __init__, __repr__ and __eq__.

Key takeaway: Magic methods let your objects work with print, len, +, == and sorting; properties, classmethods and dataclasses keep classes clean.

Notes written by Velnov. Every code example with an output was run in Python 3 to check that the output shown is exactly what Python prints.

Key concepts in this topic

Dunder methodsProtocols__str__print()__repr__Debugging__len____eq__EqualityOperator overloading__add____lt__sorted()total_ordering__getitem__Indexing__contains__in operator__call__CallablesEncapsulationConventionsName mangling@property

Jump to: Easy · Medium · Hard

Easy questions (4)

Q1. easy

What are 'magic methods' (dunder methods)?

  1. A Methods with double underscores (__str__, __len__...) that hook your class into Python's built-in behavior
  2. B Methods that run magically fast
  3. C Hidden methods you can't call
  4. D Deprecated Python 2 methods

Answer: A — Methods with double underscores (__str__, __len__...) that hook your class into Python's built-in behavior

'Dunder' = Double UNDERscore. Python calls them automatically: print(obj) calls __str__, len(obj) calls __len__, a + b calls __add__. Define them to make your objects behave like built-ins.

Dunder methodsProtocols

Q2. easy

Which method controls what print(obj) displays?

class Book:
    def __init__(self, title):
        self.title = title
    def __str__(self):
        return f"Book: {self.title}"

print(Book("Dune"))   # Book: Dune
  1. A __str__
  2. B __print__
  3. C __show__
  4. D __display__

Answer: A — __str__

__str__ returns the human-readable text for your object - used by print(), str() and f-strings. Without it you get the ugly <__main__.X object at 0x...>.

__str__print()

Q21. easy

str(obj) and f"{obj}" both call which method?

  1. A __str__
  2. B __format__ only
  3. C __text__
  4. D __value__

Answer: A — __str__

str(), print() and f-string interpolation all route through __str__ (f-strings via __format__, which defaults to __str__). One method, consistent display everywhere.

__str__f-strings

Q27. easy

dir(obj) is useful because it...

print(dir([]))
# [..., 'append', 'clear', 'copy', 'count', ...]
  1. A Lists all attributes and methods of the object - including inherited and dunder ones
  2. B Shows the object's folder on disk
  3. C Deletes the object
  4. D Sorts the object

Answer: A — Lists all attributes and methods of the object - including inherited and dunder ones

dir() is your class X-ray: every method (own, inherited, magic) in one list. Pair with help(obj.method) to read docs.

dir()Introspection

Medium questions (14)

Q4. medium

Which method makes len(obj) work for your class?

class Cart:
    def __init__(self):
        self.items = []
    def __len__(self):
        return len(self.items)

print(len(Cart()))   # 0
  1. A __len__
  2. B __size__
  3. C __count__
  4. D length()

Answer: A — __len__

len(cart) simply calls cart.__len__(). Return an integer - typically the length of an internal collection.

__len__Protocols

Q5. medium

Which method makes == compare your objects by CONTENT?

class Point:
    def __init__(self, x, y):
        self.x, self.y = x, y
    def __eq__(self, other):
        return self.x == other.x and self.y == other.y

print(Point(1, 2) == Point(1, 2))   # True
  1. A __eq__
  2. B __equals__
  3. C __compare__
  4. D __is__

Answer: A — __eq__

a == b calls a.__eq__(b). Without it, == falls back to identity (same object?). Define it to compare data instead.

__eq__Equality

Q6. medium

Which method lets you write v1 + v2 for your own Vector class?

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)

v = Vector(1, 2) + Vector(3, 4)
print(v.x, v.y)   # 4 6
  1. A __add__
  2. B __plus__
  3. C __sum__
  4. D add()

Answer: A — __add__

v1 + v2 calls v1.__add__(v2) - operator overloading. Return a NEW object with the combined values. Siblings: __sub__ (-), __mul__ (*), __truediv__ (/).

Operator overloading__add__

Q8. medium

Which method makes square brackets work: playlist[0]?

class Playlist:
    def __init__(self, songs):
        self.songs = songs
    def __getitem__(self, i):
        return self.songs[i]

p = Playlist(["a", "b"])
print(p[1])          # b
for song in p: ...    # even iteration works!
  1. A __getitem__
  2. B __index__
  3. C __bracket__
  4. D __at__

Answer: A — __getitem__

obj[key] calls obj.__getitem__(key). Bonus: defining it also enables for-loops and 'in' over your object (Python iterates by index automatically).

__getitem__Indexing

Q9. medium

Which method powers 'x in obj'?

class Playlist:
    def __contains__(self, song):
        return song in self.songs
  1. A __contains__
  2. B __in__
  3. C __has__
  4. D __member__

Answer: A — __contains__

'song in playlist' calls playlist.__contains__(song). Without it Python falls back to iterating and comparing - __contains__ lets you do it smarter/faster.

__contains__in operator

Q11. medium

What does a SINGLE leading underscore mean: self._balance?

class Account:
    def __init__(self):
        self._balance = 0   # internal by convention
  1. A A convention: 'internal - please don't touch from outside' (not enforced)
  2. B Truly private - Python blocks access
  3. C A syntax error
  4. D The attribute is constant

Answer: A — A convention: 'internal - please don't touch from outside' (not enforced)

_name is a gentleman's agreement - the language doesn't stop anyone, but tools and developers treat it as private API. Python's philosophy: 'we're all consenting adults'.

EncapsulationConventions

Q15. medium

What is a @staticmethod?

class MathUtils:
    @staticmethod
    def add(a, b):        # no self!
        return a + b

print(MathUtils.add(2, 3))   # 5
  1. A A method that gets NO automatic self/cls - a plain function stored in the class for organization
  2. B A method that can't be changed
  3. C A method shared with other classes
  4. D A method that runs at import

Answer: A — A method that gets NO automatic self/cls - a plain function stored in the class for organization

Static methods neither read nor modify instance/class state - they just belong logically to the class. Call via the class or an instance: MathUtils.add(2, 3).

@staticmethod

Q19. medium

Which method decides your object's truthiness in 'if obj:'?

class Cart:
    def __init__(self):
        self.items = []
    def __len__(self):
        return len(self.items)

if not Cart():
    print("empty cart")   # prints!
  1. A __bool__ (falling back to __len__ != 0 if absent)
  2. B __true__
  3. C __if__
  4. D Objects are always False

Answer: A — __bool__ (falling back to __len__ != 0 if absent)

if cart: calls cart.__bool__() - or, if undefined, treats the object as truthy unless __len__ returns 0. That's why empty custom containers naturally read as falsy once __len__ exists.

__bool__Truthiness

Q20. medium

Why is c.area (a @property) better Python style than c.get_area()?

  1. A Attribute syntax is cleaner, and you can start as a plain attribute and add logic later WITHOUT breaking callers
  2. B get_ methods are illegal
  3. C Properties are faster
  4. D No reason - Java style is preferred

Answer: A — Attribute syntax is cleaner, and you can start as a plain attribute and add logic later WITHOUT breaking callers

In Java you write getters defensively from day one. Python lets you expose a plain attribute now and transparently swap in a @property later - same c.area syntax for callers. So write simple attributes first; add properties only when logic is needed.

@propertyAPI design

Q23. medium

Does Python have truly PRIVATE attributes like Java's private keyword?

  1. A No - only conventions (_x) and name mangling (__x); everything remains reachable
  2. B Yes - the private keyword
  3. C Yes - via @private
  4. D Only in classes marked sealed

Answer: A — No - only conventions (_x) and name mangling (__x); everything remains reachable

Python chose openness: nothing is enforced-private. _x says 'don't', __x adds mangling, but a determined caller can reach anything. Discipline over locks.

Encapsulation philosophy

Q24. medium

Trace: class Money: def __init__(self, amt): self.amt = amt def __add__(self, other): return Money(self.amt + other.amt) def __str__(self): return f"Rs.{self.amt}" print(Money(100) + Money(50))

  1. A Rs.150
  2. B 150
  3. C Money(150)
  4. D Error

Answer: A — Rs.150

+ triggers __add__ (returning Money(150)); print then triggers __str__ ('Rs.150'). Two magic methods cooperating - exactly how int and str themselves work.

Operator overloadingTracing

Q26. medium

Which dunder pair powers the WITH statement (context managers)?

class Timer:
    def __enter__(self):
        self.start = time.time()
        return self
    def __exit__(self, *args):
        print(f"took {time.time() - self.start:.2f}s")

with Timer():
    do_work()
  1. A __enter__ and __exit__
  2. B __open__ and __close__
  3. C __start__ and __stop__
  4. D __with__ and __without__

Answer: A — __enter__ and __exit__

with obj: calls obj.__enter__() at the start and obj.__exit__() at the end - ALWAYS, even on exceptions. Define both and your class works in with statements, like open() does.

Context managerswith statement

Q29. medium

What does @dataclass (from dataclasses) generate for you?

from dataclasses import dataclass

@dataclass
class Point:
    x: int
    y: int

p = Point(1, 2)
print(p)               # Point(x=1, y=2)
print(p == Point(1, 2))  # True
  1. A __init__, __repr__ and __eq__ automatically from the declared fields
  2. B A database table
  3. C Only documentation
  4. D Getters and setters

Answer: A — __init__, __repr__ and __eq__ automatically from the declared fields

For classes that mainly hold data, @dataclass writes the boilerplate: constructor, nice repr, equality by fields. Three lines replace fifteen.

dataclassesBoilerplate

Q30. medium

Why define __repr__ on every class you debug often?

def __repr__(self):
    return f"Order(id={self.id}, status={self.status!r})"
  1. A Debuggers, logs, lists and the REPL all show __repr__ - a good one makes object state visible at a glance
  2. B Python requires it
  3. C It speeds up the class
  4. D It enables inheritance

Answer: A — Debuggers, logs, lists and the REPL all show __repr__ - a good one makes object state visible at a glance

When a log line says <Order object at 0x7f...> you know nothing; Order(id=42, status='paid') tells the whole story. One method, hours of debugging saved.

__repr__Debugging

Hard questions (12)

Q3. hard

What is the difference between __str__ and __repr__?

class Book:
    def __repr__(self):
        return f"Book({self.title!r})"

books = [Book("Dune")]
print(books)   # [Book('Dune')] - repr used inside lists!
  1. A __str__ is the friendly display for users; __repr__ is the unambiguous version for developers/debugging (shown in the REPL and inside lists)
  2. B They are interchangeable aliases
  3. C __repr__ repeats the string twice
  4. D __str__ is Python 2 only

Answer: A — __str__ is the friendly display for users; __repr__ is the unambiguous version for developers/debugging (shown in the REPL and inside lists)

print(obj) uses __str__; typing obj in the shell, or printing a LIST of objects, uses __repr__. Convention: __repr__ looks like the code to recreate the object: Book('Dune'). If you define only one, define __repr__ - str falls back to it.

__repr__Debugging

Q7. hard

What does defining __lt__ (less-than) unlock for your class?

class Student:
    def __init__(self, marks):
        self.marks = marks
    def __lt__(self, other):
        return self.marks < other.marks

print(sorted([Student(90), Student(70)])[0].marks)  # 70
  1. A The < operator AND sorting: sorted() can order your objects
  2. B Only printing
  3. C Nothing without __gt__ too
  4. D Subtraction

Answer: A — The < operator AND sorting: sorted() can order your objects

sorted() compares elements with < - so one __lt__ makes lists of your objects sortable. (functools.total_ordering can derive the other comparisons from __lt__ + __eq__.)

__lt__sorted()total_ordering

Q10. hard

What does __call__ do? class Greeter: def __call__(self, name): return f"Hi {name}" g = Greeter() print(g("Ana"))

  1. A Makes INSTANCES callable like functions - g("Ana") works, printing Hi Ana
  2. B Calls the constructor again
  3. C It's an error - objects can't be called
  4. D Renames the object

Answer: A — Makes INSTANCES callable like functions - g("Ana") works, printing Hi Ana

__call__ turns objects into function-like things: g("Ana") runs g.__call__("Ana"). Useful for objects that act like configurable functions (and it's how decorator classes work).

__call__Callables

Q12. hard

What does a DOUBLE leading underscore do: self.__secret?

class Account:
    def __init__(self):
        self.__pin = 1234

a = Account()
# a.__pin              # AttributeError
print(a._Account__pin)  # 1234 - mangled name
  1. A Name mangling - Python renames it to _ClassName__secret, making accidental outside access harder
  2. B Makes it 100% inaccessible
  3. C Deletes it after __init__
  4. D Nothing different from one underscore

Answer: A — Name mangling - Python renames it to _ClassName__secret, making accidental outside access harder

__secret becomes _Account__secret internally - obj.__secret fails with AttributeError, though obj._Account__secret still works (so it's tamper-resistant, not secure). Main real use: avoiding name clashes in subclasses.

Name manglingEncapsulation

Q13. hard

What does @property do? class Circle: @property def area(self): return 3.14 * self.r ** 2

c = Circle()
c.r = 2
print(c.area)    # 12.56 - no () needed
  1. A Lets you access area WITHOUT parentheses - c.area - computed fresh each time like an attribute
  2. B Caches the value forever
  3. C Makes area static
  4. D Prints the area

Answer: A — Lets you access area WITHOUT parentheses - c.area - computed fresh each time like an attribute

@property turns a method into a computed attribute: c.area (no parentheses!) runs the method. The Pythonic replacement for Java-style get_area() - callers see a simple attribute, you keep the logic.

@propertyGetters

Q14. hard

How do you add validation when SETTING obj.age = value?

class Person:
    @property
    def age(self):
        return self._age
    @age.setter
    def age(self, value):
        if value < 0:
            raise ValueError("age can't be negative")
        self._age = value
  1. A Define @property for age plus an @age.setter method that checks the value before storing
  2. B Python can't intercept assignment
  3. C Override __init__ only
  4. D Use a global validator

Answer: A — Define @property for age plus an @age.setter method that checks the value before storing

The property/setter pair intercepts both read and write. Assignment (p.age = -5) runs the setter, which can validate and raise. Callers still use plain attribute syntax.

SettersValidation

Q16. hard

What does @classmethod receive instead of self?

class Date:
    def __init__(self, d, m, y):
        self.d, self.m, self.y = d, m, y
    @classmethod
    def from_string(cls, text):     # 'Date' arrives as cls
        d, m, y = map(int, text.split("-"))
        return cls(d, m, y)          # builds the instance

date = Date.from_string("03-07-2026")
  1. A cls - the CLASS itself, so it can read class attributes or build instances
  2. B Nothing at all
  3. C A copy of every instance
  4. D The parent class

Answer: A — cls - the CLASS itself, so it can read class attributes or build instances

Class methods get the class as their first argument (named cls). Their killer use: ALTERNATIVE CONSTRUCTORS - building instances from different input formats.

@classmethodAlternative constructors

Q17. hard

staticmethod vs classmethod vs normal method - which first argument does each receive?

  1. A normal: self (the instance); classmethod: cls (the class); staticmethod: nothing automatic
  2. B All three receive self
  3. C normal: cls; classmethod: self; staticmethod: both
  4. D They're interchangeable

Answer: A — normal: self (the instance); classmethod: cls (the class); staticmethod: nothing automatic

The rule of thumb: needs instance data -> normal method. Needs the class (attributes, alternative constructor) -> classmethod. Needs neither but belongs thematically -> staticmethod.

Method typesChoosing

Q18. hard

What is the true division of labor between __new__ and __init__?

  1. A __new__ CREATES the object; __init__ then INITIALIZES the already-created object
  2. B __init__ creates, __new__ initializes
  3. C They are aliases
  4. D __new__ is Python 2 only

Answer: A — __new__ CREATES the object; __init__ then INITIALIZES the already-created object

Calling Dog() first runs __new__ (allocates the instance) then __init__ (fills in attributes). You almost never touch __new__ - exceptions: immutable subclasses and singletons. Strictly, __init__ is an initializer, not a constructor.

__new__Object creation

Q22. hard

print([Book("Dune")]) shows <Book object at 0x...> even though Book defines __str__. Why?

class Book:
    def __repr__(self):
        return f"Book({self.title!r})"
print([Book("Dune")])   # [Book('Dune')]
  1. A Containers display their items using __repr__, not __str__ - define __repr__ too
  2. B Lists erase methods
  3. C __str__ has a typo
  4. D print can't handle lists

Answer: A — Containers display their items using __repr__, not __str__ - define __repr__ too

A list's own str() calls repr() on every element - by design, to show unambiguous contents. This surprise is the #1 reason to always define __repr__ on your classes.

__repr__Containers

Q25. hard

v * 3 works on your Vector but 3 * v fails. Which method fixes the reversed order?

class Vector:
    def __mul__(self, n):
        return Vector(self.x * n, self.y * n)
    __rmul__ = __mul__     # 3 * v now works too
  1. A __rmul__
  2. B __mul2__
  3. C __xmul__
  4. D It cannot be fixed

Answer: A — __rmul__

3 * v first tries (3).__mul__(v) - int doesn't know Vector, so Python then tries v.__rmul__(3). The r-prefixed 'reflected' methods handle your object on the RIGHT side of an operator.

Reflected operators__rmul__

Q28. hard

What is a SINGLETON, and which method is typically overridden to make one?

class Config:
    _instance = None
    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance

print(Config() is Config())   # True
  1. A A class allowing only ONE instance ever - override __new__ to return the same stored instance
  2. B A class with one method - override __init__
  3. C A single-file module
  4. D An unmarried class

Answer: A — A class allowing only ONE instance ever - override __new__ to return the same stored instance

__new__ controls creation, so it can hand back a cached instance instead of a fresh one - every Config() call returns the same object. (In practice, a module-level instance is often the simpler Python singleton.)

Singleton__new__Design patterns

Practice “Magic Methods & Advanced OOP” as a quiz