Python · SQL · Web Dev · Java · AI/ML tracks launching soon — your one platform for all of IT
Intermediate+150 XP

Abstract Base Classes and Interfaces

Enforcing a contract across subclasses with the abc module — how larger Python codebases stay consistent.

30 min August 2026
// Part 01 — The Problem ABCs Solve

What Happens Without Any Enforced Contract

Imagine a base class PaymentProcessor that every payment provider (Stripe, PayPal, a bank transfer integration) is meant to subclass, each implementing its own charge() method. Nothing in plain Python stops someone from writing a subclass that simply forgets to implement charge() — the mistake is only discovered at runtime, the moment the missing method is actually called, potentially in production.

A silent, easy-to-make mistake with a plain base class
class PaymentProcessor:
    def charge(self, amount):
        raise NotImplementedError

class StripeProcessor(PaymentProcessor):
    def charge(self, amount):
        return f"Charged ${amount} via Stripe"

class PayPalProcessor(PaymentProcessor):
    pass   # oops — forgot to override charge()! Nothing stops this from being defined.

p = PayPalProcessor()
p.charge(50)     # NotImplementedError — but only discovered when this line actually runs

The raise NotImplementedError pattern communicates intent to a human reader, but it provides zero enforcement — PayPalProcessor was allowed to be defined, instantiated, and passed around the codebase perfectly normally, with the bug lying dormant until the exact line that calls charge() executes. Abstract base classes fix exactly this: they move the failure from "runtime, whenever this method happens to be called" to "the moment the incomplete subclass is instantiated at all."

// Part 02 — The abc Module

ABC and @abstractmethod

Python's standard library abc module provides ABC (a base class to inherit from) and the @abstractmethod decorator, which together turn the informal NotImplementedError pattern into something the interpreter actively enforces.

The same example, properly enforced
from abc import ABC, abstractmethod

class PaymentProcessor(ABC):
    @abstractmethod
    def charge(self, amount):
        ...

class StripeProcessor(PaymentProcessor):
    def charge(self, amount):
        return f"Charged ${amount} via Stripe"

class PayPalProcessor(PaymentProcessor):
    pass   # forgot to override charge()

s = StripeProcessor()     # fine — every abstract method is implemented
p = PayPalProcessor()     # TypeError, IMMEDIATELY, at instantiation:
# TypeError: Can't instantiate abstract class PayPalProcessor with abstract method charge

The critical difference: PayPalProcessor can no longer even be constructed until every abstract method inherited from PaymentProcessor has a real implementation. The bug is caught the instant the incomplete class is used, not buried until the specific missing method happens to be called — which, for a rarely-exercised code path, could otherwise take months to surface.

An ABC itself can never be instantiated directly, even with every method defined

Attempting to instantiate the abstract base class itself
pp = PaymentProcessor()
# TypeError: Can't instantiate abstract class PaymentProcessor with abstract method charge
# This fails even though PaymentProcessor "defines" charge — because it's abstract
// Part 03 — Abstract Properties and Multiple Requirements

An ABC Can Require More Than Just Methods

An abstract base class can require multiple abstract methods, and even abstract properties — any subclass must satisfy every one of them before it becomes instantiable.

A richer contract with multiple requirements
from abc import ABC, abstractmethod

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

    @abstractmethod
    def perimeter(self):
        ...

class Rectangle(Shape):
    def __init__(self, width, height):
        self.width, self.height = width, height

    @property
    def area(self):
        return self.width * self.height

    def perimeter(self):
        return 2 * (self.width + self.height)

# Circle(Shape) that only implements area(), forgetting perimeter(),
# would still raise TypeError at instantiation — EVERY abstract member must be covered.
🎯 Pro Tip
This is genuinely valuable in larger codebases with several engineers implementing different subclasses of the same base over time — a plugin system, several data-source adapters, several payment providers. The ABC acts as living, enforced documentation of exactly what a valid implementation must provide, catching an incomplete implementation immediately rather than leaving it to be discovered by whoever happens to exercise the missing piece later.
// Part 04 — Duck Typing vs Formal Interfaces

Two Different Philosophies, Both Native to Python

Python is famous for duck typing — "if it walks like a duck and quacks like a duck, it's a duck": code that calls obj.quack() does not care what class obj actually is, only that it has a quack method. ABCs might look like they contradict this philosophy by introducing formal, enforced contracts — but they coexist deliberately, for different situations.

