Python Course
Abstraction
Abstraction is the fourth pillar of object-oriented programming. Where encapsulation is about hiding data, abstraction is about hiding complexity. When you drive a car, you use a steering wheel and pedals — you do not think about the combustion cycle or transmission ratios. The complex internals are hidden behind a simple, consistent interface. That is abstraction.
In Python, abstraction is achieved primarily through abstract classes — classes that define a required interface without providing a full implementation. They say "any class that inherits from me must implement these methods" — enforcing a contract at class design time rather than discovering missing methods at runtime.
The Problem Abstraction Solves
Without abstraction, a base class can only raise NotImplementedError at runtime — after an instance is already created. Abstract classes catch the problem earlier, at instantiation time, before any code runs.
# Without abstraction — error only discovered at runtime
class Shape:
def area(self):
raise NotImplementedError("Subclasses must implement area()")
class Circle(Shape):
pass # forgot to implement area()
c = Circle() # no error yet — instance created fine
c.area() # NotImplementedError raised only NOW, at call timeWith an abstract class, Circle() itself would raise a TypeError immediately — catching the mistake at the point of instantiation, not buried somewhere in running code.
The abc Module — Abstract Base Classes
Python's abc module provides ABC (Abstract Base Class) and the @abstractmethod decorator. Any class that inherits from ABC and contains at least one @abstractmethod cannot be instantiated directly — Python enforces this at instantiation time.
Why it exists: it formalises the contract between a base class and its subclasses. Framework designers use it to guarantee that all subclasses implement the required interface before any code tries to use them.
Real-world use: Python's own standard library uses abstract base classes extensively — collections.abc.Iterable, collections.abc.Mapping, and collections.abc.Sequence all define required interfaces that custom types can implement.
# Abstract base class — enforces interface at instantiation time
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self):
"""Return the area of the shape."""
pass
@abstractmethod
def perimeter(self):
"""Return the perimeter of the shape."""
pass
def describe(self): # concrete method — available to all subclasses
print(f"{self.__class__.__name__}: area={self.area():.2f}, perimeter={self.perimeter():.2f}")
# Trying to instantiate the abstract class raises TypeError immediately
try:
s = Shape()
except TypeError as e:
print("Error:", e)- Inherit from
ABCto make a class abstract:class MyClass(ABC): - Mark required methods with
@abstractmethod— subclasses must override every one - Abstract classes can contain concrete methods — they are fully inherited by subclasses
- A subclass that does not implement all abstract methods is itself abstract and also cannot be instantiated
Implementing the Abstract Interface
Once a concrete subclass implements every abstract method, it becomes a regular class that can be instantiated normally.
# Concrete subclasses implementing the Shape interface
import math
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self): pass
@abstractmethod
def perimeter(self): pass
def describe(self):
print(f"{self.__class__.__name__}: area={self.area():.2f}, perimeter={self.perimeter():.2f}")
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self):
return math.pi * self.radius ** 2
def perimeter(self):
return 2 * math.pi * self.radius
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)
class Triangle(Shape):
def __init__(self, a, b, c):
self.a, self.b, self.c = a, b, c
def area(self):
s = self.perimeter() / 2 # Heron's formula
return math.sqrt(s * (s-self.a) * (s-self.b) * (s-self.c))
def perimeter(self):
return self.a + self.b + self.c
shapes = [Circle(5), Rectangle(4, 6), Triangle(3, 4, 5)]
for shape in shapes:
shape.describe()Rectangle: area=24.00, perimeter=20.00
Triangle: area=6.00, perimeter=12.00
- Each subclass provides its own implementation — the abstract class guarantees they all exist
describe()is defined once in the abstract base and works for all subclasses via polymorphism- Adding a new shape requires only writing the new class — no changes to existing code
Abstract Properties
You can combine @abstractmethod with @property to require that subclasses expose specific attributes as properties rather than plain methods.
# Abstract properties — require attribute-style access in subclasses
from abc import ABC, abstractmethod
class Vehicle(ABC):
@property
@abstractmethod
def fuel_type(self):
"""Subclasses must expose fuel_type as a property."""
pass
@property
@abstractmethod
def max_speed(self):
pass
def describe(self):
print(f"{self.__class__.__name__} | Fuel: {self.fuel_type} | Max speed: {self.max_speed} km/h")
class ElectricCar(Vehicle):
@property
def fuel_type(self):
return "Electric"
@property
def max_speed(self):
return 250
class PetrolBike(Vehicle):
@property
def fuel_type(self):
return "Petrol"
@property
def max_speed(self):
return 180
vehicles = [ElectricCar(), PetrolBike()]
for v in vehicles:
v.describe()PetrolBike | Fuel: Petrol | Max speed: 180 km/h
- Stack
@propertyabove@abstractmethod— the order matters - Subclasses implement it as a normal
@property— no need for@abstractmethodin the subclass - This enforces clean attribute-style access across all implementations
Real-World Example — Payment Gateway
Abstract classes shine when designing plugin-style architectures — where a core system defines the interface and different implementations are swapped in without changing the core.
# Real-world abstraction — pluggable payment gateway
from abc import ABC, abstractmethod
class PaymentGateway(ABC):
"""Abstract interface every payment provider must implement."""
@abstractmethod
def charge(self, amount: float, currency: str) -> bool:
pass
@abstractmethod
def refund(self, transaction_id: str) -> bool:
pass
@abstractmethod
def get_transaction_fee(self, amount: float) -> float:
pass
def process(self, amount, currency): # concrete — shared logic
fee = self.get_transaction_fee(amount)
total = amount + fee
print(f"Processing ${total:.2f} ({currency}) including ${fee:.2f} fee")
return self.charge(total, currency)
class StripeGateway(PaymentGateway):
def charge(self, amount, currency):
print(f"Stripe: charged ${amount:.2f} {currency}")
return True
def refund(self, transaction_id):
print(f"Stripe: refunded {transaction_id}")
return True
def get_transaction_fee(self, amount):
return round(amount * 0.029 + 0.30, 2) # 2.9% + $0.30
class PayPalGateway(PaymentGateway):
def charge(self, amount, currency):
print(f"PayPal: charged ${amount:.2f} {currency}")
return True
def refund(self, transaction_id):
print(f"PayPal: refunded {transaction_id}")
return True
def get_transaction_fee(self, amount):
return round(amount * 0.0349, 2) # 3.49%
# The calling code works identically regardless of which gateway is used
for gateway in [StripeGateway(), PayPalGateway()]:
gateway.process(100.00, "USD")
print()Stripe: charged $103.20 USD
Processing $103.49 (USD) including $3.49 fee
PayPal: charged $103.49 USD
- The abstract base guarantees every gateway has
charge(),refund(), andget_transaction_fee() - The
process()method is implemented once in the base and works correctly for all gateways - Swapping from Stripe to PayPal requires changing one line — the rest of the system is untouched
Abstraction vs Encapsulation — The Distinction
- Encapsulation — hides data and implementation details inside a class, controlling access through a defined interface
- Abstraction — hides complexity by defining what an object does without specifying how — focuses on the interface, not the internals
- They work together: an abstract class defines the interface (abstraction), while each concrete implementation controls access to its own data (encapsulation)
Summary Table
| Concept | What It Does | Key Syntax |
|---|---|---|
ABC |
Base class that enables abstract method enforcement | class MyClass(ABC): |
@abstractmethod |
Marks a method that every subclass must implement | @abstractmethod def method(self): pass |
| Abstract property | Requires a property implementation in every subclass | @property @abstractmethod |
| Concrete method in ABC | Shared logic available to all subclasses | Regular method alongside abstract ones |
| Instantiation guard | Prevents creating an incomplete object | Raised as TypeError at instantiation time |
Practice Questions
Practice 1. Which module provides ABC and @abstractmethod in Python?
Practice 2. What exception does Python raise when you try to instantiate an abstract class directly?
Practice 3. Can an abstract base class contain concrete (non-abstract) methods?
Practice 4. What is the correct decorator order when defining an abstract property?
Practice 5. What is the key difference between abstraction and encapsulation?
Quiz
Quiz 1. What happens if a subclass of an abstract class does not implement all abstract methods?
Quiz 2. What is the advantage of using @abstractmethod over just raising NotImplementedError in a base class method?
Quiz 3. In the payment gateway example, which method is defined in the abstract base and shared by all subclasses?
Quiz 4. Which of the following is true about abstract base classes in Python?
Quiz 5. What design principle does abstraction directly support in the payment gateway example?
Next up — Decorators: wrapping functions to add behaviour without changing their code.