Inheritance, Polymorphism, Duck Typing (Beginner Friendly) - Python OOP Tutorial #20
Video: Inheritance, Polymorphism, Duck Typing (Beginner Friendly) - Python OOP Tutorial #20 by Taught by Celeste AI - AI Coding Coach
Python Inheritance, Polymorphism, Duck Typing
class Sub(Base):—Subinherits everything fromBase.super().method()calls the parent. Polymorphism: different classes implementing the same interface. Duck typing: "if it has the method, call it" — Python doesn't care about the class.
Inheritance lets you build new classes from existing ones. Polymorphism lets you treat different types uniformly through a shared interface.
Base class and subclass
class Vehicle:
def __init__(self, make, model, year):
self.make = make
self.model = model
self.year = year
def describe(self):
return f"{self.year} {self.make} {self.model}"
def start(self):
return f"{self.make} engine started"
A normal class. Now a subclass:
class Car(Vehicle):
def __init__(self, make, model, year, doors):
super().__init__(make, model, year)
self.doors = doors
def describe(self):
return f"{super().describe()} ({self.doors}-door)"
class Car(Vehicle): — Car inherits everything from Vehicle. The (Vehicle) is the parent.
super().__init__(...) calls the parent's __init__. Without it, the inherited __init__ doesn't run, and you'd have to set self.make, self.model, etc. manually.
Method overriding
class Truck(Vehicle):
def __init__(self, make, model, year, payload):
super().__init__(make, model, year)
self.payload = payload
def start(self):
return f"{self.make} diesel engine started"
Truck.start overrides Vehicle.start. When you call truck.start(), Python finds start in Truck first and uses that.
To call the parent's version and extend it:
def describe(self):
return f"{super().describe()} [{self.payload} ton payload]"
super().describe() runs the inherited method. Then you can add to its result.
What inheritance gives you
Subclasses inherit:
- All attributes set by Base.__init__ (if super().__init__ is called).
- All methods defined on Base.
- All class attributes.
They can:
- Add new attributes and methods.
- Override existing methods.
- Call parent methods with super().
isinstance and issubclass
v = Vehicle("Toyota", "Camry", 2024)
car = Car("Honda", "Civic", 2023, 4)
isinstance(car, Car) # True
isinstance(car, Vehicle) # True — Car IS a Vehicle
isinstance(v, Car) # False
issubclass(Car, Vehicle) # True
issubclass(Vehicle, Car) # False
isinstance(obj, Class) checks if obj is an instance of Class (or any subclass). issubclass(A, B) checks the class relationship.
Use isinstance for runtime type checks; prefer it over type(x) == Class (which doesn't match subclasses).
Polymorphism
class Shape:
def area(self):
return 0
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self):
return math.pi * self.radius ** 2
class Rectangle(Shape):
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
shapes = [Circle(5), Rectangle(4, 6)]
for s in shapes:
print(s.area())
A function that calls s.area() works on any subclass. Each computes its own area; the caller doesn't care what type it is.
def total_area(shapes):
return sum(s.area() for s in shapes)
print(total_area(shapes))
This is polymorphism: many forms behind one interface.
Duck typing
Python doesn't require you to inherit from Shape — it just needs the .area() method:
class CustomShape:
def __init__(self, name, value):
self.name = name
self.value = value
def area(self):
return self.value
shapes = [Circle(5), Rectangle(4, 6), CustomShape("Mystery", 42)]
for s in shapes:
print(s.area())
CustomShape doesn't inherit from Shape. Doesn't matter — it has .area(), so it works wherever an area-haver is expected.
"If it walks like a duck and quacks like a duck, it is a duck."
This is duck typing: behavior matters, type doesn't. Python lets you mix unrelated classes that happen to support the same methods.
In typed languages (Java, C#), you'd need a Shape interface that all classes implement. Python skips the formality — it'll just call .area() and trust you.
abstract base classes
If you DO want to enforce "must implement .area()," use abc:
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self):
pass
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self):
return math.pi * self.radius ** 2
c = Circle(5) # OK
s = Shape() # TypeError — can't instantiate abstract class
Subclasses must override area. Forgetting raises an error at instantiation, not at first method call. Lesson 22 dives into abstract classes.
Multiple inheritance
class Flyer:
def fly(self):
return f"{self.name} flies"
class Swimmer:
def swim(self):
return f"{self.name} swims"
class Duck(Flyer, Swimmer):
def __init__(self, name):
self.name = name
d = Duck("Donald")
print(d.fly()) # Donald flies
print(d.swim()) # Donald swims
A class can inherit from multiple parents. Python uses the MRO (Method Resolution Order) to determine which method runs when a name is in multiple parents:
print(Duck.__mro__)
# (<class 'Duck'>, <class 'Flyer'>, <class 'Swimmer'>, <class 'object'>)
Left-to-right, depth-first, with cycle resolution (the C3 algorithm).
Multiple inheritance is powerful but can get tangled. Most projects use it sparingly, often via mixins — small classes that add a single capability.
super() in multiple inheritance
class A:
def greet(self):
print("A")
class B(A):
def greet(self):
super().greet()
print("B")
class C(A):
def greet(self):
super().greet()
print("C")
class D(B, C):
def greet(self):
super().greet()
print("D")
D().greet()
# A C B D
super() follows the MRO, which is D → B → C → A. The print order is reversed because each subclass calls super first. This is the cooperative multiple-inheritance pattern.
If this seems confusing — that's fair. Most code uses single inheritance.
Composition over inheritance
Inheritance is one way to share code. Composition is often better:
# Inheritance:
class ElectricCar(Car):
def __init__(self, ..., battery_kwh):
super().__init__(...)
self.battery_kwh = battery_kwh
# Composition:
class Battery:
def __init__(self, kwh):
self.kwh = kwh
class ElectricCar:
def __init__(self, ..., battery):
self.battery = battery
Composition is more flexible. has-a Battery is easier to swap than is-a Vehicle. Reach for inheritance only when there's a true "is-a" relationship.
Common stumbles
Forgetting super().__init__. Parent attributes never get set. The subclass works only as long as you don't depend on inherited state.
type(x) == Class instead of isinstance. Misses subclasses. Use isinstance(x, Class).
Overriding __init__ and changing signature. Calling code that expects the parent signature breaks. Add new params at the end with defaults.
Calling Parent.method(self) directly. Works in single inheritance but bypasses MRO. Use super().method().
Diamond inheritance confusion. D(B, C) where both B and C extend A. Method resolution can surprise. Prefer single inheritance + composition.
Treating duck typing as license to be sloppy. Document the expected interface. "This function accepts any object with .area() and .name" is real documentation.
What's next
Lesson 21: encapsulation, private attributes, @property and setters. _private, __name_mangling, and the property decorator.
Recap
class Sub(Base): for inheritance. super().__init__(...) to call parent constructor. Override methods by redefining; combine with super().method() for "extend, don't replace." Polymorphism: different classes, same method names — caller doesn't care which. Duck typing: Python doesn't require inheritance; it requires the right methods. Use multiple inheritance sparingly; prefer composition for "has-a" relationships.
Next lesson: encapsulation and properties.