Pure duck typing — no formal contract at all
def make_it_quack(duck):
    return duck.quack()   # works on ANY object with a quack() method, no inheritance required

class RealDuck:
    def quack(self): return "Quack!"

class ToyDuck:
    def quack(self): return "Squeak-quack (it's a toy)"

make_it_quack(RealDuck())   # works
make_it_quack(ToyDuck())    # also works — no shared base class needed at all

Duck typing is the right default for most everyday Python — it is flexible and requires no upfront ceremony. ABCs earn their place specifically when you want the interpreter to actively enforce that a family of related classes all implement a required set of methods, and to fail loudly and immediately (at instantiation) if one does not — most valuable in plugin-style architectures, or any codebase where several people implement subclasses of a shared base independently over time.

A brief look ahead — typing.Protocol

Python's typing module offers a third option, Protocol, which combines aspects of both: it lets you describe a required "shape" (structural typing) without requiring explicit inheritance from anything — an object satisfies a Protocol just by having the right methods, similar to duck typing, but the shape can still be checked by a type checker like mypy ahead of time. This is covered properly in the later Type Hints and Static Typing module.

// Part 05 — Real World
💼 What This Looks Like at Work

A Plugin System at a Chicago Data Platform Company

Scenario — Data platform company, Chicago · Extensible data-source system

A platform needs to support pulling data from many different sources — S3, a REST API, a local CSV drop folder — with new source types added by different engineers over time as the company grows. The team defines an ABC to guarantee every source, whoever writes it, exposes a consistent interface the rest of the pipeline can rely on.

The contract every data source must satisfy
from abc import ABC, abstractmethod

class DataSource(ABC):
    @abstractmethod
    def connect(self):
        ...

    @abstractmethod
    def fetch_records(self):
        ...

    @abstractmethod
    def close(self):
        ...

class S3Source(DataSource):
    def connect(self):
        self.client = build_s3_client()

    def fetch_records(self):
        return self.client.list_objects()

    def close(self):
        self.client = None

# A new engineer adds RestApiSource(DataSource) but forgets close() —
# TypeError at instantiation, caught in code review / CI, not in production

Why this mattered as the team grew

The pipeline code that consumes any DataSource — running source.connect(), then source.fetch_records(), then always source.close() — can rely completely on every source implementing all three methods, without ever needing to check with hasattr() or wrap calls in try/except AttributeError. As more engineers added new source types over the following year, several genuinely did forget one method during initial development — and every single time, the mistake was caught immediately by a failing test that tried to instantiate the class, well before the code reached production.

// Part 06 — Misconceptions

Four Misconceptions About Abstract Base Classes

✕ ""raise NotImplementedError in a base method is basically the same as using abc""
It communicates intent to a human reader but enforces nothing — a subclass that forgets to override the method can still be instantiated and passed around fine, only failing later when that specific method is actually called. An ABC with @abstractmethod fails immediately at instantiation instead, catching the mistake far earlier.
✕ ""Using ABCs means you've abandoned duck typing / Python's dynamic nature""
They coexist deliberately for different situations — duck typing remains the right default for most everyday code; ABCs earn their place specifically when you want an ENFORCED, fail-fast contract across a family of related subclasses, commonly in plugin-style architectures.
✕ ""An abstract base class can be instantiated as long as you don't call the abstract method""
Python raises TypeError at INSTANTIATION time (the moment you write ClassName()), not when the abstract method is called — you cannot construct an instance of a class with any unimplemented abstract method at all, regardless of whether you intended to call it.
✕ ""@abstractmethod bodies must always be exactly 'pass' or '...'""
An abstract method CAN have a real implementation, which subclasses can optionally call via super() as a shared default/partial behaviour — @abstractmethod only enforces that subclasses provide their OWN override; it does not forbid the base method from doing something.
// Part 07 — Interview Prep

5 Interview Questions — With Complete Answers

