Skip to content
Home Python Abstract Classes in Python Explained — Why, When, and How to Use Them

Abstract Classes in Python Explained — Why, When, and How to Use Them

Where developers are forged. · Structured learning · Free forever.
📍 Part of: OOP in Python → Topic 6 of 9
Abstract classes in Python enforce a contract across subclasses.
⚙️ Intermediate — basic Python knowledge assumed
In this tutorial, you'll learn
Abstract classes in Python enforce a contract across subclasses.
  • Abstract classes move an entire category of bugs from silent runtime failures to loud TypeError at object creation time — a TypeError at instantiation beats a missed payment or dropped alert by an enormous margin.
  • An abstract method can have a body — use this for shared audit logging, validation, or metric emission that every subclass should opt into via super(). The override remains mandatory; the body is opt-in shared infrastructure.
  • For pure capability contracts with no shared state — especially for third-party or plugin integration — prefer typing.Protocol over ABC. It is more Pythonic, requires no inheritance, and lets any class satisfy the contract by shape rather than lineage.
✦ 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 completely inert
  • Abstract methods can have bodies — use this to share logging or validation logic while still forcing every subclass to consciously override the method
  • @property + @abstractmethod must be stacked with @property on the outside — wrong order silently kills enforcement with no error or warning
  • For pure signature contracts with no shared state, prefer typing.Protocol over ABC — it is more Pythonic and requires no inheritance
  • Biggest mistake: forgetting to inherit from ABC makes @abstractmethod a decorative marker with zero enforcement power
🚨 START HERE
ABC Contract Violation Quick Diagnosis
Symptom-to-fix commands for production Python ABC failures.
🟡TypeError at instantiation — abstract method not implemented
Immediate ActionGet the complete list of missing abstract methods from the class itself.
Commands
python -c "from mymodule import Notifier; print(Notifier.__abstractmethods__)"
grep -rn '@abstractmethod' mymodule/notifier.py
Fix NowImplement every method in the __abstractmethods__ frozenset on the concrete subclass. Method names must match exactly — check for typos, casing differences, and missing @property decorators.
🟡@abstractmethod is present but no enforcement — class instantiates without all methods implemented
Immediate ActionVerify that ABC is actually in the class's MRO.
Commands
python -c "from mymodule import Notifier; print(Notifier.__mro__)"
grep -n 'class.*ABC\|metaclass=ABCMeta\|from abc import' mymodule/notifier.py
Fix NowIf ABC does not appear in the MRO output, the base class inherits from object, not ABC. Change class Notifier: to class Notifier(ABC): and add from abc import ABC, abstractmethod.
🟡@property @abstractmethod not enforcing — subclass instantiates without implementing the property
Immediate ActionCheck the exact decorator stacking order on the abstract property.
Commands
grep -B3 'def channel_name' mymodule/notifier.py
python -c "from mymodule import Notifier; print(Notifier.__abstractmethods__)"
Fix NowThe correct order is @property on line 1, @abstractmethod on line 2, def on line 3. If @abstractmethod appears above @property, the enforcement is silently broken. If channel_name does not appear in __abstractmethods__, the stacking order is wrong.
Production IncidentSlack Notifications Silently Dropped for 18 Hours — Missing @abstractmethod on Notifier Base ClassA SaaS platform's notification system silently dropped all Slack alerts for 18 hours because the Notifier base class was not an ABC, and a new SlackNotifier subclass forgot to implement send().
SymptomOn-call engineers missed 47 critical alerts over 18 hours. The notification dashboard showed delivered for all channels, but Slack received zero messages. No exceptions, no error logs, no trace of the failure in any monitoring system. The on-call channel looked healthy. It was not.
AssumptionThe SRE team assumed a Slack API outage or webhook misconfiguration. They spent six hours checking Slack app permissions, rotating webhook URLs, verifying rate limits, and reviewing the Slack API status page — none of which was the issue. The API was fully operational.
Root causeThe Notifier base class was a regular Python class — not an ABC — with a send() method that had a pass body. A developer added SlackNotifier, correctly overrode the channel_name property, but forgot to implement send(). Python instantiated the class without complaint. When the notification system called slack_notifier.send(message), Python resolved the call to the base class method via the MRO, which ran, did nothing, and returned None. The calling code checked if result: to decide whether to log a delivery confirmation. None is falsy, so the check evaluated to False — and the code silently skipped the delivery confirmation without raising any exception or logging any error. The dashboard showed delivered because the delivery call was never treated as failed — it was treated as if it had not happened at all.
Fix1. Converted Notifier to inherit from ABC with @abstractmethod on send() and @property @abstractmethod on channel_name. Any subclass that omits either of these now raises TypeError at instantiation time, before any notification attempt is made. 2. Added a CI test that imports every Notifier subclass, attempts to instantiate each one, and calls send() with a test message against a mock sink. 3. Added return type annotations requiring -> bool on all send() implementations, enforced by mypy in strict mode. A method that implicitly returns None would now be caught by the type checker before merge. 4. Added a runtime guard in the notification dispatcher: if send() returns anything other than True or False, raise a ValueError immediately rather than treating None as a non-delivery.
Key Lesson
Silent failures — a pass body, a None return, a do-nothing method — are worse than loud crashes. A crash stops the system. A silent failure runs in production for 18 hours while engineers chase phantom API outages.ABCs catch missing method implementations at instantiation time, which is the earliest possible moment. The bug surfaces when the object is created, not when it tries to notify 47 critical alerts to a channel that ignores them.Always enforce contracts with ABCs in notification, payment, and authentication code where a silent failure translates directly into missed alerts, uncharged customers, or security gaps.
Production Debug GuideCommon symptoms when ABCs are misused or missing in production Python systems. Most of these have no stack trace — the code runs and produces wrong results silently.
TypeError: Can't instantiate abstract class X without an implementation for abstract method YThis is ABCs working correctly. The subclass has not implemented all @abstractmethod methods. Check MyBaseClass.__abstractmethods__ to get the complete frozenset of missing method names. Implement every method in that set. The method names must match exactly — a typo creates a new method rather than satisfying the abstract requirement.
Class instantiates without error despite having @abstractmethod decorators on the base classThe base class does not inherit from ABC and does not use metaclass=ABCMeta. Without ABC in the inheritance chain, @abstractmethod has no enforcement power — it sets a __isabstractmethod__ flag on the function but no one checks it. 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 what should be an implemented method raises AttributeErrorThe class was registered as a virtual subclass via MyABC.register(). Registration bypasses abstract method enforcement entirely — isinstance returns True but no methods are verified. Manually audit the registered class to confirm every abstract method is implemented, then write a test that calls each one.
@property @abstractmethod not enforcing — subclass instantiates without implementing the propertyThe decorator stacking order is wrong. @abstractmethod must be the innermost decorator, directly above def. @property must be the outermost. The correct order top-to-bottom is: @property, @abstractmethod, def. Reversing them (@abstractmethod on top, @property below) silently breaks enforcement in Python 3.3 and later — no error, no warning, just no enforcement.
Subclass has the required method but it returns None where bool is expected, and downstream code silently skips the resultThe method exists but is not fully implemented — possibly it has a pass body or forgot a return statement. Add return type annotations and run mypy in strict mode. Add a runtime guard that raises ValueError if the method returns None when a bool is required. This is the failure mode that caused the 18-hour alert outage.

