DevHireLab
Tutorials
BootcampProblemsCode SimulatorAI InterviewSoonContact
DevHireLab
Tutorials
BootcampProblemsCode SimulatorAI InterviewSoonContact
Getting Started
01
What is Python and Why Learn It in 2026
02
Installing Python on Windows, Mac, and Linux
03
Setting up VS Code / PyCharm for Python
04
Understanding the Python Interpreter and REPL
05
Writing and Running Your First Python Program
06
Python Syntax and Indentation Rules
Variables and Data Types
01
Variables and Naming Conventions in Python
02
Numbers in Python (int, float, complex)
03
Strings and String Methods in Python
04
Booleans in Python
05
Type Conversion / Casting in Python
06
Understanding the None Type in Python
Operators
01
Arithmetic Operators in Python
02
Comparison Operators in Python
03
Logical Operators in Python
04
Assignment Operators in Python
05
Bitwise Operators in Python
06
Identity (is) and Membership (in) Operators
07
Walrus Operator (:=) in Python
Input, Output & Strings
01
input() and print() Functions in Python
02
f-strings and String Formatting in Python
03
String Slicing and Indexing in Python
04
Common String Methods (split, join, strip, replace)
05
Escape Characters in Python Strings
Control Flow
01
if, elif, else Statements in Python
02
Nested Conditionals in Python
03
for Loops in Python
04
while Loops in Python
05
break, continue, and pass Statements
06
Loop with else Clause in Python
07
match-case (Structural Pattern Matching)
Data Structures
01
Python Lists: Creation, Indexing, Slicing, Methods
02
Python Tuples and Immutability
03
Python Sets and Set Operations
04
Python Dictionaries: Keys, Values, and Methods
05
List Comprehensions in Python
06
Dictionary & Set Comprehensions in Python
07
Working with Nested Lists and Dictionaries
Functions
01
Defining and Calling Functions in Python
02
Function Arguments: Positional, Keyword, Default
03
*args and **kwargs in Python
04
Return Statements in Python Functions
05
Lambda Functions in Python
06
Recursion in Python
07
Variable Scope: Local, Global, Nonlocal
08
Docstrings and Function Annotations
Functional Programming Basics
01
map(), filter(), and reduce() in Python
02
Closures in Python Explained
03
Python Decorators: Basic to Advanced
04
Generators and yield in Python
Object-Oriented Programming (OOP)
01
Classes and Objects in Python
02
The init Constructor in Python
03
Instance vs Class Variables in Python
04
Inheritance in Python (Single, Multiple, Multilevel)
05
Polymorphism in Python
06
Encapsulation in Python
07
Abstraction in Python with the abc Module
08
Magic/Dunder Methods in Python
09
Static and Class Methods in Python
10
The @property Decorator in Python
11
Python Dataclasses Explained
Error Handling
01
try, except, finally in Python
02
Handling Multiple Exceptions in Python
03
Raising Custom Exceptions in Python
04
Understanding Python's Exception Hierarchy
05
Assertions in Python
File Handling
01
Reading and Writing Text Files in Python
02
Working with the with Statement in Python
03
Working with CSV Files in Python
04
Working with JSON Files in Python
05
Working with Directories (os, pathlib) in Python
Modules and Packages
01
Importing Built-in Modules in Python
02
Creating Your Own Modules in Python
03
Understanding Packages and init.py
04
Python Standard Library Overview
05
Installing Packages with pip
Advanced Core Concepts
01
Iterators and Iterables in Python
02
Context Managers in Python (with, contextlib)
03
Multithreading Basics in Python
04
Multiprocessing Basics in Python
05
Asyncio Basics: async/await in Python
06
Memory Management and Garbage Collection in Python
07
Type Hints in Python (typing module)

Abstraction in Python with the abc Module

python abstraction is about defining what a class must do, without dictating how it does it — exposing an essential interface while hiding implementation complexity behind it. This article covers the abc module python provides for formalizing that interface, including @abstractmethod, abstract properties, and how Abstract Base Classes compare to the more lightweight typing.Protocol covered in the earlier polymorphism article.

What Is Abstraction?

Abstraction means hiding implementation complexity and exposing only the essential interface — defining what a class needs to do, without dictating how it accomplishes it internally.

A real-world framing

Think of it like a federal banking regulation: the regulation states that a bank must be able to process a withdrawal. It says absolutely nothing about how any particular bank actually implements that internally — the database technology, the specific validation steps, the internal ledger system. Each bank builds its own implementation however it sees fit, as long as the interface the regulation demands — "must process a withdrawal" — is genuinely satisfied.

