Skip to content
wannacode

Abstract classes

Problem

Zero square meters

You are writing a program that orders tiles for a terrace. The terrace has three parts: a 4 × 3 m rectangle, a round part with a radius of 1 m, and a triangle with a base of 2 m and a height of 3 m. There is a parent Shape with area(), which returns 0, and each shape overrides it.

The colleague who wrote Triangle forgot:

class Triangle(Shape):
def __init__(self, base, height):
self._base = base
self._height = height
terrace = [Rectangle(4, 3), Circle(1), Triangle(2, 3)]
total = 0
for s in terrace:
total += s.area()
print(f"Tiles: {total:.2f} m²")
Output
Tiles: 15.14 m²

You need 18.14 m². The program gives no error, and the order is 3 m² short. The parent's zero has hidden the missing method.

And what is Shape()? A shape with no shape. An object like that has no reason to exist.

The idea

Abstract class: a promise the child classes keep

Shape is not a real shape. All it says is "every shape has an area". So make it abstract:

from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self):
pass
  • ABC (Abstract Base Class) and abstractmethod come from the abc module, so the first line is from abc import ABC, abstractmethod.
  • An abstract method has @abstractmethod above it. Its body is only pass, "nothing". You cannot give a sensible area to "a shape in general", so you give none.
  • An abstract class inherits from ABC and cannot have objects: Shape() stops the program with TypeError.
  • Every child class has to write the method. Otherwise you cannot make objects from it either.

Python has no compiler that looks over the program ahead of time. The check happens when the object is created. Now the forgotten Triangle stops the program right on the line that builds the terrace, before a wrong order gets calculated:

"TypeError: Can't instantiate abstract class Triangle without an implementation for abstract method 'area'".

A program that stops with a clear error is better than a silent error in the order.
More precisely, if you are curious
  • A child class that does not write the abstract method stays abstract by itself, without you saying so. The promise passes on to its own child classes.
  • A child of ABC without a single abstract method is not abstract: you can make objects from it. The ban comes from the abstract methods.
  • @abstractmethod without ABC stops nothing: Shape() gets created, and area() returns None.

See it

The triangle keeps the promise

Add the method to Triangle:

class Triangle(Shape):
# ... __init__ as before
def area(self):
return self._base * self._height / 2
terrace = [Rectangle(4, 3), Circle(1), Triangle(2, 3)]
total = 0
for s in terrace:
total += s.area()
print(f"Tiles: {total:.2f} m²")
Output
Tiles: 18.14 m²

The loop is the same. The list holds shapes of three classes, and s.area() calls the method of each one. The polymorphism from the last lesson works with an abstract parent too.

Try it

Find the bug

The program stops on one of the lines. Which one?

Pick the line with the bug.

The idea

Abstract, but not empty

An abstract class can have everything an ordinary one has: fields, a constructor, ordinary methods. Let every shape have a name and a method that describes it:

class Shape(ABC):
def __init__(self, name):
self._name = name
@abstractmethod
def area(self):
pass
def describe(self):
return f"{self._name}: {self.area():.2f} m²"

describe() is written once, in Shape. It calls area(), which in Shape is only a promise. That is not a problem: describe() is always called on a real shape, and a real shape has an area.

You do not make objects from Shape, but its __init__ is useful: the child classes call it.

class Circle(Shape):
def __init__(self, radius):
super().__init__("circle")
self._radius = radius
# ... area() as before
terrace = [Rectangle(4, 3), Circle(1), Triangle(2, 3)]
for s in terrace:
print(s.describe())
Output
rectangle: 12.00 m²
circle: 3.14 m²
triangle: 3.00 m²

The parent writes what is shared (the name, the description), and the child classes write what is different (the area).

@abstractmethod
def area(self):
pass
def describe(self):
return f"{self._name}: {self.area():.2f} m²"
s = Circle(1)
print(s.describe())
Step by step, in words
  1. Shape promises area(): @abstractmethod, and the body is only pass. There can be no object that is only a Shape.
  2. The object is a circle: the Circle part has its own area(). It keeps the promise.
  3. s.describe() is written in Shape and works for every shape.
  4. It calls area(). The object is a circle, so the Circle method runs: π · 1 · 1.
  5. describe() puts the text together. What is shared lives in the parent, what differs lives in the child class.
  6. The output is the area of the circle, with two digits after the decimal point.

Try it

Whose method?

What will the program print?

# Shape and Circle are from the card above
s = Circle(2)
print(s.describe())
Answer

Try it

What is missing?

What has to go in the blank so that you cannot create an object of Shape?

class Shape():
    @abstractmethod
    def area(self):
        pass

Exercise

Shapes and animals

Solve the tasks in your editor.

1. Area and perimeter. An abstract class Shape with a name and two abstract methods: area() and perimeter(). Child classes Circle (radius) and Rectangle (width and height). Put a circle with a radius of 5 and a 4 × 2.5 rectangle in a list and print both values with two digits after the decimal point.

Output
Circle: area 78.54, perimeter 31.42
Rectangle: area 10.00, perimeter 13.00

2. Animals. An abstract class Animal with an abstract method sound() and an ordinary method breathe() that prints "The animal breathes." Child classes Cat and Dog. Call the sound of both in a loop, then breathe() on the first one.

Output
Meow, meow!
Woof, woof!
The animal breathes.

Exercise

Vehicles and performances

3. Vehicles. An abstract class Vehicle with an abstract method move(). SimpleCar and Bicycle inherit from it. Bicycle also has its own method pedal().

Output
The car sets off
The bicycle sets off
Pedaling
Try garage[1].pedal(): it works, because there is a bicycle there. And garage[0].pedal()? Why does the program stop only on that line and not earlier?

4. Performances. An abstract class Performance with a title and an abstract method start(). DanceMove and Song inherit from it. Song also has a method sing().

Output
Starting the dance moonwalk
Starting the song 'Bohemian Rhapsody'
Singing 'Bohemian Rhapsody'

More tasks

  • Triangle. Add Triangle with its three sides to task 1. The perimeter is their sum, and the area comes from Heron's formula: s = perimeter / 2, area = √(s(s − a)(s − b)(s − c)). For sides 3, 4 and 5 the area is 6.00 and the perimeter is 12.00.
  • Abstract employee. Make Employee from the last lesson abstract, with an abstract monthly_pay(). Add FullTime (the salary is paid in full) and keep Manager and Intern. What does the program gain? What does it lose?

Test

Check yourself

1. Shape inherits from ABC and has an abstract area(). Which one works?
2. What does @abstractmethod above a method say?
3. Triangle inherits from the abstract Shape but does not write area(). What happens?
4. Can an abstract class have __init__, fields and ordinary methods?

Made by Ivo Gergov as part of Evo Tech's campaign for accessible programming education ·