Most Python tutorials teach you classes by having you make a Dog that barks and a Cat that meows. That is fine for learning syntax, but it skips the single most important question in real-world software: how do you guarantee that every class in a family of related classes actually implements the methods it is supposed to? Without a mechanism to enforce that contract, you end up with a PaymentProcessor subclass that forgets to implement process_payment, and you only find out at 2 AM when a customer complains their order did not go through and the charge never fired.

Abstract classes solve exactly that problem. Python's abc module lets you define a base class that acts as a blueprint — it declares which methods must exist in every subclass and refuses to let you instantiate anything that has not honoured that contract. This moves an entire category of bugs from runtime to instantiation time, which is a meaningful shift in where you discover problems. Finding a bug when you create an object is infinitely better than finding it while processing a payment.

By the end of this article you will understand why abstract classes exist rather than just how to write them, when to reach for them versus a regular base class or typing.Protocol, and you will have seen three real-world patterns you can use immediately. You will also understand the decorator stacking gotcha that silently breaks enforcement for hundreds of codebases every year.

The Problem Abstract Classes Are Actually Solving

Before writing a single line of ABC code, it is worth understanding the specific failure mode that makes ABCs necessary. If you do not feel this pain clearly, you will treat ABCs as a formality rather than a genuine safety mechanism.

Suppose you are building a notification system. You create a base Notifier class with a send method, then write EmailNotifier, SMSNotifier, and PushNotifier. Everything works correctly because every developer on your team so far has read the code, understood the convention, and implemented send properly.

Then a new engineer joins. They add SlackNotifier, override the channel_name property (which they found in the docs), but miss the send method. No error is raised. The class definition is syntactically valid. Python instantiates it without complaint. The notification pipeline calls send on the SlackNotifier instance, Python walks up the MRO, finds send on the base class, calls the pass body, gets None back, and the message silently vanishes.

No stack trace. No log line. No monitoring alert. Just 18 hours of missed alerts and an SRE team chasing a Slack API outage that never existed.

This is the silent inheritance trap — the most dangerous failure mode in Python's class system. Abstract classes break out of it by making the contract explicit and machine-enforced. The TypeError you get at instantiation time is not an obstacle. It is the system working exactly as it should.

io/thecodeforge/notifications/silent_trap.py · PYTHON
1234567891011121314151617181920212223242526272829303132333435363738394041
# ============================================================
# PART 1: The problem — a regular base class with no enforcement
# ============================================================

class Notifier:
    """
    Intends every subclass to override send().
    But Python has no mechanism to enforce that intent here.
    The comment is the only contract. Comments are not contracts.
    """

    def send(self, message: str, recipient: str) -> bool:
        # This does nothing and returns None (falsy).
        # Python won't raise any error if a subclass skips this.
        pass


class EmailNotifier(Notifier):
    def send(self, message: str, recipient: str) -> bool:
        print(f"[EMAIL] To: {recipient} | {message}")
        return True


class SlackNotifier(Notifier):
    # Developer forgot send(). Python does not care.
    # This class definition is completely valid as far as the interpreter knows.
    pass


# Both instantiate without error
email = EmailNotifier()
slack = SlackNotifier()  # Should fail — but doesn't

result_email = email.send("Deploy complete", "alice@example.com")
result_slack = slack.send("Deploy complete", "#alerts")  # Silently does nothing

print(f"Email delivered: {result_email}")  # True
print(f"Slack delivered: {result_slack}")  # None — falsy, silent failure

# In production: if result_slack: log_delivery() — this never runs.
# The alert is never logged as failed either. It just disappears.
▶ Output
[EMAIL] To: alice@example.com | Deploy complete
Email delivered: True
Slack delivered: None
⚠ Silent Failures Are the Worst Kind of Bug
When a method does nothing instead of raising an error, you get no stack trace, no log line, and no monitoring alert to investigate. Your dashboard shows green. Your system reports success. Your users experience the failure. Abstract classes trade that silent failure for an immediate, loud TypeError at the moment the incomplete object is created — which is always the better deal. You want to hear about the problem as early as possible, not 18 hours later during a post-mortem.
📊 Production Insight
Silent inheritance through a pass body or None return is Python's most dangerous OOP footgun. It hides bugs for hours or days in production with no signal in any monitoring system.
A TypeError at instantiation time is infinitely preferable to a silent failure at 2 AM during a critical customer transaction.
Rule: if a method must be overridden by every subclass, mark it @abstractmethod. Never rely on comments, naming conventions, or the hope that developers will read the docs.
🎯 Key Takeaway
The silent inheritance trap — a base class method with pass body, subclass forgets to override — is the failure mode ABCs are designed to prevent.
Abstract classes convert silent failures into loud TypeErrors at object creation time, the earliest possible moment.
Always use ABCs when a method must exist on every subclass. Comments and empty method bodies are not contracts — the interpreter cannot enforce them.