Where abstraction fits among the OOP pillars

This article completes a set covered across this series: encapsulation (controlling access to an object's internal state), inheritance (building new classes on top of existing ones), polymorphism (the same method behaving differently depending on the object), and now abstraction — defining a required interface, explicitly, that other classes must conform to. Abstraction and encapsulation are closely related but distinct: encapsulation hides an object's data; abstraction hides implementation details, focusing purely on defining the contract a class must fulfill.

2. Creating an Abstract Base Class with abc

Basic setup

Python's abc module provides ABC (a base class to inherit from) and abstractmethod (a decorator marking a method as required):

from abc import ABC, abstractmethod

class PaymentProvider(ABC):
    @abstractmethod
    def process_payment(self, amount):
        pass

PaymentProvider inherits from ABC, marking it as an abstract base class. process_payment() is decorated with @abstractmethod, declaring it as a required placeholder — a method every subclass must implement, with no default implementation provided here at all.

What happens if a subclass doesn't implement it
class Incomplete(PaymentProvider):
    pass   # forgot to implement process_payment()

provider = Incomplete()
# TypeError: Can't instantiate abstract class Incomplete with abstract method process_payment

This is the entire point of using an ABC over plain duck typing: the error happens immediately, at the moment Incomplete() is called — not buried somewhere much later in your program's execution, whenever some far-away piece of code finally happens to call process_payment() on an incomplete object and gets a confusing AttributeError at an inconvenient time. Abstract base classes convert what would otherwise be a silent, deferred bug into an immediate, unmissable one.

A working implementation
class Stripe(PaymentProvider):
    def process_payment(self, amount):
        print(f"Processing  via Stripe")

stripe = Stripe()
stripe.process_payment(100)   # Processing $100 via Stripe

Stripe genuinely implements process_payment(), so it instantiates without any issue.

Concrete Methods and Abstract Properties in ABCs

ABCs can include fully implemented methods too

An abstract base class isn't limited to abstract methods alone — it can also include fully implemented (concrete) methods that subclasses inherit and reuse directly, without needing to redefine them:

from abc import ABC, abstractmethod

class PaymentProvider(ABC):
    @abstractmethod
    def process_payment(self, amount):
        pass

    def log_transaction(self, amount):   # concrete — not abstract, no decorator
        print(f"Logging transaction: ")

class Stripe(PaymentProvider):
    def process_payment(self, amount):
        self.log_transaction(amount)   # reusing the inherited concrete method
        print(f"Processing  via Stripe")

stripe = Stripe()
stripe.process_payment(50)
# Logging transaction: $50
# Processing $50 via Stripe

log_transaction() is shared, reusable behavior every subclass gets for free, while process_payment() remains something each subclass must define for itself.

A subtlety: abstract methods can have partial implementations

This is a genuinely useful, somewhat lesser-known detail: an abstract method itself can still contain real code — subclasses are required to override it, but they can call that partial implementation via super() to extend it, rather than fully replacing it from scratch:

from abc import ABC, abstractmethod

class PaymentProvider(ABC):
    @abstractmethod
    def process_payment(self, amount):
        print(f"Base processing steps for ")

class Stripe(PaymentProvider):
    def process_payment(self, amount):
        super().process_payment(amount)   # runs the abstract method's own partial logic first
        print("Stripe-specific processing")

stripe = Stripe()
stripe.process_payment(75)
# Base processing steps for $75
# Stripe-specific processing

Even though process_payment is marked @abstractmethod and must be overridden, its own body still runs correctly when explicitly called via super() — this can be genuinely useful for sharing common setup logic across every subclass's implementation, while still requiring each subclass to provide its own specific behavior on top.

Abstract properties

Combining @property (covered in the earlier encapsulation article) with @abstractmethod lets you require subclasses to implement a specific attribute-like interface, not just a method:

from abc import ABC, abstractmethod

class Animal(ABC):
    @property
    @abstractmethod
    def species(self):
        pass

class Dog(Animal):
    @property
    def species(self):
        return "Canis familiaris"

dog = Dog()
print(dog.species)   # Canis familiaris — accessed like a plain attribute

Any subclass of Animal that doesn't define its own species property will fail to instantiate at all, with the same clear TypeError shown earlier — exactly the same enforcement as an abstract method, applied specifically to something meant to be accessed as a property rather than called as a method.

A Practical Example: Enforcing a Consistent Interface

Here's a fuller worked example combining several concepts covered so far — an abstract Employee class with required behavior, implemented differently by two concrete subclasses:

from abc import ABC, abstractmethod

class Employee(ABC):
    def __init__(self, name):
        self.name = name

    @abstractmethod
    def calculate_pay(self):
        pass

    def display_info(self):   # concrete — shared by every subclass
        print(f"{self.name}: ")

class SalariedEmployee(Employee):
    def __init__(self, name, annual_salary):
        super().__init__(name)
        self.annual_salary = annual_salary

    def calculate_pay(self):
        return self.annual_salary / 12

class HourlyEmployee(Employee):
    def __init__(self, name, hourly_rate, hours_worked):
        super().__init__(name)
        self.hourly_rate = hourly_rate
        self.hours_worked = hours_worked

    def calculate_pay(self):
        return self.hourly_rate * self.hours_worked

employees = [
    SalariedEmployee("Alex", 72000),
    HourlyEmployee("Sam", 25, 160),
]

for employee in employees:
    employee.display_info()
# Alex: $6000.00
# Sam: $4000.00

Both subclasses genuinely calculate pay in completely different ways, but display_info() — defined once, on the abstract base class — works identically for both, purely because both subclasses correctly fulfilled the calculate_pay() contract the abstract class required.

Why this matters in real projects

The genuine, practical payoff here is preventing what's sometimes called the "Wild West" problem: without an enforced interface, different developers working on the same codebase (or even the same developer, working months apart) can end up building inconsistent classes that quietly violate assumptions elsewhere in the system — a class that's supposed to support calculate_pay() but is missing it, silently breaking any code that assumes every Employee supports that method. An ABC catches this immediately and loudly, at the moment an incomplete class is instantiated, rather than allowing that inconsistency to sit undetected until it eventually causes a real, confusing failure somewhere else entirely.

A brief mention of register()

The abc module also supports declaring "virtual subclasses" via .register() — classes that are recognized as satisfying an ABC's interface for isinstance()/issubclass() checks, without any actual inheritance relationship at all:

class DuckPayment:
    def process_payment(self, amount):
        print(f"Processing  the duck-typed way")

PaymentProvider.register(DuckPayment)

print(isinstance(DuckPayment(), PaymentProvider))   # True — even without inheriting from it

This is a fairly advanced, less commonly used escape hatch — genuinely useful in specific scenarios where you want a class to be recognized as fulfilling an ABC's contract for type-checking purposes, without actually restructuring its class hierarchy to formally inherit from that ABC.

ABC vs. typing.Protocol: Choosing the Right Tool

The core distinction

As introduced in the earlier polymorphism article: ABCs enforce the contract at instantiation time, through actual inheritance — a subclass must formally inherit from the ABC, and Python will refuse to create an instance if required methods are missing. typing.Protocol (from the typing module, available since Python 3.8) formalizes duck typing specifically for static type checkers, without requiring any inheritance relationship at all — a class satisfies a Protocol purely by happening to have the right methods with the right signatures, checked by a tool like Mypy rather than enforced by Python itself at runtime.

from abc import ABC, abstractmethod
from typing import Protocol

# ABC — enforced at runtime, requires inheritance
class Speaker(ABC):
    @abstractmethod
    def speak(self) -> str:
        pass

# Protocol — checked by static analysis, no inheritance required
class SpeakerProtocol(Protocol):
    def speak(self) -> str:
        ...
When to reach for each

Reach for an ABC in larger codebases, and especially in plugin-style architectures, where genuine runtime enforcement matters — you want Python itself to actively refuse to create an incomplete implementation, catching mistakes immediately rather than trusting every developer to remember every required method.

Reach for typing.Protocol when you want a lightweight, formal interface definition — genuinely useful for static type checking and editor tooling — without dictating an actual class hierarchy. This is particularly valuable when you're defining an interface against classes you don't own or can't modify (say, from a third-party library), since Protocol never requires those classes to be changed to explicitly inherit from anything at all.

The quick takeaway

Abstraction isn't about making code more complex for its own sake — it's about making the required interface explicit, so that new subclasses (or new implementations, in the Protocol case) can be added safely, later, by anyone, without needing to read through and understand the entire existing codebase first, and without risk of silently breaking assumptions that other code already depends on.

PREVIOUSNEXT LESSON