Building on an existing class
Sometimes a new class is an existing class plus a twist. Inheritance expresses that: the child class gets every method and attribute of the parent for free, then adds or overrides what differs.
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"
Dog(Animal) reads as "Dog is an Animal". Dog did not define __init__, so the parent's runs. It did define speak, so its version overrides the parent's.
super(): extend instead of replace
Often the child wants to add to the parent's __init__ rather than replace it. super() calls the parent's version so you do not repeat its work:
class Puppy(Dog): def __init__(self, name, months): super().__init__(name) # parent stores name self.months = months # child adds its own data
When you call a method, Python looks for it on the object's own class first, then walks up the chain of parents and uses the first match. That lookup path is why overriding works.
Use inheritance for genuine is-a relationships. If the honest sentence is "has a" (a Car has an Engine), store the object as an attribute instead. Overusing inheritance is a classic beginner-to-intermediate mistake.
You will consume inheritance constantly even if you rarely write deep hierarchies of your own: custom exceptions subclass Exception (unit 8), Counter subclasses dict (unit 6), and every web framework hands you base classes to extend.
Overriding, inheriting, and extending
Three subclasses show the three things a child class can do. Dog overrides speak with its own version, Cat inherits everything unchanged, and Puppy extends __init__ by calling super() and then adding a little more.
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): pass class Puppy(Dog): def __init__(self, name, months): super().__init__(name) self.months = months print(Dog("rex").speak()) print(Cat("whiskers").speak()) pup = Puppy("biscuit", 3) print(pup.speak(), "at", pup.months, "months")
Output
rex says woof
whiskers makes a sound
biscuit says woof at 3 monthsCat has an empty body and still works, because it picks up both __init__ and speak from Animal. Puppy inherits from Dog, two levels down, so it gets the woof version of speak for free while adding a months attribute of its own.
Manager
Manager inherits from Employee. Its __init__ takes name, salary, and team_size, hands the first two up to the parent with super().__init__, and stores the third itself. It also overrides describe to mention the team.
class Employee: def __init__(self, name, salary): self.name = name self.salary = salary def describe(self): return f"{self.name} earns {self.salary}" class Manager(Employee): def __init__(self, name, salary, team_size): super().__init__(name, salary) self.team_size = team_size def describe(self): return f"{self.name} earns {self.salary} and leads {self.team_size} people" m = Manager("ada", 90000, 5) print(m.describe())
Output
ada earns 90000 and leads 5 people
The parent name goes in parentheses on the class line, class Manager(Employee):, and that single word is what grants access to everything Employee defines. super().__init__(name, salary) reuses the parent setup instead of retyping those two assignments, which matters when the parent later grows a third field. The override of describe writes a fresh f-string here, though calling super().describe() and appending to the returned string is an equally valid style.
How Python finds a method
Given class C(B) and class B(A), with greet defined only on A, calling c.greet() on an instance of C runs A's version.
Python walks the inheritance chain in order, checking C, then B, then A, and uses the first definition it finds. That one rule explains both behaviors you have seen in this lesson. Inheriting for free happens when no closer class defines the name, and overriding happens when a child does define it, shadowing the parent version because the search stops earlier.
Is-a versus has-a
A Car class that needs an Engine should not inherit from Engine. A car has an engine, so the engine belongs in an attribute: self.engine = Engine(). That arrangement is called composition.
Inheritance states an is-a fact about the world. A Dog is an Animal, so everything true of animals is true of dogs. A car is not a kind of engine, so an inheritance link there would be a lie in the code, and it would also hand Car every engine method whether or not it makes sense on a car.
| Relationship | Model it with | Example |
|---|---|---|
| is-a | inheritance | class Dog(Animal) |
| has-a | composition | self.engine = Engine() |
Reaching for inheritance just to share a few lines of code between unrelated things produces hierarchies that are painful to change later. When in doubt, compose.