Skip to content
Home Python Abstract Base Classes in Python: Enforce Contracts, Not Just Code

Abstract Base Classes in Python: Enforce Contracts, Not Just Code

Where developers are forged. · Structured learning · Free forever.
📍 Part of: Advanced Python → Topic 12 of 17
Abstract Base Classes in Python let you enforce method contracts across subclasses.
⚙️ Intermediate — basic Python knowledge assumed
In this tutorial, you'll learn
Abstract Base Classes in Python let you enforce method contracts across subclasses.
  • ABCs enforce method contracts at instantiation time — TypeError fires the moment a broken subclass is created, before any business logic, any database write, or any customer-facing operation executes.
  • @abstractmethod only has enforcement power when the class inherits from ABC or uses metaclass=ABCMeta. Without that inheritance, the decorator is purely decorative — it sets a flag but Python never checks it.
  • Mixing abstract methods, abstract properties, and concrete methods in the same ABC is the standard production pattern. Abstract for what subclasses must implement. Concrete for shared utilities all subclasses inherit for free.
✦ Plain-English analogy ✦ Real code with output ✦ Interview questions
Quick Answer
  • ABCs enforce method contracts at instantiation time — TypeError fires before any business logic runs, not buried in a production log at 2 AM
  • @abstractmethod only works when your class inherits from ABC or uses metaclass=ABCMeta — without that inheritance, the decorator is decorative, not functional
  • ABCs can mix abstract and concrete methods — use concrete for shared utilities, abstract for subclass-specific logic that must exist
  • register() bypasses abstract method enforcement entirely — it is a type tag for isinstance() checks, not a validation mechanism
  • Without ABCs, missing methods silently return None until called at runtime — the nastiest class of Python bugs because nothing crashes, everything just quietly does the wrong thing
  • Use ABCs for frameworks, plugins, and team contracts where you cannot control or review who writes subclasses
🚨 START HERE
ABC Implementation Quick Diagnosis
Symptom-to-fix commands for ABC-related failures in Python projects.
🟡TypeError on instantiation claiming abstract methods are not implemented — but you are sure you implemented them
Immediate ActionPrint the exact set of abstract methods the ABC requires, then compare against the subclass methods.
Commands
python -c "from your_module import YourABC; print(YourABC.__abstractmethods__)"
python -c "from your_module import YourSubclass; print([m for m in dir(YourSubclass) if not m.startswith('_')])"
Fix NowCompare the two sets. Look for typos, casing differences, or missing @property implementations. The method name must match exactly — charge vs _charge is a miss.
🟡@abstractmethod is not enforcing anything — subclass with missing methods instantiates without error
Immediate ActionVerify the base class actually inherits from ABC.
Commands
python -c "from your_module import YourBase; print(type(YourBase)); print(YourBase.__mro__)"
grep -n 'class.*ABC\|ABCMeta\|from abc' your_module.py
Fix NowIf ABC does not appear in the MRO, the base class is a regular class. Add from abc import ABC, abstractmethod and change the class declaration to class YourBase(ABC):.
🟡isinstance() returns True for a class that does not implement the required methods
Immediate ActionCheck whether the class was registered as a virtual subclass rather than inheriting from the ABC.
Commands
python -c "from your_module import YourABC, SuspectClass; print(issubclass(SuspectClass, YourABC)); print(SuspectClass in YourABC.__subclasses__())"
grep -n 'register' your_module.py
Fix NowIf issubclass() returns True but __subclasses__() does not include the class, it was registered — not inherited. register() does not enforce abstract methods. Write a test that calls each abstract method on the registered class instance.
Production IncidentPayment Processor Returns None Instead of True — $47K in Uncharged Orders Over 3 DaysA fintech startup shipped a new CryptoProcessor subclass that omitted the charge() method. The base class returned None silently for every charge attempt, skipping payment collection for 72 hours across 1,400 orders.
SymptomRevenue reconciliation on Monday morning showed a $47K discrepancy between orders placed and payments collected over the weekend. No exceptions in the logs. No error alerts. No crash reports. The system ran perfectly — it just did not charge anyone. Every CryptoProcessor.charge() call returned None, which the calling code interpreted as a falsy result and silently skipped the payment confirmation step.
AssumptionThe engineering team spent the first two days debugging network connectivity and API credentials for the crypto payment gateway. They assumed the gateway was down, rate-limiting, or returning error codes that were being swallowed. The gateway was fine — it was never called.
Root causeThe PaymentProcessor base class was a regular Python class — not an ABC. It defined charge() as a method with pass in the body, which returns None implicitly. The CryptoProcessor subclass implemented refund() and get_balance() but forgot to implement charge(). Python instantiated CryptoProcessor without complaint. When the order processing pipeline called processor.charge(amount), Python resolved the method via the MRO and found it on the base class. The base class method ran, did nothing, and returned None. The calling code used if result: to check the charge outcome — None is falsy in Python, so the conditional evaluated to False, and the payment confirmation was silently skipped. No TypeError, no AttributeError, no log entry. The order was marked as placed but uncharged.
Fix1. Converted PaymentProcessor from a regular class to an ABC with @abstractmethod on charge(), refund(), and get_balance(). Any subclass that omits any of these methods now raises TypeError at instantiation time — before any order processing code can execute. 2. Added a CI test that imports every PaymentProcessor subclass module and attempts to instantiate each one. If any instantiation raises TypeError, the build fails with a clear message naming the missing method. 3. Added return type annotations requiring -> bool on all charge() and refund() methods, enforced by mypy in strict mode. A return type of None would now be caught by the type checker before merge. 4. Added a runtime invariant check in the order pipeline: if processor.charge(amount) returns anything other than True or False, raise a RuntimeError immediately rather than interpreting None as falsy. 5. Added a linting rule using pylint's abstract-class-instantiated check to catch missing implementations during code review, before the code reaches CI.
Key Lesson
Returning None silently is one of the nastiest failure modes in Python. It passes truthiness checks as falsy, it does not raise an exception, and it produces no log output. The code runs to completion without any signal that something went wrong.ABCs catch missing methods at instantiation time — the earliest possible moment — before any business logic, any database write, or any external API call executes.Always enforce contracts with ABCs in payment processing, authentication, data pipeline, and any other code path where a silent failure causes revenue loss, data corruption, or security exposure.A base class that defines a method with pass is not a contract — it is a trap. It looks like an interface but provides no enforcement. ABCs are the mechanism Python gives you to make the contract real.
Production Debug GuideCommon symptoms when ABCs are misused or missing in production Python systems.
TypeError: Can't instantiate abstract class X without an implementation for abstract method YThe subclass has not implemented all @abstractmethod methods defined in the ABC. This is ABCs working correctly — the error is telling you exactly what is missing. Check the full list of unimplemented methods by printing MyBaseClass.__abstractmethods__. Implement every method in that set. The method names must match exactly — including casing.
Class instantiates without error despite having @abstractmethod decorators on the base class — no TypeError raisedThe base class does not inherit from ABC and does not use metaclass=ABCMeta. Without this inheritance, @abstractmethod is just a regular decorator that sets a flag on the function object — it has no enforcement power whatsoever. Fix: change class MyBase: to class MyBase(ABC): and add from abc import ABC, abstractmethod at the top of the file.
isinstance(obj, MyABC) returns True but calling a method that should exist on the object raises AttributeErrorThe class was registered as a virtual subclass via MyABC.register(TheClass). Registration makes isinstance() return True but does not verify or enforce that the registered class implements any abstract methods. Manually verify that the registered class has every method defined in the ABC. Write a test that calls each abstract method on an instance of the registered class.
Subclass implements what looks like every abstract method but TypeError still fires on instantiation, claiming a method is missingCheck for three things in order: (1) Method name typos — process_payment vs process_payments will not be caught by anything except an exact string match. (2) Forgetting to implement an abstract @property — properties defined with @property and @abstractmethod must be overridden with a @property in the subclass, not a regular method. (3) Forgetting that @abstractmethod must be the innermost decorator — @abstractmethod must appear directly above the def line, below @property or @classmethod or @staticmethod.
A method call on a subclass returns None unexpectedly even though the subclass appears to override the methodThe subclass method may have a different signature or name from the abstract method, causing Python to resolve the call to the base class method via the MRO. Print type(obj).__mro__ to see the resolution order. Call inspect.getmembers(obj, predicate=inspect.ismethod) to see which methods are actually bound to the instance. Verify the method name and signature match the ABC definition exactly.

