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

Updated 21 Sep 20265 min read

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

shape.area()

The object's area method

A valid area

Overriding

child.render()

The subclass implementation

Replaces inherited behaviour

Operator polymorphism

a + b

Data-model hooks such as __add__

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:

python
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:

Code
Rectangle: 12.00
Circle: 12.57
Total: 24.57

The 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.

Rectangle and Circle objects each answer the same area() call, giving 12.00 and 12.57 that sum to a total of 24.57.

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

print(obj) or str(obj)

__str__

repr(obj)

__repr__

a + b

__add__

abs(obj)

__abs__

a == b

__eq__

len(obj)

__len__

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:

python
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.y

Now run:

python
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.

Vector2D addition resolves v1 + v2 through __add__ to Vector2D(x=2, y=6), while v1 + 10 returns NotImplemented and raises TypeError.

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

v1 + v2 raises TypeError

_add_ has one underscore on each side

Spell it __add__

str(obj) raises TypeError

__str__ returns 42

Return a string

Addition ends in RecursionError

__add__ returns self + other

Construct the result directly

Raising fails immediately

raise NotImplemented treats a sentinel as an exception

return NotImplemented

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:

  1. Use Rectangle(5, 2) and Circle(1). Their printed areas are 10.00 and 3.14, and the total is 13.14.

  2. Compute Vector2D(5, -1) + Vector2D(-2, 4). The result is Vector2D(x=3, y=3), whose magnitude is sqrt(18), approximately 4.24.

  3. Implement scalar-only __mul__ so Vector2D(3, 4) * 2 becomes Vector2D(x=6, y=8). Return NotImplemented when 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.