Vehicle Garage β
Master Inheritance
Through a Real Project.
Inheritance is the second pillar of OOP. This comprehensive tutorial walks you through
a complete vehicle hierarchy β Vehicle as the parent, with Car, Motorcycle,
and Truck as subclasses. You'll master super(), method overriding,
polymorphism, and class hierarchy design through 20+ live code examples
and an interactive garage manager.
What You'll Learn From This Project
π A Complete Inheritance Tutorial
This tutorial uses a Vehicle Garage to teach you inheritance
through hands-on practice. You'll see how a single parent class β
Vehicle β defines shared behavior, while subclasses
like Car, Motorcycle, and Truck extend and specialize it.
By the end of this tutorial, you'll understand:
How to use super() to call parent constructors,
how to override methods,
how inheritance enables polymorphism,
how to design class hierarchies, and
when to use inheritance vs composition.
Every code example is fully interactive β you can copy and run each snippet right in your browser using Pyodide. The live garage app below is powered by the exact same class hierarchy.
Why Inheritance Matters
Inheritance is one of the most powerful tools in object-oriented programming. It enables:
- β Code Reuse: Write shared logic once in a parent class, and all subclasses inherit it automatically.
- β Extensibility: Add new subclasses without modifying existing, tested code β following the Open/Closed Principle.
- β Polymorphism: Write code that works with any object that conforms to a shared interface.
- β Natural Modeling: Class hierarchies mirror real-world relationships β a Dog is an Animal, a Truck is a Vehicle.
In the real world: Frameworks like Django, PyTorch, and Flask rely heavily on inheritance. When you write class MyModel(nn.Module) in PyTorch, you're using inheritance to plug your custom neural network into the training pipeline.
Why Inheritance is Essential for Every Programmer
Why Every Programmer Should Learn Inheritance
Inheritance is not an optional feature β it is a fundamental paradigm that shapes how software is designed, maintained, and scaled. Here's why you cannot afford to ignore it.
1. It's Everywhere in Professional Codebases: Open any major Python framework β Django, PyTorch, Flask, FastAPI β and you'll find inheritance everywhere. class MyView(View), class MyModel(nn.Module), class MyForm(Form). Without understanding inheritance, you're reading these frameworks as magic rather than understanding their architecture.
2. It's the Foundation of Design Patterns: The most widely used design patterns β Template Method, Factory Method, Strategy, Observer β rely on inheritance or polymorphic interfaces. Mastering inheritance opens the door to understanding these patterns.
3. It Prevents Catastrophic Duplication: In large teams, code duplication is the root of bugs. When business logic changes, duplicated code creates a maintenance nightmare. Inheritance centralizes shared logic, reducing the surface area for errors.
4. It Enables Polymorphism: Polymorphism β one interface, many implementations β is the key to writing flexible, decoupled code. Inheritance is the classic way to achieve polymorphism in OOP.
5. It's a Career Differentiator: Understanding inheritance separates "coders who type syntax" from "engineers who design systems." Interviewers at top companies consistently probe OOP fundamentals, and inheritance is almost always on the list.
π― The Bottom Line: Inheritance is to OOP what the foundation is to a building. You can't build skyscrapers without understanding the foundation β and you can't build robust, maintainable software systems without mastering inheritance.
Significance in the Programming Community
The programming community has built entire ecosystems around inheritance. Here's how it shapes the industry:
- ποΈ Framework Architecture: Every major Python framework uses inheritance to provide "hooks" where developers can plug in custom behavior. Django's class-based views, PyTorch's neural network modules, Flask's request handlers β all built on inheritance.
- π§© Mixins and Multiple Inheritance: Python's unique support for multiple inheritance has spawned the "mixin" pattern β small, composable classes that add specific behaviors. This pattern is used extensively in Django, SQLAlchemy, and other libraries.
- π Educational Standard: Inheritance is taught in every introductory programming course, CS curriculum, and bootcamp. It's part of the shared vocabulary that every developer needs to communicate effectively.
-
π€
AI/ML Ecosystem: The AI community uses inheritance extensively.
nn.Modulein PyTorch andkeras.Modelin TensorFlow are both parent classes that you inherit from to build custom architectures.
π‘ Quick Fact: According to the 2023 Stack Overflow Developer Survey, Python is the third most popular language. The majority of Python's popularity comes from its rich ecosystem of frameworks β all of which rely heavily on inheritance.
Building the Vehicle Hierarchy
Step 1: The Parent Class β Vehicle
Every vehicle needs a brand, a license plate, a mileage tracker, and a service log. These are universal β they belong to the parent. This is the foundation of our hierarchy.
class Vehicle: def __init__(self, brand: str, plate: str): self.brand = brand self.plate = plate self.mileage = 0 self.log = [] def drive(self, miles: int) -> None: self.mileage += miles self.log.append(f"Driven {miles} miles") def service(self) -> str: entry = f"Serviced at {self.mileage} miles" self.log.append(entry) return entry def action(self) -> str: return f"{self.brand} is just sitting in the garage." def info(self) -> str: return f"{self.brand} ({self.plate}) β {self.mileage} mi, {len(self.log)} services"
π Line-by-Line Explanation
- class Vehicle: Defines a new class named
Vehicle. This will be our parent class. - def __init__(self, brand, plate): The constructor method. Called when a new
Vehicleobject is created. - self.brand = brand: Stores the brand (e.g., "Toyota") as an attribute on the object.
- self.plate = plate: Stores the license plate (e.g., "ABC-1234").
- self.mileage = 0: Initializes mileage to 0. Every vehicle starts with zero miles.
- self.log = []: Creates an empty list to store service history entries.
- def drive(self, miles): A method that adds miles to the vehicle's mileage and logs the action.
- self.mileage += miles: Increments the total mileage by the specified number of miles.
- self.log.append(...): Adds a new entry to the service log.
- def service(self): Logs a service event and returns the entry as a string.
- def action(self): Returns a generic action string. This is meant to be overridden by subclasses.
- def info(self): Returns a formatted string with the vehicle's current state.
Key insight: drive(), service(), and info() are written once. Every subclass will inherit them automatically β no duplication, no inconsistency.
fuel_efficiency calculation, we add it once here and every vehicle type benefits immediately.
Step 2: Simple Subclassing β Car and Motorcycle
A Car is a Vehicle β it inherits everything. The only thing it changes is the action() method, overriding it with a honk. This is the most common form of inheritance: extending the parent with specialized behavior.
class Car(Vehicle): def action(self) -> str: return f"{self.brand} honks: Beep beep!" class Motorcycle(Vehicle): def action(self) -> str: return f"{self.brand} pops a wheelie!" my_car = Car("Tesla", "EV-2024") my_car.drive(50) print(my_car.info()) print(my_car.action())
π Line-by-Line Explanation
- class Car(Vehicle): Defines
Caras a subclass ofVehicle. The parentheses after the class name indicate inheritance. - def action(self): Overrides the parent's
action()method. This is method overriding. - return f"{self.brand} honks: Beep beep!": Returns a car-specific action string. Note that
self.brandcomes from the parent β we didn't need to redefine it. - class Motorcycle(Vehicle): Another subclass of
Vehicle. - my_car = Car("Tesla", "EV-2024"): Creates an instance of
Car. The constructor callsVehicle.__init__automatically. - my_car.drive(50): Calls the inherited
drive()method fromVehicle. - print(my_car.info()): Calls the inherited
info()method. Output:"Tesla (EV-2024) β 50 mi, 1 services" - print(my_car.action()): Calls the overridden
action()method inCar. Output:"Tesla honks: Beep beep!"
Try it: Create a Car and a Motorcycle in the live garage below. Drive each one, then click their specialized action button. Notice how drive() and service() work identically, but action() differs.
Step 3: Extending __init__ β Truck
Trucks need an extra attribute: cargo capacity. This requires overriding __init__ and using super() to call the parent constructor. This is one of the most important patterns in inheritance.
class Truck(Vehicle): def __init__(self, brand: str, plate: str, cargo_capacity: int): super().__init__(brand, plate) self.cargo_capacity = cargo_capacity def action(self) -> str: return f"{self.brand} hauls up to {self.cargo_capacity}kg of cargo." truck = Truck("Volvo", "TRK-777", 1500) print(truck.info()) print(truck.action())
π Line-by-Line Explanation
- class Truck(Vehicle):
Truckinherits fromVehicle. - def __init__(self, brand, plate, cargo_capacity): Overrides the parent's constructor to accept an additional
cargo_capacityparameter. - super().__init__(brand, plate): This is the critical line.
super()returns a reference to the parent class. We callVehicle.__init__to set upbrand,plate,mileage, andlog. - self.cargo_capacity = cargo_capacity: After the parent's setup is complete, we add the truck-specific attribute.
- def action(self): Overrides
action()to return a truck-specific message that includes the cargo capacity. - truck = Truck("Volvo", "TRK-777", 1500): Creates a
Truckwith all three arguments. - print(truck.info()): Calls the inherited
info()method. Output:"Volvo (TRK-777) β 0 mi, 0 services" - print(truck.action()): Calls the overridden
action(). Output:"Volvo hauls up to 1500kg of cargo."
Important: Without super().__init__(brand, plate), the Truck would have no brand, plate, mileage, or log. super() ensures the parent's initialization runs before the child adds its own attributes.
Step 4: Method Overriding in Depth
When a subclass defines a method with the same name as a parent method, it overrides the parent's version. But sometimes you want to extend β not replace β the parent's behavior.
class ElectricCar(Car): def __init__(self, brand: str, plate: str, battery_capacity: int): super().__init__(brand, plate) self.battery_capacity = battery_capacity def action(self) -> str: base_action = super().action() return f"{base_action} And it's electric with {self.battery_capacity}kWh battery!" def charge(self) -> str: return f"{self.brand} is charging β‘" tesla = ElectricCar("Tesla", "EV-2024", 75) print(tesla.action()) print(tesla.charge())
π Line-by-Line Explanation
- class ElectricCar(Car):
ElectricCarinherits fromCar, which itself inherits fromVehicle. This is multilevel inheritance. - def __init__(self, brand, plate, battery_capacity): Overrides the constructor to add a battery capacity attribute.
- super().__init__(brand, plate): Calls
Car.__init__, which callsVehicle.__init__. The entire chain executes. - def action(self): Overrides
action()to extend, not replace, the parent's behavior. - base_action = super().action(): Calls the
action()method from the parent (Car), which returns the honk message. - return f"{base_action} And it's electric...": Takes the parent's result and adds electric-specific information.
- def charge(self): A new method that only exists on
ElectricCar. - tesla = ElectricCar("Tesla", "EV-2024", 75): Creates an electric car instance.
- print(tesla.action()): Output:
"Tesla honks: Beep beep! And it's electric with 75kWh battery!"β notice it includes both the honk and the electric message.
Pattern: super().method() inside an override lets you extend the parent's logic instead of replacing it. This is how you build on existing behavior rather than duplicating it.
Step 5: The Complete Hierarchy in Action
Here's the entire hierarchy working together. Run this to see how inheritance enables polymorphism β the loop treats every vehicle uniformly, yet each one behaves correctly.
fleet = [ Car("Toyota", "ABC-1234"), Motorcycle("Ducati", "MC-5521"), Truck("Ford", "TRK-9090", cargo_capacity=2000), ElectricCar("Tesla", "EV-2024", battery_capacity=75), ] for v in fleet: v.drive(100) print(v.info()) print(" β ", v.action()) print(" β ", v.service()) print("---")
π Line-by-Line Explanation
- fleet = [...] : Creates a list containing objects of different subclasses β
Car,Motorcycle,Truck, andElectricCar. - for v in fleet: Iterates over each vehicle in the list. The loop doesn't know or care about the specific type of each vehicle.
- v.drive(100): Calls the inherited
drive()method. It works the same way for every vehicle type. - print(v.info()): Calls the inherited
info()method. Works the same for all types. - print(v.action()): This is polymorphism in action. Python resolves the correct
action()method based on the actual type ofvat runtime. - print(v.service()): Calls the inherited
service()method. Works the same for all.
- Code Reuse:
drive()andservice()are written once inVehiclebut work for every subclass. - Method Overriding: Each subclass supplies its own
action(), replacing the parent's generic version. - super() Chaining:
ElectricCar.action()callsCar.action()viasuper(), which in Python resolves through the MRO. - Polymorphism: The loop treats each item as a
Vehicle, but Python resolves the correct method at runtime based on the actual object type.
The Live Garage
Add vehicles, drive them, and watch inheritance in action. The same Vehicle β Car/Motorcycle/Truck hierarchy powers this app. Your fleet is saved in local storage.
Understanding super()
Why super() Matters
super() is Python's way of calling a method from the parent class. It's essential for extending parent behavior without duplicating code, and it's the foundation of cooperative multiple inheritance.
Parent Access
Call the parent's version of a method from inside the child's override.
Constructor Chaining
Ensure the parent's __init__ runs before the child adds its own attributes.
MRO Resolution
In multiple inheritance, super() follows the Method Resolution Order, calling each parent exactly once.
Cooperative Design
Classes designed with super() can be mixed and matched safely β the foundation of mixins.
class Child(Parent): def __init__(self, parent_arg, child_arg): super().__init__(parent_arg) self.child_arg = child_arg def method(self): result = super().method() return result + " and child's addition"
π Line-by-Line Explanation
- class Child(Parent):
Childinherits fromParent. - def __init__(self, parent_arg, child_arg): Overrides the constructor.
- super().__init__(parent_arg): Calls
Parent.__init__with the appropriate arguments. This ensures the parent's state is properly initialized. - self.child_arg = child_arg: After the parent's setup, adds the child-specific attribute.
- def method(self): Overrides
method()from the parent. - result = super().method(): Calls the parent's version of
method()and stores the result. - return result + " and child's addition": Takes the parent's result and extends it with child-specific behavior.
Common pitfall: Forgetting to call super().__init__() means the parent's attributes (like brand and plate in our Vehicle) never get set. Always call it first unless you have a very specific reason not to.
super() doesn't just call the immediate parent β it calls the next class in the MRO. This means super() works correctly even with complex diamond hierarchies.
Polymorphism Through Inheritance
One Interface, Many Behaviors
Polymorphism lets you write code that works with any object that conforms to a shared interface. In our vehicle hierarchy, that interface is defined by Vehicle.
def garage_report(vehicles): for v in vehicles: print(v.info()) print(" β ", v.action()) garage_report([ Car("BMW", "BMW-123"), Motorcycle("Harley", "MC-883"), Truck("Scania", "SC-456", 1800), ElectricCar("Rivian", "EV-555", 105), ])
π Line-by-Line Explanation
- def garage_report(vehicles): Defines a function that takes a list of vehicles.
- for v in vehicles: Iterates over each vehicle in the list.
- print(v.info()): Calls
info()on the vehicle. Works for any subclass. - print(v.action()): Calls
action()on the vehicle. Python resolves the correct method based on the actual type. - garage_report([...]): Calls the function with a list containing four different vehicle types.
Why this matters: You can add new vehicle types β Bicycle, Boat, Plane β and garage_report() will handle them without any changes, as long as they implement info() and action().
Duck typing vs inheritance: Python's polymorphism works through both inheritance (the classic OOP approach) and duck typing (objects just need to have the right methods). Inheritance-based polymorphism is more explicit and provides better IDE support.
Inheritance Design Patterns
Template Method Pattern
The Template Method pattern defines the skeleton of an algorithm in a parent method, letting subclasses override specific steps without changing the algorithm's structure.
class Vehicle: def maintenance_checklist(self) -> list: checks = ["Check tires", "Check brakes"] checks.extend(self._specific_checks()) return checks def _specific_checks(self) -> list: return [] class Car(Vehicle): def _specific_checks(self) -> list: return ["Check engine oil", "Check transmission fluid"] class Truck(Vehicle): def _specific_checks(self) -> list: return ["Check air brakes", "Check load securing", "Check hydraulic systems"] print(Car("Toyota", "ABC-123").maintenance_checklist()) print(Truck("Ford", "TRK-777", 2000).maintenance_checklist())
Why it works: The parent defines what needs to be done (the algorithm), and subclasses define how specific steps are performed. This promotes code reuse while allowing flexibility.
Inheritance vs Composition
Knowing when to inherit vs when to compose is one of the most important skills in OOP design.
| Aspect | Inheritance | Composition |
|---|---|---|
| Relationship | "is-a" β Dog is an Animal | "has-a" β Car has an Engine |
| Code Reuse | Automatic via inheritance | Manual via delegation |
| Flexibility | Tight coupling to parent | Loose coupling, easier to change |
| When to use | True hierarchies, shared core behavior | Behavior sharing across unrelated classes |
| Example | Car(Vehicle) | Car(engine=Engine()) |
Rule of thumb: Use inheritance when there's a clear "is-a" relationship. Use composition when you need to share behavior across unrelated classes (like logging, caching, or serialization).
Mixin Pattern
Mixins are small, reusable classes that provide specific functionality to be inherited by other classes. They're designed to be used with multiple inheritance.
class JSONMixin: def to_json(self) -> str: import json return json.dumps(self.to_dict()) class CSVMixin: def to_csv(self) -> str: return ",".join(self.to_dict().values()) class Vehicle(JSONMixin, CSVMixin): def to_dict(self) -> dict: return {"brand": self.brand, "plate": self.plate, "mileage": self.mileage} v = Vehicle("Toyota", "ABC-123") print(v.to_json()) print(v.to_csv())
Key insight: Mixins are a powerful way to compose behavior across unrelated classes. They're used extensively in Django (mixins for views, forms, and permissions) and other Python frameworks.
Inheritance FAQ
What is inheritance in Python?
+Inheritance is a mechanism where a new class (subclass) derives attributes and methods from an existing class (parent). It represents an "is-a" relationship β a Car is a Vehicle. The subclass can use, extend, or override the parent's behavior.
What's the difference between inheritance and composition?
+Inheritance creates an "is-a" relationship (Dog is an Animal). Composition creates a "has-a" relationship (Car has an Engine). Inheritance is for when the subclass is a specialized version of the parent. Composition is for when you want to reuse functionality across unrelated classes. Use inheritance for hierarchies, composition for behavior sharing.
What does super() do in Python?
+super() returns a proxy object that delegates method calls to the parent class. It's used to call the parent's version of a method from inside an override. The two most common use cases are: calling super().__init__() in the child's constructor to initialize parent attributes, and calling super().method() inside an override to extend the parent's behavior.
What is method overriding?
+Method overriding is when a subclass defines a method with the same name as a method in its parent class. Python uses the subclass's version instead of the parent's. This lets subclasses customize behavior without changing the parent. You can also use super() to call the parent's version inside the override, effectively extending the parent's behavior.
What is multiple inheritance and how does MRO work?
+Multiple inheritance is when a class inherits from more than one parent. Python uses the C3 linearization algorithm to compute the Method Resolution Order (MRO), which determines the order in which parent classes are searched for methods. You can view the MRO using ClassName.__mro__. Python's super() follows this order, calling the next class in the chain.
What's the difference between private, protected, and public in inheritance?
+Python's conventions: Public attributes have no leading underscore (e.g., brand) β accessible everywhere. Protected attributes have one leading underscore (e.g., _internal) β a convention indicating "internal use only." Private attributes have two leading underscores (e.g., __secret) β Python name-mangles them to avoid accidental overriding. In practice, Python relies on developer discipline rather than enforced access controls.
Sharpen Your Skills
Apply what you've learned with these hands-on exercises. Each one builds on the inheritance concepts covered in this tutorial.
π Exercise 1: Add a Bicycle
Create a Bicycle class that inherits from Vehicle. Override action() to return something like "{brand} pedals forward!". Add a gear attribute and a shift_gear() method.
π Exercise 2: Extend Car with Hybrid
Create a HybridCar class that inherits from Car. Add an electric_range attribute. Override action() to include the electric range: "{brand} honks! Range: {range} miles electric."
super().action() to get the honk, then add your range message.
π Exercise 3: Fleet Summary
Write a function fleet_summary(vehicles) that returns a dictionary with keys: total_vehicles, total_mileage, and total_services. Iterate through the list and sum the values.
len(vehicles), sum v.mileage, and sum len(v.log).
β° Exercise 4: Service Reminder
Add a needs_service() method to Vehicle that returns True if the vehicle has driven 100 miles since its last service. Override it in Truck to trigger service every 80 miles instead.
last_service_mileage in the constructor and update it when service() is called.
π Exercise 5: Mixin for Logging
Create a LogMixin class with a to_log() method that returns a JSON-like string of the vehicle's attributes. Mix it into Vehicle using multiple inheritance.
to_log() can return f'{{"brand": "{self.brand}", "mileage": {self.mileage}}}'
π‘ Pro Tip: Try implementing these exercises in the live garage code above or in your local Python environment. The best way to learn inheritance is to write code, break it, and fix it.
Knowledge Check
Reinforce what you've learned. Each question targets a core inheritance concept from this tutorial.
Great effort β review any missed questions above.
Subscribe to Poetic Bytes
Level up your Python knowledge.
In-depth Python tutorials, AI insights, and software engineering guides β delivered straight to your inbox. No spam, ever.
π Join 8,400+ developers. Your email is never shared or sold.
More Inheritance Projects
Apply what you've learned to these other inheritance-based projects:
Wrapping Up: Your Inheritance Journey
π Inheritance Mastery Checklist
- Created a parent class β
Vehiclewith shared attributes and methods that every subclass inherits. - Built subclasses β
Car,Motorcycle, andTruckβ each extending the parent with specialized behavior. - Used
super()to call the parent constructor and extend initialization without duplicating code. - Overridden methods β each subclass supplies its own
action()while inheritingdrive()andservice(). - Leveraged polymorphism β wrote code that works with any
Vehicle, regardless of subclass, through dynamic method dispatch. - Applied design patterns β Template Method, Mixin pattern, and understood when to use inheritance vs composition.
- Built a live, interactive app powered by the exact same class hierarchy, with persistence via local storage.
- Understood the essentials β why inheritance matters, its significance in the programming community, and how to think about class hierarchies.
Final Words
Inheritance is not just a programming feature β it's a way of thinking about software design. It teaches you to see patterns, recognize shared behavior, and build systems that grow gracefully. Every major Python framework, every design pattern, every well-structured codebase relies on inheritance in some form.
The vehicle hierarchy you built in this tutorial is a microcosm of how real systems work.
Django models inherit from models.Model. PyTorch layers inherit from
nn.Module. Flask views inherit from View. The pattern is everywhere
β and now you understand it.
β¨ "Inheritance is how software grows without repeating itself. Master this, and you'll start seeing hierarchies everywhere β in code, in nature, in organizations. Keep building, keep extending, and remember: the best subclass is the one that adds exactly what's needed, and nothing more."
β Glenn Junsay Pansensoy, Poetic Bytes