Inheritance Syntax:

Let's make some tweaks to our class to make it a bit more generic: We now have a Vehicle class that looks very similar to our Car class.

In our __init__ method, we've added a capacity parameter, that way a vehicle object can keep track of how many max passengers they can have.

This means that we can create Vehicles that have a capacity of 3 passengers, or 1, or 6, or 20! They will all use the same logic.

How about the types of vehicles we have? Cars and BigCars and MotorBikes? It seems like we could just add another attribute to the Vehicle class to track that, something like:

class Vehicle:
	def __init__(self, license, capacity, vehicle_type):
		self.license = license
		self.capacity = capacity
		self.available = True
		self.passengers = []
		self.vehicle_type = vehicle_type

But what happens if we want to add functionality to some types of vehicles, but not others? Say for example that we let our users order food in our apps. We're happy to let cars and bikes deliver that order, but we don't want to use bigcars for that.


def order(self):
	if self.vehicle_type == 'car' or self.vehicle_type == 'bike':
		# place the order
	else:
		# ignore the order

With every new functionality though, we'd have to ask ourselves: Does this apply to all the vehicle types we support? which ones allow it, which ones don't?

Things can get even more annoying if we introduce a new type of vehicle. Imagine if we also want to support Bikes. We'd have to go through all our methods and see if a Bike should or shouldn't be able to perform that action.

There has to be a better way! and there is. Let's talk about Inheritance

Inheritance

Inheritance is one of the pillars of Object-Oriented Programming. It enables us to create a new class that already has all the data and functionality of another class. In this case, we may want to create a Car class that inherits from the Vehicle class.

We leave our Vehicle class unchanged, but we create two new classes: Car and MotorBike.

You will notice that those class definitions are very light: No __init__, only an __str__ method we added to help us demonstrate differences between the various classes.

Here we say that Car is a subclass of Vehicle. We can also say that Car is a children class of Vehicle, or that Car extends Vehicle

Similarly, we say that Vehicle is a parent class to Car.

As you can tell however, the two objects we create for our new classes are able to add passengers and drop them off. That is because a Car object is a Vehicle object, and can do everything a Vehicle can.

This is very helpful! if we make a change to the Vehicle class, all its subclasses will be able to benefit, while we also keep our ability to customize each subclass.

Let's focus on the syntax once again:

def my_subclass(ParentClass):
	# define custom code for the subclass

	# All methods of your parent are by default methods of the subclass

Let's look into this code in Python Tutor to get a sense for what's going on in-memory. To simplify our analysis, we only look at Vehicle and Car:

After Step 1 and Step 2, we see 2 classes defined in our global frame. Note how it's clear to Python that the Car class extends the Vehicle class.

In Step 3, as we create a new Car object, you can see that we call the __init__ method of the Vehicle class. This makes sense because we did not define a specific __init__ for Car, and a Car is a Vehicle so we can rely on that constructor.

In Step 10, we want to print our object. Step 11 takes us to the __str__ method we defined for Car. If we hadn't defined this method, we would look for the __str__ method of the parent class, etc.

In Step 14, we want to use the request method. In Step 15 we see the code jump into the Vehicle class' method definitions and executing the code there!

Conclusion:

We can use inheritance to save ourselves from repeating a lot of similar code.

Inheritance is ideal in scenarios where we have many related models to implement, particularly if model_A is a kind of model_B. We can define model_B, and have model_A inherit methods and data from it.

We can customize our subclasses with their own methods, and we can also call methods from their parent classes. This also applies for grandparent classes, or great-grandparent classes! you can keep this going as much as needed.

What happens in scenarios where we don't want to inherit everything the parent class has to offer? Let's find out in the next section.