Most Python developers spend their early years writing classes that work by convention — they just hope their teammates implement the right methods. That works fine until a junior developer on your team ships a new PaymentProcessor subclass that forgets to implement the charge() method, and you only find out at 2 AM when a customer's payment silently returns None instead of processing. Abstract Base Classes exist precisely to prevent that 2 AM call.

The problem ABCs solve is about enforcing structure at the right time. Without them, Python is a very trusting language — it will not complain that your class is missing a critical method until the exact moment that method gets called in running production code. By then, a transaction may have already committed, an order may have been confirmed, or a report may have been sent with missing data. ABCs shift that error to the moment someone tries to instantiate the class, so broken contracts are caught immediately — during development, during CI, during import — not buried in production logs three days after the broken code shipped.

By the end of this article you will understand why ABCs exist and what specific failure mode they prevent, how to design a real contract using the abc module with abstract methods, abstract properties, and concrete shared utilities, when to reach for ABCs versus duck typing versus plain inheritance, and the subtle gotchas — including register() and missing ABC inheritance — that trip up experienced developers regularly. You will walk away able to use ABCs confidently in any professional codebase and able to explain the decision clearly in a code review or a system design interview.

Why ABCs Exist — The Problem They Solve That Nothing Else Does

Python is a dynamically typed language with no compile step. This means there is no compiler to check that your class implements all the methods it is supposed to before the code runs. The check happens at runtime — and specifically, at the moment the missing method is called, not when the object is created.

This creates a specific failure mode that ABCs are designed to prevent. Consider a base class PaymentProcessor with a method charge(). A developer writes a subclass CryptoProcessor and forgets to implement charge(). Without ABCs, Python instantiates CryptoProcessor without complaint. The missing charge() method is not discovered until a customer's order triggers processor.charge(amount) in production — potentially days or weeks after the code was deployed.

