Inheriting one class from another

Share
Copied to clipboard.
Series: Classes
Trey Hunner smiling in a t-shirt against a yellow wall
Trey Hunner
4 min. read Watch as video Python 3.8—3.12
Python Morsels
Watch as video
04:08

How does class inheritance work in Python?

Creating a class that inherits from another class

We have a class called FancyCounter, that inherits from another class, Python's Counter (from the collections module):

from collections import Counter


class FancyCounter(Counter):
    def commonest(self):
        (value1, count1), (value2, count2) = self.most_common(2)
        if count1 == count2:
            raise ValueError("No unique most common value")
        return value1

The way we know we're inheriting from the Counter class because when we defined FancyCounter, just after the class name we put parentheses and wrote Counter inside them.

To create a class that inherits from another class, after the class name you'll put parentheses and then list any classes that your class inherits from.

In a function definition, parentheses after the function name represent arguments that the function accepts. In a class definition the parentheses after the class name instead represent the classes being inherited from.

Usually when practicing class inheritance in Python, we inherit from just one class. You can inherit from multiple classes (that's called multiple inheritance), but it's a little bit rare. We'll only discuss single-class inheritance right now.

Methods are inherited from parent classes

To use our FancyCounter class, we can call it (just like any other class):

>>> from fancy_counter import FancyCounter
>>> letters = FancyCounter("Hello there!")

Our class will accept a string when we call it because the Counter class has implemented a __init__ method (an initializer method).

Our class also has a __repr__ method for a nice string representation:

>>> letters
FancyCounter({'e': 3, 'l': 2, 'H': 1, 'o': 1, ' ': 1, 't': 1, 'h': 1, 'r': 1, '!': 1})

It even has a bunch of other functionality too. For example, it has overridden what happens when you use square brackets to assign key-value pairs on class instances:

>>> letters['l'] = -2
>>> letters
FancyCounter({'e': 3, 'H': 1, 'o': 1, ' ': 1, 't': 1, 'h': 1, 'r': 1, '!': 1, 'l': -2})

We can assign key-value pairs because our parent class, Counter creates dictionary-like objects (a.k.a. mappings).

All of that functionality was inherited from the Counter class.

Adding new functionality while inheriting

So our FancyCounter class inherited all of the functionality that our Counter class has but we've also extended it by adding an additional method, commonest, which will give us the most common item in our class.

When we call the commonest method, we'll get the letter e (which occurs three times in the string we originally gave to our FancyCounter object):

>>> letters.commonest()
'e'

Our commonest method relies on the most_common method, which we didn't define but which our parent class, Counter, did define:

    def commonest(self):
        (value1, count1), (value2, count2) = self.most_common(2)
        if count1 == count2:
            raise ValueError("No unique most common value")
        return value1

Our FancyCounter class has a most_commonest method because our parent class, Counter defined it for us!

Overriding inherited methods

If we wanted to customize what happens when we assigned to a key-value pair in this class, we could do that by overriding the __setitem__ method. For example, let's make it so that if we assign a key to a negative value, it instead assigns it to 0.

Before when we assigned letters['l'] to -2, we'd like it to be set to 0 instead of -2 (it's -2 here because we haven't customized this yet):

>>> letters['l'] = -2
>>> letters['l']
-2

To customize this behavior we'll make a __setitem__ method that accepts self, key, and value because that's what __setitem__ is given by Python when it's called:

    def __setitem__(self, key, value):
        value = max(0, value)

The above __setitem__ method basically says: if value is negative, set it to 0.

If we stop writing our __setitem__ at this point, it wouldn't be very useful. In fact that __setitem__ method would do nothing at all: it wouldn't give an error, but it wouldn't actually do anything either!

In order to do something useful we need to call our parent class's __setitem__ method. We can call our parent class' __setitem__ method by using super.

    def __setitem__(self, key, value):
        value = max(0, value)
        return super().__setitem__(key, value)

We're calling super().__setitem__(key, value), which will call the __setitem__ method on our parent class (Counter) with key and our new non-negative value.

Here's a full implementation of this new version of our FancyCounter class:

from collections import Counter


class FancyCounter(Counter):
    def commonest(self):
        (value1, count1), (value2, count2) = self.most_common(2)
        if count1 == count2:
            raise ValueError("No unique most common value")
        return value1
    def __setitem__(self, key, value):
        value = max(0, value)
        return super().__setitem__(key, value)

To use this class we'll call it and pass in a string again:

>>> from fancy_counter import FancyCounter
>>> letters = FancyCounter("Hello there!")

But this time, if we assign a key to a negative value, we'll see that it will be assigned to0 instead:

>>> letters['l'] = -2
>>> letters['l']
0

Class exercises for advanced beginners

Want some more practice with classes in Python?

The Diving Into Classes exercise path includes 6 class exercises for advanced beginners. Python Morsels also includes dozens more exercises on classes and object-oriented Python as well.

✨ Try the Diving Into Classes exercises ✨

A Python Tip Every Week

Need to fill-in gaps in your Python skills? I send weekly emails designed to do just that.

Python Morsels
Watch as video
04:08