Embercode
✕
Object-Oriented ProgrammingLesson 3 of 17

50 min read

Inheritance in Python classes

Why it matters

Programs grow by adding variations of things you already have. A cat is an animal with a few differences. A car is a vehicle with four wheels and doors. A premium customer is a customer who gets a discount. If you copy the common parts into each new class, a fix to one copy will not reach the others, and the copies drift apart over time. Inheritance lets you write the common parts once and build the variations on top. It is one of the three pillars of object-oriented design, along with encapsulation and polymorphism, and interviewers ask about it directly.

The idea from first principles

Imagine you are writing a list of animals for a zoo. Every animal has a name, can eat, and can make a sound. A dog makes a barking sound. A cat makes a meowing sound. Without inheritance, you write a full class for the dog with its name, its eating code, and its sound. You write the same name and eating code again for the cat. If the way animals eat changes, you change it in both places.

With inheritance, you write one general class Animal with the name and the eating code. Then you write Dog and Cat as special kinds of Animal. They get the name and the eating code for free. They only write what is different, which is the sound.

The words for this are:

  • The parent class, also called the superclass or base class, is the general one. Here it is Animal.
  • The child class, also called the subclass or derived class, is the special one. Here it is Dog or Cat.
  • The class list in parentheses after the class name connects them. class Dog(Animal): means "Dog is an Animal, with more." In Java this is written with extends.

An everyday comparison helps. A child inherits some features from a parent, such as the family name or the colour of the eyes. They also have features of their own. The inherited ones are not copied by hand. They come with the family.

Read the link as "is a". A dog is an animal. A cat is an animal. Because of that, a dog has everything an animal has.

Inheritance is not the same as copying. The child does not get a copy of the parent's code that it can change separately. It gets a link. A change in the parent's code shows up in every child that does not override it. That link is powerful, and it is also the source of many design mistakes, which the last sections cover.

The syntax, step by step

A Python class can list one or more parents in its brackets. Python allows several parents, which is called multiple inheritance. Java allows only one parent class. Most Python code uses one parent, and this lesson does the same. The rule keeps the family tree simple: every class has a clear line of ancestors going up to the top, which is object in Python.

A child class gets:

  • every attribute and method of the parent, including the ones whose names start with a single underscore, which are only a convention,
  • the right to add new attributes and methods,
  • the right to override a parent method, which means writing a new version of it with the same name and parameters.

A child cannot take away a parent's method. It can only replace it with its own version.

The constructor chain

A child object contains the parent's part as well as its own part. So the parent's __init__ must run to set up the parent's attributes. In Java, the compiler inserts this call for you. In Python, nothing is inserted. If the child defines __init__ and does not call the parent's, the parent's attributes are never set. You call the parent with super().__init__(...). The word super() gives you the parent class.

The first worked example shows the order in which the initializers run.

Python · editable

The output is:

Animal constructor for Generic
Animal constructor for Rex
Dog constructor for Rex
Generic makes a sound
Rex says woof

Trace the Dog("Rex") line:

  1. Python calls Dog.__init__ with name equal to "Rex".
  2. The first line is super().__init__(name). Python pauses the Dog initializer and runs the Animal initializer with the same name.
  3. The Animal initializer sets its attribute, then prints Animal constructor for Rex.
  4. The Animal initializer finishes. Python goes back to the Dog initializer.
  5. The Dog initializer prints Dog constructor for Rex and finishes.

The parent part is always built first. A dog's initializer may use the parent's attributes, so those attributes must already be set. The rule is the same for every level: the top class is built first, then each child in turn.

The line a.speak() runs the Animal version of speak on the generic animal. The line d.speak() runs the Dog version, because the dog's class overrides it. Python chooses the version by looking at the object's real class, not at the name the variable has. The section on method lookup explains how.

Overriding and super() for methods

