Embercode
✕
Object-Oriented ProgrammingLesson 4 of 17

50 min read

Polymorphism and abstract base classes in Python

Why it matters

Most real programs handle a mix of things. A shop has books, food and electronics. A drawing app has circles, squares and lines. A notification system sends email and text messages. In each case you want one loop, one function, or one list that works with every kind, without a long chain of if statements that checks the kind each time. Polymorphism and abstraction are the two ideas that make this possible. They also come up often in design interviews.

The idea from first principles

Start with a problem. You have a list of shapes. Each shape knows how to compute its area, but the formula is different for each kind. Without any object-oriented ideas, you might write a function that takes a shape code, such as 1 for a circle and 2 for a square, then uses a long if to pick the formula. When you add a triangle, you must find every such function and add another branch to each one. Miss one, and the program quietly gives the wrong answer.

Object-oriented design turns the question around. Each shape class holds its own formula. The loop does not ask "what kind are you?". It says "give me your area", and the object knows how to answer. Adding a triangle means writing one new class. No existing loop changes.

Two words name the two halves of this:

  • Polymorphism means "many forms". One piece of code, such as s.area(), can run different code depending on which object it gets. The same method name takes many forms.
  • Abstraction means "keep only what matters". You describe the shared promise, such as "every shape has an area", and leave out the details that differ. In Python, an abstract base class is the tool for this. It is a class that other classes must inherit from, and that refuses to be built if a promised method is missing.

An everyday comparison: a power socket is an abstraction. Any device with the right plug works with it. The socket does not know whether the device is a kettle or a laptop. It only promises a voltage and a shape. Polymorphism is the device side: many devices, each doing its own thing, all accepting the same plug.

Python makes polymorphism very easy. A function does not declare the type of its parameter. It calls a method on whatever it receives. If the object has the method, the call works. This style is called duck typing: if an object walks like a duck and quacks like a duck, treat it as a duck. The next sections show when duck typing is enough, and when an abstract base class is the better tool.

Method calls always go to the object

Python method calls look up the method on the object's real class, at run time. The variable name does not matter, because a name is only a label that points to an object. A call such as s.area() runs the area of whatever class s points to. This is called dynamic dispatch. Dispatch means choosing which method to run. Dynamic means the choice happens while the program runs.

The rule in one line: the object's class decides which version of a method runs.

Worked example 1: a list of shapes with one loop

Here is a program with a base class Shape and three different kinds: a rectangle, a circle and a square. The square is a special rectangle, so it extends Rectangle. The loop treats all of them as shapes and prints each area and the total.

Python · editable

The output is:

Rectangle 12.00
Circle 3.14
Square 4.00
total 19.14

Trace the loop for the three objects:

  1. The list holds three references. The list itself does not know the kinds.
  2. For the rectangle, s.name() returns the class name Rectangle, and s.area() runs the rectangle's version, 3 * 4, which is 12. The format .2f prints it as 12.00.
  3. For the circle, s.area() runs the circle's version, math.pi * 1 * 1, which is 3.14159.... The format rounds it to 3.14.
  4. For the square, type(self).__name__ returns Square, even though name is written in Shape. The square has no area of its own, so the inherited Rectangle.area runs, with width and height both equal to 2. That gives 4.00.
  5. The total is the sum of the three exact areas, 19.14159..., which prints as 19.14.

The loop never names a kind. If you add a Triangle class with its own area, you add it to the list and the loop works unchanged. This is the practical value of the design.

Two details matter here. The method name uses type(self), which returns the actual class of the object at run time. You rarely need it in daily code, but it makes the dispatch visible in this example. Second, the format :.2f means "a decimal number with two digits after the point". Python's f-strings, the text that starts with f", put the value into the text, and the part after the colon controls the format.

The class Shape has an area method that only raises NotImplementedError. That is a plain way to say "children must provide this", but Python does not check it until the method is called. The next section shows a stronger tool.

Abstract base classes

An abstract base class is a class that cannot be created directly, and that lists the methods every child must provide. Python provides this in the module abc. Inherit from ABC, and mark each promised method with @abstractmethod.

Python · editable

The output is:

TypeError: Can't instantiate abstract class Shape without an implementation for abstract method 'area'
TypeError: Can't instantiate abstract class Half without an implementation for abstract method 'area'

Python refuses to build the object. The check happens when you call the class, not when the program starts, so an error appears on the line that tries to create the object. The message names the missing method, area. The class Half inherits area as abstract, because it did not write one, so it cannot be created either.

An abstract base class can hold more than abstract methods. It can have an __init__, and normal methods with bodies. The name method in the first example is a normal method. Every shape gets it for free, and no child needs to write it. The abstract base class is a mix of finished shared code and unfinished promises.

The three dots ... in def area(self): ... are a body that does nothing. They are the common way to write an abstract method. The docstring or pass also work.

The rule of one parent

Python allows a class to inherit from several parents, but the common pattern is still one parent for shared code. A base class with shared code and state is a good fit for one parent. A shared ability that many unrelated classes need is a better fit for a small base class with only abstract methods, or for a protocol from the typing module. Use the abstract base class when the children share a promise and some code.

Overriding and dispatch through the parent

The first example shows the core rule in action: a child's method replaces the parent's method for objects of the child class. This works through every level of the family tree. A three-level chain where the top class defines a method, the middle class overrides it, and the bottom class does not, runs the middle version for a bottom object, because that is the most specific version it has.

Dynamic dispatch has one more consequence. The parent code can call a method that the children override, and the children's versions run. A template method is a pattern built on this idea. The parent defines the order of steps, and the children fill in the steps. The next example uses it to render reports.

Python · editable

The output is:

== Sales ==
total 120
== Stock ==
items 7

render is written once, in Report. It calls title() and body(), which each child defines. When render runs on a SalesReport, the call to title() runs the sales version, and the call to body() runs the sales version too. The parent decides the structure, and the children decide the content. This is the template method pattern. Python has no final keyword that the interpreter enforces. The @final decorator from typing only helps a type checker. The convention is to write a comment or a docstring that says the method is not meant to be overridden.

Matching on a type

Sometimes a function must choose a rule by the kind of its input, such as a number, a text or a list. In Java this uses a chain of instanceof checks. In Python 3.10 and newer, the match statement does the same job with class patterns. A pattern such as int(i) checks that the value is an int, and it also gives the value a name, i, that you can use. An if after the pattern adds a condition.

Python · editable

The output is:

something else
big number 42
text of length 5
something else

The first pattern int(i) if i > 10 matches the number 42, and it fails for 5, because 5 is not larger than 10. The text "hello" matches the str pattern. The value 3.5 is a float, not an int, so it falls through to the last case. The underscore _ matches anything, and it works as the default case.

Expression trees: a real use of polymorphism

A more interesting use of polymorphism is a tree of objects. Each node is an expression, such as a number or a sum, and the tree has a value that is computed by asking each node for its answer. This design is used in compilers, spreadsheet engines and query planners, and it is a good example for interviews.

Look at the expression (2 + 3) * (4 + 1). It is a tree. The top node is a multiplication. Its two children are additions, and each addition has two number leaves.

To find the value, you ask the top node. It asks its left child, which asks its two leaves, and then adds them. It does the same on the right. Then the top node multiplies the two answers. The question "what is your value?" travels down the tree and the answers travel back up. The shape of the work follows the shape of the tree.

The next program builds this tree. Each kind of node is a class. Each class writes its own eval (computes the value) and show (prints the expression as text). The main part of the program calls them on the top node only. Every node below it runs its own version.

Python · editable

The output is:

((2 + 3) * (4 + 1)) = 25

Look at what the main part does. It calls e.show() and e.eval(), both on the top node, which is a Mul. The Mul version of eval calls self.left.eval() and self.right.eval(). Those are Add objects, so the Add version runs. Each Add calls its two Num children, which return the numbers they hold. No node has a long if that checks its kind. Each one knows its own job.

The tree walk you just did is a tree traversal: visiting every node in a tree in a fixed order. Here the order is depth first, which means go all the way down one branch before the next. The leaves are reached first, and the top node is the last one finished. This is why the output is built from the bottom up. The show method builds the text the same way, so the parentheses come from the tree's shape, not from a rule about precedence.

