Abstract Base Classes

Abstract base classes let you define a required interface that subclasses must implement, turning duck typing's informal expectations into an enforced, explicit contract.

An abstract base class (ABC) declares a set of methods that any concrete subclass must implement, while preventing the abstract class itself from being instantiated directly. Python provides this through the standard library's abc module: the ABC base class (or ABCMeta metaclass) plus the @abstractmethod decorator.

Why Abstract Base Classes

Duck typing (see the Polymorphism topic) lets any object with the right methods be used interchangeably, but it offers no guarantee, and no early error, if an object is missing a required method -- the failure only appears when that method is actually called. Abstract base classes formalize the expected interface: attempting to instantiate a subclass that hasn't implemented every required method raises TypeError immediately, at construction time, rather than later at the point of use.

Basic Syntax: ABC and @abstractmethod

Import ABC and abstractmethod from the standard library's abc module. Inherit from ABC, and decorate any method that subclasses must provide with @abstractmethod. The method body is typically just a docstring, ..., or pass -- it exists to declare the required signature, not to run.

from abc import ABC, abstractmethod


class Shape(ABC):
    @abstractmethod
    def area(self) -> float:
        """Return the shape's area."""

    @abstractmethod
    def perimeter(self) -> float:
        """Return the shape's perimeter."""

Abstract Classes Cannot Be Instantiated

Attempting to construct Shape() directly raises TypeError, because Shape still has unimplemented abstract methods. This check happens the moment you call the constructor, giving an immediate, clear error rather than a confusing failure somewhere downstream.

try:
    s = Shape()
except TypeError as e:
    print("caught:", e)
# caught: Can't instantiate abstract class Shape with abstract methods area, perimeter

Implementing the Abstract Interface

A subclass becomes concrete (instantiable) once it overrides every abstract method inherited from its ABC. Any abstract method left unimplemented keeps the subclass abstract too, and it also cannot be instantiated.

class Circle(Shape):
    def __init__(self, radius: float) -> None:
        self.radius = radius

    def area(self) -> float:
        return 3.14159 * self.radius ** 2

    def perimeter(self) -> float:
        return 2 * 3.14159 * self.radius


c = Circle(3)         # works -- both abstract methods implemented
print(c.area())
class Incomplete(Shape):
    def area(self) -> float:
        return 0.0
    # perimeter() not implemented

try:
    Incomplete()
except TypeError as e:
    print("caught:", e)  # still abstract -- perimeter is missing

Abstract Classes Can Still Have Concrete Methods

An ABC isn't limited to abstract methods -- it can mix abstract methods (must be overridden) with ordinary concrete methods (inherited as-is, usable immediately), letting a base class provide real shared behavior alongside the parts it requires subclasses to fill in.

from abc import ABC, abstractmethod


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

    def describe(self) -> str:  # concrete -- shared by every subclass unchanged
        return f"{type(self).__name__} with area {self.area():.2f}"

Abstract Properties

@abstractmethod can be combined with @property to require subclasses to provide a specific attribute-like value, not just a method. Stack @property above @abstractmethod.

from abc import ABC, abstractmethod


class Employee(ABC):
    @property
    @abstractmethod
    def role(self) -> str: ...


class Engineer(Employee):
    @property
    def role(self) -> str:
        return "Engineer"


print(Engineer().role)  # 'Engineer'

ABCMeta: the Metaclass Behind ABC

ABC is simply a convenience base class built on ABCMeta, the metaclass that actually implements the enforcement (tracking which methods are abstract and blocking instantiation while any remain). You can use ABCMeta directly via metaclass=ABCMeta instead of inheriting ABC; in practice, inheriting ABC is simpler and is what almost all code uses.

from abc import ABCMeta, abstractmethod


class Shape(metaclass=ABCMeta):  # equivalent to class Shape(ABC):
    @abstractmethod
    def area(self) -> float: ...

Virtual Subclasses: register()