Overriding means writing a method in the child with the same name and the same parameters as a method in the parent. The child's version replaces the parent's version for child objects. In Java, the @Override annotation asks the compiler to check that the method really overrides something. Python has no such check built into the language. The standard library has a decorator with the same purpose, typing.override. It does nothing when the code runs, but a type checker such as mypy reports a mistake in the name. The standard tools do not require it, so the examples here leave it out.

Sometimes the child wants to reuse the parent's version and add to it. super() lets a method call the parent's version of the same method. super().describe() means "run the parent's describe, on this object".

The next program uses a vehicle hierarchy. It reads a list of vehicles and prints each description. The car adds to the vehicle's description by calling super().describe().

Type this into the program's input:

3
car 4
bike 0
car 2

The first line is the number of vehicles. Each line after it has a kind, car or bike, and a number. For a car the number is the door count. For a bike the number is ignored.

class Vehicle:
    def __init__(self, wheels):
        self.wheels = wheels
 
    def describe(self):
        return f"vehicle with {self.wheels} wheels"
 
 
class Car(Vehicle):
    def __init__(self, doors):
        super().__init__(4)
        self.doors = doors
 
    def describe(self):
        return super().describe() + f" and {self.doors} doors"
 
 
class Bike(Vehicle):
    def __init__(self):
        super().__init__(2)
 
 
n = int(input())
vehicles = []
for _ in range(n):
    kind, number = input().split()
    if kind == "car":
        vehicles.append(Car(int(number)))
    else:
        vehicles.append(Bike())
 
total = 0
for v in vehicles:
    print(v.describe())
    total = total + v.wheels
print(f"total wheels: {total}")

The output is:

vehicle with 4 wheels and 4 doors
vehicle with 2 wheels
vehicle with 4 wheels and 2 doors
total wheels: 10

Here is what happens for the first car:

  • The list holds a Car object. The list does not care about the type. It only holds references.
  • Car(4) runs Car.__init__. It calls super().__init__(4), which sets wheels to 4 on this car. Then it sets doors to 4.
  • In the loop, v.describe() calls the Car version, because the object is a car.
  • The Car version calls super().describe(). That runs the Vehicle version, which returns "vehicle with 4 wheels". The car adds " and 4 doors" to it.
  • v.wheels reads the attribute that the Vehicle initializer set. It is 4.

The bike has no describe of its own, so Python finds the parent's version. Its output is the plain line for two wheels.

The child's initializer calls super().__init__(4) with a fixed value. The car always has four wheels, so the car's initializer supplies that value to its parent, and the caller only passes the doors. This is a common pattern. The child decides some of the parent's values and passes the rest through.

Calling a chain of parents

Inheritance can go more than one level deep. A class can extend a class that extends another. Each level can override the method and call super(). When you call the method on the bottom object, the calls travel up the chain to the top, and then the results come back down. The next example has three levels, and each one adds a word.

Python · editable

The output is:

base > middle > leaf

Here is the order of the calls. b.greet() runs Leaf.greet. That method needs super().greet() before it can finish, so it calls Middle.greet. That method needs its own super().greet(), so it calls Base.greet. The base method has no parent to call, so it returns "base". Then Middle adds " > middle" and returns. Then Leaf adds " > leaf" and returns the final text.

This is a chain of calls that each wait for the next one, which is exactly the pattern a recursion stack shows. Each call is still waiting for its answer until the base method returns. After that point, the calls finish from the top of the stack downward, and the text grows at each step.

Method lookup: which version runs

When you call a method on an object, Python needs to find the version to run. It starts at the object's own class. If that class defines the method, that version runs. If not, Python looks at the parent class, then the grandparent, and so on up to object, the top class that every class inherits from. The first version found is the one that runs.

Python keeps the search order in an attribute called __mro__, which stands for method resolution order. You can print it:

Python · editable

The output is:

['Dog', 'Mammal', 'Animal', 'object']

The list shows the order in which Python searches for a name. For a class with one parent, the order is a straight line up the family tree. With several parents, the order is more complicated, and the __mro__ attribute shows it. You do not need to memorise the rule to write good code, but this list is the answer when you wonder which version will run.

