Polymorphism and Dunder Methods in Python: Runnable Examples and Exercises
See how one Python operation supports different types, then build a Vector2D class with readable output, addition, magnitude and equality. Includes runnable code, protocol failures and exercises.
KnowledgeGate Team
Exam prep & CS education

Why does 2 + 3 produce 5, while "Py" + "thon" produces "Python"? A user-defined object can make that same + syntax meaningful too. Python chooses behaviour from the object and the protocol it supports, not from a long chain of type checks written by the caller. Unrelated shapes can answer the same method call, and a user-defined vector can control addition, magnitude, equality and display through documented hooks.
Polymorphism in Python: One Operation, Different Behaviours
Polymorphism means that one operation can have valid, type-specific behaviour. len("GATE") is 4, while len(["OS", "DBMS", "CN"]) is 3. Similarly, 2 + 3 is 5, while "Py" + "thon" is "Python". The call stays familiar, but the receiving type supplies the behaviour.
Mechanism | Call site | What decides the behaviour | Expected result |
|---|---|---|---|
Duck typing |
| The object's | A valid area |
Overriding |
| The subclass implementation | Replaces inherited behaviour |
Operator polymorphism |
| Data-model hooks such as | Type-specific addition |
Duck typing lets unrelated objects satisfy the same method protocol. Overriding happens when a subclass replaces an inherited implementation. Operator polymorphism connects syntax such as + to Python's data model. Ordinary Python functions do not use Java-style or C++-style compile-time overload selection by parameter signature. The Coding & Skills category places these ideas in a broader programming path.
Method Polymorphism Without Type-Check Chains
These classes are unrelated, but both offer a meaningful area() method:
import math
class Rectangle:
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
class Circle:
def __init__(self, radius):
self.radius = radius
def area(self):
return math.pi * self.radius ** 2
shapes = [Rectangle(4, 3), Circle(2)]
total = 0
for shape in shapes:
value = shape.area()
total += value
print(f"{type(shape).__name__}: {value:.2f}")
print(f"Total: {total:.2f}")The output is:
Rectangle: 12.00
Circle: 12.57
Total: 24.57The rectangle contributes 4 * 3 = 12. The circle contributes pi * 2^2 = 4pi, approximately 12.5664. Their sum is approximately 24.5664, which formats as 24.57.
The loop never asks whether an item is a rectangle or a circle. Each object only needs a callable area() with the expected meaning. A shared base class could document that contract, but duck typing does not require one. This keeps the call site stable and avoids an expanding if isinstance(...) chain.

Dunder Methods Are Python Data-Model Hooks
“Dunder” means double underscore. These methods are hooks that Python consults when public syntax or a built-in operation is used.
Visible operation | Hook Python consults |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
For Vector2D(3, 4), the reader-facing str() form will be <3, 4>, while the debugging repr() form will be Vector2D(x=3, y=4). If __str__ is absent, str() falls back to __repr__.
Implement a documented hook only when the type has a sensible contract for it. Do not invent a name such as __display__ and expect Python to call it, and do not assume every class needs every hook. In routine code, use obj1 + obj2 or repr(obj) instead of calling a dunder method directly.
Fully Worked Vector2D Example
The Vector2D class defines representation, addition, magnitude and equality:
from math import hypot
class Vector2D:
def __init__(self, x, y):
self.x = x
self.y = y
def __repr__(self):
return f"Vector2D(x={self.x}, y={self.y})"
def __str__(self):
return f"<{self.x}, {self.y}>"
def __add__(self, other):
if not isinstance(other, Vector2D):
return NotImplemented
return Vector2D(self.x + other.x, self.y + other.y)
def __abs__(self):
return hypot(self.x, self.y)
def __eq__(self, other):
if not isinstance(other, Vector2D):
return NotImplemented
return self.x == other.x and self.y == other.yNow run:
v1 = Vector2D(3, 4)
v2 = Vector2D(-1, 2)
print(v1)
print(repr(v1))
print(repr(v1 + v2))
print(abs(v1))
print(v1 == Vector2D(3, 4))The exact outputs are <3, 4>, Vector2D(x=3, y=4), Vector2D(x=2, y=6), 5.0 and True. Addition works component by component: (3 + -1, 4 + 2) = (2, 6). Magnitude is sqrt(3^2 + 4^2) = sqrt(9 + 16) = sqrt(25) = 5.0.