How Python's ABC Module Enforces the Contract

Python's abc module provides two tools that work together: the ABC base class and the @abstractmethod decorator. Together they flip the switch from please remember to implement this to you cannot create this object until you do.

When you inherit from ABC and decorate a method with @abstractmethod, Python's ABCMeta metaclass registers that method as an unresolved obligation. Every time someone tries to instantiate any class in that hierarchy, ABCMeta checks whether every abstract method has been overridden in the concrete class. If even one is missing, Python raises TypeError with a message that names the exact missing method.

Two important nuances worth understanding before you write any ABC code: First, you can provide a body inside an abstract method. This is not a contradiction — the method is still abstract and still requires override, but the body provides shared logic that subclasses can access via super(). Use this for logging, validation, or timestamp recording that every implementation needs. Second, abstract methods work on regular methods, class methods with @classmethod, static methods with @staticmethod, and properties with @property. Each has a specific decorator stacking order, and getting the order wrong silently breaks enforcement.

The key rule for properties: @property must be the outermost decorator (first line above def), and @abstractmethod must be the innermost (second line above def). Reversing them produces no error — the enforcement simply stops working.

io/thecodeforge/notifications/abstract_notifier.py · PYTHON
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145
from abc import ABC, abstractmethod
from datetime import datetime
from typing import Optional


class Notifier(ABC):
    """
    Abstract base class for all notification channels.

    Contract:
      - send() must be implemented by every concrete subclass
      - channel_name must be declared as a property by every subclass

    Any subclass that omits either of these raises TypeError at instantiation
    time — before any notification attempt is made, before any business logic
    runs, and long before any alert can be silently dropped.
    """

    def __init__(self, sender_id: str) -> None:
        """
        Abstract classes CAN have constructors.
        This initialises shared state every notifier needs.
        Subclasses call super().__init__(sender_id) to reuse this.
        """
        self._sender_id = sender_id
        self._dispatch_count = 0

    @abstractmethod
    def send(self, message: str, recipient: str) -> bool:
        """
        Send a notification to a recipient.

        Returns True if delivery was confirmed, False if it failed.
        Must NEVER return None — callers rely on the boolean result.

        Abstract methods CAN have a body. Subclasses can call super().send()
        to get the shared audit logging below without reimplementing it.
        The override is still mandatory — this body is opt-in, not default.
        """
        # Shared audit logic — any subclass can opt in via super().send()
        self._dispatch_count += 1
        print(
            f"  [AUDIT] Channel={self.channel_name} "
            f"Sender={self._sender_id} "
            f"At={datetime.utcnow().strftime('%H:%M:%S')} UTC "
            f"Attempt=#{self._dispatch_count}"
        )
        return False  # Subclass overrides the meaningful return value

    @property
    @abstractmethod
    def channel_name(self) -> str:
        """
        Human-readable name for this notification channel.

        CORRECT stacking order:
          @property       ← outermost, line 1 above def
          @abstractmethod ← innermost, line 2 above def
          def channel_name(self) -> str:

        WRONG order (@abstractmethod above @property):
          silently breaks enforcement in Python 3.3+ with no error.
        """
        ...

    def get_stats(self) -> dict:
        """Concrete shared method — all subclasses inherit this for free."""
        return {
            "channel": self.channel_name,
            "sender_id": self._sender_id,
            "total_dispatches": self._dispatch_count,
        }


class EmailNotifier(Notifier):
    """Sends notifications via email."""

    def __init__(self, sender_id: str, smtp_host: str) -> None:
        super().__init__(sender_id)  # initialise shared state from ABC
        self._smtp_host = smtp_host

    @property
    def channel_name(self) -> str:
        return "Email"

    def send(self, message: str, recipient: str) -> bool:
        super().send(message, recipient)  # runs shared audit logging
        print(f"  [Email] SMTP:{self._smtp_host} To:{recipient} | {message[:80]}")
        return True


class SMSNotifier(Notifier):
    """Sends notifications via SMS with a 160-character limit."""

    @property
    def channel_name(self) -> str:
        return "SMS"

    def send(self, message: str, recipient: str) -> bool:
        super().send(message, recipient)
        truncated = message[:160]
        print(f"  [SMS] To:{recipient} | {truncated}")
        return True


class SlackNotifier(Notifier):
    """Broken — forgot to implement send()."""

    @property
    def channel_name(self) -> str:
        return "Slack"
    # send() is missing — ABC will catch this at instantiation time


# ============================================================
# Demo
# ============================================================

print("=== Creating valid notifiers ===")
email = EmailNotifier(sender_id="platform-svc", smtp_host="smtp.company.com")
sms = SMSNotifier(sender_id="platform-svc")

print("\n=== Sending notifications ===")
email.send("Deployment to prod-eu-west-1 succeeded.", "oncall@company.com")
print()
sms.send("Your verification code is 829341.", "+14155550199")

print("\n=== Stats (concrete inherited method) ===")
print(email.get_stats())
print(sms.get_stats())

print("\n=== Attempting to instantiate broken SlackNotifier ===")
try:
    slack = SlackNotifier(sender_id="platform-svc")
except TypeError as e:
    print(f"TypeError caught — ABC enforcement working: {e}")

print("\n=== Attempting to instantiate the abstract base itself ===")
try:
    base = Notifier(sender_id="test")
except TypeError as e:
    print(f"TypeError caught — cannot instantiate abstract class: {e}")

print("\n=== Missing abstract methods on the base ===")
print(f"Notifier.__abstractmethods__ = {Notifier.__abstractmethods__}")
▶ Output
=== Creating valid notifiers ===

