🤖 Robot Factory
Every robot needs a name, a battery, and the ability to charge. Yet a cleaning robot, a delivery robot, and a security robot each perform completely different work. Inheritance lets us capture the common parts once and specialize only what is unique.
🎯 By the end of this tutorial, you will be able to:
- Identify when inheritance is the right tool to eliminate code duplication
- Design a base class that captures shared state and behavior
- Create subclasses that extend the base class using
super() - Override methods to provide specialized behavior
- Use polymorphism to write flexible, reusable code
- Apply the Open/Closed Principle in practice
- Recognize common inheritance pitfalls and how to avoid them
What Happens Without Inheritance?
Imagine you need three different robots. Without inheritance, you'd write three nearly identical classes:
Without Inheritance — Duplicated Code
⚠️ The Problem: The __init__, charge(), and status() logic is duplicated in every class. If you need to change the charging logic, you must update it in three places. If you add a new robot type, you copy-paste again. This is brittle, error-prone, and violates the DRY (Don't Repeat Yourself) principle.
💡 The Solution: Use inheritance to extract the shared code into a base class. The subclasses will then inherit the common behavior and only define what makes them unique.
📋 Your Learning Progress
Step 1: The Robot Base Class
We move all shared code into a single Robot class. This class captures the common interface that all robots will share.
The Base Robot Class
🔑 Key Points:
- The base class contains all the common code — once.
run_task()raisesNotImplementedErrorto force subclasses to provide their own implementation.- This is a lightweight version of an abstract method.
- Any new robot type can now inherit from
Robotand only overriderun_task().
✅ Result: The duplication is gone. All robots share the same __init__, charge(), and status() logic. If we need to change how charging works, we change it once in the base class, and every robot automatically gets the improvement.
Step 2: Creating the Subclasses
Now we create three subclasses, each of which:
- Inherits from
Robot - Calls
super().__init__()to initialize the parent - Adds its own specialized attribute (e.g.,
floor_type) - Overrides
run_task()with its own logic
The CleaningBot Subclass
🔑 Why super().__init__() is essential: If you forget this call, the subclass will not have self.name or self.battery defined. This will cause an AttributeError when you try to use status() or run_task(). Always initialize the parent first!
The DeliveryBot and SecurityBot Subclasses
The Full Robot Factory
Here is the complete implementation. Click Run to execute it in your browser — or Copy to paste into your own environment.
class Robot: def __init__(self, name: str, battery: int = 100): self.name = name self.battery = battery def charge(self) -> None: self.battery = 100 print(f"⚡ {self.name} charged to 100%") def status(self) -> str: return f"🤖 {self.name}: battery {self.battery}%" def run_task(self) -> str: raise NotImplementedError("Subclasses must implement run_task()") class CleaningBot(Robot): def __init__(self, name: str, battery: int = 100, floor_type: str = "tile"): super().__init__(name, battery) self.floor_type = floor_type def run_task(self) -> str: if self.battery < 20: return f"🧹 {self.name} needs to charge before cleaning {self.floor_type}." self.battery -= 15 return f"🧹 {self.name} is vacuuming the {self.floor_type} floor. Battery now {self.battery}%" class DeliveryBot(Robot): def __init__(self, name: str, battery: int = 100, destination: str = "warehouse"): super().__init__(name, battery) self.destination = destination def run_task(self) -> str: if self.battery < 30: return f"📦 {self.name} cannot deliver — battery too low." self.battery -= 25 return f"📦 {self.name} delivered package to {self.destination}. Battery now {self.battery}%" class SecurityBot(Robot): def __init__(self, name: str, battery: int = 100, patrol_zone: str = "perimeter"): super().__init__(name, battery) self.patrol_zone = patrol_zone def run_task(self) -> str: if self.battery < 10: return f"🔒 {self.name} is too low on battery to patrol." self.battery -= 10 return f"🔒 {self.name} is patrolling the {self.patrol_zone}. Battery now {self.battery}%" cleaner = CleaningBot("Dusty", floor_type="carpet") delivery = DeliveryBot("Speedy", destination="downtown") security = SecurityBot("Guardian", patrol_zone="building B") robots = [cleaner, delivery, security] for bot in robots: print(bot.status()) print(bot.run_task()) print("---") print("\n🔋 After a full charge:") cleaner.charge() print(cleaner.run_task())
💡 Try it yourself: Change the battery thresholds, invent a new robot type, or add a second specialized method. Because the code runs in the browser (via Pyodide), you can experiment instantly and see the results without leaving the page.
What You'll See
When you click Run, the program creates three robots, asks each for its status, then asks each to perform its task.
🔍 What to notice:
- Each robot reports the same style of status line because
status()lives in the base class. - Each robot produces a different task message and drains a different amount of battery because
run_task()was overridden. - The final two lines prove that a method defined only in the parent (
charge()) is fully available on a child instance — no extra code required. - This is polymorphism in action: the same call (
run_task()) produces different results depending on the actual type of the robot.
The Core Ideas Illustrated by the Robot Factory
This project is deliberately small so the important ideas stand out clearly. Below is what each part of the design is teaching you.
Single Inheritance
Each subclass has exactly one parent. Keeps the hierarchy simple and predictable.
super()
Always call the parent constructor. Forgetting this is a common bug.
Method Overriding
Subclasses replace or specialize behavior while reusing everything else.
Polymorphism
Same call, different behavior. Write code that works with any robot.
Open/Closed
Open for extension, closed for modification. Add new robots without changing existing code.
NotImplementedError
A lightweight way to enforce that subclasses implement required methods.
class CleaningBot(Robot):
Every subclass has exactly one parent. This keeps the hierarchy simple and predictable. Python also supports multiple inheritance, but single inheritance is the foundation you should master first.
super()
super().__init__(name, battery)
The subclass is responsible for initializing the parts it inherits. super() finds the correct parent class and calls its __init__. After that call finishes, the subclass adds its own attributes (floor_type, destination, patrol_zone). This two-step initialization is the standard pattern.
Each robot redefines only run_task(). The methods status() and charge() are never rewritten; they are simply inherited. Overriding lets a subclass replace or specialize behavior while still reusing everything else.
for bot in robots: print(bot.run_task())
The loop does not care whether the object is a CleaningBot, DeliveryBot, or SecurityBot. It only knows that every object supports the same interface (status and run_task). This is polymorphism: the same call produces different results depending on the actual type of the object.
The base Robot class is closed for modification (we do not need to change it when we invent a new robot). It is open for extension (we simply write a new subclass). Adding a ChefBot or a FirefighterBot requires zero changes to existing code.
NotImplementedError
def run_task(self) -> str: raise NotImplementedError(...)
This is a lightweight way to say "every real robot must define its task." In larger projects you would often use abc.ABC and @abstractmethod, but the principle is the same.
Inheritance vs Composition
A common question: When should I use inheritance, and when should I use composition? Here's a quick comparison:
| Inheritance (is-a) | Composition (has-a) |
|---|---|
| "A CleaningBot is a Robot" | "A Robot has a BatteryManager" |
| Use when you want to reuse behavior from a parent class | Use when you want to delegate behavior to another object |
| Changes to the base class affect all subclasses | Changes to the component affect only the class that uses it |
| Can lead to deep, rigid hierarchies if overused | More flexible and easier to test |
| Prefer when: The relationship is truly "is-a" and the subclass is a clear subtype | Prefer when: The relationship is "has-a" and you want to avoid tight coupling |
🧠 Mental Model to Keep: The base class is a contract + shared toolbox. Subclasses accept the contract and only customize what makes them unique. Once this model is solid, the rest of object-oriented design (abstract base classes, mixins, composition over inheritance, dependency injection) becomes much easier to learn.
Common Mistakes to Avoid
| Mistake | Better Approach |
|---|---|
| Copy-pasting the same methods into every subclass | Put shared logic in the base class |
Forgetting to call super().__init__() | Always initialize the parent first |
| Making the base class know about every possible subclass | Keep the base class general; let subclasses specialize |
| Overriding methods just to change a tiny detail | Prefer composition or parameters when the difference is small |
| Deep inheritance trees (5+ levels) | Prefer shallow hierarchies + composition |
Real-World Connections
Reinforce Your Learning
Try these exercises to solidify your understanding of inheritance. Each one builds on the concepts you've just learned.
🧪 Exercise 1: Add a ChefBot
Create a new robot that follows the same contract. A ChefBot should have a specialty attribute and a run_task() that cooks a dish. It should drain 20% battery per cooking session and require at least 25% battery to start.
Your ChefBot should inherit from Robot, call super().__init__(), and override run_task() with the logic above. Make sure to check battery before draining!
🧪 Exercise 2: Fix the Broken Robot
This code is broken — it will cause an AttributeError. Can you fix it?
Add super().__init__(name, battery=100) to the __init__ method. This initializes the parent class, giving the instance self.name and self.battery.
class FixedBot(Robot): def __init__(self, name: str, battery: int = 100): super().__init__(name, battery)
🧪 Exercise 3: Predict the Output
What will this code print when run? Try to work it out in your head, then click "Show Result" to check.
🤖 Tester: battery 50%
Testing... battery now 45%
🤖 Tester: battery 45%
⚡ Tester charged to 100%
🤖 Tester: battery 100%
Notice that status() and charge() are inherited from Robot — no extra code needed!
📝 Test Your Understanding
Question 1: What does super().__init__() do?
Question 2: Why does the base Robot class raise NotImplementedError in run_task()?
Question 3: Which methods are inherited (not overridden) in the CleaningBot class?
Question 4: What concept is demonstrated by for bot in robots: bot.run_task()?
Question 5: What is the Open/Closed Principle?
Question 6: Which is better — inheritance or composition?
More Inheritance Projects
Each of these projects is built around a class hierarchy that demonstrates inheritance in a different domain. Click any to explore.