Intermediate Python 📂 Class and Object · 7 of 10 43 min read

Python OOP — Abstraction with ABC

Master abstraction — OOP's principle of showing what a class does while hiding how it does it. Learn Python's abc module, ABC, and @abstractmethod, understand why abstract classes can't be instantiated directly, and see how one abstract interface lets many concrete implementations coexist. Includes a coffee-machine analogy, three animated SVG diagrams (contract, blocked instantiation, polymorphic dispatch), Payment Gateway and Shape hierarchy examples, plus 10 golden rules.

Section 01

The Story That Explains Abstraction

The Coffee Machine — Press a Button, Get Coffee
You walk up to a coffee machine and press "Espresso". A minute later, espresso appears. You never had to:

Grind the beans. Measure 18 grams. Tamp with 30 pounds of force. Regulate water to 93°C. Time the extraction to 25 seconds. Purge the group head afterwards.

All of that happens behind a single button. What you use is the interface — Espresso, Latte, Cappuccino, Off. What actually runs is the implementation — pumps, sensors, heating elements, timers.

Now imagine you buy a different machine — a drip coffee-maker, a French press, or a capsule machine. Same idea: "press this, get coffee." Every machine implements "brew" completely differently, but from your perspective they all offer the same simple action.

That is abstraction. Expose what a class does; hide how it does it. Every real-world coffee machine, payment method, database driver, and file format handler in software works this way.
🔬
The Core Insight

Abstraction is the fourth pillar of OOP (alongside encapsulation, inheritance, and polymorphism). It says: define a contract that describes what an object should do, let each concrete class fill in the details. Python enforces this contract using Abstract Base Classes (ABC) and the @abstractmethod decorator.


Section 02

What Abstraction Really Means — Interface vs Implementation

Every abstraction has two sides. The interface is the promise: "if you're a Shape, you must be able to tell me your area." The implementation is how each concrete class fulfils that promise — a circle uses π·r², a rectangle uses w × h.

🏠 Interface (Abstract)
Says what must be done
Declares method names and signatures
Does not say how
Cannot be instantiated directly
Written once — reused by every implementation
🛠️ Implementation (Concrete)
Says how it's done
Fills in every abstract method
Free to use any algorithm internally
Can be instantiated and used
Multiple concrete classes can implement the same interface

Why It Matters

Without abstraction, callers have to know which specific class they're dealing with. With abstraction, callers depend on the interface only — every new implementation slots in without a single line of caller code changing.

# Without abstraction — the caller must handle every payment type explicitly
def process(payment_type, data, amount):
    if payment_type == "credit":
        # Do card-specific stuff
        ...
    elif payment_type == "upi":
        # Do UPI-specific stuff
        ...
    elif payment_type == "crypto":
        # Do crypto-specific stuff
        ...
    # Adding a new payment method → edit this function → break existing callers

# WITH abstraction — the caller just talks to the interface
def process(payment: PaymentMethod, amount):
    payment.authenticate()
    payment.charge(amount)
    # A new payment method? Just implement PaymentMethod. No changes here.

Section 03

Introducing ABC and @abstractmethod

Python's abc module ("Abstract Base Classes") gives you two things: ABC, a base class to inherit from, and @abstractmethod, a decorator that marks a method as must be overridden.

from abc import ABC, abstractmethod

class Shape(ABC):                     # inherit from ABC → this class is abstract
    # ── Abstract methods: no body needed, just a signature ──
    @abstractmethod
    def area(self):
        pass

    @abstractmethod
    def perimeter(self):
        pass


# Try to instantiate the abstract class directly → error
try:
    s = Shape()
except TypeError as e:
    print("Blocked:", e)
# Blocked: Can't instantiate abstract class Shape with abstract methods area, perimeter
💡
Two Small Rules to Remember

Any class that inherits from ABC and has at least one @abstractmethod is abstract. Abstract classes cannot be instantiated until every abstract method has a concrete implementation. Those two rules are the whole enforcement mechanism.


Section 04

Animated Diagram — The Contract and Its Implementations

Watch how one abstract Shape contract sits at the top, declaring area() and perimeter() as promises. Three concrete classes below — Circle, Rectangle, Triangle — each fulfil the promise in their own way. The interface is uniform; the implementations differ.

ABSTRACT CONTRACT  ·  MANY IMPLEMENTATIONS
ABSTRACT (ABC) class Shape(ABC): @abstractmethod def area(self): ... def perimeter(self): ... CIRCLE (Shape) def area(self): return π · r² def perimeter(self): return 2π · r contract fulfilled RECTANGLE (Shape) def area(self): return w · h def perimeter(self): return 2(w + h) contract fulfilled TRIANGLE (Shape) def area(self): return √(s·(s-a)…) def perimeter(self): return a + b + c contract fulfilled