Expression trees make a good interview question. A common version asks you to write eval for a tree of nodes given as a structure, and to handle division by zero or a missing child. The answer is always a recursive walk: for a leaf, return its value; for an operator, combine the answers from the two children. The object-oriented version puts each rule in its own class, so adding a new operator, such as subtraction, means adding one class with one eval and one show.

Python has no overloading: what replaces it

In Java, a class can have two methods with the same name and different parameter lists, such as print(String) and print(Object). Java picks one at compile time, using the declared type of each argument. Python does not have this feature. If a class defines two methods with the same name, the second one replaces the first.

Python gives you three tools for the same goal.

  1. A default value or *args in one method. One method accepts the different forms. This is the most common choice.
  2. A check of the type inside the method, with isinstance or match.
  3. functools.singledispatch, a decorator that picks a function by the type of its first argument.

The third tool is the closest to Java overloading, but it works differently. It chooses by the actual type of the value at run time, not by the declared type of a variable. The next example shows the difference.

Python · editable

The output is:

object: 42
string: hi

The name o has no declared type in Python. The function show looks at the object "hi", finds that it is a str, and runs the str version. In Java, the same call with a variable declared as Object would run the Object version. Python makes the decision at run time, from the object, so there is no declared-type rule to remember.

Methods behave in the same way. A method call runs the version in the object's class, as the printer example below shows:

Python · editable

The output is:

fancy object: hi
fancy object: 42

FancyPrinter has one print_it method, and it replaces the parent's method for every argument. There is only one version to choose from, so the argument does not change the choice. If you need a different rule for strings, check the type inside the method, or use singledispatchmethod from the same module.

Casting down is not needed in Python

Java sometimes needs a cast to call a method that only a child class has. A cast tells the compiler "trust me, this object is a child of that type", and Java checks the claim at run time. Python has no cast. A variable is just a label, so you can call any method on any object. If the method does not exist, Python raises AttributeError at that line.

Python code that needs a specific kind checks it first with isinstance, or with a match pattern.

Python · editable

The output is:

2
no radius

The isinstance check returns True for a Circle and for any subclass of it, and False for every other object. The check is a plain question, so no error can be raised by it.

If you find yourself checking the type a lot, the design may be wrong. The better fix is usually to add the method to the parent, with a default body that does nothing, or to move the child-only work into the child class.

Abstraction as a design tool

Abstraction is not only a keyword. It is the choice of what to show. A good abstract base class answers one question: what does every kind of this thing promise? For a shape, the promise is an area. For a payment, the promise is a total. For a notifier, the promise is a message sent. Everything else is an implementation detail that belongs to the child.

The next program shows a notifier that sends a message through different channels. The channel decides the format. The caller only uses send.

Type this into the program's input:

3
email Welcome aboard
sms Your code is 4821
email Invoice ready
from abc import ABC, abstractmethod
 
 
class Notifier(ABC):
    @abstractmethod
    def format(self, message):
        ...
 
    def send(self, message):
        return "sent " + self.format(message)
 
 
class EmailNotifier(Notifier):
    def format(self, message):
        return "[email] " + message
 
 
class SmsNotifier(Notifier):
    def format(self, message):
        if len(message) > 12:
            return "[sms] " + message[:12] + "..."
        return "[sms] " + message
 
 
n = int(input())
for _ in range(n):
    line = input()
    kind, message = line.split(" ", 1)
    note = SmsNotifier() if kind == "sms" else EmailNotifier()
    print(note.send(message))

The output is:

sent [email] Welcome aboard
sent [sms] Your code is...
sent [email] Invoice ready

The send method is written once, in Notifier. It calls format, which each channel defines. The caller holds a notifier and never asks what kind it is. If you add a push-notification channel, you write one class with one format method, and send works for it without change.

Notice how the program reads each line. input() reads a whole line, including spaces, because each message can contain spaces. line.split(" ", 1) splits the line at the first space only. The second argument, 1, means "at most one split", so the rest of the message keeps its spaces. The slice message[:12] takes the first twelve characters.

Choosing between an abstract base class and a plain class