The search always starts at the object's real class, never at the variable's type. A Mammal object would look in Mammal first. If Mammal did not define speak, it would go up to Animal. A Dog object looks in Dog first, which has its own version, so nothing changes.

Attributes are shared, not hidden

In Java, a child class can declare a field with the same name as the parent's field, and the object then holds two fields with one name. Python does not work this way. An object has one dictionary of attributes. The parent's initializer and the child's initializer write into the same dictionary. The last write wins.

Python · editable

The output is:

child field
child field

The child's initializer ran after the parent's, so label holds "child field". The method show reads the same attribute, so it returns the child's value too. The object has only one label attribute, so the child's value replaces the parent's.

The surprise appears with class attributes, which are shared by all objects of a class. An attribute assigned in the class body, not in __init__, lives on the class. A child class can give it a new value. An object finds the value on its own class first, then on the parent.

Python · editable

The output is:

parent class attr child class attr

Each class has its own label, and the object looks for label on its real class first. This is the same idea as overriding, applied to a value.

The lesson for daily work is simple. Do not reuse an attribute name in the child by accident. The child's assignment changes the value that the parent code reads. Choose a different name, or call the parent's method and let the parent own the value.

Python has no protected keyword. The single underscore marks an attribute as internal to the class and its subclasses. The encapsulation lesson covers the convention in more detail.

Inheritance and isinstance

Sometimes the code needs to know which class an object belongs to. The built-in function isinstance answers that. isinstance(obj, Dog) is true when obj is a dog or an instance of a subclass of dog. It is false for None, so you do not need a separate check for that.

The next program uses isinstance with a payment system. Each payment type has a different fee.

Type this into the program's input:

3
card 500
cash 300
card 250
class Payment:
    def __init__(self, amount):
        self.amount = amount
 
    def fee(self):
        return 0
 
    def total(self):
        return self.amount + self.fee()
 
    def label(self):
        return "payment"
 
 
class CardPayment(Payment):
    def fee(self):
        return self.amount * 2 // 100
 
    def label(self):
        return "card"
 
 
class CashPayment(Payment):
    def label(self):
        return "cash"
 
 
n = int(input())
grand = 0
for _ in range(n):
    kind, amount = input().split()
    if kind == "card":
        p = CardPayment(int(amount))
    else:
        p = CashPayment(int(amount))
    print(f"{p.label()} {p.amount} -> {p.total()}")
    grand = grand + p.total()
print(f"total {grand}")

The output is:

card 500 -> 510
cash 300 -> 300
card 250 -> 255
total 1065

The program does not need isinstance. The total method is written once, in Payment. It calls fee(), and each subclass decides what the fee is. The card class computes two percent, and the cash class uses the parent's zero. This design is better than a long if that checks the kind, because adding a new payment type means adding one class, not editing a chain of checks.

You will still see isinstance in real code, mostly in methods such as __eq__ and in code that handles special cases. The next example shows its result for a subclass:

Python · editable

The output is:

True True
False

A puppy is a dog, so isinstance(pup, Dog) is true. A plain dog is not a puppy, so the last check is false.

The fee arithmetic uses //, which is integer division. It cuts off the fraction. For 500 it is 1000 // 100, which is 10. For 250 it is 500 // 100, which is 5. A real payment system would use an exact type such as an integer count of cents and a clear rounding rule, but for this example whole numbers are enough.

The implicit super call

Java inserts a call to the parent's constructor when you write none. Python does not. If a child defines __init__ and leaves out the call to the parent, the parent's attributes are missing. The error appears later, when the code reads one of them.

Python · editable

The output is:

AttributeError: 'Child2' object has no attribute 'label'

The object was created without error, because Child2.__init__ ran without problems. The problem is the missing label attribute, which only the parent's initializer sets. The fix is to write super().__init__() as the first line of the child's __init__.

When a child does not define __init__ at all, Python uses the parent's __init__ directly. In that case the arguments you pass to the child go to the parent. If the parent needs an argument that the caller does not pass, Python raises a TypeError that names the parent's method:

