Python · Lesson 10 of 15
Inheritance and Special Methods
Reuse behavior with subclasses, override methods, and make objects print and compare nicely.
- Intermediate
- 16 min read
- 3 objectives
Before this lessonLesson 9: Classes and Objects
What you will learn
- Create a subclass with super()
- Override methods
- Add __repr__ and __eq__
In the previous OOP lesson you built a class from scratch. Inheritance lets a new class start from an existing one, reusing its attributes and methods and adding or changing only what is different. The existing class is the parent (base class); the new one is the child (subclass). Use it for a real "is a" relationship: a Dog is an Animal.
Your first subclass
Put the parent's name in parentheses. The child gets everything the parent has. Inside the child's __init__, call super().__init__(...) so the parent still sets up its part.
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
return f"{self.name} makes a sound"
class Dog(Animal):
def __init__(self, name, breed):
super().__init__(name)
self.breed = breed
rex = Dog("Rex", "Beagle")
print(rex.name, rex.breed)
print(rex.speak())Rex Beagle Rex makes a sound
Overriding methods
A subclass can define a method with the same name to replace the parent's version. This is polymorphism: code that calls speak() works on any animal, and each one answers in its own way, without an if chain checking the type.
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
return f"{self.name} makes a sound"
class Dog(Animal):
def speak(self):
return f"{self.name} says Woof"
class Cat(Animal):
def speak(self):
return f"{self.name} says Meow"
for pet in [Dog("Rex"), Cat("Tom"), Animal("Generic")]:
print(pet.speak())Rex says Woof Tom says Meow Generic makes a sound
Extending instead of replacing
Often you want to add to the parent's behavior. Call super().method() inside the override to run the parent's version first and then continue.
class Account:
def __init__(self, balance=0):
self.balance = balance
def describe(self):
return f"balance={self.balance}"
class Savings(Account):
def __init__(self, balance=0, rate=0.02):
super().__init__(balance)
self.rate = rate
def describe(self):
return super().describe() + f", rate={self.rate:.0%}"
print(Savings(500).describe())balance=500, rate=2%
isinstance and issubclass
isinstance(obj, Class) is true if the object was created from that class or any subclass. Prefer it over comparing type(obj) directly.
class Animal: ...
class Dog(Animal): ...
d = Dog()
print(isinstance(d, Dog), isinstance(d, Animal), isinstance(d, str))
print(issubclass(Dog, Animal))True True False True
Special (dunder) methods
Methods with double underscores let your objects behave like built-in ones. __repr__ controls how an object prints in a debugger or the REPL, __str__ what print() shows, and __eq__ what == means. Without them, two objects with identical data are considered different and print as an unhelpful memory address.
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __repr__(self):
return f"Point({self.x}, {self.y})"
def __eq__(self, other):
return isinstance(other, Point) and (self.x, self.y) == (other.x, other.y)
def __add__(self, other):
return Point(self.x + other.x, self.y + other.y)
a, b = Point(1, 2), Point(1, 2)
print(a)
print(a == b, a is b)
print(a + Point(10, 10))Point(1, 2) True False Point(11, 12)
Common mistakes
- Forgetting
super().__init__(), so the parent's attributes are never set and you getAttributeErrorlater. - Overriding a method with a different signature, which breaks code that expects the parent's contract.
- Using inheritance just to share a few lines of code when a plain function would do.
# Write your solution here