A question that comes up often is whether a class should be abstract. Use these checks.

  • Is there a meaningful object of this exact type? If yes, the class can be concrete. If the only useful objects are the children, make it abstract.
  • Does the class have shared code that every child needs? If yes, a base class saves copying. If the children share only a promise, a small abstract class with only abstract methods is enough.
  • Do you want to stop someone from creating it by accident? Inheriting from ABC with an abstract method prevents that.

A small test: finish the sentence "a Shape is a ...". If the answer is a list of kinds, such as "a circle or a square", the parent is abstract. If the parent is a useful thing on its own, the class can be concrete.

Why polymorphism beats a long if

Before polymorphism, a common approach was to store a number or a string as the kind of thing, then use a chain of if checks in every function that needed the kind. Consider a function that computes a fee. It checks whether the kind is 1, then 2, and so on. The same chain appears in the area function, the printing function and the tax function. Each new kind means a change in every function. A missed function gives a wrong answer that may not show up until the new kind is used in real work.

With polymorphism, the kind lives with its behaviour. The fee class knows its own fee. The area class knows its own area. A new kind adds one class, and the existing functions that use the shared method work without a change. An abstract base class also helps: Python refuses to create a child that is missing a promised method, which the long if can never do. If you forget the new kind's area, the error appears when you try to create that object, instead of quietly returning zero.

This does not mean if statements are always wrong. A check of user input, a range test, or a single boundary condition belongs in an if. The rule of thumb is this: if the same chain of checks repeats across several functions and keeps growing, move the behaviour into the classes.

Designing a base class for many children

When you design an abstract base class, think about what every child must do and what they all share. The base class should hold the shared steps and the common attributes. The children should provide only what differs. A base class that holds too little forces each child to copy code. A base class that holds too much forces children to inherit behaviour that does not apply to them.

A useful check is to list the children you expect today and one you might add next year. If the base class's abstract methods make sense for all of them, the design is sound. If the next child would have to write an empty method just to satisfy the base class, the abstract method probably belongs somewhere else, or it should be a normal method with a default body.

Common mistakes

Trying to create an abstract class. The error reads TypeError: Can't instantiate abstract class Shape without an implementation for abstract method 'area'. Create a concrete child instead.

Forgetting to implement an abstract method. A child that inherits from an abstract base class must write every abstract method, or the child cannot be created either. The error names the missing method, as in the Half example above.

Expecting the variable's type to choose the method. It does not. The object's class does. A Shape variable holding a Circle runs the circle's area.

Expecting the argument's declared type to choose an overload. Python has no overloading. A method with the same name replaces the earlier one. Use default values, isinstance, match, or singledispatch to choose by argument.

Writing an abstract method without ABC. If the class does not inherit from ABC and does not use the abstractmethod decorator, nothing is checked. A method that only raises NotImplementedError fails later, when it is called. The error then appears far from the place where the mistake was made.

Looping over a list with None in it. Calling a method on None raises AttributeError: 'NoneType' object has no attribute 'area'. Check the list before the loop, or make sure every slot is filled.

Using try to choose a branch. An AttributeError raised by a missing method is an error, not a way to pick a case. Use isinstance or a method on the parent class instead.

Debugging walkthrough: the wrong formula runs

Suppose the total area in a drawing app is wrong, and one shape seems to use another shape's formula. Work through this list.

  1. Print the class of the object just before the call, with print(type(s).__name__, s.area()). If the printed name is not the shape you expected, the bug is in the creation code, not in the formula.
  2. If the class is right, check the method's name and parameters in the child. A method named area that takes an extra parameter does not override area(). Calling s.area() then fails with a TypeError about missing arguments, so the mistake is visible.
  3. If the child's method has the right name and the formula is still wrong, read its body. The bug is in the formula.
  4. If a method works in one class and not another, check the class's method resolution order with print([c.__name__ for c in type(s).__mro__]). It shows where Python looked for the method.

Variations and edge cases

A method in an abstract base class can be a normal method that the children should not change. Python cannot enforce that rule, so write a comment or a docstring that says so. A method can also be a static method, which belongs to the class and does not receive self. A static method is chosen from the class name, not from an object, so it does not take part in dispatch. You will meet static methods in the interfaces lesson.

An abstract base class can have an __init__. A child can call it with super().__init__(...), which is how the base class's attributes get set. Remember that Python does not call the base class's __init__ for you, as the inheritance lesson showed.