An ABC can also recognize a class as a 'virtual subclass' via .register(), without that class actually inheriting from the ABC. A registered virtual subclass passes isinstance()/issubclass() checks against the ABC, but does NOT get the instantiation enforcement (it isn't required to implement the abstract methods, and MRO/super() do not include the ABC). This is a looser, opt-in relationship, useful mainly for making preexisting or unrelated classes recognized under an ABC's umbrella for type checks.

register() is a lighter-weight, non-enforcing relationship -- it affects isinstance/issubclass results only, not construction-time checking or attribute inheritance.

from abc import ABC, abstractmethod


class Drawable(ABC):
    @abstractmethod
    def draw(self) -> None: ...


class LegacyWidget:  # doesn't inherit from Drawable at all
    def draw(self) -> None:
        print("drawing legacy widget")


Drawable.register(LegacyWidget)
print(issubclass(LegacyWidget, Drawable))  # True -- recognized, despite no inheritance
print(isinstance(LegacyWidget(), Drawable))  # True

Practical Usage: Defining a Plugin Interface

ABCs are common for defining a stable interface that multiple independent implementations must satisfy -- e.g. a family of data exporters, storage backends, or payment processors that a larger system treats interchangeably.

from abc import ABC, abstractmethod


class Exporter(ABC):
    @abstractmethod
    def export(self, data: dict) -> str: ...


class JSONExporter(Exporter):
    def export(self, data: dict) -> str:
        import json
        return json.dumps(data)


class CSVExporter(Exporter):
    def export(self, data: dict) -> str:
        return ",".join(f"{k}={v}" for k, v in data.items())


def run_export(exporter: Exporter, data: dict) -> str:
    return exporter.export(data)  # works with any concrete Exporter subclass

The Standard Library's Own ABCs: collections.abc

Python's standard library defines its own abstract base classes for container/iteration behavior in collections.abc (e.g. Iterable, Sequence, Mapping) -- these describe the protocols the built-in list, dict, and str already satisfy, and can be used both for isinstance() checks and as base classes when building a fully custom container.

from collections.abc import Iterable

print(isinstance([1, 2, 3], Iterable))  # True
print(isinstance(42, Iterable))          # False

Edge Cases

The abstract-method check only fires at instantiation of a subclass, not when the abstract class itself is merely defined or subclassed further without instantiating -- an intermediate subclass can leave methods unimplemented and remain abstract, deferring the requirement to whichever subclass is finally instantiated. Also, __init_subclass__ or metaclass conflicts can arise if you try to combine ABCMeta with another custom metaclass; keep the class hierarchy simple unless you specifically need that.

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


class PartiallyImplemented(Shape):
    pass  # still abstract -- fine to define, just can't be instantiated


class FullyImplemented(PartiallyImplemented):
    def area(self) -> float:
        return 1.0


FullyImplemented()  # works -- the requirement was satisfied further down the chain

Key Takeaways

ABC (from the abc module) plus @abstractmethod let you require subclasses to implement specific methods, enforced at instantiation time.

A class with any unimplemented abstract method cannot be instantiated -- TypeError is raised immediately, not later at the point of use.

An ABC can mix required abstract methods with ordinary concrete methods that subclasses inherit unchanged.

ABC is a convenience wrapper over the ABCMeta metaclass, which does the actual enforcement.

register() creates a looser 'virtual subclass' relationship recognized by isinstance()/issubclass() but without inheritance or enforcement.

class Shape: # forgot ABC @abstractmethod def area(self):... Shape() # no error -- abstractmethod alone does nothing without ABCMeta from abc import ABC, abstractmethod class Shape(ABC): @abstractmethod def area(self):... Shape() # TypeError -- properly enforced

@abstractmethod only has an effect when the class's metaclass is ABCMeta (via inheriting ABC or setting metaclass=ABCMeta); on an ordinary class it's just a decorator with no enforcement.

class Circle(Shape): def area(self): return 3.14 * self.r ** 2 # perimeter still abstract, but code assumes Circle() will work class Circle(Shape): def area(self): return 3.14 * self.r ** 2 def perimeter(self): return 2 * 3.14 * self.r # all abstract methods now implemented

A subclass remains abstract (and un-instantiable) until every abstract method inherited from the ABC is overridden, not just some of them.

Drawable.register(LegacyWidget) # assuming LegacyWidget now has Drawable's concrete methods, or super() reaches Drawable class LegacyWidget: def draw(self): ...

register() creates a virtual subclass relationship recognized only by isinstance()/issubclass() checks -- it does not add the ABC to the class's actual MRO, so concrete methods and super() calls to the ABC are not available.

What happens if you try to instantiate a class that inherits from ABC and still has an unimplemented @abstractmethod?

TypeError is raised immediately at the point of instantiation, listing the still-unimplemented abstract method names, rather than allowing construction to succeed and failing later when the method is called.

Can an abstract base class define ordinary, non-abstract methods that subclasses inherit as-is?

Yes -- an ABC can freely mix @abstractmethod-decorated methods (which subclasses must override) with regular concrete methods (inherited unchanged), letting the base class provide real shared behavior alongside its required interface.

What is the relationship between ABC and ABCMeta?

ABC is a convenience base class that already has ABCMeta as its metaclass; inheriting from ABC is equivalent to writing class Foo(metaclass=ABCMeta):, and ABCMeta is what actually implements the abstract-method tracking and instantiation blocking.

How does register()-based virtual subclassing differ from actually inheriting from an abstract base class?

register() only makes isinstance()/issubclass() report True for the registered class; it does not add the ABC to the registered class's MRO, does not require the registered class to implement any abstract methods, and gives no access to the ABC's concrete methods via inheritance or super().

If Shape is an ABC with an abstract method area(), and PartiallyImplemented(Shape) doesn't implement area(), is it valid to define PartiallyImplemented as a class, and can it be instantiated?

Defining it is fine -- PartiallyImplemented simply remains abstract itself; it cannot be instantiated until some subclass in the chain finally implements area().

Why do abstract base classes provide a stronger guarantee than plain duck typing for defining a shared interface?

Because they enforce the interface at instantiation time (an incomplete implementation raises TypeError immediately), whereas duck typing only fails later, at the moment a missing method is actually called, giving no upfront guarantee.

How would you require subclasses to provide a specific read-only attribute (not a callable method) via an ABC?

Stack @property above @abstractmethod on the method in the ABC (in that order); subclasses must then provide their own @property with the same name to become concrete.

Write an abstract base class PaymentProcessor with an abstract method process(amount: float) -> str, and two concrete subclasses CreditCardProcessor and PayPalProcessor implementing it differently.

from abc import ABC, abstractmethod class PaymentProcessor(ABC): @abstractmethod def process(self, amount: float) -> str: ... class CreditCardProcessor(PaymentProcessor): def process(self, amount: float) -> str: return f"Charged ${amount:.2f} to credit card" class PayPalProcessor(PaymentProcessor): def process(self, amount: float) -> str: return f"Charged ${amount:.2f} via PayPal" for proc in [CreditCardProcessor(), PayPalProcessor()]: print(proc.process(19.99))

Both subclasses implement process(), so both can be instantiated; PaymentProcessor() itself would raise TypeError since it has an unimplemented abstract method.

What does this print, and why can't Mid be instantiated even though Base isn't instantiated directly here?

'abstract' -- Mid inherits run() as still-abstract because it never provides its own implementation, so Mid remains an abstract class itself and instantiating it raises TypeError, caught by the except clause.

Write an abstract base class Notifier with an abstract property channel (a str) and a concrete method announce(msg) that uses self.channel. Implement one subclass, EmailNotifier.

from abc import ABC, abstractmethod class Notifier(ABC): @property @abstractmethod def channel(self) -> str:... def announce(self, msg: str) -> str: return f"[{self.channel}] {msg}" class EmailNotifier(Notifier): @property def channel(self) -> str: return "email" print(EmailNotifier().announce("Build succeeded")) # [email] Build succeeded channel is an abstract property that EmailNotifier must implement to become concrete; announce() is a concrete method defined once on Notifier that all subclasses inherit and reuse via self.channel.

from abc import ABC, abstractmethod

class Base(ABC):
    @abstractmethod
    def run(self): ...

class Mid(Base):
    pass

try:
    Mid()
except TypeError:
    print("abstract")