=== Sending notifications ===
[AUDIT] Channel=Email Sender=platform-svc At=14:22:07 UTC Attempt=#1
[Email] SMTP:smtp.company.com To:oncall@company.com | Deployment to prod-eu-west-1 succeeded.

[AUDIT] Channel=SMS Sender=platform-svc At=14:22:07 UTC Attempt=#1
[SMS] To:+14155550199 | Your verification code is 829341.

=== Stats (concrete inherited method) ===
{'channel': 'Email', 'sender_id': 'platform-svc', 'total_dispatches': 1}
{'channel': 'SMS', 'sender_id': 'platform-svc', 'total_dispatches': 1}

=== Attempting to instantiate broken SlackNotifier ===
TypeError caught — ABC enforcement working: Can't instantiate abstract class SlackNotifier without an implementation for abstract method 'send'

=== Attempting to instantiate the abstract base itself ===
TypeError caught — cannot instantiate abstract class: Can't instantiate abstract class Notifier without an implementation for abstract methods 'channel_name', 'send'

=== Missing abstract methods on the base ===
Notifier.__abstractmethods__ = frozenset({'channel_name', 'send'})
Mental Model
The Abstract Method Body Is Opt-In Shared Logic, Not a Default
An abstract method with a body is not a contradiction. It is a two-level contract: you must override this method, and you may optionally call super() to reuse the shared logic inside it.
  • Without super(): the subclass owns the full implementation. The abstract method body is never executed.
  • With super(): the subclass runs the shared logic first (audit logging, validation, timestamps) then adds its own specific behaviour.
  • The override is always mandatory — the abstract keyword does not change because the body exists.
  • This pattern is sometimes called a hook method: the base defines what happens, the subclass decides whether to build on it or replace it entirely.
  • Use this when every subclass needs the same infrastructure behaviour but has distinct domain logic on top of it.
📊 Production Insight
Abstract method bodies are underused in Python ABCs. They are the correct place for audit logging, metric emission, rate limit checks, and validation that every implementation needs — code you would otherwise copy-paste into each subclass.
The @property + @abstractmethod stacking order bug is the most common silent ABC failure in production codebases. There is no error, no warning, and no linting rule that catches it by default.
Rule: add @property @abstractmethod properties to a test that instantiates a class which intentionally omits the property. If the test does not raise TypeError, the stacking order is wrong.
🎯 Key Takeaway
ABC enforcement works because ABCMeta intercepts instantiation and checks __abstractmethods__ before creating any object.
Abstract methods can have bodies — use this for shared infrastructure logic that subclasses opt into via super().
The @property @abstractmethod stacking order is not optional: @property must be outermost, @abstractmethod innermost. Wrong order silently breaks enforcement.

A Real-World Payment Pipeline — Abstract Classes in Production Context

Notification systems are a clean teaching example, but let us look at the failure mode that hurts the most: payment processing. A missing method in a payment processor does not just drop a Slack message — it silently skips a charge, and you find out when revenue reconciliation runs at the end of the month.

This example builds a complete payment pipeline with abstract classes: a base PaymentProcessor with abstract methods for the critical path (charge, refund, validate_card), abstract properties for configuration (currency, processor_name), and concrete methods for the shared infrastructure (logging, receipt formatting). Every concrete processor — Stripe, PayPal, crypto — inherits the infrastructure and is forced to implement the critical path.

The template method pattern is central here: the process_payment method is a concrete final-style method on the abstract class that calls validate_card, then charge, in a fixed sequence. No subclass can skip validation to speed up a checkout flow. The sequence is enforced by the abstract class, not by documentation.

io/thecodeforge/payments/payment_processor.py · PYTHON
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212
from abc import ABC, abstractmethod
from dataclasses import dataclass
from datetime import datetime
from typing import Optional


@dataclass
class PaymentResult:
    success: bool
    transaction_id: Optional[str]
    amount: float
    currency: str
    error_message: Optional[str] = None


class PaymentProcessor(ABC):
    """
    Abstract base for all payment processors.

    The process_payment() template method enforces the sequence:
      1. validate_card()  — must not be skippable
      2. charge()         — only runs if validation passes
      3. _log_transaction() — shared audit infrastructure

    Subclasses implement the domain-specific steps.
    The sequence is owned by this class and cannot drift.
    """

    def __init__(self, api_key: str, environment: str = "production") -> None:
        """Shared initialisation — every processor needs credentials and env."""
        if not api_key:
            raise ValueError("API key cannot be empty")
        self._api_key = api_key
        self._environment = environment
        self._transaction_count = 0

    # ── Abstract properties — configuration contracts ──

    @property
    @abstractmethod
    def processor_name(self) -> str:
        """Human-readable processor name (e.g., 'Stripe', 'PayPal')."""
        ...

    @property
    @abstractmethod
    def supported_currencies(self) -> list[str]:
        """List of ISO 4217 currency codes this processor accepts."""
        ...

    # ── Abstract methods — critical path ──

    @abstractmethod
    def validate_card(self, card_token: str) -> bool:
        """
        Validate payment method before attempting charge.
        Must return True if valid, False if invalid.
        Must NEVER return None.
        """
        ...

    @abstractmethod
    def charge(
        self, amount: float, currency: str, card_token: str
    ) -> PaymentResult:
        """
        Execute the charge against the payment gateway.
        Must return a PaymentResult — never raise silently.
        """
        ...

    @abstractmethod
    def refund(self, transaction_id: str, amount: float) -> PaymentResult:
        """Issue a full or partial refund."""
        ...

    # ── Template method — fixed pipeline, cannot be overridden ──

    def process_payment(
        self, amount: float, currency: str, card_token: str
    ) -> PaymentResult:
        """
        The fixed payment pipeline. Validation always precedes charge.
        No subclass can reorder or skip steps.
        """
        if currency not in self.supported_currencies:
            return PaymentResult(
                success=False,
                transaction_id=None,
                amount=amount,
                currency=currency,
                error_message=(
                    f"{self.processor_name} does not support {currency}. "
                    f"Supported: {self.supported_currencies}"
                )
            )

        if not self.validate_card(card_token):
            return PaymentResult(
                success=False,
                transaction_id=None,
                amount=amount,
                currency=currency,
                error_message="Card validation failed"
            )

        result = self.charge(amount, currency, card_token)
        self._log_transaction(result)
        return result

    # ── Concrete shared infrastructure ──

    def _log_transaction(self, result: PaymentResult) -> None:
        """Shared audit logging — all processors inherit this."""
        self._transaction_count += 1
        status = "SUCCESS" if result.success else "FAILED"
        print(
            f"[AUDIT] {self.processor_name} | {status} | "
            f"{result.currency} {result.amount:.2f} | "
            f"TxID={result.transaction_id} | "
            f"Env={self._environment} | "
            f"At={datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S')} UTC"
        )

    def get_session_stats(self) -> dict:
        return {
            "processor": self.processor_name,
            "environment": self._environment,
            "transactions_this_session": self._transaction_count,
            "supported_currencies": self.supported_currencies,
        }