Worse, if the base class has a default implementation of charge() that returns None or does nothing, the method call succeeds — it just does the wrong thing. No exception, no error, no log entry. The code silently skips the payment. This is the failure mode that caused the $47K incident described above.

ABCs solve this by shifting the enforcement to instantiation time. When a class inherits from ABC and marks methods with @abstractmethod, Python checks at the moment you call CryptoProcessor() whether all abstract methods have been implemented. If any are missing, Python raises TypeError immediately — before the object exists, before any method is called, before any business logic executes. The error message names the exact missing method.

This is not a minor convenience. It is the difference between catching a bug during development or CI and discovering it through a revenue reconciliation report three days after deployment.

io/thecodeforge/abc/why_abcs_exist.py · PYTHON
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374
# ============================================================
# WITHOUT ABC: Missing method discovered at call time (or never)
# ============================================================

class PaymentProcessorUnsafe:
    """Regular base class — no enforcement."""

    def charge(self, amount: float) -> bool:
        # This default implementation is a TRAP.
        # It looks like an interface but returns None silently.
        pass

    def refund(self, amount: float) -> bool:
        pass


class CryptoProcessorBroken(PaymentProcessorUnsafe):
    """Developer forgot to implement charge(). Python does not care."""

    def refund(self, amount: float) -> bool:
        print(f"Refunding ${amount} in crypto")
        return True
    # charge() is missing — but this class instantiates fine.


broken = CryptoProcessorBroken()  # No error. No warning. Nothing.
result = broken.charge(99.99)     # Calls base class method. Returns None.
print(f"Charge result: {result}")  # None
print(f"Truthiness:    {bool(result)}")  # False

# In production: if result: send_confirmation() — skipped.
# The customer's order goes through. The payment does not.

print()

# ============================================================
# WITH ABC: Missing method caught at instantiation time
# ============================================================

from abc import ABC, abstractmethod


class PaymentProcessorSafe(ABC):
    """ABC — enforces the contract. Cannot be instantiated directly."""

    @abstractmethod
    def charge(self, amount: float) -> bool:
        """Charge the customer. Must return True on success."""
        pass

    @abstractmethod
    def refund(self, amount: float) -> bool:
        """Refund the customer. Must return True on success."""
        pass


class CryptoProcessorStillBroken(PaymentProcessorSafe):
    """Same developer, same mistake — forgot charge()."""

    def refund(self, amount: float) -> bool:
        print(f"Refunding ${amount} in crypto")
        return True


# This line raises TypeError IMMEDIATELY — before any business logic runs.
try:
    broken_safe = CryptoProcessorStillBroken()
except TypeError as e:
    print(f"ABC caught the bug at instantiation time:")
    print(f"  {e}")
    print(f"  Missing methods: {PaymentProcessorSafe.__abstractmethods__}")

# The bug is caught during development, during import, during CI —
# never in production, never at 2 AM, never silently.
▶ Output
Charge result: None
Truthiness: False

ABC caught the bug at instantiation time:
Can't instantiate abstract class CryptoProcessorStillBroken without an implementation for abstract method 'charge'
Missing methods: frozenset({'charge'})
Mental Model
ABCs Shift the Error Left — From Call Time to Creation Time
The question is not whether the bug will be found. The question is when — during development or during a live customer transaction.
  • Without ABCs: Python discovers the missing method when the method is called in production. If the base class has a default that returns None, the method call succeeds silently and the bug is never discovered through error monitoring.
  • With ABCs: Python discovers the missing method the instant the class is instantiated. TypeError fires immediately with a clear message naming the exact missing method.
  • ABCs do not add runtime overhead to method calls — the check happens once at instantiation, not on every call.
  • The cost of an ABC is one import statement and one decorator per method. The cost of not having one is a 2 AM page when a customer's payment silently fails.
  • ABCs are especially valuable when you cannot control who writes subclasses — plugin architectures, framework extensions, team contracts across organizational boundaries.
📊 Production Insight
The most dangerous Python bug pattern is a base class method that returns None silently when it should have been overridden.
None is falsy, so if result: checks evaluate to False, and the code path that should handle the result is silently skipped.
Rule: any base class method that subclasses are expected to override should be @abstractmethod on an ABC — never a regular method with pass or raise NotImplementedError.
🎯 Key Takeaway
ABCs catch missing method implementations at instantiation time — the earliest possible moment in a Python program's lifecycle.
Without ABCs, a missing method is discovered at call time (if the base class raises) or never (if the base class returns None silently).
The TypeError from an ABC is the clearest, most actionable error message Python produces for contract violations — it names the exact missing method and the exact class.

Building a Real Contract — Abstract Methods, Abstract Properties, and Concrete Utilities

ABCs are not just a single-trick mechanism for marking methods as required. They support a rich contract design that includes abstract methods (must be implemented by every subclass), abstract properties (configuration-style contracts where each subclass must provide a value), and concrete methods (shared utility code that all subclasses inherit for free).

This combination is what makes ABCs genuinely useful in production. A well-designed ABC does three things simultaneously: it forces subclasses to implement the domain-specific logic (abstract methods), it forces subclasses to declare their configuration (abstract properties), and it provides shared infrastructure that every subclass benefits from without reimplementing (concrete methods).