Python · editable

The output is:

TypeError: Animal2.__init__() missing 1 required positional argument: 'name'

The message names Animal2.__init__, which is the method that was missing an argument. The fix is either to give Cat its own __init__ that passes a fixed name up, or to give the parent's name a default value.

Python · editable

The output is:

cat

Python has no rule that super() must be the first statement, as Java does. The rule that matters is the order of effects. Call the parent's initializer before you use any attribute that it sets.

Composition: the other way to reuse code

Inheritance expresses "is a". A Dog is an Animal. Composition expresses "has a". A Car has an Engine. A car does not inherit from an engine. It keeps an engine in an attribute and asks it to do work. Both designs reuse code, but they behave differently when things change.

Imagine a Car that inherits from Engine. The car gets every method of the engine, including methods that make no sense for a car, such as reset or tune. A caller who holds a Car can call them, and the car exposes the engine's internal rules. If the engine changes its rules later, every car changes with it, and the car's own rules may break. Programmers call this the fragile base class problem. A small change in the parent breaks many children in ways nobody planned.

Composition avoids this. The Car holds an Engine in an attribute and offers only the methods that a car needs, such as start and drive. The engine can change its internal rules without the car's public methods changing. The car can also swap in a different engine while the program runs, which inheritance cannot do.

Python · editable

The output is:

engine with 120 hp started
engine with 90 hp started

The car forwards the start call to its engine. Replacing the engine changes what the car does, and no subclass is needed.

A simple test helps. Say the sentence "a Car is an Engine" out loud. It sounds wrong, so the design should use composition. Then say "a Dog is an Animal". It sounds right, so inheritance fits. When the test is unclear, prefer composition, because it is easier to change later.

Following a call through three levels

Trace what happens when Manager(5000) runs, where Manager extends Salaried, which extends Employee. Python first creates the object. Then it runs Manager.__init__, which must call super().__init__(...). That runs Salaried.__init__, which in turn calls Employee.__init__. The top class runs first and sets its attributes. Control returns to Salaried, which sets its own attributes, then to Manager, which sets its last attributes. Only then does the call that created the object return it.

Now suppose a caller asks pay() on that manager. The Manager version runs first. It calls super().pay(), which runs the Salaried version. That version computes the monthly salary times twelve and returns it. The manager version adds the bonus and returns the total. A caller who held the object as an Employee got exactly this result, with no special code in the caller.

The order matters when initializers depend on each other. If the Employee initializer reads an attribute that only Manager sets, Python raises AttributeError, because the child part is not ready yet. Keep initializers simple, and do not call methods that a child may override from inside __init__.

Common mistakes

Leaving out super().__init__(...). The child's object has none of the parent's attributes. The error appears later, as AttributeError: 'Child2' object has no attribute 'label', on the line that reads the attribute. Read the name of the attribute, then find the initializer that should have set it.

Passing the wrong number of arguments to the parent. A parent that needs a name must receive one. The error names the parent's method, for example TypeError: Animal2.__init__() missing 1 required positional argument: 'name'. Pass the value from the child, or give the parent a default.

Using a different parameter list by mistake. def describe(self, prefix) in the child does not override def describe(self) in the parent. The two methods have different parameters, so calling describe() without an argument runs the parent's version. Python does not warn you. A type checker that reads typing.override reports it.

Reusing an attribute name in the child. As the attribute example shows, the child's assignment changes the value that the parent code reads. Use a different name, or let the parent own the value.

Inheriting for the wrong reason. A common error is to extend a class just to reuse some code. A Stack class that extends a Dog class is a bad design, because a stack is not a dog. Inherit only when the child really is a kind of the parent. A useful test: can you say "the child is a kind of parent" and have it sound true for every case? If not, store the other object in an attribute instead.

Deep inheritance trees. A chain of six levels is hard to read, because a method may be defined in any level. Two or three levels is usually enough. Many experienced Python programmers prefer composition, which means putting one object inside another, for most reuse.