# ── Concrete implementation: Stripe ──

class StripeProcessor(PaymentProcessor):

    @property
    def processor_name(self) -> str:
        return "Stripe"

    @property
    def supported_currencies(self) -> list[str]:
        return ["USD", "EUR", "GBP", "CAD", "AUD"]

    def validate_card(self, card_token: str) -> bool:
        is_valid = card_token.startswith("tok_") and len(card_token) > 6
        print(f"  [Stripe] Card validation: {'PASSED' if is_valid else 'FAILED'}")
        return is_valid

    def charge(
        self, amount: float, currency: str, card_token: str
    ) -> PaymentResult:
        print(f"  [Stripe] Charging {currency} {amount:.2f} via {card_token}")
        return PaymentResult(
            success=True,
            transaction_id="ch_stripe_" + card_token[-6:],
            amount=amount,
            currency=currency
        )

    def refund(self, transaction_id: str, amount: float) -> PaymentResult:
        print(f"  [Stripe] Issuing refund of {amount:.2f} for {transaction_id}")
        return PaymentResult(
            success=True,
            transaction_id="re_" + transaction_id,
            amount=amount,
            currency="USD"
        )


# ── Broken processor — forgot charge() ──

class BrokenProcessor(PaymentProcessor):
    """Simulates the 'forgot an abstract method' bug."""

    @property
    def processor_name(self) -> str:
        return "BrokenPay"

    @property
    def supported_currencies(self) -> list[str]:
        return ["USD"]

    def validate_card(self, card_token: str) -> bool:
        return True

    def refund(self, transaction_id: str, amount: float) -> PaymentResult:
        return PaymentResult(True, transaction_id, amount, "USD")

    # charge() is missing — ABC catches this


# ── Demo ──

print("=== Payment Processing Demo ===")
stripe = StripeProcessor(api_key="sk_live_abc123", environment="production")

result = stripe.process_payment(149.99, "USD", "tok_visa_4242")
print(f"  Result: success={result.success} txid={result.transaction_id}\n")

result = stripe.process_payment(49.99, "JPY", "tok_visa_4242")  # unsupported currency
print(f"  Result: success={result.success} error='{result.error_message}'\n")

print(stripe.get_session_stats())

print("\n=== Broken processor — ABC enforcement ===")
try:
    broken = BrokenProcessor(api_key="test_key")
except TypeError as e:
    print(f"TypeError at instantiation: {e}")
    print("The uncharged order never happened. ABC caught it before any customer was affected.")
▶ Output
=== Payment Processing Demo ===
[Stripe] Card validation: PASSED
[Stripe] Charging USD 149.99 via tok_visa_4242
[AUDIT] Stripe | SUCCESS | USD 149.99 | TxID=ch_stripe_4_4242 | Env=production | At=2026-04-15 14:22:07 UTC
Result: success=True txid=ch_stripe_4_4242

Result: success=False error='Stripe does not support JPY. Supported: ['USD', 'EUR', 'GBP', 'CAD', 'AUD']'

{'processor': 'Stripe', 'environment': 'production', 'transactions_this_session': 1, 'supported_currencies': ['USD', 'EUR', 'GBP', 'CAD', 'AUD']}

=== Broken processor — ABC enforcement ===
TypeError at instantiation: Can't instantiate abstract class BrokenProcessor without an implementation for abstract method 'charge'
The uncharged order never happened. ABC caught it before any customer was affected.
🔥The Template Method Pattern — Lock the Sequence, Delegate the Steps
  • The abstract class owns the algorithm order — validate, then charge, then log.
  • Subclasses own the individual steps — how to validate, how to charge, where to log.
  • This eliminates an entire category of bugs where a subclass reorders steps or skips one to 'optimise'.
  • Python does not have a final keyword, but the intent of process_payment() not being abstract is the signal: it is the algorithm, not a step.
  • Combine template method on the abstract class with abstract methods for each step — this is the production-grade pattern for any multi-step pipeline.
📊 Production Insight
Payment processing, data pipelines, and export workflows all share the same structural problem: a fixed sequence of steps where skipping any one step causes data loss or financial damage.
The template method on an abstract class is the correct solution — the sequence lives in one method that subclasses cannot override, and each step is abstract so every subclass must provide its implementation.
Rule: any multi-step process where order matters and steps must not be skipped belongs in a template method on an abstract class.
🎯 Key Takeaway
Abstract classes are essential in payment, auth, and data pipeline code where a silent missing-method failure causes financial damage or data loss.
The template method pattern — concrete method that calls abstract steps in a fixed order — prevents step reordering and step skipping across every subclass.
Concrete shared infrastructure methods (logging, stats, receipt formatting) belong on the abstract class so every subclass inherits them without duplication.

ABC vs typing.Protocol vs Regular Base Class — Choosing the Right Tool

