Execute Program

Python in Detail: Subclassing

Welcome to the Subclassing lesson!

This lesson is shown as static text below. However, it's designed to be used interactively. Click the button below to start!

  • Classes can inherit from other classes, establishing a parent/child relationship. The child class inherits attributes and methods from its parents, as if we'd defined them directly in the child.

  • As an example, we might write a Tiger class that inherits from Cat. (This isn't ideal, since tigers aren't actually house cats. We'll revisit this issue at the end of this lesson.)

  • >
    class Cat:
    def __init__(self, name):
    self.name = name

    def needed_calories(self):
    return 300

    def needs(self):
    return f"{self.name} needs {self.needed_calories()} calories"
  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    keanu = Cat("Keanu")
    keanu.needs()
    Result:
    'Keanu needs 300 calories'Pass Icon
  • Tigers share many characteristics with cats. We could duplicate Cat's code in the new Tiger class, but duplicating code is rarely an ideal solution. Instead, Tiger can inherit from Cat with the syntax class Tiger(Cat). This way, Tiger gets all of Cat's methods and attributes, and can also define its own methods and attributes.

  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    class Tiger(Cat):
    can_roar = True

    big_keanu = Tiger("Big Keanu")
  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    big_keanu.needed_calories()
    Result:
    300Pass Icon
  • (A tiger needs to eat more than 300 calories, but we'll deal with that in a moment.)

  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    big_keanu.can_roar
    Result:
    TruePass Icon
  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    big_keanu.needs()
    Result:
    'Big Keanu needs 300 calories'Pass Icon
  • We say that Tiger is a "subclass", "derived class", or "child class" of Cat. Conversely, Cat is Tiger's "superclass", "base class", or "parent class". These terms are all common, so we'll use them interchangeably in this course.

  • We can decide whether one class is a subclass of another with the built-in issubclass function.

  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    issubclass(Tiger, Cat)
    Result:
    TruePass Icon
  • Like isinstance, we can pass a tuple of classes to ask "is this class a subclass of any of these other classes?"

  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    issubclass(Tiger, (int, Cat, str))
    Result:
    TruePass Icon
  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    issubclass(Tiger, (int, bool, str))
    Result:
    FalsePass Icon
  • In an earlier lesson, we saw that any instance can override class attributes. The instance attribute shadows the class attribute, meaning that the instance attribute takes precedence.

  • >
    class Cat:
    favorite_food = "salmon"

    def __init__(self, name):
    self.name = name

    keanu = Cat("Keanu")
    keanu.favorite_food = "tuna"

    keanu.favorite_food
    Result:
    'tuna'Pass Icon
  • Subclasses add another detail to how we think about attribute access. When we access an attribute:

    • Python checks for an instance attribute.
    • Then it checks for an attribute on the instance's class.
    • Then it checks the class's superclass.
    • Then it checks the class's superclass's superclass, and so on.
  • Methods in Tiger can refer to attributes and methods defined in Cat. In the next example, the .roar method uses the name attribute and needed_calories method from Cat. Neither is defined in Tiger!

  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    class Tiger(Cat):
    def roar(self):
    return f"{self.name} roars that it needs {self.needed_calories()} calories!"

    big_keanu = Tiger("Big Keanu")
    big_keanu.roar()
    Result:
    'Big Keanu roars that it needs 300 calories!'Pass Icon
  • We can also override a method originally defined in a superclass. Below, we update the definition of needed_calories in the Tiger class. That changes the return value of .needs, since that method calls the needed_calories method. This works even though .needs was originally defined in Cat.

  • (Note that we're calling the original Cat.needs method here, not the new Tiger.roar method defined above.)

  • >
    class Cat:
    def __init__(self, name):
    self.name = name

    def needed_calories(self):
    return 300

    def needs(self):
    return f"{self.name} needs {self.needed_calories()} calories"

    class Tiger(Cat):
    def needed_calories(self):
    return 6000
  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    big_keanu = Tiger("Big Keanu")
    big_keanu.needs()
    Result:
    'Big Keanu needs 6000 calories'Pass Icon
  • When we call .needs, Python dynamically determines which .needed_calories method to use. The rules are the same as what we showed above for attributes: Python checks the instance, then its class, then the superclass, then the superclass's superclass, and so on. (Ultimately, this happens because Python methods are really just attributes, but that's a topic for a different lesson.)

  • It's often useful for a subclass's method to call the same method in its parent class. In the previous example, we hard-coded the calorie needs for the Tiger class to 6,000 calories. We can also determine its needs dynamically by calling Cat.needed_calories(self) and modifying the return value.

  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    class Tiger(Cat):
    def needed_calories(self):
    return Cat.needed_calories(self) * 20

    big_keanu = Tiger("Big Keanu")
    big_keanu.needs()
    Result:
    'Big Keanu needs 6000 calories'Pass Icon
  • In that example, control passed back and forth between Tiger and Cat:

    • We called the Cat.needs method on big_keanu.
    • Cat.needs called self.needed_calories, which is actually Tiger.needed_calories.
    • Tiger.needed_calories called Cat.needed_calories.
  • Each method call is dynamically dispatched, independent of previous method calls. That's why control flow can go Cat -> Tiger -> Cat, as shown above.

  • Now Tiger's calorie needs are tied to Cat's. If we later change Cat.needed_calories to return 250, then Tiger.needed_calories will return 5,000 (250 * 20).

  • We can also use super() in place of the parent class' name. For example, inside of a method on Tiger, super().needed_calories() calls Cat.needed_calories(self).

  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    class Tiger(Cat):
    def needed_calories(self):
    return super().needed_calories() * 20

    big_keanu = Tiger("Big Keanu")
    big_keanu.needed_calories()
    Result:
    6000Pass Icon
  • Why would we use super() when we know that the parent class is Cat? In this example, they do the same thing, so either one works. But in a future lesson, we'll see multiple inheritance, where a subclass simultaneously inherits from more than one parent class. In that case, super() will make a big difference.

  • All tigers have stripes, but only some have a white coat, so we'll add that as an argument to the constructor. The updated Tiger class below takes two arguments: name and white_coat. The name is set in the parent class, Cat, so Tiger only sets the white_coat attribute. But the next example has a bug: accessing big_keanu.name causes an AttributeError.

  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    class Tiger(Cat):
    def __init__(self, name, white_coat):
    self.white_coat = white_coat

    big_keanu = Tiger("Big Keanu", False)
    big_keanu.name
    Result:
    AttributeError: 'Tiger' object has no attribute 'name'Pass Icon
  • In the earlier examples, Tiger didn't define .__init__ at all, but we could still access the tiger's .name. That worked because Tiger inherited the Cat.__init__ method. However, now we're defining a new Tiger.__init__ method, so Python calls that. The result is that Cat.__init__ is never called, so it never gets a chance to set self.name.

  • One possible solution is to set self.name = name inside of Tiger.__init__. But that's a bad idea because it duplicates existing functionality in Cat. We often say that Cat "owns" the name attribute, so it's responsible for setting it.

  • Instead, Tiger.__init__ can call Cat.__init__. Constructors are just methods, so we can use the super() function that we saw earlier: super().__init__(name). Note that we pass along the name argument, just like we would if we were instantiating a new Cat.

  • >
    class Cat:
    def __init__(self, name):
    self.name = name

    def needed_calories(self):
    return 300

    def needs(self):
    return f"{self.name} needs {self.needed_calories()} calories"

    class Tiger(Cat):
    def __init__(self, name, white_coat):
    super().__init__(name)
    self.white_coat = white_coat
  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    big_keanu = Tiger("Big Keanu", False)
    big_keanu.name
    Result:
    'Big Keanu'Pass Icon
  • In real-world code, it's common for the subclass constructor to call the superclass's constructor. When we forget to do that, we often see AttributeErrors, like the one that we got for .name previously.

  • Two final notes about subclassing. First, super actually takes two arguments: the class and the current instance. In the examples above, super() was a shorthand for super(Tiger, self). Modern versions of Python fill those in automatically, so we can call super() without any arguments to get the same result. In older Python code, you may see super(SomeClass, self).

  • Second, we mentioned that Tiger inheriting from Cat doesn't really make sense: tigers aren't house cats. Real-world code often has questionable inheritance relationships like this, but we can make them more correct if we want to. In this case, cats and tigers share a common family, Felidae, but they have different subfamilies, genuses, and species. Here's an inheritance hierarchy modeling cats' and tigers' full scientific classification:

  • >
    # All classifications up to the Felidae family that cats and tigers share.
    class EukaryotaDomain:
    ...
    class AnimaliaKingdom(EukaryotaDomain):
    ...
    class ChordataPhylum(AnimaliaKingdom):
    ...
    class MammaliaClass(ChordataPhylum):
    ...
    class CarnivoraOrder(MammaliaClass):
    ...
    class FeliformiaSuborder(CarnivoraOrder):
    ...
    class FelidaeFamily(FeliformiaSuborder):
    ...

    # Cat: Felidae -> Felinae -> Felis -> Felis catus ("cat").
    class FelinaeSubfamily(FelidaeFamily):
    ...
    class FelisGenus(FelinaeSubfamily):
    ...
    class FelisCatus(FelisGenus):
    ...

    # Tiger: Felidae -> Pantherinae -> Panthera -> Panthera tigris ("tiger").
    class PantherinaeSubfamily(FelidaeFamily):
    ...
    class PantheraGenus(PantherinaeSubfamily):
    ...
    class PantheraTigris(PantheraGenus):
    ...
  • That may be more correct, but it's an extreme example of why we often allow imperfections in our real-world inheritance hierarchies. In many cases, the "perfect" hierarchy is cumbersome. It might be easier to model tigers as special house cats, even though that's not actually true.

  • Here's a code problem:

    Cheetahs share some characteristics with house cats. However, cheetahs need to eat seven times as many calories per day.

    A Cat class is already defined below. Define a new Cheetah class that inherits from Cat. Its constructor should take two arguments: name and is_king. ("King cheetahs" have a rare mutation that results in distinct stripes on their back.)

    The Cheetah class should also have its own .needed_calories method that overrides Cat's. You can calculate a cheetah's calorie requirements by multiplying a cat's .needed_calories() by seven. Be sure to let Cat set name by calling super().__init__(name).

    class Cat:
    def __init__(self, name):
    self.name = name
    self._calorie_requirement = 300

    def needed_calories(self):
    return self._calorie_requirement

    def needs(self):
    return f"{self.name} needs {self.needed_calories()} calories"
    class Cheetah(Cat):
    def __init__(self, name, is_king):
    super().__init__(name)
    self.is_king = is_king

    def needed_calories(self):
    return super().needed_calories() * 7
    fast_keanu = Cheetah("Fast Keanu", False)
    assert fast_keanu.needed_calories() == 2100
    assert fast_keanu.name == "Fast Keanu"
    assert fast_keanu.is_king == False

    # Changing a cheetah's `Cat._calorie_requirement` attribute also the cheetah's
    # needed calories.
    fast_keanu._calorie_requirement = 280
    assert fast_keanu.needed_calories() == 1960
    Goal:
    None
    Yours:
    NonePass Icon