What problem does an abstract base class solve that raise NotImplementedError alone does not?
raise NotImplementedError only fails when the specific unimplemented method is actually CALLED at runtime — a subclass that forgot to override it can still be freely instantiated and passed around the codebase until that exact call happens. ABC + @abstractmethod moves the failure to instantiation time itself: an incomplete subclass raises TypeError the moment ClassName() is written, catching the mistake far earlier, often in tests or CI rather than production.
Can you instantiate a class that inherits from ABC and defines every abstract method?
Yes — once every abstract method (and abstract property) is overridden with a concrete implementation, the subclass becomes fully instantiable like any normal class. The ABC itself, and any subclass still missing even one required member, cannot be instantiated.
How do duck typing and abstract base classes coexist in Python's design philosophy?
They serve different needs. Duck typing (relying on an object simply having the right method, with no shared base class required) is the flexible, low-ceremony default for most code. ABCs are reached for when you specifically want an enforced, fail-fast contract across a family of related subclasses — commonly plugin systems or multi-team codebases where consistency needs to be guaranteed, not just hoped for.
What happens if you try to instantiate the abstract base class itself, even if it provides implementations for all its abstract methods?
It still raises TypeError — a class is considered abstract, and therefore uninstantiable, as long as it has ANY method decorated with @abstractmethod, regardless of whether that method has a body. Abstractness is a property of the class itself, not of whether the methods happen to be implemented.
How would you require a subclass to implement a computed value, not just a method?
Stack @property above @abstractmethod on the same method (in that order) to define an abstract property — subclasses must then implement it as an actual @property returning a value, and the class remains uninstantiable until they do, exactly like an abstract method.
// Common Mistakes

ABC Mistakes Beginners Make Constantly

Forgetting to inherit from ABC
@abstractmethod has no enforcement effect at all unless the class itself also inherits from abc.ABC (or uses ABCMeta as its metaclass) — a plain class with @abstractmethod-decorated methods can be instantiated normally, silently ignoring the decorator.
Expecting an ABC to stop a subclass from adding EXTRA methods beyond the contract
ABCs only enforce a MINIMUM required set of methods — subclasses are always free to add additional methods and attributes beyond what the abstract base requires. There is no mechanism to forbid extra members.
Stacking @abstractmethod and @property in the wrong order
@property must be the OUTER (topmost) decorator, with @abstractmethod directly above the method definition — writing them in the reverse order does not correctly register the method as both abstract and a property.
Assuming ABCs provide any runtime type-checking beyond "was every abstract method implemented"
ABCs do not check method SIGNATURES (parameter names, types, or counts) — a subclass can implement charge(self, amount) as charge(self, amount, currency="USD") and satisfy the ABC just fine. For genuine signature-level checking, reach for type hints and a static checker like mypy.
// Error Library

Errors You Will Hit With Abstract Base Classes — And Exactly Why

TypeError: Can't instantiate abstract class PayPalProcessor with abstract method charge
Cause: A subclass of an ABC did not override one or more of the base class's @abstractmethod-decorated members.
Fix: Implement every abstract method (and abstract property) listed in the error — Python names exactly which ones are still missing.
TypeError: Can't instantiate abstract class PaymentProcessor with abstract method charge
Cause: Attempting to instantiate the ABC itself, not a subclass — a class remains abstract, and therefore uninstantiable, as long as it has any unimplemented @abstractmethod, regardless of whether you meant to use it directly.
Fix: Instantiate a concrete subclass instead, never the ABC itself.
TypeError: metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases
Cause: A class attempts to inherit from both ABC and another class that uses a different, incompatible custom metaclass.
Fix: This is rare in everyday code; it typically requires either dropping ABC in favour of typing.Protocol, or defining a combined metaclass that satisfies both parents — usually a sign the design should be reconsidered.

🎯 Key Takeaways

  • raise NotImplementedError alone only fails when the method is actually called; abc.ABC + @abstractmethod fails immediately at instantiation, catching incomplete subclasses far earlier.
  • A class inheriting from ABC cannot be instantiated until every @abstractmethod (and abstract @property) it declares has a concrete override in the subclass.
  • The ABC itself can never be instantiated directly, even if all its abstract methods happen to have implementations — abstractness is a property of the class, not of whether the bodies are filled in.
  • ABCs and duck typing coexist deliberately — duck typing for flexible everyday code, ABCs for enforced contracts across plugin-style or multi-team class families.
  • ABCs check WHICH methods exist, not their signatures — for real signature-level checking, pair them with type hints and mypy (covered in a later module).
  • typing.Protocol offers a structural-typing alternative that does not require explicit inheritance — worth knowing about even before its full coverage later in this track.

What comes next

Module 25 begins the Intermediate & Functional Python phase with a deep dive into *args and **kwargs — every way Python lets you pass arguments to a function.

Module 25 → *args, **kwargs and Function Arguments Deep Dive
Share

Discussion

0

Have a better approach? Found something outdated? Share it — your knowledge helps everyone learning here.

Continue with GitHub
Loading...