Python gives you three mechanisms for sharing behaviour and enforcing structure across related classes: regular inheritance, ABCs, and typing.Protocol. Knowing which to reach for in a given situation is what separates a developer who knows the syntax from one who makes sound architectural decisions.

A regular base class is the wrong choice when any of the method slots must be overridden. Empty method bodies and pass returns look like defaults but provide no enforcement. They are a convention, not a contract. If you find yourself writing a method body that does nothing and hoping developers will override it, you want an ABC.

ABCs are the right choice when your related types share instance state (fields initialised in __init__), when you need instantiation-time enforcement (TypeError fires before any business logic), and when you want to provide concrete shared infrastructure (logging, validation, template methods) alongside the required contract. The ABC is both the contract and the shared library.

typing.Protocol is the right choice when you need a pure capability contract with no shared state, when the types that will satisfy the contract may already have their own base classes and cannot inherit from yours, or when you want static analysis tools like mypy to check conformance without any runtime inheritance. Protocol is structural subtyping — if an object has the right methods with the right signatures, it satisfies the Protocol regardless of what it inherits from. This is more Pythonic for plugin systems and third-party integration.

The practical heuristic: if you need shared state and shared implementation, use ABC. If you need only a contract that any type can satisfy, use Protocol. Never use a regular base class when method override is not optional.

io/thecodeforge/notifications/abc_vs_protocol.py · PYTHON
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111
from abc import ABC, abstractmethod
from typing import Protocol, runtime_checkable


# ── Option 1: typing.Protocol — pure contract, no inheritance required ──

@runtime_checkable
class NotifierProtocol(Protocol):
    """
    A pure capability contract using Protocol.

    Any object that has send() and channel_name with matching signatures
    satisfies this Protocol — no inheritance from NotifierProtocol required.
    This is structural subtyping: shape matters, not lineage.
    """

    def send(self, message: str, recipient: str) -> bool:
        ...

    @property
    def channel_name(self) -> str:
        ...


# ── Option 2: ABC — contract + shared state + shared implementation ──

class AbstractNotifier(ABC):
    """
    An ABC that combines the Protocol contract with shared infrastructure.

    Use this when you want:
      - Instantiation-time TypeError enforcement (not just mypy warnings)
      - Shared state (__init__ fields) inherited by all subclasses
      - Shared concrete methods (logging, stats) all subclasses get for free
    """

    def __init__(self, sender_id: str) -> None:
        self._sender_id = sender_id
        self._sent_count = 0

    @abstractmethod
    def send(self, message: str, recipient: str) -> bool:
        ...

    @property
    @abstractmethod
    def channel_name(self) -> str:
        ...

    def get_stats(self) -> dict:
        """Shared concrete method — all subclasses inherit this."""
        return {
            "sender_id": self._sender_id,
            "channel": self.channel_name,
            "sent": self._sent_count,
        }


# ── Satisfies Protocol without inheriting from it ──

class ThirdPartyEmailClient:
    """
    From a third-party library — we cannot modify this class.
    It has the right methods, so it satisfies NotifierProtocol
    without any inheritance from our code.
    """

    @property
    def channel_name(self) -> str:
        return "ThirdPartyEmail"

    def send(self, message: str, recipient: str) -> bool:
        print(f"  [ThirdPartyEmail] To:{recipient} | {message}")
        return True


# ── Satisfies ABC by inheriting from it ──

class SlackNotifier(AbstractNotifier):

    @property
    def channel_name(self) -> str:
        return "Slack"

    def send(self, message: str, recipient: str) -> bool:
        self._sent_count += 1
        print(f"  [Slack] To:{recipient} | {message}")
        return True


# ── Demo: both work as notification channels ──

print("=== Protocol isinstance check ===")
client = ThirdPartyEmailClient()
print(f"ThirdPartyEmailClient satisfies NotifierProtocol: {isinstance(client, NotifierProtocol)}")
# True — because it has the right method signatures, regardless of inheritance

print("\n=== ABC enforcement ===")
slack = SlackNotifier(sender_id="platform-svc")
slack.send("Deployment complete", "#alerts")
print(slack.get_stats())  # Inherited concrete method

print("\n=== Which approach to use? ===")
print("Protocol: third-party integration, no shared state, type checking via mypy")
print("ABC:      shared state needed, instantiation-time enforcement, shared infrastructure")


# ── The gotcha: Protocol isinstance without @runtime_checkable ──
# Without @runtime_checkable, isinstance(obj, NotifierProtocol) raises TypeError.
# Add @runtime_checkable when you need runtime isinstance checks.
# Without it, Protocol is a static analysis tool only.
▶ Output
=== Protocol isinstance check ===
ThirdPartyEmailClient satisfies NotifierProtocol: True

=== ABC enforcement ===
[Slack] To:#alerts | Deployment complete
{'sender_id': 'platform-svc', 'channel': 'Slack', 'sent': 1}