The example below defines a ReportExporter ABC for a plugin architecture. Any team can write a new exporter — CSV, Markdown, PDF, Parquet — and the ABC guarantees that every exporter implements export() and validate_data() and declares its file_extension. The ABC also provides a concrete get_output_filename() method that all exporters inherit without modification.

io/thecodeforge/abc/report_exporter.py · PYTHON
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177
from abc import ABC, abstractmethod
from typing import List, Dict, Any


class ReportExporter(ABC):
    """
    ABC defining the contract for all report exporters.

    Contract:
      - file_extension (property): must return the file extension string
      - export(): must write data to a file and return the full path
      - validate_data(): must validate data before export; raise ValueError on failure

    Shared utilities:
      - get_output_filename(): builds the filename from base name and extension
      - log_export(): logs export operations (all subclasses get this for free)

    Any subclass that does not implement ALL abstract methods and properties
    will raise TypeError at instantiation time — before any data is processed.
    """

    @property
    @abstractmethod
    def file_extension(self) -> str:
        """
        Return the file extension for this exporter (e.g., 'csv', 'md', 'pdf').
        This is an abstract PROPERTY — subclasses must use @property to override.
        """
        pass

    @abstractmethod
    def export(self, data: List[Dict[str, Any]], output_path: str) -> str:
        """
        Export the report data to a file.
        Must return the absolute path to the created file.
        Must call validate_data() before writing.
        """
        pass

    @abstractmethod
    def validate_data(self, data: List[Dict[str, Any]]) -> bool:
        """
        Validate the data before exporting.
        Must raise ValueError on invalid data.
        Must return True on valid data.
        """
        pass

    # --- Concrete methods: shared utilities, free for all subclasses ---

    def get_output_filename(self, base_name: str) -> str:
        """
        Build the output filename using the subclass's file_extension property.
        All subclasses inherit this without reimplementing.
        """
        return f"{base_name}.{self.file_extension}"

    def log_export(self, path: str, row_count: int) -> None:
        """
        Standardised export logging. Every exporter logs in the same format.
        """
        print(f"[{self.__class__.__name__}] Exported {row_count} rows to {path}")


# ============================================================
# Concrete implementation: CsvExporter
# ============================================================

class CsvExporter(ReportExporter):
    """Exports reports to CSV format."""

    @property
    def file_extension(self) -> str:
        return "csv"

    def validate_data(self, data: List[Dict[str, Any]]) -> bool:
        if not data:
            raise ValueError("Cannot export empty dataset to CSV")
        if not all(isinstance(row, dict) for row in data):
            raise ValueError("All rows must be dictionaries")
        # Verify all rows have the same keys (consistent schema)
        keys = set(data[0].keys())
        for i, row in enumerate(data[1:], start=1):
            if set(row.keys()) != keys:
                raise ValueError(
                    f"Row {i} has different keys than row 0: "
                    f"expected {keys}, got {set(row.keys())}"
                )
        return True

    def export(self, data: List[Dict[str, Any]], output_path: str) -> str:
        self.validate_data(data)
        full_path = f"{output_path}/{self.get_output_filename('report')}"

        headers = list(data[0].keys())
        print(f"Writing CSV to: {full_path}")
        print(f"  Headers: {', '.join(headers)}")
        print(f"  Rows: {len(data)}")

        self.log_export(full_path, len(data))
        return full_path


# ============================================================
# Concrete implementation: MarkdownExporter
# ============================================================

class MarkdownExporter(ReportExporter):
    """Exports reports as Markdown tables."""

    @property
    def file_extension(self) -> str:
        return "md"

    def validate_data(self, data: List[Dict[str, Any]]) -> bool:
        if not data:
            raise ValueError("Cannot export empty dataset to Markdown")
        if len(data) > 1000:
            raise ValueError(
                f"Markdown export limited to 1000 rows for readability, got {len(data)}"
            )
        return True

    def export(self, data: List[Dict[str, Any]], output_path: str) -> str:
        self.validate_data(data)
        full_path = f"{output_path}/{self.get_output_filename('report')}"

        print(f"Writing Markdown table to: {full_path}")
        print(f"  Rows: {len(data)}")

        self.log_export(full_path, len(data))
        return full_path


# ============================================================
# Broken implementation: demonstrates ABC enforcement
# ============================================================

class BrokenExporter(ReportExporter):
    """Forgot to implement validate_data() and file_extension."""

    def export(self, data: List[Dict[str, Any]], output_path: str) -> str:
        return f"{output_path}/report.broken"


# ============================================================
# Plugin loader: trusts the ABC contract blindly
# ============================================================

def run_export(exporter: ReportExporter, data: List[Dict], path: str) -> None:
    """
    Accepts ANY ReportExporter. Does not check which concrete class it is.
    The ABC guarantees .export(), .validate_data(), and .file_extension exist.
    """
    print(f"\nUsing exporter: {exporter.__class__.__name__} (.{exporter.file_extension})")
    output = exporter.export(data, path)
    print(f"Export complete: {output}")


# Sample data
sales_data = [
    {"product": "Widget A", "units_sold": 120, "revenue": 2400.00},
    {"product": "Widget B", "units_sold": 85,  "revenue": 3400.00},
    {"product": "Gadget X", "units_sold": 200, "revenue": 9800.00},
]

