Welcome to the Treehouse Community

Want to collaborate on code errors? Have bugs you need feedback on? Looking for an extra set of eyes on your latest project? Get support with fellow developers, designers, and programmers of all backgrounds and skill levels here with the Treehouse Community! While you're at it, check out some resources Treehouse students have shared here.

Looking to learn something new?

Treehouse offers a seven day free trial for new students. Get access to thousands of hours of content and join thousands of Treehouse students and alumni in the community today.

Start your free trial

Python

Niki Molnar
Niki Molnar
25,698 Points

Why do you need to use super()?

When introducing inheritance, the tutor says that by splitting out the Character class initially, you are no longer able to set "sneaky".

The original code was:

class Thief(Character):
    sneaky = True

    def __init__(self, name, sneaky=True, **kwargs):
        self.name = name
        self.sneaky = sneaky

        for key, value in kwargs.items():
            setattr(self, key, value)

The separated code is as follows:

class Character:
    def __init__(self, name, **kwargs):
        self.name = name

        for key, value in kwargs.items():
            setattr(self, key, value)

class Thief(Character):
    sneaky = True

but if I do the following on the new code:

raffles = Thief("Raffles")
print(raffles.sneaky)      # prints True
raffles.sneaky = False
print(raffles.sneaky)      # prints False

This changes sneaky without using super() on the Thief class, presumably because it's being passed to Character as a kwarg?

I don't understand why the tutor is saying we can't change sneaky anymore without super()

2 Answers

Alex Koumparos
seal-mask
.a{fill-rule:evenodd;}techdegree
Alex Koumparos
Python Development Techdegree Student 36,887 Points

Hi Niki,

Kenneth's example probably wasn't the best example to illustrate super(). As you mentioned, the parent class's initialiser can take any number of keyword arguments, so we have the ability to set any attribute we like on the subclass and super() seems redundant.

Let's consider instead a totally different example.

When creating a subclass we want to inherit the characteristics and the behaviours of the parent, rather than rewriting them ourselves.

For example:

class Monster:

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

We could implement a Zombie subclass like this:

class Zombie(Monster):

    def __init__(self, name, hungry=True):
        self.name = name
        self.hungry = hungry
        self.undead = True

Here we are overriding the initialiser of Monster. We've repeated ourselves, which is bad, and because we are totally reimplementing the __init__() method, we're missing out on the benefit of inheritance: delegating behaviour to the parent that we don't need to implement ourselves (and missing out on getting updated behaviour if Monster gets updated in the future).

So, we could rewrite our Zombie class as follows:

class Zombie(Monster):

    def __init__(self, name, hungry=True):
        Monster.__init__(self, name, hungry)
        self.undead = True

This is much better, instead of overriding Monster's initialiser, we are extending it: we're preserving its functionality and relying on it to do whatever needs to be done with name and hungry and just implementing our additional functionality: giving the Zombie the undead attribute.

However, this code isn't very portable: it depends on the parent class always being Monster. At a first glance, this is obviously true: it's right there in the class definition. But, we can't guarantee where we (or our future selves) will be in the inheritance hierarchy. Suppose, down the road, someone wants to extend Zombie to enable new functionality, such as a 'Runner' zombie.

When we (or they) subclass Zombie to make the extended zombie, we might do this:

class RunnerZombie(Zombie):

    surprisingly_fast = True

We haven't overridden or extended Zombie's __init__(), so we inherit its initialiser unchanged. We might reasonably assume that RunnerZombie's initialiser delegates behaviour to its parent (Zombie) but it doesn't: it delegates behaviour to Monster. If during the lifecycle of our application, Monster acquires behaviour that Zombie shouldn't have (or Zombie overrides Monster's behaviour), hardcoding our delegation to Monster will break RunnerZombie's behaviour.

Accordingly, we need some keyword that we can use in our class to refer to the immediate parent class instead of hardcoding the name of the class that we think will be the parent. This is why super() exists.

We can rewrite Zombie as follows:

class Zombie(Monster):

    def __init__(self, name, hungry=True):
        super().__init__(self, name, hungry)
        self.undead = True

Now future subclasses will get their initialiser behaviour from their immediate parent instead of Monster.

Hope that clears things up.

Happy coding,

Alex

Niki Molnar
Niki Molnar
25,698 Points

Alex, that is amazing!

Thank you so much for taking the time to explain it so well!

Best

Niki

Jaclyn Kandel
Jaclyn Kandel
7,579 Points

This was very helpful. One question, would the RunnerZombie class require a super() that delegates behavior to Zombie, which delegates behavior to Monster? Or would we use your original RunnerZombie code? Thanks!

Thanks so much for this explination so Super() is used to extend a class attr in a much safer way without expecifications correct?

Alex Koumparos
seal-mask
.a{fill-rule:evenodd;}techdegree
Alex Koumparos
Python Development Techdegree Student 36,887 Points

Hi Adrian Torrente Tenreiro

It's not about safety, exactly. It's more about absolute versus relative references. When we use super(), such as super().__init__() we are referring to the current object's direct parent, which is a relative reference. Compare this to Zombie.__init__() which will always refer absolutely to Zombie's initializer, regardless of where we actually are in the inheritance hierarchy. Neither approach is strictly 'safer' than the other, the correct use depends on context.

Hope that's clear.

Cheers

Alex

Alex Koumparos
seal-mask
.a{fill-rule:evenodd;}techdegree
Alex Koumparos
Python Development Techdegree Student 36,887 Points

Hi Jaclyn Kandel

Assuming we just want to preserve the existing behaviour, it is fine to leave RunnerZombie as it is. Since it is a child of Zombie and it has no __init__ method of its own, it inherits the __init__ of Zombie.

We only need to use super() on RunnerZombie when we decide to augment RunnerZombie's behaviour in some way. When that time comes we can give RunnerZombie its own __init__ method, call super() from inside the __init__ method to pick up Zombie's initialiser and then write our new behaviour. It would look something like this:

def __init__(self):
    super()  # run the parent's initialiser
    self.instantiate_unique_behaviour()  # do something unique to RunnerZombie

Hope that clears things up.

Cheers,

Alex

Alex,

These are really great explanations, greatly appreciated.

Tufan T.
Tufan T.
3,534 Points

Alex, Excellent details; many thanks for taking the time to write these... Through your notes, I can now understand Super-Duper video much better! Thanks,