=== Which approach to use? ===
Protocol: third-party integration, no shared state, type checking via mypy
ABC: shared state needed, instantiation-time enforcement, shared infrastructure
🔥The Interview Answer That Shows Architectural Thinking
When asked 'ABCs versus Protocol in Python', do not just list features. Say this: 'I use Protocol when I need a pure capability contract that any type can satisfy regardless of its inheritance — especially for third-party integration or plugin systems where I cannot control what the implementors inherit from. I use ABC when my related types share instance state and I need instantiation-time enforcement, not just mypy warnings. In practice, I often define both: a Protocol for the public interface that external code depends on, and an optional ABC that provides shared infrastructure for implementors who want it. This gives consumers the flexibility of Protocol and the convenience of shared implementation.'
📊 Production Insight
Protocol is the right tool for plugin systems and third-party integration — any type that has the right method signatures satisfies the contract without modifying the third-party code.
ABC is the right tool when shared state and instantiation-time enforcement matter more than inheritance flexibility.
Rule: start with Protocol for the public contract. Add an optional ABC for the shared implementation. Let consumers choose which to use based on their constraints.
🎯 Key Takeaway
Regular base class with empty methods is never the right choice when override is mandatory — it provides convention without enforcement.
ABC provides instantiation-time enforcement and shared infrastructure but requires inheritance and allows only one abstract base per class.
Protocol provides structural subtyping without inheritance — any class with matching methods satisfies it — but enforcement relies on mypy rather than the runtime.
🗂 ABC vs typing.Protocol vs Regular Base Class
Choose based on whether you need shared state, runtime enforcement, or flexibility for third-party types.
Feature / AspectAbstract Base Class (ABC)typing.ProtocolRegular Base Class
Contract enforcementAt instantiation time — TypeError if any abstract method is missingAt static analysis time — mypy warning if methods are missing. Runtime check only with @runtime_checkable.Never — missing method overrides are silently inherited as no-ops
Shared instance stateYes — __init__ fields shared across all subclassesNo — Protocol cannot hold instance fieldsYes — same as ABC
Shared concrete methodsYes — inherited by all subclasses without reimplementingNo — Protocol only defines signaturesYes — but override is not enforced
Requires inheritance?Yes — subclass must inherit from the ABCNo — any class with matching method signatures satisfies the ProtocolYes — subclass must inherit from the base class
Works with isinstance()?Yes — including virtual subclasses via register()Yes if @runtime_checkable is present — otherwise isinstance raises TypeErrorYes — for real subclasses only
Best forRelated types sharing state and infrastructure with runtime enforcementPure interface contracts, plugin systems, third-party integrationSharing concrete logic with no enforcement — use sparingly
Available sincePython 2.6 (abc module)Python 3.8 (typing.Protocol)Always
Catches missing methodsAt object creation time — TypeError before any business logic runsAt static analysis time via mypy or pyright, not at runtimeNever — no enforcement mechanism exists

🎯 Key Takeaways

  • Abstract classes move an entire category of bugs from silent runtime failures to loud TypeError at object creation time — a TypeError at instantiation beats a missed payment or dropped alert by an enormous margin.
  • An abstract method can have a body — use this for shared audit logging, validation, or metric emission that every subclass should opt into via super(). The override remains mandatory; the body is opt-in shared infrastructure.
  • For pure capability contracts with no shared state — especially for third-party or plugin integration — prefer typing.Protocol over ABC. It is more Pythonic, requires no inheritance, and lets any class satisfy the contract by shape rather than lineage.
  • The @property and @abstractmethod stacking order is not a style preference — @property must be outermost, @abstractmethod must be innermost. Reversed order silently breaks enforcement with no error, no warning, and no linting feedback.

⚠ Common Mistakes to Avoid

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

    @abstractmethod decorators are present but Python happily instantiates incomplete subclasses without any error or warning. The decorator sets a flag on the function but ABCMeta never checks that flag because the class does not use ABCMeta as its metaclass.

    Fix

    Always write class MyBase(ABC): and import ABC from the abc module: from abc import ABC, abstractmethod. Without ABC in the inheritance chain, @abstractmethod is completely inert — it is a decorative marker with no runtime enforcement power. Verify by printing MyBase.__abstractmethods__ — if it is empty when it should not be, the class does not inherit from ABC.

    Stacking @property and @abstractmethod in the wrong order
    Symptom

    Writing @abstractmethod on the top line and @property below it (the reversed order) instead of @property on top and @abstractmethod below. The result is that the abstract enforcement is silently ignored in Python 3.3 and later. The subclass instantiates without implementing the property, with no error and no warning.

    Fix

    The correct stacking order is always @property as the outermost decorator (first line above def), then @abstractmethod as the innermost (second line above def). This is counterintuitive because decorators apply bottom-up, but the result is that @abstractmethod is applied to the function first, then @property wraps it. Add this specific combination to your ABC tests: create a subclass that intentionally omits the property and assert that TypeError is raised. If no TypeError is raised, the stacking order is wrong.

    Expecting partial implementation to allow instantiation
    Symptom

    A subclass implements two of three abstract methods and the developer expects to instantiate it for the two methods that are done. Python raises TypeError for the missing third method regardless of how many were implemented.

    Fix

    Every single abstract method must be overridden — there is no partial credit, no grace period, and no way to mark some abstract methods as optional. If you genuinely need an intermediate partially-implemented class, mark it abstract itself: class IntermediateBase(ConcreteParent, ABC): — this defers the remaining obligations to the next concrete class in the hierarchy.

    Using ABC.register() expecting it to enforce the contract
    Symptom

    isinstance(obj, MyABC) returns True for a registered class even if that class has none of the abstract methods implemented. No TypeError is raised, no warning is produced, and calling any of the supposedly implemented methods raises AttributeError.

    Fix

    register() is a type declaration for isinstance checks, not a validation mechanism. It is designed for integrating legacy or third-party code that you cannot modify but that happens to implement the required interface. After any register() call, write a test that instantiates the registered class and calls every method defined in the ABC. This is the manual verification that register() deliberately skips.