A list of a base type can hold objects of different children, as the shapes example shows. This is the most common form of polymorphism you will write. Lists of a base type work the same way.

Edge case: a child that overrides __eq__ or __str__. Every class inherits from object, so every class has these methods. Overriding them is common. The inheritance and operator overloading lessons cover the rules for doing it correctly.

Interview-style questions

What is polymorphism? One method call that runs different code depending on the object. In Python, this happens because the method is looked up on the object's class at run time.

What is the difference between overloading and overriding? Overloading means one class has several methods with the same name and different parameters. Python does not have overloading. Overriding means a child replaces a parent's method with a method of the same name, chosen at run time from the object's class.

What is an abstract base class for? Sharing code and stating a promise that every child must keep, without allowing an object of the abstract idea itself.

Can you create an object of an abstract base class? No. You can create objects of its concrete children. Python raises TypeError when you try.

What is dynamic dispatch? Choosing which version of a method to run based on the object's real class, at run time.

Why use an expression tree in an interview? It shows recursion, polymorphism and the idea of a tree walk in one problem. Each node answers a question about itself, and the answers combine up the tree.

Real-world uses

A payment system has a PaymentMethod abstract base class with a charge promise. Cards, bank transfers and wallets each implement it. The checkout code calls charge and never checks which method the customer chose. A logging library has a Handler class, and the console handler, the file handler and the network handler each decide where a message goes. A spreadsheet engine can store a formula as an expression tree, the same design you just built.

The pattern is the same each time. The shared code calls one promise. Each kind fills in its own part. New kinds are added as new classes, not as edits to old code.

Practice

Try each exercise before opening the answer.

Exercise 1: an area for each shape (easy)

Write an abstract base class Shape with an abstract method area(). Write Square with a side and Triangle with a base and a height (area is base times height divided by two). Put one of each in a list, print each area with one decimal place, and print the count of shapes.

Use a square with side 3 and a triangle with base 4 and height 5.

Expected output:

9.0
10.0
2

Show the answer

Python · editable

The loop calls area without knowing the kind. The / operator always gives a float, so the triangle gives 10.0. The count is the length of the list, which is 2.

Exercise 2: predict the dispatch (easy)

Given a class A with a method f that returns "A", and a class B(A) whose f returns "B", what does x = B() followed by print(x.f()) print? Explain the rule.

Show the answer

It prints B. The method is looked up on the object's class, which is B, and B has its own f. The name x does not decide anything.

Exercise 3: why does it fail? (easy)

Explain why this code stops with an error, and fix it.

from abc import ABC, abstractmethod
 
 
class Animal(ABC):
    @abstractmethod
    def sound(self):
        ...
 
 
class Cow(Animal):
    def sound(self):
        return "moo"
 
 
a = Animal()
print(a.sound())

Show the answer

Animal is an abstract base class, so Animal() raises TypeError: Can't instantiate abstract class Animal without an implementation for abstract method 'sound'. The fix is to create a concrete child: a = Cow(). The rest of the code is valid, because Cow provides the sound method that Animal promised.

Exercise 4: what does it print? (medium)

class Base:
    def name(self):
        return "base"
 
    def hello(self):
        return "hello from " + self.name()
 
 
class Derived(Base):
    def name(self):
        return "derived"
 
 
b = Derived()
print(b.hello())

Show the answer

It prints hello from derived. hello is written in Base and calls self.name(). Inside hello, the call is dispatched on the object, which is a Derived, so the derived version runs. This is the template method idea: the parent's code calls the child's version.

Exercise 5: a total of mixed items (medium)

Write an abstract base class Item with an abstract method price(). Write Book with a fixed price of 12 and Food with a price set by its initializer. Put three items in a list and print the total price.

Expected output:

Total: 30

Use Book(), Food(5) and Food(13).

Show the answer

Python · editable

The total is 12 + 5 + 13, which is 30. The loop adds each item's price through the shared method.

Exercise 6: the expression tree with subtraction (medium)

Add a subtraction node Sub to the expression tree from this lesson. It should subtract its right child from its left child. Build the expression (10 - 4) * 3 and print the expression text and its value.

Expected output:

((10 - 4) * 3) = 18

Show the answer

Python · editable

The new class needs only two methods. Nothing else in the tree changes, which is the benefit of the design. 10 - 4 is 6, and 6 * 3 is 18.

Exercise 7: overloading or overriding? (medium)

Python has no overloading, so a second def with the same name in one class replaces the first. Write the class below and predict what Child().run(1) and Child().run("a") print. Explain why the parent's version is not used.

class Parent:
    def run(self, x):
        return "parent"
 
 
class Child(Parent):
    def run(self, x):
        return "child"
 
    def run(self, x, y):
        return "child two"

Show the answer

Child().run(1) and Child().run("a") both raise TypeError: Child.run() missing 1 required positional argument: 'y'. The second def run in Child replaces the first one, so the one-argument version no longer exists in Child. Python does not fall back to the parent's version, because the name run is found in Child first. The fix is to give the method one definition with a default value, such as def run(self, x, y=None).

Exercise 8: a safe check (medium)

Given an object s that may be a Circle or a Square, write code that prints the radius only when s is a circle, and prints no radius otherwise. Use isinstance. Test it with Circle(2) and with Square().

Show the answer

Python · editable

The isinstance check is a plain question, so no error is raised. The else branch handles every other shape.

Exercise 9: a missing method (hard)

The following program stops with an error. Read the error message, which names the missing method, and fix the program so it prints 4.

from abc import ABC, abstractmethod
 
 
class Shape(ABC):
    @abstractmethod
    def area(self):
        ...
 
 
class Square(Shape):
    def __init__(self, side):
        self.side = side
 
 
s = Square(2)
print(s.area())

Show the answer

The error is TypeError: Can't instantiate abstract class Square without an implementation for abstract method 'area'. Its message names the missing method, and the program prints 4 once the method exists. Square inherits the abstract area without writing one. Add the method. This replaces the Square class from the question, and Shape stays as it was:

class Square(Shape):
    def __init__(self, side):
        self.side = side
 
    def area(self):
        return self.side * self.side

With side 2 the area is 4, so the program prints 4.

Exercise 10: the template method (hard)

Write an abstract base class Greeting with a method build(name) that returns the text "<word>, <name>!". The word comes from an abstract method word(). Write English (returns Hello), French (returns Bonjour) and Hindi (returns Namaste). Print one greeting for each, for the name Ravi, using a list of Greeting objects.

Expected output:

Hello, Ravi!
Bonjour, Ravi!
Namaste, Ravi!

Show the answer

Python · editable

The format is written once in build. Each child supplies only its word. The method build is not meant to be overridden, so a comment in a real project should say so.

Exercise 11: a product with a tax (hard)

Write an abstract base class Product with an abstract method base() and a normal method total() that returns base() plus a tax of 10 percent (use integer division, base() // 10). Write Book with base 200 and Gadget with base 500. Print the total for each of two products, one line each.

Expected output:

220
550

Show the answer

Python · editable

total is written once in the base class and calls base(), which each child defines. For the book, 200 + 20 is 220. For the gadget, 500 + 50 is 550.

Exercise 12: why not check the type everywhere? (hard)

A student has a list of shapes and writes a loop that checks isinstance for Circle, then for Square, and prints each shape's size in a different way each time. Explain why this design is weak, and describe the change that removes the checks.

Show the answer

The loop depends on the list of kinds, and every new shape needs another check and another branch. The same chain then appears in every function that handles shapes. The better design puts the common behaviour in the base class. If each shape needs to describe its size, add an abstract method such as size_text() to Shape, and let each child return its own text. The loop then calls size_text() on every element, with no checks.

Summary

  • A method call runs the version in the object's class, at run time. The variable name does not decide the method.
  • An abstract base class, from abc, lists promised methods and refuses to create an object that is missing one.
  • A base class can hold shared code, such as a template method that calls the children's methods.
  • Python has no overloading. Use default values, isinstance, match or singledispatch to choose behaviour by argument.
  • Python has no casts. Check the kind with isinstance before you use a child-only method, or move the work into the parent.
  • An expression tree is a tree of objects where each node computes its own value. Adding a new kind means adding one class.
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.

← Inheritance in Python classes