Same two method names — area() and perimeter() — in every subclass. Three completely different formulas underneath. That's abstraction.


Section 05

Abstract Classes Cannot Be Instantiated

This is the guarantee that makes abstract classes useful. Python refuses to create an instance of an abstract class until every abstract method has a concrete implementation. Try, and you get a TypeError at construction time — before any of your code can run.

from abc import ABC, abstractmethod

class Shape(ABC):
    @abstractmethod
    def area(self):
        pass

    @abstractmethod
    def perimeter(self):
        pass


# ── Attempt 1: instantiate the abstract class directly ─────
try:
    s = Shape()
except TypeError as e:
    print("Blocked:", e)


# ── Attempt 2: subclass but forget one abstract method ─────
class HalfCircle(Shape):
    def area(self):
        return 42
    # perimeter() is still abstract — HalfCircle is still abstract too

try:
    h = HalfCircle()
except TypeError as e:
    print("Blocked:", e)


# ── Attempt 3: fully concrete — works ──────────────────────
class Square(Shape):
    def __init__(self, side):
        self.side = side
    def area(self):
        return self.side ** 2
    def perimeter(self):
        return 4 * self.side

sq = Square(5)                    # works — every abstract method is implemented
print(sq.area(), sq.perimeter())     # 25 20
OUTPUT
Blocked: Can't instantiate abstract class Shape with abstract methods area, perimeter Blocked: Can't instantiate abstract class HalfCircle with abstract method perimeter 25 20

Animated Diagram — Instantiation Attempts

Three creation attempts, three different outcomes. The abstract class itself is blocked. A half-implemented subclass is blocked. Only the fully-concrete subclass survives.

INSTANTIATION  ·  ABSTRACT BLOCKED · CONCRETE ALLOWED
ATTEMPT 1 Shape() the abstract class itself ATTEMPT 2 HalfCircle() missing perimeter() ATTEMPT 3 Square(5) both methods implemented PYTHON'S ABC GATE checks every @abstractmethod is implemented before allowing instantiation contract enforcement TypeError missing area, perimeter TypeError missing perimeter() Square(5) contract met object created

Every Shape(...) call passes through Python's ABC gate. If any abstract method is unimplemented, the object is never created.


Section 06

Abstract + Concrete Methods Together

An abstract class isn't only abstract methods. It can also contain fully implemented methods that every subclass inherits for free. That's how you share common behaviour while still enforcing the contract.

from abc import ABC, abstractmethod

class PaymentMethod(ABC):
    def __init__(self, holder):
        self.holder = holder                # concrete — shared setup

    # ── ABSTRACT — every subclass must implement ──
    @abstractmethod
    def authenticate(self):
        pass

    @abstractmethod
    def charge(self, amount):
        pass

    # ── CONCRETE — shared, but subclasses can still override ──
    def log_transaction(self, action, amount):
        print(f"  [log] {self.holder}: {action} of ${amount}")


class CreditCard(PaymentMethod):
    def __init__(self, holder, number):
        super().__init__(holder)          # reuse the base setup
        self.number = number

    def authenticate(self):
        print(f"Verifying card ****{self.number[-4:]}")
        return True

    def charge(self, amount):
        print(f"Charging ${amount} to card")
        self.log_transaction("charge", amount)   # inherited from base — no duplication
🔑
The Split You Want

Mark methods abstract when every subclass genuinely differs — the algorithm itself changes per subclass. Leave methods concrete when they'd be copy-pasted identically into every subclass. That gives you enforcement where it matters and reuse where it's free.


Section 07

Animated Diagram — Polymorphism Through Abstraction

This is where abstraction pays off. A single function process(payment, amount) accepts any object that satisfies the PaymentMethod contract. Three concrete objects flow through it. Each one dispatches to its own charge() implementation — same interface, different behaviour, zero if/else.

ONE INTERFACE  ·  MANY IMPLEMENTATIONS  ·  ZERO BRANCHES
CreditCard alice card ****3456 UPIPayment bob bob@upi CryptoWallet chandra 0xabc... GENERIC FUNCTION def process( payment: PaymentMethod ): payment.charge(x) charge() card gateway API charge() UPI debit charge() blockchain broadcast

The generic function calls charge() once. Each payment type routes to its own implementation. Adding a fourth method means writing a new class — nothing in process() changes.


Section 08

Practical Example 1 — Payment Gateway With Multiple Methods