# These work — both satisfy the ABC contract
run_export(CsvExporter(), sales_data, "/tmp/reports")
run_export(MarkdownExporter(), sales_data, "/tmp/reports")

# This fails at instantiation — ABC catches the missing methods
print("\n--- Attempting to instantiate BrokenExporter ---")
try:
    run_export(BrokenExporter(), sales_data, "/tmp/reports")
except TypeError as e:
    print(f"ABC enforcement: {e}")
    print(f"Missing methods: {ReportExporter.__abstractmethods__}")
▶ Output
Using exporter: CsvExporter (.csv)
Writing CSV to: /tmp/reports/report.csv
Headers: product, units_sold, revenue
Rows: 3
[CsvExporter] Exported 3 rows to /tmp/reports/report.csv
Export complete: /tmp/reports/report.csv

Using exporter: MarkdownExporter (.md)
Writing Markdown table to: /tmp/reports/report.md
Rows: 3
[MarkdownExporter] Exported 3 rows to /tmp/reports/report.md
Export complete: /tmp/reports/report.md

--- Attempting to instantiate BrokenExporter ---
ABC enforcement: Can't instantiate abstract class BrokenExporter without an implementation for abstract methods 'file_extension', 'validate_data'
Missing methods: frozenset({'file_extension', 'validate_data'})
🔥The Interview Answer That Separates Junior From Senior
When asked 'why use ABCs instead of just duck typing?' say this: 'Duck typing catches missing methods at call time — when the method is actually invoked during execution. ABCs catch them at instantiation time — before any business logic, any database write, or any API call executes. In production systems where a silent failure causes revenue loss or data corruption, failing loudly at creation time is always better than failing silently at call time. ABCs are the mechanism Python gives you to enforce that guarantee.'
📊 Production Insight
Plugin architectures are the canonical ABC use case. You cannot review or control every subclass that third-party developers or other teams write.
Abstract properties (@property + @abstractmethod) enforce configuration-style contracts — every exporter must declare its file extension, every processor must declare its currency, every connector must declare its timeout.
Rule: design the ABC first, publish it as the contract, then let teams implement concrete subclasses independently. The ABC is the API boundary — it is the thing everyone agrees on.
🎯 Key Takeaway
ABCs shine in plugin architectures where you ship the interface and other people implement the plugins.
Mixing abstract methods, abstract properties, and concrete utility methods in the same ABC is the standard production pattern — not an edge case.
The ABC defines the contract. The concrete methods provide shared infrastructure. The plugin loader trusts the contract blindly because the ABC makes that trust safe.

ABCs vs Duck Typing vs Regular Inheritance — When to Reach for Each

Python gives you three mechanisms for sharing behaviour and enforcing structure: duck typing, regular inheritance, and ABCs. Knowing which to reach for in a given situation separates intermediate Python developers from senior ones.

Duck typing is the default in Python and it is the right choice for most code. If you are writing a function that calls .read() on something, you do not need an ABC — just call .read() and let Python raise AttributeError if the object does not support it. Duck typing keeps the code simple, flexible, and Pythonic. It is ideal for scripts, utility functions, and any situation where you control all the code and can see every caller and every implementation.

Regular inheritance is right when you want to share concrete implementation across a class hierarchy and the base class itself is a valid, instantiable thing. A Vehicle class with real working methods like start_engine() and stop_engine(), extended by Car and Truck that add specific behaviour, is plain inheritance. The base class is not abstract — it does real work on its own.

ABCs are the right choice when: (1) you are building a framework or library that other people will extend, and you cannot review or control their implementations; (2) you need to guarantee an interface exists without providing a default implementation; (3) you want isinstance() checks that reflect logical type membership rather than just the class hierarchy; or (4) you need to catch missing methods at instantiation time rather than at call time, because call-time failures in your domain are expensive or dangerous.

The abc module also supports register() — a mechanism for declaring that an existing class satisfies an ABC's interface without modifying the class or making it inherit from the ABC. This is Python's escape hatch for working with legacy or third-party code that you cannot change but that does implement the required methods.

io/thecodeforge/abc/register_example.py · PYTHON
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374
from abc import ABC, abstractmethod


class Serializable(ABC):
    """ABC representing anything that can be serialized to a string."""

    @abstractmethod
    def serialize(self) -> str:
        """Convert this object to a string representation."""
        pass


class LegacyDataRecord:
    """
    This class was written before Serializable existed.
    It lives in a third-party library we cannot modify.
    But it does implement serialize() — so logically it qualifies.
    """

    def __init__(self, source: str, data: str):
        self.source = source
        self.data = data

    def serialize(self) -> str:
        return f'{{"source": "{self.source}", "data": "{self.data}"}}'


class LegacyRecordWithoutSerialize:
    """
    This class does NOT implement serialize().
    Registering it is a mistake — but Python will not stop you.
    """

    def __init__(self, value: int):
        self.value = value


# Register the legacy class as a virtual subclass of Serializable.
# This tells Python's ABC machinery that LegacyDataRecord honours
# the Serializable contract — purely a type declaration.
Serializable.register(LegacyDataRecord)