Interview Questions on This Topic

  • QCan you instantiate an abstract class in Python, and what exactly happens if you try? What if the abstract class has no abstract methods defined?JuniorReveal
    You cannot instantiate an abstract class that has any unimplemented abstract methods. Python raises TypeError with a message that names the exact missing methods. This happens at instantiation time — when you call the class like a function — not when you define the class or import the module. If the abstract class inherits from ABC but has zero abstract methods — because all methods are concrete — the behaviour is the same: Python still raises TypeError if you try to instantiate the abstract class directly. The ABC metaclass blocks instantiation of any class explicitly marked as abstract, regardless of whether abstract methods exist. This is intentional: the abstract modifier signals that the class is conceptually incomplete even if all current methods happen to be implemented. The only way to allow instantiation is to remove ABC from the inheritance chain, or to create a concrete subclass that satisfies all abstract obligations and instantiate that instead.
  • QWhat is the difference between an abstract method that has a body and one that just has ... or pass? When would you provide an implementation in an abstract method?Mid-levelReveal
    Both are valid abstract methods and both require override by every concrete subclass. The difference is in what happens when a subclass calls super().method_name(). With ... or pass, the abstract method body does nothing. Calling super() in the subclass is technically valid but returns None or does nothing useful. With a real body, the abstract method provides shared logic that subclasses can opt into via super(). The override is still mandatory — the subclass must implement the method — but it can choose to call super() to reuse the base logic. Use a body when every subclass needs the same infrastructure behaviour but you still want each subclass to consciously own its implementation. Common patterns include audit logging (the base records the timestamp and transaction ID, the subclass provides the channel-specific delivery), validation that all subclasses need before their specific logic, and metric emission that should happen regardless of which concrete class is used. This pattern is sometimes called a hook method: the base defines what infrastructure runs, and the subclass decides whether to build on it or replace it entirely. The key insight is that having a body does not make the method optional — it just makes the shared behaviour available to subclasses that want it.
  • QHow do you enforce that a subclass must override a property rather than a regular method? Walk through the exact decorator stacking order and why it matters.Mid-levelReveal
    Use @property combined with @abstractmethod, with @property as the outermost decorator and @abstractmethod as the innermost. The correct structure top to bottom is: @property on line one, @abstractmethod on line two, def method_name on line three. The reason the order matters is how Python applies decorators. Decorators apply bottom-up: @abstractmethod is applied to the function first, creating an abstract function object with the __isabstractmethod__ flag set to True. Then @property wraps that abstract function, creating a property object. The ABCMeta metaclass checks __isabstractmethod__ on the property object and correctly identifies it as an abstract requirement. If you reverse the order — @abstractmethod on the top line, @property on the second — @property is applied to the function first, creating a property object. Then @abstractmethod is applied to the property object, which does not propagate the abstract flag correctly in Python 3.3 and later. The result is that the property is created normally but the abstract enforcement is silently dropped. The subclass instantiates without implementing the property, and there is no error, no warning, and no indication that the contract has been violated. The best way to catch this in production is to write a specific test: create a subclass that intentionally omits the abstract property and assert that instantiating it raises TypeError. If no TypeError is raised, the stacking order is wrong.
  • QYou are building a plugin system where third-party developers write notification handlers. How would you use ABCs versus Protocols to enforce the plugin contract, and what trade-offs does each approach have?SeniorReveal
    I would use both, in a pattern that separates the public contract from the optional shared infrastructure. First, I would define a NotifierProtocol using typing.Protocol with @runtime_checkable. This is the public contract — send(message, recipient) returning bool, and channel_name as a property. Third-party developers satisfy this Protocol without any inheritance from our code, which is critical because they may already extend some other framework base class and cannot inherit from ours. The plugin loader checks isinstance(plugin, NotifierProtocol) at registration time to verify the shape is correct. Second, I would provide an AbstractNotifier ABC that implements NotifierProtocol and adds shared infrastructure: audit logging in the template method, retry logic, rate limit tracking, and a get_stats() concrete method. Developers who start from scratch can extend AbstractNotifier and get all the infrastructure for free. Developers who cannot use our base class implement the Protocol directly. The trade-offs are real. ABC gives instantiation-time TypeError enforcement — the bug surfaces the moment someone tries to create the object. Protocol relies on mypy or pyright for enforcement, which only fires if the CI pipeline runs static analysis. Without @runtime_checkable, you cannot even use isinstance at runtime. With @runtime_checkable, isinstance checks only verify method names exist, not their signatures. For critical systems like payments or authentication, I would require ABC inheritance and document that the ABC is the integration point. For flexible plugin systems where third-party developers need maximum freedom, I would use Protocol for the contract and provide ABC as a convenience. The contract tests are mandatory either way: a shared test suite that every plugin must pass, verifying that the right methods return the right types and behave correctly on edge cases.

Frequently Asked Questions

Can a Python abstract class have a constructor (__init__)?

Yes, and this is one of the key advantages of ABC over typing.Protocol. An abstract class can have a fully functional __init__ with instance variables, validation logic, and shared dependency injection. Concrete subclasses call super().__init__() to reuse it. This shared initialisation is a primary reason to choose ABC over Protocol when your related types need common state — a sender ID, a logger instance, credentials, or a configuration object — that should be initialised consistently for every implementation.

What happens if I inherit from an abstract class but do not implement all abstract methods?

Python raises TypeError the moment you try to instantiate the subclass — not when you define the class, not when you import the module, but specifically when you call the class to create an object. The error message explicitly names every missing method. The subclass class definition itself is perfectly legal and compiles without error. Only the instantiation attempt fails. This timing means you can define intermediate abstract classes that leave some methods still abstract and defer the obligation to the next concrete class in the hierarchy.

Is a Python abstract class the same as a Java interface?

Not quite, and the differences matter. A Java interface is a pure contract with no state and no implementation prior to Java 8 default methods. A Python abstract class can have both abstract methods and fully implemented concrete methods, instance fields via __init__, and shared constructors. The closer Python equivalent to a Java interface is typing.Protocol for pure contracts, or an ABC where every method is abstract and __init__ has no instance fields — something between the two. In practice, Python ABCs are closer to Java abstract classes than Java interfaces, which is exactly the right comparison given the naming.

When should I use ABC.register() and when should I avoid it?

Use register() only for legacy or third-party code that you cannot modify but that genuinely implements the required interface. It makes isinstance() return True for the registered class without requiring any inheritance change in code you do not control. Never use register() for classes you own — if you own the class, make it inherit from the ABC and get real enforcement. The critical gotcha: register() provides zero abstract method enforcement. isinstance() will return True even if the registered class has none of the required methods. Always write a test that calls every abstract method on an instance of any registered class to manually verify the contract is actually satisfied.

🔥
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.

← PreviousMagic Methods in PythonNext →Multiple Inheritance in Python
Forged with 🔥 at TheCodeForge.io — Where Developers Are Forged