Debugging walkthrough: a method that does not run the way you expect

Suppose you call describe() on a car and the output does not include the door count. The cause is almost always one of these three.

  1. The method name or parameters do not match. You wrote describe(self, prefix) in the child. Check the signature against the parent. If the child's name is misspelled, the parent's version runs.
  2. The child method never calls super() or never adds its own part. Read the child method line by line. The return must include the extra text.
  3. The object is not the class you think it is. Print type(car) and Car.__mro__ to see the real class and the search order.

To find which version runs, add a print line as the first statement of each version. Print the class name, such as "Car.describe". The printed order tells you which versions ran and in what order. Remove the prints afterwards.

Variations and edge cases

A child can call a parent method without super() by naming the parent class directly, such as Vehicle.describe(self). This works, but it breaks when the class list changes, so super() is the usual choice.

A class can be marked as final with the decorator typing.final. Like override, this is read by type checkers. Python does not stop a subclass from being written, as the encapsulation lesson showed for a final class. Use the decorator to tell readers and tools that a method must not be overridden.

A child class can be abstract, which means it cannot be created directly. Abstract classes are the subject of the next lesson.

Edge case: super().name looks only in the parent classes. It does not see values stored on the object, such as self.name = ... set in __init__. Use self.name for those. It can read a class attribute that the parent defines.

Edge case: an object created from a child class has all the attributes of the parent. The parent's internal attributes, which start with an underscore, are still visible to the child's code. The convention asks the child to use the parent's methods instead.

Interview-style questions

What is inheritance? A way to build a new class from an existing one, so the new class gets its attributes and methods and can add or change some. The child is a special kind of the parent.

What does super() do? Inside a method, super().method() calls the parent's version of the method. Inside __init__, super().__init__(...) runs the parent's initializer. Call it before you use any attribute the parent sets.

Can a Python class have two parents? Yes. Python allows multiple inheritance, and it uses the method resolution order to decide where to look for a method. Java does not allow two parent classes. Java classes can implement several interfaces instead.

What is the difference between overriding and overloading? Overriding replaces a parent's method with a method of the same name in the child. Overloading means several methods with the same name and different parameters. Python does not support overloading in the way Java does, because a later def with the same name replaces the earlier one in the same class.

Are attributes overridden? No. A child's assignment to an attribute with the same name changes the single attribute the object has. The class attribute is the one place where the child's value hides the parent's value.

Why does the parent initializer need to run first? The child's extra work may depend on the parent's attributes. Set up the parent part before the child part.

When should you not use inheritance? When the child is not really a kind of the parent. Or when you only want to reuse a few methods. Composition, where one object holds another, is often a better choice.

Real-world uses

Frameworks use inheritance heavily. A web framework might provide a base Controller class with a method that handles errors. Your controller subclasses it and overrides only the parts it needs. A game engine has a base Entity class with a position and a method to update it. Each enemy, item and door subclasses it. A user interface toolkit has a base Widget class, and buttons, text boxes and sliders are subclasses that add their own drawing code.

In all these cases the parent holds the shared behaviour, and the child specialises it. The pattern is valuable when the family is stable. If you add a new kind of thing every week, inheritance can get tangled, and a list of interchangeable parts often works better.

Practice

Attempt each exercise first. The answers are below each exercise.

Exercise 1: a Shape parent (easy)

Write a class Shape whose __init__ takes a name and stores it. Add a method describe() that returns the name. Write a child class Square that extends Shape, has an __init__ taking a side length, passes the name "square" to the parent, and adds a method area() that returns the side times itself. In the main part of the program, create a square with side 4 and print its description and its area.

Expected output:

square
16

Show the answer

Python · editable

The child passes "square" up with super().__init__, so the parent's initializer sets the name. The child adds only the side and the area method, which is what makes it a square.

Exercise 2: override and extend (easy)

Add a method describe() to Square that returns the parent's text followed by the side, using super().describe(). For a square of side 4 the result should be square side 4. Print it in the main part of the program.