# DANGER: We can also register a class that does NOT implement serialize().
# Python does not check. register() is trust-based, not validated.
Serializable.register(LegacyRecordWithoutSerialize)


# --- Demonstrate the behaviour ---

legacy_good = LegacyDataRecord("legacy_system", "opaque_blob")
legacy_bad = LegacyRecordWithoutSerialize(42)

print("=== isinstance() checks ===")
print(f"LegacyDataRecord is Serializable?          {isinstance(legacy_good, Serializable)}")
print(f"LegacyRecordWithoutSerialize is Serializable? {isinstance(legacy_bad, Serializable)}")

print(f"\n=== __subclasses__() — only REAL subclasses, not registered ===")
print(f"Serializable.__subclasses__(): {Serializable.__subclasses__()}")
# Empty — registered classes do not appear in __subclasses__()

print(f"\n=== Calling serialize() ===")
print(f"Good legacy record: {legacy_good.serialize()}")

try:
    legacy_bad.serialize()
except AttributeError as e:
    print(f"Bad legacy record: AttributeError — {e}")
    print("  register() did NOT verify that serialize() exists.")
    print("  isinstance() returns True, but the method is missing.")

print(f"\n=== Key insight ===")
print("register() is a type tag for isinstance() — NOT a validation mechanism.")
print("Use it only for code you cannot modify that genuinely implements the interface.")
print("Always write a test that calls each abstract method after registering.")
▶ Output
=== isinstance() checks ===
LegacyDataRecord is Serializable? True
LegacyRecordWithoutSerialize is Serializable? True

=== __subclasses__() — only REAL subclasses, not registered ===
Serializable.__subclasses__(): []

=== Calling serialize() ===
Good legacy record: {"source": "legacy_system", "data": "opaque_blob"}
Bad legacy record: AttributeError — 'LegacyRecordWithoutSerialize' object has no attribute 'serialize'
register() did NOT verify that serialize() exists.
isinstance() returns True, but the method is missing.

=== Key insight ===
register() is a type tag for isinstance() — NOT a validation mechanism.
Use it only for code you cannot modify that genuinely implements the interface.
Always write a test that calls each abstract method after registering.
⚠ register() Is a Trust Declaration, Not a Validation Mechanism
register() makes isinstance() return True for a class that does not inherit from the ABC. It does not check or enforce that the class implements any abstract methods. You can register a completely empty class and isinstance() will happily return True. Use register() only for third-party or legacy code that you cannot modify and that you have manually verified implements the required interface. Never use register() as a shortcut to avoid implementing abstract methods in your own code — it defeats the entire purpose of using ABCs in the first place. After every register() call, write a test that instantiates the registered class and calls every method defined in the ABC. This is the manual enforcement that register() deliberately skips.
📊 Production Insight
register() is a type tag, not validation — isinstance() returns True even if no abstract methods exist on the registered class.
Use register() exclusively for third-party code you cannot modify. Never for your own classes.
Rule: after registering a class, write a test that calls each abstract method on an instance of the registered class. This is the manual verification that register() deliberately does not provide.
🎯 Key Takeaway
Duck typing for scripts and utility code where you control everything. Regular inheritance for shared concrete implementation. ABCs for enforced contracts across team or organisational boundaries.
register() trades safety for legacy compatibility — it is an escape hatch, not a shortcut.
The decision is driven by who controls the subclasses: if you control them, duck typing is fine. If you do not, ABCs are the enforcement mechanism.
🗂 ABCs vs Duck Typing vs Regular Inheritance — Complete Comparison
When to use each approach and what you trade off. Default to duck typing unless you have a specific reason to use the others.
Feature / AspectAbstract Base Classes (ABC)Regular InheritanceDuck Typing
Contract enforcementAt instantiation time — TypeError if any abstract method is missing, with a clear error message naming the exact methodNo enforcement — missing methods silently return None (if base has a default) or raise AttributeError only at call timeNo enforcement — AttributeError only when the missing method is actually called during execution
Base class instantiable?No — raises TypeError. The base class is a contract, not a usable object.Yes — the base class can be used directly. This is by design when the base does real work.N/A — no formal base class needed. Objects are defined by what methods they have, not what they inherit from.
Error discovery timingEarliest — at object creation time, before any business logic runsLate — when the missing method is actually called in running code, which may be days after deploymentLatest — at call time in running code. Same timing as regular inheritance but with no base class to provide even a broken default.
isinstance() supportYes — including virtual subclasses via register(). Supports logical type membership beyond the class hierarchy.Yes — but only for classes that directly inherit from the base classNo — no class hierarchy to check against. You can only check for method existence via hasattr().
Best use caseFrameworks, plugin systems, public APIs, team contracts where you cannot control who writes subclassesSharing concrete implementation across related classes where the base class itself is a valid, instantiable thingScripts, utility functions, single-developer projects, any code where you control all callers and all implementations
Can mix abstract and concrete methods?Yes — this is the standard production pattern. Abstract for what subclasses must implement. Concrete for shared utilities.Yes — all methods are concrete by defaultN/A — no base class, no methods to share
Abstract property supportYes — @property combined with @abstractmethod enforces configuration-style contractsNo built-in equivalent — you can use @property but there is no mechanism to require subclasses to override itNo equivalent
Python import requiredfrom abc import ABC, abstractmethodNone — built-in language featureNone — built-in language feature

