Python · lesson 15 of 34
Beginner
16- 1 Introduction to Python
- 2 Setting Up Python
- 3 Python Variables and Type System
- 4 Python Data Types
- 5 Python Operators
- 6 Python Strings
- 7 Control Flow in Python
- 8 Python Functions and Parameters
- 9 Python Data Structures
- 10 Python Collections
- 11 Python Modules and Packages
- 12 Object-Oriented Programming in Python
- 13 Encapsulation and Name Mangling in Python
- 14 Polymorphism and Duck Typing in Python
- 15 Abstraction with Abstract Base Classes in Python
- 21 Building a Python CLI Tool
Intermediate
12- 16 Multiple Inheritance and MRO in Python
- 17 Composition vs Inheritance in Python
- 18 Python Dataclasses
- 19 Error Handling in Python
- 20 File I/O, pathlib, and JSON in Python
- 22 Python Decorators and Closures
- 23 Python Generators
- 24 Python Context Managers
- 25 Python Concurrency
- 27 Testing Python Code
- 28 Python Security
- 34 Python Interview Prep
Advanced
6Abstraction with Abstract Base Classes in Python
Use Python's abc module to define abstract base classes that force subclasses to implement required methods before they can be instantiated.
By SkillByExample · Updated
Abstraction means a base class states what subclasses must do, without dictating how. Python’s abc module enforces this: an abstract method must be overridden before a class can be instantiated. Output below is from Python 3.13.
Declaring Required Methods
ABC combined with @abstractmethod blocks instantiation of any class — base or subclass — that hasn’t implemented every abstract method.
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self) -> float:
...
@abstractmethod
def perimeter(self) -> float:
...
def describe(self) -> str: # a concrete method - not every method must be abstract
return f"Area: {self.area():.2f}, Perimeter: {self.perimeter():.2f}"
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self):
import math
return math.pi * self.radius ** 2
def perimeter(self):
import math
return 2 * math.pi * self.radius
try:
Shape()
except TypeError as e:
print(f"TypeError: {e}")
c = Circle(5)
print(c.describe())
$ python shapes.py
TypeError: Can't instantiate abstract class Shape without an implementation for abstract methods 'area', 'perimeter'
Area: 78.54, Perimeter: 31.42
Shape() fails immediately — Python checks for unimplemented abstract methods at instantiation time, not when they’re first called. describe() shows that a base class can mix abstract methods (area, perimeter) with a concrete one that every subclass inherits for free.
A Subclass That Forgets a Method
The check applies to subclasses too. Implementing only some of the abstract methods still leaves a class abstract.
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self) -> float:
...
@abstractmethod
def perimeter(self) -> float:
...
class Square(Shape):
def __init__(self, side):
self.side = side
def area(self):
return self.side ** 2
# perimeter() is forgotten - the subclass is still abstract
try:
Square(4)
except TypeError as e:
print(f"TypeError: {e}")
print(sorted(Shape.__abstractmethods__))
print(sorted(Square.__abstractmethods__))
$ python incomplete_subclass.py
TypeError: Can't instantiate abstract class Square without an implementation for abstract method 'perimeter'
['area', 'perimeter']
['perimeter']
__abstractmethods__ is how Python tracks this — it’s a frozenset (wrapped in sorted() here, because set order can change from run to run) that shrinks as a subclass fills in implementations, and a class can only be instantiated once it’s empty. Square still lists perimeter, so instantiating it fails with the same TypeError as the empty base class.
Practice
1. Add a Rectangle subclass of Shape that implements both area and perimeter, then call describe() on it.
Solution
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self) -> float: ...
@abstractmethod
def perimeter(self) -> float: ...
def describe(self) -> str:
return f"Area: {self.area():.2f}, Perimeter: {self.perimeter():.2f}"
class Rectangle(Shape):
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
def perimeter(self):
return 2 * (self.width + self.height)
print(Rectangle(3, 4).describe())
$ python rectangle.py
Area: 12.00, Perimeter: 14.00
With both abstract methods implemented, Rectangle.__abstractmethods__ is empty, so it instantiates and inherits describe() unchanged.
2. Add an abstract name property to Shape using @property stacked with @abstractmethod, and implement it in Circle.
Solution
import math
from abc import ABC, abstractmethod
class Shape(ABC):
@property
@abstractmethod
def name(self) -> str: ...
@abstractmethod
def area(self) -> float: ...
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
@property
def name(self):
return "circle"
def area(self):
return math.pi * self.radius ** 2
c = Circle(2)
print(c.name, round(c.area(), 2))
print(sorted(Shape.__abstractmethods__))
$ python abstract_property.py
circle 12.57
['area', 'name']
@property must be the outer decorator so the abstract flag set by @abstractmethod lands on the getter. name then appears in __abstractmethods__ exactly like a regular method.
3. Assign Shape.area = lambda self: 0 after the class is defined, then attempt Shape() again. Does patching in a method after the fact satisfy the abstract-method check?
Solution
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self) -> float: ...
Shape.area = lambda self: 0
try:
Shape()
except TypeError as e:
print(f"TypeError: {e}")
print(sorted(Shape.__abstractmethods__))
$ python patched.py
TypeError: Can't instantiate abstract class Shape without an implementation for abstract method 'area'
['area']
Still fails. __abstractmethods__ is computed once, when the class is created, and is not re-checked when you patch the class later. (abc.update_abstractmethods(Shape), added in 3.10, recomputes it if you really need to.)
4. Without abc, write a “soft” abstract base class where an unimplemented method raises NotImplementedError only when called. Compare when the failure surfaces versus the ABC version.
Solution
class SoftShape:
def area(self):
raise NotImplementedError(f"{type(self).__name__} must implement area()")
s = SoftShape() # succeeds: no instantiation-time check
print("created", type(s).__name__)
try:
s.area() # fails only now, when actually called
except NotImplementedError as e:
print(f"NotImplementedError: {e}")
$ python soft_abstract.py
created SoftShape
NotImplementedError: SoftShape must implement area()
ABC catches the mistake at construction; the NotImplementedError convention only catches it when the missing method is actually used, which can be much later in a program’s run.