Expected output:

square side 4

Show the answer

Python · editable

The override reuses the parent's text with super().describe() and adds the side.

Exercise 3: predict the initializer order (easy)

Given a three-level chain A (top), B(A), C(B), each with an __init__ that prints its own letter and calls the parent's __init__ with super(), what does C() print? Explain why.

Show the answer

It prints A, then B, then C. Each initializer calls its parent first, so the top class runs first and the bottom class last. If C did not call super().__init__(), only C would print, and the attributes that A and B set would be missing.

Exercise 4: what does it print? (easy)

Predict the output, then check.

class A:
    def who(self):
        return "A"
 
 
class B(A):
    def who(self):
        return "B"
 
 
x = B()
print(x.who())
y = A()
print(y.who())

Show the answer

It prints B and then A. The first object is a B, so its version of who runs. Python looks at the object's real class, not at the name x. The second object is a plain A.

Exercise 5: a bank with a fee (medium)

Write a class Account whose __init__ takes a balance and stores it. Add a method withdraw(amount) that subtracts the amount. Write a child class FeeAccount that charges a fee of 2 extra on every withdrawal, by overriding withdraw and calling super().withdraw(amount + 2). Test with a balance of 100: withdraw 10 from a FeeAccount, then print the balance.

Expected output:

88

Show the answer

Python · editable

The object is a FeeAccount, so its withdraw runs first and passes 12 to the Account version. That leaves 88.

Exercise 6: read shapes from input (medium)

Read n and then n lines. Each line is either square side or circle radius. Write a Shape parent with a method area() that returns 0, and children Square and Circle (use 3 * r * r for the circle area as an approximation). Print each area on its own line, then the total area. Put the reading in the main part of the program.

Input:

3
square 2
circle 1
square 3

Expected output:

4
3
9
16

Show the answer

class Shape:
    def area(self):
        return 0
 
 
class Square(Shape):
    def __init__(self, side):
        self.side = side
 
    def area(self):
        return self.side * self.side
 
 
class Circle(Shape):
    def __init__(self, radius):
        self.radius = radius
 
    def area(self):
        return 3 * self.radius * self.radius
 
 
n = int(input())
shapes = []
for _ in range(n):
    kind, size = input().split()
    if kind == "square":
        shapes.append(Square(int(size)))
    else:
        shapes.append(Circle(int(size)))
 
total = 0
for s in shapes:
    print(s.area())
    total = total + s.area()
print(total)

The areas are 4, 3 and 9. The total is 16. The circle uses 3 as an approximate value for pi, which keeps the answer a whole number.

Exercise 7: spot the bug (medium)

Find the bug in this code and explain why the output is wrong:

class Parent:
    def __init__(self):
        self.name = "parent"
 
 
class Child(Parent):
    def __init__(self):
        self.name = "child"
 
 
p = Child()
print(p.name)

Show the answer

The output is child, which may not be what the author wanted. The child's __init__ does not call super().__init__(), so the parent's initializer never runs. Here the child sets name itself, so the output looks right. Any other attribute that the parent sets would be missing from the object. The fix is to call super().__init__() first, and then set the child's own attributes. The class attribute example in this lesson shows the other case, where a child's value hides the parent's value.

Exercise 8: a three-level greeting (medium)

Write a chain of three classes, Animal, Mammal(Animal) and Dog(Mammal). Each has a method kind(). Animal returns "animal", and each child returns the parent's text with " > " and its own word, so Dog returns animal > mammal > dog. Print kind() for a Dog object.

Expected output:

animal > mammal > dog

Show the answer

Python · editable

Each level calls super().kind() before adding its word, so the chain builds the text from the top down.

Exercise 9: the missing argument (medium)

The following code stops with an error when it runs. Find the reason from the error message, and fix it in two different ways: once by passing a value from the child, and once by giving the parent's name a default value.

class Animal:
    def __init__(self, name):
        self.name = name
 
 
class Cat(Animal):
    pass
 
 