🎯 Key Takeaways

  • ABCs enforce method contracts at instantiation time — TypeError fires the moment a broken subclass is created, before any business logic, any database write, or any customer-facing operation executes.
  • @abstractmethod only has enforcement power when the class inherits from ABC or uses metaclass=ABCMeta. Without that inheritance, the decorator is purely decorative — it sets a flag but Python never checks it.
  • Mixing abstract methods, abstract properties, and concrete methods in the same ABC is the standard production pattern. Abstract for what subclasses must implement. Concrete for shared utilities all subclasses inherit for free.
  • register() is a type tag for isinstance() — not a validation mechanism. It is designed for legacy and third-party code you cannot modify. Never use it to skip implementing abstract methods in your own classes.
  • The most dangerous Python bug pattern is a base class method that returns None silently when it should have been overridden. ABCs eliminate this entire class of bug by making the override mandatory.

⚠ Common Mistakes to Avoid

    Forgetting to inherit from ABC — writing @abstractmethod on a regular class
    Symptom

    You add @abstractmethod decorators to methods on a base class, but the class inherits from object (or nothing at all). Subclasses that are missing abstract methods instantiate without any error. The decorators are silently ignored. The bug you were trying to prevent still exists.

    Fix

    Always inherit from ABC explicitly: class MyBase(ABC):. Alternatively, use metaclass=ABCMeta, but ABC is cleaner for most cases. Without ABC or ABCMeta in the class hierarchy, @abstractmethod is just a regular function decorator that sets a __isabstractmethod__ flag on the function object — it has no enforcement power.

    Implementing only some abstract methods and expecting the subclass to instantiate
    Symptom

    You subclass an ABC and implement three of four abstract methods. When you instantiate the subclass, Python raises TypeError naming the one missing method. You expected partial implementation to work because the methods you did implement are 'the important ones.'

    Fix

    Every single abstract method must be overridden in the concrete subclass. There is no partial credit. Before you start implementing, check the full list of required methods: print(MyABC.__abstractmethods__). This returns a frozenset of all method names that must be implemented. Implement every one of them.

    Using register() thinking it enforces the abstract method contract
    Symptom

    You register a class via MyABC.register(SomeClass). isinstance(obj, MyABC) returns True. You pass the object to code that calls an abstract method. AttributeError is raised because the registered class never implemented the method.

    Fix

    register() is a type tag — it makes isinstance() return True but does not verify or enforce that any abstract methods exist on the registered class. After registering, write an explicit test that instantiates the registered class and calls every method defined in the ABC. If you find yourself registering your own classes, stop — you should be inheriting from the ABC instead.

    Putting shared business logic in abstract methods instead of concrete methods
    Symptom

    Subclasses call super().abstract_method() expecting to get shared behaviour from the base class. They get None or NotImplementedError because the abstract method body is empty. The shared logic that every subclass needs is in the wrong place.

    Fix

    Abstract methods define the contract — what must be implemented. Concrete methods in the ABC provide shared behaviour — what all subclasses get for free. Keep them separate. If you have logic that every subclass should share, put it in a concrete method on the ABC. If you have logic that each subclass must provide its own version of, mark it @abstractmethod.

    Stacking decorators in the wrong order on abstract properties or class methods
    Symptom

    You combine @abstractmethod with @property or @classmethod but TypeError is not raised when the subclass omits the method. Or the method exists but behaves as a regular method instead of a property or classmethod.

    Fix

    @abstractmethod must always be the innermost decorator — the one closest to the def line. For abstract properties: @property above @abstractmethod. For abstract class methods: @classmethod above @abstractmethod. The order matters because Python applies decorators bottom-up. If @abstractmethod is not innermost, the ABC machinery does not see the method as abstract.