from abc import ABC, abstractmethod


class PaymentMethod(ABC):
    """Abstract base for every payment method."""

    def __init__(self, holder):
        self.holder = holder

    # ══ ABSTRACT — every payment method must implement ═══════
    @abstractmethod
    def authenticate(self) -> bool:
        """Verify the payer's identity."""
        ...

    @abstractmethod
    def charge(self, amount) -> None:
        """Deduct the amount from the payer."""
        ...

    @abstractmethod
    def refund(self, amount) -> None:
        """Return the amount to the payer."""
        ...

    # ══ CONCRETE — inherited by every payment method ═════════
    def log(self, action, amount):
        print(f"  [log] {self.holder}: {action} of {amount}")


class CreditCard(PaymentMethod):
    def __init__(self, holder, card_number):
        super().__init__(holder)
        self.card_number = card_number

    def authenticate(self):
        print(f"Verifying card ****{self.card_number[-4:]}")
        return True

    def charge(self, amount):
        print(f"Card charged ${amount}")
        self.log("card charge", amount)

    def refund(self, amount):
        print(f"Card refunded ${amount}")
        self.log("card refund", amount)


class UPIPayment(PaymentMethod):
    def __init__(self, holder, upi_id):
        super().__init__(holder)
        self.upi_id = upi_id

    def authenticate(self):
        print(f"UPI PIN check for {self.upi_id}")
        return True

    def charge(self, amount):
        print(f"Debiting ₹{amount} via UPI")
        self.log("UPI charge", amount)

    def refund(self, amount):
        print(f"Refunding ₹{amount} to UPI")
        self.log("UPI refund", amount)


class CryptoWallet(PaymentMethod):
    def __init__(self, holder, wallet_address):
        super().__init__(holder)
        self.wallet_address = wallet_address

    def authenticate(self):
        print("Verifying wallet signature")
        return True

    def charge(self, amount):
        print(f"Broadcasting {amount} to blockchain")
        self.log("crypto charge", amount)

    def refund(self, amount):
        print(f"Broadcasting refund of {amount}")
        self.log("crypto refund", amount)


# ══ Generic processor — does NOT know which method it holds ══
def process_purchase(payment: PaymentMethod, amount):
    print(f"\n▶ Processing for {payment.holder}:")
    if payment.authenticate():
        payment.charge(amount)


# One list of different concrete types, one uniform loop
payments = [
    CreditCard("Alice",   "4111111111113456"),
    UPIPayment("Bob",     "bob@upi"),
    CryptoWallet("Chandra", "0xabc123"),
]

for p in payments:
    process_purchase(p, 500)


# Trying to instantiate the abstract base is blocked
try:
    PaymentMethod("Anyone")
except TypeError as e:
    print("\nBlocked:", e)
OUTPUT
▶ Processing for Alice: Verifying card ****3456 Card charged $500 [log] Alice: card charge of 500 ▶ Processing for Bob: UPI PIN check for bob@upi Debiting ₹500 via UPI [log] Bob: UPI charge of 500 ▶ Processing for Chandra: Verifying wallet signature Broadcasting 500 to blockchain [log] Chandra: crypto charge of 500 Blocked: Can't instantiate abstract class PaymentMethod with abstract methods authenticate, charge, refund
🏆
The Real Payoff

process_purchase has no if payment_type == anywhere. It receives "some PaymentMethod" and trusts the interface. Adding ApplePay next month means writing one new subclass and adding it to the list — nothing else changes. That's abstraction earning its keep.


Section 09

Practical Example 2 — Shape Hierarchy

from abc import ABC, abstractmethod
import math