c = Cat()

Show the answer

The message is TypeError: Animal.__init__() missing 1 required positional argument: 'name'. Cat has no __init__, so Python uses the parent's, and Cat() gives it no name.

Fix one, pass a value up (this replaces the Cat class from the question, and Animal stays as it was):

class Cat(Animal):
    def __init__(self):
        super().__init__("cat")

Fix two, give the parent's parameter a default:

class Animal:
    def __init__(self, name="unnamed"):
        self.name = name

Either fix works. The first is usually better, because the child decides the name. The second gives every animal a default name, which may be the right design if most animals really are unnamed at first.

Exercise 10: count the fees (hard)

Write a parent class Ticket whose __init__ stores a price, and a method total() that returns the price. Write Premium(Ticket) that adds a fee of 5 to the total by overriding total(), and Child(Ticket) that takes 3 off the total, but never below 0. Read n and then n lines of the form premium 20, child 4 or plain 10. Print the total of all tickets on one line as Total: N.

Input:

3
premium 20
child 4
plain 10

Expected output:

Total: 36

Show the answer

class Ticket:
    def __init__(self, price):
        self.price = price
 
    def total(self):
        return self.price
 
 
class Premium(Ticket):
    def total(self):
        return super().total() + 5
 
 
class Child(Ticket):
    def total(self):
        return max(super().total() - 3, 0)
 
 
n = int(input())
total_sum = 0
for _ in range(n):
    kind, price = input().split()
    if kind == "premium":
        t = Premium(int(price))
    elif kind == "child":
        t = Child(int(price))
    else:
        t = Ticket(int(price))
    total_sum = total_sum + t.total()
print(f"Total: {total_sum}")

Premium 20 gives 25. Child 4 gives 1 (4 minus 3). Plain 10 gives 10. The sum is 36. The Child version never goes below 0, so a price of 2 would give 0, not -1. The function max returns the larger of its two arguments.

Exercise 11: a protected attribute (hard)

Write a class Counter whose __init__ sets an internal attribute _count to 0, and a method show() that prints it. Write DoubleCounter(Counter) that has a method bump() adding 2 to _count directly. In the main part of the program, create a DoubleCounter, call bump() three times, then call show().

Expected output:

6

Show the answer

Python · editable

The subclass uses _count directly. The single underscore is a convention, and nothing stops the subclass from using it. Each bump adds 2, so three calls give 6. A version with a method for the change, such as add(amount) in Counter, would let the parent check the rule in one place.

Exercise 12: a design choice (hard)

A student writes class Stack(Dog): to get a stack that also barks. Explain what is wrong with this design, and describe a better one that still gives the stack the ability to store items and a bark.

Show the answer

Inheritance means "is a kind of". A stack is not a kind of dog. Subclassing Dog would give the stack a bark method and a name that have nothing to do with stacking, and every stack would be a dog to the rest of the program. The better design keeps the two things separate. Write a Stack class that holds its items in a list attribute and offers push and pop. Write a Dog class with its own bark method. If a program needs both, it creates one of each, or gives the stack a dog as an attribute. The test is simple: say "a stack is a dog" out loud, and if it sounds wrong, do not inherit.

Summary

  • Inheritance builds a child class from a parent. The child gets the parent's attributes and methods, and can add its own.
  • Call the parent's initializer with super().__init__(...). Python does not call it for you, so a child that skips it has missing attributes.
  • Overriding replaces a parent's method with a method of the same name. super().method() calls the parent's version, which is how a child adds to a method instead of replacing it.
  • Python looks up methods by the object's real class and the method resolution order, __mro__.
  • Attributes are not hidden by subclasses. The child's assignment changes the one attribute the object has.
  • Use inheritance only when the child really is a kind of the parent. When it is not, store the other object in an attribute.
Practice this topic: coding problems for Object-Oriented Programming →

Stuck? Ask the tutor

Ask about anything in this lesson. Answers are short and use simple words.

Sign in to ask the tutor.

← Encapsulation in Python classes