Inheritance Best Practices
00:00 You’ve now learned how to write what is fundamentally the same software project using both inheritance and composition. The rest of this course will be about learning when to use each one, and more importantly, when not to use them.
00:18 We’ll start with inheritance. First off, it’s a good idea to try to follow the Liskov substitution principle when you can. If you remember, this states that if some interface requires a specific type of object, you can substitute in a class that is a child of that type so long as it properly inherits the interface it needs.
We use this in our employee management program—both the productivity and payroll systems required a list of employees to operate on—but instead of just giving them
Employee objects we gave them subclasses of
Additionally, the Python language requires that we make exception types inherit from
BaseException, but we inherited from
Exception, which is okay because
Exception itself inherits from
01:24 When you have two classes and you’re trying to determine whether or not you should inherit one from the other, think about the is a relationship that inheritance models. In general, you want to use inheritance only if the relationship works in one direction—that is, A is a B but B is not an A.
01:50 If you can justify the relationship in both directions, you should avoid making one inherit from the other. To understand why, let’s take a look at a code demo.
Let’s say we are writing a program to calculate the areas of various shapes. I’m writing this in a new module called
02:14 There are two shapes we need to be able to handle: squares and rectangles. Squares and rectangles are pretty similar. A square is just a rectangle but with equal sides.
02:28 A rectangle, on the other hand, does not have to have sides of equal length. At the same time, you could argue that a rectangle is just a square with uneven lengths.
It sounds like this relationship works either way, so we probably shouldn’t use inheritance. To show you what would happen if we do, I’m going to create a
Rectangle class and then a
Square class that inherits from
Rectangle. I’ll make the
Rectangle have instance attributes for the length and the height.
I’m using a leading underscore (
_) to show that we should not access or modify these attributes from outside of the
In order to calculate the area, I’ll create a new method called
.area(). This will simply return the
._length times the
I’m going to add this
@property decorator to the method so that we can access it as if it were an attribute and not a method. That just means that we’ll call the method without parentheses.
Now, I’m going to create a new class called
Square, which will inherit from
Rectangle. This class will have a different interface for its requirements.
It will accept only a
side_size and then it will initialize its parent constructor, passing in that size for both the
length and the
That looks like it should be good. Let’s try this out by instantiating the classes. I’ll create a
rectangle object that will store a
This should give us an area of
8. To check that I’ll say
assert rectangle.area == 8. The
assert keyword acts almost as a conditional.
If the Boolean statement evaluates to
True, execution continues to the next line.
False, execution stops and an
AssertionError is raised. Also, notice how we didn’t call the
.area() method with parentheses.
It almost looks like it’s an attribute. That’s because we marked that method as a property. We can use a property for this method because we aren’t passing any data to the
.area() method. We’re just calling it, so it calculates the area based on the instance attributes already set in the object. Next, I’ll create a square of length
Just like before, we want to assert that the area of this square is
If all this checks out I’ll print
It looks like everything is okay so far. Right now,
Square just acts as a specialized
Rectangle. But now what happens if we have to add support for resizing the rectangle after it’s been created? That doesn’t sound too hard.
All we have to do is create a method called
.resize(), which will change the instance attributes of the
Rectangle to some new values.
06:01 I’ll add this new method to our list of assertions.
All right, that seems to work fine. What about resizing a square? The
Square class inherited the
.resize() method from the
06:20 I’ll give it some dimensions that are not equal.
And now we see that it’s working, unfortunately. We’ve just created a
Square object and transformed it into what’s really a rectangle, but it’s still of type
Square; now it’s just an invalid square. There are ways that we can fix this, but it’s going to be awkward any way we do it.
We could override the
.resize() method in the
Square class, ignoring the
height parameter, but then we’d have a bunch of
Rectangle objects where only some of them are resized with two parameters—the rectangle—and others—the squares—are resized with one. The
Square and the
Rectangle require different interfaces because their
.resize() methods require a different number of arguments.
@mojkol The rectangle-square inheritance is a classic example of the Liskov Substitution Principle (the “L” in SOLID) violation, leading to subtle bugs. In a nutshell, code that expects rectangles should work correctly with instances of rectangle’s subclasses without knowing the difference. Since squares exhibit different behaviors and properties than rectangles, you may be forced to check the type information at runtime, which breaks polymorphism. There’s a great lecture available for free on YouTube by Uncle Bob about the SOLID principles, which goes into more detail about the rectangle-square problem.
Become a Member to join the conversation.
mojkol on Dec. 18, 2021
First, thank you for the great tutorial! Let me ask a quick question. I just don’t understand why the Square class can’t inherit the Rectangle? You said, because resize() method requires two parameters for the Rectangle class and it requires one parameter for the Square class. What confuses me is that, the same goes for the init() method. init() of Rectangle accepts two parameters and the init() of Square accepts one parameter! So, why this fact is not a problem for init(), but it is a problem for resize()?! Thanks!