Python Polymorphism & Encapsulation
Polymorphism means “many forms”: different objects respond to the same method call in their own way. Encapsulation means keeping an object’s data behind its methods so it cannot be put into an invalid state. Together with inheritance they are the core ideas of object-oriented programming.
- Polymorphism with overriding and duck typing
- Operator overloading with special methods
- Private attributes and name mangling
- Getters and setters with @property
Polymorphism through overriding
Each class implements the same method name. Code that calls the method does not need to know which class it is dealing with.
class Shape: def area(self): raise NotImplementedError class Circle(Shape): def __init__(self, r): self.r = r def area(self): return round(3.14159 * self.r ** 2, 2) class Square(Shape): def __init__(self, side): self.side = side def area(self): return self.side ** 2 for shape in [Circle(1), Square(3), Circle(2)]: print(type(shape).__name__, shape.area())Circle 3.14 Square 9 Circle 12.57
Duck typing
Python does not require a shared parent class. If an object has the method you call, it works. “If it walks like a duck and quacks like a duck, it is a duck.”
class PdfReport: def export(self): return "report.pdf" class CsvReport: def export(self): return "report.csv" def save_all(reports): for r in reports: print("saved", r.export()) save_all([PdfReport(), CsvReport()])saved report.pdf saved report.csv
Built-in functions are polymorphic too: len() works on strings, lists and dicts because each type implements __len__.
for thing in ["abc", [1, 2], {"a": 1}, range(10)]: print(type(thing).__name__, len(thing))str 3 list 2 dict 1 range 10
Operator overloading
Special methods let your own classes work with +, ==, < and friends:
class Money: def __init__(self, amount): self.amount = amount def __add__(self, other): return Money(self.amount + other.amount) def __lt__(self, other): return self.amount < other.amount def __repr__(self): return f"Money({self.amount})" a, b = Money(250), Money(100)print(a + b)print(a < b, sorted([a, b]))Money(350) False [Money(100), Money(250)]
| Operator | Special method |
|---|---|
a + b | __add__ |
a - b | __sub__ |
a * b | __mul__ |
a == b | __eq__ |
a < b | __lt__ |
len(a) | __len__ |
str(a) | __str__ |
Encapsulation: protecting data
Python has no true private attributes, but it has two conventions:
_name(one underscore): “internal, please do not touch from outside”.__name(two underscores): Python renames it to_ClassName__name(name mangling), so it is hard to reach by accident.
class Wallet: def __init__(self): self._currency = "INR" # internal by convention self.__balance = 0 # name-mangled def add(self, amount): self.__balance += amount def balance(self): return self.__balance w = Wallet()w.add(500)print(w.balance(), w._currency)print(w._Wallet__balance) # still reachable if you insistprint(w.__balance)500 INR
500
Traceback (most recent call last):
File "main.py", line 16, in <module>
print(w.__balance)
^^^^^^^^^^^
AttributeError: 'Wallet' object has no attribute '__balance'. Did you mean '.balance' instead of '.__balance'?Getters and setters with @property
@property lets you validate values while keeping simple attribute syntax for the caller:
class Product: def __init__(self, name, price): self.name = name self.price = price # goes through the setter below @property def price(self): return self._price @price.setter def price(self, value): if value < 0: raise ValueError("price cannot be negative") self._price = value @property def price_with_tax(self): # read-only computed attribute return round(self._price * 1.18, 2) p = Product("Mouse", 349)p.price = 399print(p.price, p.price_with_tax) try: p.price = -5except ValueError as e: print("Error:", e)399 470.82 Error: price cannot be negative
Start with plain public attributes. Switch to @property only when you need validation or a computed value; callers do not need to change their code.
Check your understanding
Predict the output
class Cat: def speak(self): return "meow"class Robot: def speak(self): return "beep" print([x.speak() for x in (Cat(), Robot())])['meow', 'beep']
Cat and Robot share no parent class, but both have speak(), so duck typing lets the loop call it on each.
Quick quiz
1. What does duck typing mean?
Python cares about behaviour, not declared types.
2. What does __add__ enable?
a + b calls a.__add__(b).
3. What happens to self.__secret inside class Box?
Name mangling makes accidental access harder but not impossible.
Exercise: temperature with validation
Create a Thermostat with a target property that only accepts values from 16 to 30 and raises ValueError otherwise.
set target to 24, then try 35
24 Rejected: 35 is outside 16-30 24
Hint 1
Store the real value in self._target inside the setter.
class Thermostat: def __init__(self, target=22): self.target = target @property def target(self): return self._target @target.setter def target(self, value): if not 16 <= value <= 30: raise ValueError(f"{value} is outside 16-30") self._target = value t = Thermostat()t.target = 24print(t.target)try: t.target = 35except ValueError as e: print("Rejected:", e)print(t.target)24 Rejected: 35 is outside 16-30 24
Every assignment to target, including the one in __init__, goes through the setter, so an invalid value can never be stored. The failed assignment leaves the old value unchanged.
Program Practice
48 Python OOP Programs
Every program has a step-by-step explanation, colour-coded code and its real output.
Polymorphism and encapsulation FAQ
What is polymorphism in Python with an example?
It is the ability of different objects to respond to the same call. len("abc") and len([1, 2]) both work, and shape.area() returns the right result for a Circle or a Square.
Does Python have private variables?
Not strictly. A leading underscore marks an attribute as internal, and a double underscore triggers name mangling. Both are conventions that discourage outside access.
What is @property in Python?
A decorator that turns a method into an attribute-like getter. Add @name.setter to run validation when the attribute is assigned.
Does Python support method overloading?
Not by signature. Define one method with default or variable arguments (*args) instead. Operator overloading is supported through special methods such as __add__.