Building on the fundamentals of Python classes and objects, it's time to explore more advanced OOP concepts that make your code more efficient, organized, and scalable. In this tutorial, we'll dive into class variables and inheritance - two powerful features that distinguish object-oriented programming from simple function-based programming.
๐ฏ What You'll Learn: In this advanced OOP tutorial, you'll master:
- The difference between class variables and instance variables
- How to create and use shared class attributes
- Understanding inheritance and parent-child class relationships
- Creating subclasses that extend parent functionality
- Using the
super()
function to call parent methods - Method overriding for customizing inherited behavior
- Real-world inheritance examples with electric cars
- Best practices for designing class hierarchies
๐ Continuing Our OOP Journey
In our previous tutorial, we created basic Car classes with instance variables and methods. Now we'll extend this knowledge with more sophisticated OOP concepts that professional developers use every day.
Prerequisites
Before we begin, make sure you have:
- Understanding of Python classes, objects, and methods from Part 1
- Python 3 installed and working on your system
- Completed the basic Car class tutorial
- A text editor for creating Python files
๐๏ธ Understanding Class Variables vs Instance Variables
Let's start by exploring the difference between class variables (shared by all objects) and instance variables (unique to each object).
Creating Our Second OOP File
touch oop2.py
Let's examine what happens when we add class variables to our Car class:
nano oop2.py
Let's create a Car class with a class variable:
class Car:
wheels = 4 # This is a class variable - shared by all cars
def __init__(self, brand, model):
self.brand = brand # These are instance variables
self.model = model # Unique to each car object
def drive(self):
print(f"The {self.brand} {self.model} is now driving.")
Let's view our file:
cat oop2.py
Output:
class Car:
wheels = 4
def __init__(self, brand, model):
self.brand = brand
self.model = model
def drive(self):
print(f"The {self.brand} {self.model} is now driving.")
Testing Class Variables
Let's add code to test how class variables work:
nano oop2.py
Our file now includes a test:
class Car:
wheels = 4 # Class variable
def __init__(self, brand, model):
self.brand = brand
self.model = model
def drive(self):
print(f"The {self.brand} {self.model} is now driving.")
# Test class variable access
print(f"Number of wheels: {Car.wheels}")
Let's run this to see class variables in action:
python oop2.py
Output:
Number of wheels: 4
โ
Class Variables: Notice that we accessed Car.wheels
directly from the class, without creating any objects! Class variables belong to the class itself, not to individual objects.
๐ Class Variables vs Instance Variables Comparison
Aspect | Class Variables | Instance Variables |
---|---|---|
Definition | Defined in class body, outside methods | Defined inside init with self |
Sharing | Shared by all objects of the class | Unique to each object instance |
Access | ClassName.variable or object.variable | object.variable only |
Memory | One copy for entire class | Separate copy for each object |
Use Case | Constants, counters, shared properties | Object-specific data |
๐ Building the Foundation for Inheritance
Before diving into inheritance, let's create a robust parent class. We'll start with our third file:
ls
Output:
oop1.py oop2.py
touch oop3.py
Let's build a comprehensive Car class that will serve as our parent class:
nano oop3.py
We'll start with a basic parent class:
class Car:
def __init__(self, make, model, year):
self.make = make
self.model = model
self.year = year
def describe_car(self):
print(f"{self.year} {self.make} {self.model}")
Let's view our initial parent class:
cat oop3.py
Output:
class Car:
def __init__(self, make, model, year):
self.make = make
self.model = model
self.year = year
def describe_car(self):
print(f"{self.year} {self.make} {self.model}")
๐ Creating Your First Inheritance Relationship
Now comes the exciting part - creating a child class that inherits from the parent class. Let's add an ElectricCar class:
nano oop3.py
Our file now includes inheritance:
class Car:
def __init__(self, make, model, year):
self.make = make
self.model = model
self.year = year
def describe_car(self):
print(f"{self.year} {self.make} {self.model}")
class ElectricCar(Car): # ElectricCar inherits from Car
def __init__(self, make, model, year, battery_size=75):
super().__init__(make, model, year) # Call parent constructor
self.battery_size = battery_size # Add electric-specific attribute
def describe_battery(self):
print(f"This car has a {self.battery_size}-kWh battery.")
Let's view our inheritance structure:
cat oop3.py
Output:
class Car:
def __init__(self, make, model, year):
self.make = make
self.model = model
self.year = year
def describe_car(self):
print(f"{self.year} {self.make} {self.model}")
class ElectricCar(Car):
def __init__(self, make, model, year, battery_size=75):
super().__init__(make, model, year)
self.battery_size = battery_size
def describe_battery(self):
print(f"This car has a {self.battery_size}-kWh battery.")
๐ Understanding Inheritance Components
Let's break down the inheritance syntax:
Component | Syntax | Purpose |
---|---|---|
Parent Class | class Car: | Base class that provides common functionality |
Child Class | class ElectricCar(Car): | Inherits all methods and attributes from Car |
super() | super().init(make, model, year) | Calls the parent class constructor |
Additional Attributes | self.battery_size = battery_size | Child-specific attributes not in parent |
โ
The super()
Function: This special function allows child classes to call methods from their parent class. It's essential for properly initializing inherited objects.
๐ฏ Method Overriding: Customizing Inherited Behavior
One of the most powerful features of inheritance is the ability to override parent methods to provide specialized behavior. Let's add method overriding:
nano oop3.py
Our enhanced file now includes method overriding:
class Car:
def __init__(self, make, model, year):
self.make = make
self.model = model
self.year = year
def describe_car(self):
print(f"{self.year} {self.make} {self.model}")
class ElectricCar(Car):
def __init__(self, make, model, year, battery_size=75):
super().__init__(make, model, year)
self.battery_size = battery_size
def describe_battery(self):
print(f"This car has a {self.battery_size}-kWh battery.")
def describe_car(self): # Override parent method
super().describe_car() # Call parent method first
self.describe_battery() # Add electric-specific info
Let's view the complete inheritance example:
cat oop3.py
Output:
class Car:
def __init__(self, make, model, year):
self.make = make
self.model = model
self.year = year
def describe_car(self):
print(f"{self.year} {self.make} {self.model}")
class ElectricCar(Car):
def __init__(self, make, model, year, battery_size=75):
super().__init__(make, model, year)
self.battery_size = battery_size
def describe_battery(self):
print(f"This car has a {self.battery_size}-kWh battery.")
def describe_car(self):
super().describe_car()
self.describe_battery()
๐งช Testing Our Inheritance Implementation
Let's add object creation and method calls to test our inheritance:
nano oop3.py
Our complete file now includes testing:
class Car:
def __init__(self, make, model, year):
self.make = make
self.model = model
self.year = year
def describe_car(self):
print(f"{self.year} {self.make} {self.model}")
class ElectricCar(Car):
def __init__(self, make, model, year, battery_size=75):
super().__init__(make, model, year)
self.battery_size = battery_size
def describe_battery(self):
print(f"This car has a {self.battery_size}-kWh battery.")
def describe_car(self):
super().describe_car()
self.describe_battery()
my_tesla = ElectricCar('Tesla', 'Model S', 2022)
my_tesla.describe_car()
Let's view our complete implementation:
cat oop3.py
Output:
class Car:
def __init__(self, make, model, year):
self.make = make
self.model = model
self.year = year
def describe_car(self):
print(f"{self.year} {self.make} {self.model}")
class ElectricCar(Car):
def __init__(self, make, model, year, battery_size=75):
super().__init__(make, model, year)
self.battery_size = battery_size
def describe_battery(self):
print(f"This car has a {self.battery_size}-kWh battery.")
def describe_car(self):
super().describe_car()
self.describe_battery()
my_tesla = ElectricCar('Tesla', 'Model S', 2022)
my_tesla.describe_car()
Now let's test our inheritance:
python oop3.py
Output:
2022 Tesla Model S
This car has a 75-kWh battery.
๐ Inheritance Working! The ElectricCar object successfully called both the inherited describe_car()
method from Car and its own describe_battery()
method.
๐ Testing with Custom Battery Size
Let's test with a custom battery size to see how parameters work in inheritance:
nano oop3.py
Let's modify the test to use a custom battery size:
class Car:
def __init__(self, make, model, year):
self.make = make
self.model = model
self.year = year
def describe_car(self):
print(f"{self.year} {self.make} {self.model}")
class ElectricCar(Car):
def __init__(self, make, model, year, battery_size=75):
super().__init__(make, model, year)
self.battery_size = battery_size
def describe_battery(self):
print(f"This car has a {self.battery_size}-kWh battery.")
def describe_car(self):
super().describe_car()
self.describe_battery()
my_tesla = ElectricCar('Tesla', 'Model S', 2022, 100) # Custom battery size
my_tesla.describe_car()
Let's view the updated test:
cat oop3.py
Output:
class Car:
def __init__(self, make, model, year):
self.make = make
self.model = model
self.year = year
def describe_car(self):
print(f"{self.year} {self.make} {self.model}")
class ElectricCar(Car):
def __init__(self, make, model, year, battery_size=75):
super().__init__(make, model, year)
self.battery_size = battery_size
def describe_battery(self):
print(f"This car has a {self.battery_size}-kWh battery.")
def describe_car(self):
super().describe_car()
self.describe_battery()
my_tesla = ElectricCar('Tesla', 'Model S', 2022, 100)
my_tesla.describe_car()
Let's run the test with custom battery:
python oop3.py
Output:
2022 Tesla Model S
This car has a 100-kWh battery.
๐ Understanding the Inheritance Flow
Let's trace through what happens when we create and use an ElectricCar object:
Step | Code | What Happens |
---|---|---|
1 | my_tesla = ElectricCar('Tesla', 'Model S', 2022, 100) | Python creates ElectricCar object |
2 | ElectricCar.init called | Child constructor executes |
3 | super().init(make, model, year) | Parent constructor called |
4 | self.battery_size = 100 | Child-specific attribute set |
5 | my_tesla.describe_car() | Overridden method called |
6 | super().describe_car() | Parent method executes |
7 | self.describe_battery() | Child-specific method executes |
๐ Real-World Inheritance Example
Let's create a more comprehensive example that shows the power of inheritance:
class Vehicle:
"""Base class for all vehicles"""
total_vehicles = 0 # Class variable to count all vehicles
def __init__(self, make, model, year, color="White"):
self.make = make
self.model = model
self.year = year
self.color = color
self.is_running = False
Vehicle.total_vehicles += 1 # Increment class variable
def start_engine(self):
if not self.is_running:
self.is_running = True
print(f"The {self.make} {self.model} engine has started.")
else:
print(f"The {self.make} {self.model} is already running.")
def stop_engine(self):
if self.is_running:
self.is_running = False
print(f"The {self.make} {self.model} engine has stopped.")
else:
print(f"The {self.make} {self.model} is already off.")
def describe_vehicle(self):
status = "running" if self.is_running else "parked"
print(f"{self.year} {self.color} {self.make} {self.model} - Currently {status}")
@classmethod
def get_total_vehicles(cls):
return cls.total_vehicles
class Car(Vehicle):
"""Standard gasoline car"""
def __init__(self, make, model, year, color="White", fuel_capacity=50):
super().__init__(make, model, year, color)
self.fuel_capacity = fuel_capacity
self.fuel_level = fuel_capacity # Start with full tank
def refuel(self):
self.fuel_level = self.fuel_capacity
print(f"The {self.make} {self.model} has been refueled to {self.fuel_capacity} liters.")
def describe_vehicle(self):
super().describe_vehicle()
print(f"Fuel: {self.fuel_level}/{self.fuel_capacity} liters")
class ElectricCar(Vehicle):
"""Electric vehicle with battery"""
def __init__(self, make, model, year, color="White", battery_capacity=75):
super().__init__(make, model, year, color)
self.battery_capacity = battery_capacity
self.battery_level = battery_capacity # Start fully charged
self.charging = False
def charge_battery(self):
if not self.charging:
self.charging = True
print(f"Started charging the {self.make} {self.model}...")
self.battery_level = self.battery_capacity
self.charging = False
print(f"Charging complete! Battery: {self.battery_capacity} kWh")
else:
print(f"The {self.make} {self.model} is already charging.")
def describe_vehicle(self):
super().describe_vehicle()
print(f"Battery: {self.battery_level}/{self.battery_capacity} kWh")
if self.charging:
print("Currently charging...")
class Motorcycle(Vehicle):
"""Two-wheeled motorcycle"""
def __init__(self, make, model, year, color="Black", engine_size=600):
super().__init__(make, model, year, color)
self.engine_size = engine_size
self.has_sidecar = False
def add_sidecar(self):
if not self.has_sidecar:
self.has_sidecar = True
print(f"Sidecar added to the {self.make} {self.model}.")
else:
print(f"The {self.make} {self.model} already has a sidecar.")
def describe_vehicle(self):
super().describe_vehicle()
print(f"Engine: {self.engine_size}cc")
if self.has_sidecar:
print("Equipped with sidecar")
# Test the vehicle hierarchy
print("=== Creating Vehicles ===")
regular_car = Car("Toyota", "Camry", 2023, "Blue", 60)
electric_car = ElectricCar("Tesla", "Model 3", 2023, "Red", 80)
motorcycle = Motorcycle("Harley-Davidson", "Sportster", 2023, "Black", 883)
print(f"\nTotal vehicles created: {Vehicle.get_total_vehicles()}")
print("\n=== Testing Vehicle Operations ===")
for vehicle in [regular_car, electric_car, motorcycle]:
print(f"\n--- {vehicle.make} {vehicle.model} ---")
vehicle.describe_vehicle()
vehicle.start_engine()
vehicle.describe_vehicle()
# Test specific methods
if isinstance(vehicle, Car) and not isinstance(vehicle, ElectricCar):
vehicle.refuel()
elif isinstance(vehicle, ElectricCar):
vehicle.charge_battery()
elif isinstance(vehicle, Motorcycle):
vehicle.add_sidecar()
vehicle.stop_engine()
๐ฏ Best Practices for Inheritance
Practice | Why It Matters | Example |
---|---|---|
Use super() consistently | Ensures proper initialization chain | super().init(args) |
Follow IS-A relationship | ElectricCar IS-A Car makes logical sense | Car โ ElectricCar, Vehicle โ Car |
Keep inheritance shallow | Deep hierarchies become hard to maintain | Max 3-4 levels deep |
Override thoughtfully | Maintain expected behavior | Call super() then add functionality |
โ ๏ธ Common Inheritance Pitfalls
1. Forgetting to Call super() in Constructor
class ElectricCar(Car):
def __init__(self, make, model, year, battery_size):
# Missing super().__init__() - parent attributes won't be set!
self.battery_size = battery_size
class ElectricCar(Car):
def __init__(self, make, model, year, battery_size):
super().__init__(make, model, year) # Initialize parent first
self.battery_size = battery_size
2. Incorrect Method Overriding
class ElectricCar(Car):
def describe_car(self):
print(f"Battery: {self.battery_size} kWh") # Lost car info!
class ElectricCar(Car):
def describe_car(self):
super().describe_car() # Keep parent functionality
print(f"Battery: {self.battery_size} kWh") # Add new info
3. Breaking the IS-A Relationship
class Car(Wheel): # This doesn't make logical sense
pass
class Car:
def __init__(self):
self.wheels = [Wheel(), Wheel(), Wheel(), Wheel()]
๐ฏ Key Takeaways
โ Remember These Points
- Class Variables: Shared by all instances, defined at class level
- Inheritance: Child classes inherit all attributes and methods from parents
- super(): Essential for calling parent class methods properly
- Method Overriding: Customize inherited behavior while maintaining functionality
- IS-A Relationship: Inheritance should represent a logical "is a" relationship
๐งช Practice Challenge
Try creating your own inheritance hierarchy:
- Create a
Shape
base class witharea()
andperimeter()
methods - Create
Rectangle
andCircle
child classes that inherit fromShape
- Override the area and perimeter methods in each child class
- Add specific methods like
is_square()
to Rectangle - Test creating objects and calling both inherited and overridden methods
๐ What's Next?
Continue your Python OOP journey with these advanced topics:
- Multiple Inheritance: Inheriting from multiple parent classes
- Abstract Base Classes: Creating interfaces and enforcing method implementation
- Property Decorators: Creating getter/setter methods for controlled attribute access
- Class Methods and Static Methods: Alternative method types for different use cases
๐ Congratulations! You've mastered Python inheritance and class variables. You can now create sophisticated object hierarchies that share common functionality while adding specialized features. These concepts are fundamental to building large, maintainable applications.
๐ Summary
In this comprehensive tutorial, you learned:
- Class Variables: How to create shared attributes across all instances
- Inheritance Fundamentals: Creating parent-child class relationships
- Constructor Chaining: Using
super()
to properly initialize inherited objects - Method Overriding: Customizing inherited methods for specialized behavior
- Real-World Applications: Building vehicle hierarchies with multiple inheritance levels
- Best Practices: Following IS-A relationships and proper inheritance design
- Common Pitfalls: Avoiding mistakes that break inheritance functionality
Ready to become a Python OOP expert? Explore our complete Python Programming Series for more advanced tutorials including multiple inheritance, abstract classes, and design patterns.