Python classes support inheritance with a simple, readable syntax, and polymorphism happens naturally through duck typing rather than requiring explicit interfaces.
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
return f"{self.name} makes a sound."
class Dog(Animal): # Dog inherits from Animal
def speak(self): # overrides the parent's method
return f"{self.name} barks."
class Cat(Animal):
def speak(self):
return f"{self.name} meows."
def __init__(self, name, indoor=True):
super().__init__(name) # call the parent's __init__ explicitly
self.indoor = indoor
animals = [Dog("Rex"), Cat("Whiskers")]
for animal in animals:
print(animal.speak()) # calls the RIGHT version automatically for each type
class Dog(Animal): establishes inheritance — Dog gets everything Animal defines, and can override any method by simply redefining it with the same name. super().__init__(name) explicitly calls the parent class's constructor, the standard way to extend (rather than replace) a parent's initialization logic.
Unlike C++/Java, Python doesn't need a virtual keyword for polymorphism — every method is effectively "virtual" by default, and the loop above calling animal.speak() automatically runs whichever version matches each object's actual class, with no special declaration required.
This works because Python relies on duck typing: "if it walks like a duck and quacks like a duck, it's a duck" — code generally doesn't check an object's exact type before calling a method, it simply calls the method and trusts that the object supports it, whatever its actual class happens to be. This is more flexible than strict interface-based polymorphism but places more responsibility on tests and, optionally, type hints with Protocol (covered later) to catch mismatches.