class Shape(ABC):
    @abstractmethod
    def area(self):
        ...

    @abstractmethod
    def perimeter(self):
        ...

    # Concrete — shared by every shape
    def describe(self):
        name = type(self).__name__
        print(f"{name:10s}  area={self.area():7.2f}  perimeter={self.perimeter():7.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, w, h):
        self.w, self.h = w, h
    def area(self):
        return self.w * self.h
    def perimeter(self):
        return 2 * (self.w + self.h)


class Triangle(Shape):
    def __init__(self, a, b, c):
        self.a, self.b, self.c = a, b, c
    def area(self):
        s = (self.a + self.b + self.c) / 2
        return math.sqrt(s * (s - self.a) * (s - self.b) * (s - self.c))
    def perimeter(self):
        return self.a + self.b + self.c


shapes: list[Shape] = [
    Circle(5),
    Rectangle(4, 6),
    Triangle(3, 4, 5),
]

for s in shapes:
    s.describe()   # inherited concrete method, uses each shape's own area/perimeter
OUTPUT
Circle area= 78.54 perimeter= 31.42 Rectangle area= 24.00 perimeter= 20.00 Triangle area= 6.00 perimeter= 12.00

Section 10

Abstraction vs Encapsulation — Two Different Ideas

Beginners often confuse these two. They're both about "hiding," but they hide different things.

🏘️ Abstraction
Hides complexity: what the class does vs how
Tool: ABC + @abstractmethod
Answers: "what interface must every X follow?"
Enforced at the class-design level
Example: Shape declares area(); Circle implements it
🔒 Encapsulation
Hides data: private state behind a public API
Tool: _x, __x naming + @property
Answers: "who can read or change this attribute?"
Enforced at the attribute-access level
Example: _balance can only be set via validated deposit()
🔮
Both Together in Real Code

A well-designed class typically uses both. Abstraction defines the class's contract with the outside world (public methods every subclass must implement). Encapsulation hides its data (private attributes accessed only through those public methods). One controls the shape of your class; the other controls the doors and locks.


Section 11

Common Mistakes (and Fixes)

MistakeWhat Goes WrongFix
Forgetting to inherit from ABC @abstractmethod is silently ignored — the class is instantiable Make sure the base class inherits from abc.ABC
Forgetting the @abstractmethod decorator The method is just a regular method; subclasses aren't forced to override Add @abstractmethod above every method that must be overridden
Trying to instantiate the abstract base TypeError at construction — often surprising in test code Never call PaymentMethod(...); instantiate a concrete subclass
Missing one abstract method in a subclass The subclass is still abstract; calling Subclass() raises TypeError Implement every abstract method — or leave the subclass abstract on purpose
Changing an abstract method's signature Subclasses that used the old signature break; polymorphism collapses Keep signatures stable. If new args are needed, give them defaults
Overusing abstract classes for one implementation Adds ceremony without benefit — one child, one boilerplate parent Only introduce an abstract base when you have (or genuinely expect) two+ implementations
Confusing abstract with "not implemented yet" Callers assume the method exists; runtime NotImplementedError everywhere Abstract methods are a design signal ("subclass must fill this in"), not a TODO marker

Section 12

Quick Reference

TaskSyntaxNotes
Import ABC supportfrom abc import ABC, abstractmethodStandard library, no install
Declare abstract classclass Shape(ABC):Inherits from ABC
Declare abstract method@abstractmethod + def m(self): ...Body can be pass or ...
Concrete method in ABCJust define normally — no decoratorInherited by every subclass
Implement in subclassOverride with the same signatureAll abstract methods must be present
Instantiate concrete classobj = ConcreteClass(...)Works once every abstract method is implemented
Check if a class is abstractClassName.__abstractmethods__Frozenset of unimplemented names
Type-hint against interfacedef f(p: PaymentMethod):Callers can pass any subclass
Common patternConcrete method calls abstract methodsTemplate Method pattern

Section 13

Golden Rules

🏦 Abstraction — Non-Negotiable Rules
1
Abstraction is about separating what from how. The abstract class defines the contract (what every subclass must be able to do). Each concrete class defines the implementation (how it does it).
2
To make a class abstract, inherit from abc.ABC and decorate at least one method with @abstractmethod. Both are required — neither alone works.
3
Abstract classes cannot be instantiated directly. Trying raises TypeError. This is the enforcement mechanism, and it's the point.
4
A subclass must implement every abstract method before it can be instantiated. Miss one and the subclass stays abstract too. Add all of them, and you unlock construction.
5
Abstract classes can contain concrete methods as well. Use them for behaviour that's identical across every subclass — like a shared log(), describe(), or setup routine. Reuse everything you can; abstract only what genuinely differs.
6
Write functions to accept the abstract type, not concrete types: def process(p: PaymentMethod), not def process(p: CreditCard). Any subclass slots in automatically — that's polymorphism through abstraction.
7
Only introduce an abstract base when you have two or more real implementations (or will very soon). Adding an ABC for a single subclass is ceremony with no payoff.
8
Keep abstract method signatures stable. Changing them breaks every subclass silently. If you need new parameters, add them with defaults so old subclasses keep working.
9
Abstraction ≠ encapsulation. Abstraction hides complexity behind a contract. Encapsulation hides data behind an API. A good class uses both. Confusing them leads to over-designed hierarchies and under-protected data.
10
When in doubt, ask: "Would every subclass write this method exactly the same way?" If yes → put it in the base as a concrete method. If every subclass does it differently → mark it @abstractmethod. That single question sorts the two apart.