Interview Questions on This Topic

  • QWhat is the difference between an Abstract Base Class and a regular base class in Python, and what practical problem does using ABCs solve that regular inheritance does not?JuniorReveal
    A regular base class can be instantiated directly, and missing methods in subclasses are only discovered at call time — when the method is actually invoked during execution. If the base class has a default implementation that returns None, the missing override is never discovered at all. The code runs silently and produces wrong results. An ABC cannot be instantiated directly and any subclass that fails to implement all @abstractmethod methods raises TypeError at the moment of instantiation — before the object exists, before any method is called, before any business logic executes. The error message names the exact missing method. The practical problem ABCs solve is contract enforcement in codebases where you cannot control who writes subclasses. In plugin architectures, framework extensions, and multi-team projects, you need a guarantee that every concrete subclass implements the required interface. Regular inheritance provides no such guarantee — it trusts developers to remember. ABCs make the trust unnecessary by having Python enforce it.
  • QIf you use ABC.register() to register a class as a virtual subclass, does Python still enforce that the class implements all abstract methods? Why or why not, and when is register() the correct choice?Mid-levelReveal
    No. register() bypasses abstract method enforcement entirely. It is a type declaration that makes isinstance() and issubclass() return True for the registered class, but Python does not verify or require that the class implements any abstract methods at all. You can register an empty class with no methods and isinstance() will return True. This is intentional. register() exists for a specific use case: integrating with legacy or third-party code that you cannot modify — code that was written before your ABC existed but that happens to implement the required interface. You cannot make that code inherit from your ABC because you do not control it. register() lets you declare the type relationship without modifying the class. The trade-off is that you lose the instantiation-time enforcement that is the primary value of ABCs. After registering a class, you should always write a test that instantiates it and calls every abstract method to manually verify the interface is actually implemented. register() is a trust declaration — you are telling Python 'I have manually verified this class implements the contract.' If you have not actually verified it, you have a bug waiting to happen. register() should never be used for classes you control. If you can modify the class, make it inherit from the ABC and get the enforcement for free.
  • QCan an Abstract Base Class have concrete non-abstract methods? If so, give a use case where mixing abstract and concrete methods in the same ABC is the right design choice.Mid-levelReveal
    Yes, ABCs can freely mix abstract and concrete methods. This is not an edge case — it is the standard production pattern for ABCs. A concrete use case: a ReportExporter ABC defines export() and validate_data() as abstract methods — each subclass must provide its own implementation because the export format and validation rules are different for CSV, Markdown, PDF, and Parquet. The ABC also defines an abstract property file_extension that each subclass must declare. But the ABC provides a concrete get_output_filename() method that builds the output filename from a base name and the subclass's file_extension. This method is identical for all exporters — there is no reason for subclasses to reimplement it. Another common pattern: a concrete log_transaction() method on a PaymentProcessor ABC that formats and writes transaction records in a standardised format. Every payment processor — Stripe, PayPal, crypto — logs transactions the same way. The logging logic lives in the ABC as a concrete method. The charge() and refund() methods are abstract because each processor's payment logic is different. The principle: abstract methods define what each subclass must provide. Concrete methods provide what all subclasses share. A well-designed ABC does both.
  • QYou are building a plugin system where third-party developers write data exporters. How would you use ABCs to enforce the plugin contract, and what would you do differently if some plugins are written in other languages or run as separate processes?SeniorReveal
    For Python plugins running in the same process, I would define a ReportExporter ABC with @abstractmethod on export() and validate_data(), and @property combined with @abstractmethod on file_extension. The plugin loader imports each plugin module and instantiates the exporter class. If any abstract method is missing, Python raises TypeError at instantiation time — before any data is processed, before any file is written. The ABC is the contract: I publish it, third-party developers implement it, and the ABC machinery enforces it automatically without any runtime checking code on my side. For cross-language plugins or out-of-process plugins — a Go exporter communicating over gRPC, or a containerised service exposing an HTTP API — ABCs cannot enforce anything because the plugin is not a Python class in the same runtime. In that case, I would use contract testing: define the expected interface as a shared specification (protobuf for gRPC, OpenAPI for HTTP, JSON schema for message-based), and run a validation test harness that sends test requests to the plugin and asserts correct responses for every method in the contract. The test harness runs as part of the plugin's CI pipeline, not mine. The principle is the same in both cases: define the contract once, enforce it before production traffic flows. ABCs provide language-level enforcement within Python. Contract tests provide runtime enforcement across process and language boundaries.

Frequently Asked Questions

Does Python have interfaces like Java or TypeScript?

Python does not have a formal interface keyword, but Abstract Base Classes serve the same structural purpose. An ABC where every method is decorated with @abstractmethod and no concrete implementation is provided is functionally identical to a Java interface or a TypeScript interface — it defines a contract that implementing classes must satisfy. Python also offers typing.Protocol for structural subtyping (checking that an object has the right methods without requiring inheritance), which is closer to TypeScript's structural type system. For enforcement at instantiation time, ABCs are the standard mechanism.

Can you instantiate an Abstract Base Class directly?

No — and that is the entire point. If you try to call MyABC() where MyABC has any unimplemented abstract methods, Python raises TypeError immediately: "Can't instantiate abstract class MyABC without an implementation for abstract method 'method_name'". The ABC is a contract, not a usable object. Only concrete subclasses that implement every abstract method can be instantiated.

What is the difference between @abstractmethod and raising NotImplementedError in the base class?

They catch the problem at different times. NotImplementedError is raised when the unimplemented method is called at runtime — which could be days or weeks after the broken code was deployed. If the method is never called in your test suite, the bug is never discovered. @abstractmethod on an ABC is enforced at instantiation time — Python refuses to create the object at all if the method is missing. The bug is caught during development, during import, during CI — never in production. ABCs also produce a clearer error message that names the exact missing method and the exact class.

When should I use typing.Protocol instead of ABC?

Use Protocol when you want structural subtyping — checking that an object has the right methods based on its structure, without requiring it to inherit from a specific base class. Protocol works with static type checkers like mypy and does not enforce anything at runtime. Use ABC when you want runtime enforcement at instantiation time — a TypeError that fires before any code runs. The practical guidance: Protocol is for static analysis and type checking during development. ABC is for runtime enforcement in production. They are complementary — you can use both in the same codebase for different purposes.

🔥
Naren Founder & Author

Developer and founder of TheCodeForge. I built this site because I was tired of tutorials that explain what to type without explaining why it works. Every article here is written to make concepts actually click.

← PreviousPython SlotsNext →Python Design Patterns
Forged with 🔥 at TheCodeForge.io — Where Developers Are Forged