NotImplemented, Reflected Operations and Equality
Returning NotImplemented tells Python that this operand pairing is unsupported. Python may then try the reflected operation, such as __radd__, on the other operand. Raising NotImplementedError means something different: a method body is deliberately unfinished.
Here, v1 + 10 reaches NotImplemented, finds no supported reflected operation and ends with TypeError. That is better than silently inventing meaningless vector arithmetic. An __radd__ method belongs only in a class where mixed-type addition has a clear definition, so it should not be bolted on merely to make 10 + v1 run.
For equality, v1 == (3, 4) becomes False after the vector and tuple equality paths reject the pairing. By contrast, v1 == Vector2D(3, 4) is True because both coordinates match. Value equality also affects hashing. Do not add __hash__ unless the class is immutable and its equality and hash contracts are deliberately consistent.
Common Dunder and Polymorphism Errors
Symptom | Cause | Fix |
|---|---|---|
|
| Spell it |
|
| Return a string |
Addition ends in |
| Construct the result directly |
Raising fails immediately |
|
|
A matching method name is not enough if its meaning is incompatible with the protocol. Broad try/except blocks around every polymorphic call also hide real defects. In particular, __len__ must return a non-negative integer, not a float or an arbitrary measurement.
For debugging, print type(obj), check whether the exact hook exists, call the public operation, and reduce the case to one operand pair. Invoke obj.__add__(other) directly only for a focused trace or test.
How Programming Questions Test This, Plus Exercises
Common questions ask you to trace output, identify the invoked dunder, distinguish overriding from duck typing, explain NotImplemented, or compare __str__ with __repr__. Practise with these checkable tasks:
Use
Rectangle(5, 2)andCircle(1). Their printed areas are10.00and3.14, and the total is13.14.Compute
Vector2D(5, -1) + Vector2D(-2, 4). The result isVector2D(x=3, y=3), whose magnitude issqrt(18), approximately4.24.Implement scalar-only
__mul__soVector2D(3, 4) * 2becomesVector2D(x=6, y=8). ReturnNotImplementedwhen the other operand is a vector.
For adjacent problem-solving practice, read the dynamic programming worked example and the sorting algorithms comparison. Those posts do not teach dunder methods, but they provide more opportunities to trace code and reason from exact values.
Short Version and Next Step
Polymorphism keeps the call site stable while different objects supply useful behaviour. Duck typing depends on behaviour rather than declared ancestry. Dunder methods connect a class to Python syntax and built-ins. Unsupported binary operations should return NotImplemented so Python can follow its normal resolution protocol.
Run the Vector2D program and predict every output before execution, then complete the scalar multiplication exercise. If you need the broader language sequence, continue with Python Programming. Follow it with DSA Using Python for structured problem solving.
Keep learning

Pandas Basics in Python: Build, Clean and Analyse a DataFrame Step by Step
Follow one student dataset from its first DataFrame to a clean city summary, while learning how selection, missing values and vectorised calculations really work.

Python Operators and Expressions: Precedence, Types and Worked Output Traces
Trace Python expressions without guessing. This guide connects operator families, precedence, types, short-circuiting and exact output through worked examples.

CSV Files Explained: Parsing Rules, Worked Records and Exam Traps
Learn why commas and newlines are not always boundaries, trace a quote-aware parser, validate text fields with a schema, and calculate processing costs.

Inheritance in Python: A Practical Tutorial with Examples
Learn how Python classes inherit state and behaviour through runnable examples. Trace super(), overrides, MRO, common mistakes, and three focused exercises.