Python Lesson 35 – Abstraction | Dataplexa

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 time
NotImplementedError: Subclasses must implement area()

With 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)
Error: Can't instantiate abstract class Shape with abstract methods area, perimeter
  • Inherit from ABC to 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()
Circle: area=78.54, perimeter=31.42
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()
ElectricCar | Fuel: Electric | Max speed: 250 km/h
PetrolBike | Fuel: Petrol | Max speed: 180 km/h
  • Stack @property above @abstractmethod — the order matters
  • Subclasses implement it as a normal @property — no need for @abstractmethod in 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()
Processing $103.20 (USD) including $3.20 fee
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(), and get_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.