Python Abstract Classes (ABC)
1Python Abstract Classes: overview
An abstract class is a base class that lists the methods every child must provide without saying how they work, and Python refuses to create objects from it until all of those methods have real code.
Before this lesson: Python Inheritance, Python Polymorphism and Duck Typing
2Python Abstract Classes: diagram
3Python Abstract Classes: explained step by step
In the inheritance lesson, Shape.area() returned 0 so that children could override it. That hides a quiet flaw: if someone writes a new Triangle and forgets area(), nothing complains. The triangle just reports an area of 0, and you discover the bug far away from the line that caused it.
An abstract class closes that gap. It is a parent class that says what every child must be able to do and leaves how to the children. Unlike a placeholder method, the promise is enforced by Python.
When a parent is only a promise
Some parents are never meant to be used on their own. There is no such thing as "just a shape", only circles, squares and triangles. There is no "just a payment method", only cards, bank transfers and vouchers. The parent exists to state a contract: every Shape has an area(), every PaymentMethod can pay(amount), every Exporter can export(rows). Code that knows only the contract can then work with any child, including ones written next year.
Your first abstract class
Inherit from ABC (short for Abstract Base Class) and put @abstractmethod above each method the children must provide. The body is usually just a docstring, because it is not meant to do the work:
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self):
"""Every shape must say how to work out its area."""
class Square(Shape):
def __init__(self, side):
self.side = side
def area(self):
return self.side ** 2
print(Square(4).area())
try:
Shape()
except TypeError:
print("Cannot create a Shape: it is abstract")Shape() fails because area() has no real code yet. In Python 3.13 the message reads TypeError: Can't instantiate abstract class Shape without an implementation for abstract method 'area'. Notice when it happens: the moment you try to create the object, not later when some distant code calls area().
Every abstract method must be filled in
A child becomes concrete, meaning you may create objects from it, only when it replaces every abstract method with a real one. If it fills in just some, it is still abstract and fails in the same way. That is the safety net you want when a teammate adds a new subclass and forgets a method. Each abstract class also keeps the names still missing in __abstractmethods__, which is handy for debugging:
from abc import ABC, abstractmethod
class PaymentMethod(ABC):
@abstractmethod
def pay(self, amount):
"""Take the money and return a receipt line."""
@abstractmethod
def refund(self, amount):
"""Give the money back and return a receipt line."""
class CardPayment(PaymentMethod):
def pay(self, amount):
return f"Charged ${amount} to the card"
def refund(self, amount):
return f"Returned ${amount} to the card"
class GiftVoucher(PaymentMethod):
def pay(self, amount):
return f"Used ${amount} of voucher"
print(CardPayment().pay(30))
try:
GiftVoucher()
except TypeError:
print("GiftVoucher is still abstract")
print(sorted(GiftVoucher.__abstractmethods__))Abstract classes can still hold real code
Being abstract does not change anything else about a class. It can have an __init__, attributes and ordinary methods, and its children inherit them unchanged. Only the methods marked @abstractmethod have to be replaced.
The template method pattern
This is where abstract classes really pay off. Write one concrete method that fixes the order of the steps, and make the individual steps abstract. The parent owns the recipe; each child supplies its own ingredients. Below, export() never changes, but what a header and a row look like is decided by the child:
from abc import ABC, abstractmethod
class Exporter(ABC):
def __init__(self, title):
self.title = title
def export(self, rows):
lines = [self.header()]
for row in rows:
lines.append(self.format_row(row))
return "\n".join(lines)
@abstractmethod
def header(self):
"""First line of the output."""
@abstractmethod
def format_row(self, row):
"""One output line for one row."""
class CsvExporter(Exporter):
def header(self):
return "name,price"
def format_row(self, row):
return f"{row[0]},{row[1]}"
class MarkdownExporter(Exporter):
def header(self):
return f"# {self.title}"
def format_row(self, row):
return f"- {row[0]}: ${row[1]}"
rows = [("Lamp", 24.5), ("Mug", 8)]
print(CsvExporter("Stock").export(rows))
print(MarkdownExporter("Stock").export(rows))Nothing in export() knows about CSV or Markdown, so adding a JsonExporter later would not touch it.
Abstract properties
A property can be abstract too, which forces children to provide a value instead of a method. Stack the decorators in this order: @property on the outside and @abstractmethod directly above the def.
from abc import ABC, abstractmethod
class Employee(ABC):
def __init__(self, name):
self.name = name
@property
@abstractmethod
def monthly_pay(self):
"""Each kind of employee works its pay out differently."""
def summary(self):
return f"{self.name} earns {self.monthly_pay} a month"
class Salaried(Employee):
def __init__(self, name, salary):
super().__init__(name)
self.salary = salary
@property
def monthly_pay(self):
return self.salary
class Hourly(Employee):
def __init__(self, name, rate, hours):
super().__init__(name)
self.rate = rate
self.hours = hours
@property
def monthly_pay(self):
return self.rate * self.hours
print(Salaried("Asha", 3200).summary())
print(Hourly("Ben", 20, 150).summary())isinstance() and abstract classes
An abstract class works as a type for everything that keeps its promise, so isinstance(obj, Shape) is True for every child. A function can use that to check its input once, at the door.
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self):
"""Return the area."""
class Square(Shape):
def __init__(self, side):
self.side = side
def area(self):
return self.side ** 2
class Tile:
def area(self):
return 9
for thing in [Square(2), Tile()]:
print(type(thing).__name__, isinstance(thing, Shape), thing.area())ABC or duck typing?
Tile has no Shape parent, yet its area() works, so any code that just calls .area() is happy with it. That is duck typing: "if it has the method, it can do the job". It needs no base class and it is Python's default style.
Reach for an abstract class when:
- you own a family of classes and want a forgotten method to be an error at creation time, not a crash halfway through the program;
- the children share real code, such as a template method or a common __init__;
- you want isinstance() to mean "this object keeps the contract".
Stay with duck typing when only one or two functions use the objects, when the classes come from different libraries you cannot change, or when a base class would add ceremony without any shared code.
One limit to remember: Python only checks that the names exist. It does not check the parameters or whether the method does anything sensible, so a child that defines area(self, extra) still slips through.
4Python Abstract Classes: syntax
from abc import ABC, abstractmethod
class Base(ABC): # inherit from ABC
def __init__(self, name): # normal code is allowed too
self.name = name
@abstractmethod
def step(self): # every child MUST replace this
"""Describe the contract here."""
@property
@abstractmethod # abstract property: @property goes on the outside
def size(self):
"""Children must provide a value."""
def run(self): # concrete method built from abstract steps
return f"{self.name}: {self.step()}"
class Child(Base):
def step(self):
return "done"
@property
def size(self):
return 3
Base("x") # TypeError: can't instantiate abstract class
Child("x").run() # works: nothing abstract is left
isinstance(Child("x"), Base) # True
5Python Abstract Classes: run the example
Press Run (or Ctrl + Enter). The output (step 6) appears directly below the code. Change anything and run it again.
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self):
pass
def describe(self):
return f"{type(self).__name__}: area {self.area()}"
class Rectangle(Shape):
def __init__(self, w, h):
self.w, self.h = w, h
def area(self):
return self.w * self.h
try:
Shape()
except TypeError:
print("Shape is abstract")
print(Rectangle(3, 4).describe())
print(isinstance(Rectangle(1, 1), Shape))Expected outputShape is abstract
Rectangle: area 12
True
7Python Abstract Classes: how the example works
Hover or tap a step to highlight its lines in the example above.
- Line 1
ABCandabstractmethodcome from the standardabcmodule. - Line 3-6
Shapeinherits fromABC, andarea()is marked@abstractmethod. Its body (pass) never has to do anything: it only records the promise that every shape will have anarea(). - Line 8-9
describe()is an ordinary, concrete method that every child inherits. It callsself.area(), so it uses whicheverarea()the object's own class provides. - Line 11-16
Rectangleis a concrete child: it stores its sides and replacesarea()with real code. Because no abstract method is left, you can createRectangleobjects. - Line 18-21
Shape()raisesTypeErrorbecausearea()has no implementation. Theexceptbranch prints a fixed message, so the program carries on. - Line 22The inherited
describe()callsRectangle'sarea()(3 * 4 = 12), andtype(self).__name__gives the name of the object's class. - Line 23A
Rectanglecounts as aShapeforisinstance(), so this printsTrue.
8Python Abstract Classes: common mistakes
Forgetting to inherit from ABC
✗ Wrong
from abc import abstractmethod
class Shape:
@abstractmethod
def area(self):
pass
class Triangle(Shape):
pass
tri = Triangle()
print("Triangle created, area:", tri.area())✓ Right
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self):
pass
class Triangle(Shape):
def __init__(self, base, height):
self.base = base
self.height = height
def area(self):
return self.base * self.height / 2
print(Triangle(3, 4).area())@abstractmethod only has an effect inside a class built from ABC (or with metaclass=ABCMeta). Without it the decorator is ignored, so nothing stops you from creating incomplete objects. Add ABC to the parent.
Misspelling or forgetting an abstract method in the child
✗ Wrong
from abc import ABC, abstractmethod
class Exporter(ABC):
@abstractmethod
def export(self, rows):
pass
class CsvExporter(Exporter):
def exprot(self, rows):
return ",".join(rows)
exporter = CsvExporter()✓ Right
from abc import ABC, abstractmethod
class Exporter(ABC):
@abstractmethod
def export(self, rows):
pass
class CsvExporter(Exporter):
def export(self, rows):
return ",".join(rows)
exporter = CsvExporter()
print(exporter.export(["Lamp", "Mug"]))A child only counts as implementing a method when the name matches exactly. exprot is just a new method, so export is still abstract. The error message names the missing method, so read it carefully.
Creating an object from the abstract class itself
✗ Wrong
from abc import ABC, abstractmethod
class PaymentMethod(ABC):
@abstractmethod
def pay(self, amount):
pass
method = PaymentMethod()
method.pay(20)✓ Right
from abc import ABC, abstractmethod
class PaymentMethod(ABC):
@abstractmethod
def pay(self, amount):
pass
class CardPayment(PaymentMethod):
def pay(self, amount):
return f"Paid ${amount} by card"
method = CardPayment()
print(method.pay(20))An abstract class is a blueprint for its children, not something you use directly. Create an object of a concrete child instead, and pass it around wherever a PaymentMethod is expected.
Stacking @abstractmethod and @property in the wrong order
✗ Wrong
from abc import ABC, abstractmethod
class Employee(ABC):
@abstractmethod
@property
def monthly_pay(self):
pass✓ Right
from abc import ABC, abstractmethod
class Employee(ABC):
@property
@abstractmethod
def monthly_pay(self):
pass
class Intern(Employee):
@property
def monthly_pay(self):
return 900
print(Intern().monthly_pay)Decorators apply from the bottom up. @property must be the outer decorator and @abstractmethod the inner one, so that abstractmethod marks the plain function before property wraps it.
9Python Abstract Classes: exercises
Write your answer, then press Check. The checks run in your browser.
1. Write an abstract Greeter
Write code Easy ✓ CompletedCreate an abstract class Greeter with one abstract method greet(self, name). Then write two children:
FormalGreeter, whosegreetreturns"Good day, <name>."CasualGreeter, whosegreetreturns"Hey <name>!"
For example CasualGreeter().greet("Ben") returns "Hey Ben!".
from abc import ABC, abstractmethod
# 1. class Greeter(ABC) with an abstract greet(self, name)
# 2. class FormalGreeter(Greeter): greet returns "Good day, <name>."
# 3. class CasualGreeter(Greeter): greet returns "Hey <name>!"
class Greeter(ABC): and put @abstractmethod directly above def greet(self, name):. The children inherit from Greeter and each define their own greet.from abc import ABC, abstractmethod
class Greeter(ABC):
@abstractmethod
def greet(self, name):
pass
class FormalGreeter(Greeter):
def greet(self, name):
return f"Good day, {name}."
class CasualGreeter(Greeter):
def greet(self, name):
return f"Hey {name}!"
for greeter in [FormalGreeter(), CasualGreeter()]:
print(greeter.greet("Asha"))
2. Predict the notifier
Predict the output Medium ✓ CompletedOne abstract class, two children. Type exactly what the program prints.
from abc import ABC, abstractmethod
class Notifier(ABC):
@abstractmethod
def send(self, text):
pass
@abstractmethod
def channel(self):
pass
def notify(self, text):
return f"[{self.channel()}] {self.send(text)}"
class EmailNotifier(Notifier):
def send(self, text):
return text.upper()
def channel(self):
return "email"
class HalfNotifier(Notifier):
def send(self, text):
return text
try:
HalfNotifier()
print("half created")
except TypeError:
print("half is abstract")
print(EmailNotifier().notify("hi"))
print(sorted(HalfNotifier.__abstractmethods__))
print(isinstance(EmailNotifier(), Notifier))
@abstractmethod is replaced. Which one does HalfNotifier still lack? notify() is a normal method that calls the other two.half is abstract
[email] HI
['channel']
True
2 more exercises → 6 more examples
Questions learners ask
What is the difference between an abstract class and an interface in Python?
Python has no separate interface keyword. An abstract class whose methods are all abstract plays the role of an interface: it lists what children must provide. Unlike interfaces in some other languages, a Python abstract class may also hold normal methods and attributes, which is what makes patterns like the template method possible.
Should I use @abstractmethod or raise NotImplementedError?
@abstractmethod stops you from creating an object of an incomplete class, so the mistake appears immediately and the error names the missing method. Raising NotImplementedError inside a base method only fails when that method is finally called, which can be much later. Prefer @abstractmethod for classes you design to be subclassed.
Can an abstract class have an __init__ and normal methods?
Yes. An abstract class is an ordinary class apart from the rule that you cannot create objects from it while abstract methods remain. Children can call its init with super().init(...) and inherit every concrete method.
Further reading
10Next lesson
Up nextPython DataclassesA dataclass is a normal class where you list the fields once, with type hints, and Python writes __init__, __repr__ and…Related:
Python InheritancePython Polymorphism and Duck TypingPython Composition: Has-A vs Is-A
← Python Composition: Has-A vs Is-A · All Object-Oriented Python lessons · Practise in the IDE