Day 20 · ~17m

Abstract Base Classes: Formal Contracts That Raise Errors at Definition Time

After Protocol, when would you use ABC? For shared state in __init__, enforced super() calls, strict membership, and TypeError raised at instantiation.

student (thinking)

So yesterday I learned that Protocol is about structure — if you have the right methods, you're good. But I still see ABC everywhere in our codebase. After learning Protocol, when would I actually use ABC?

teacher (focused)

Here's the honest difference. Protocol says "if you walk and quack like a duck, I'll treat you as a duck." ABC says "you must explicitly inherit from Duck, and if you do not implement every abstract method, Python raises an error the moment you try to create an instance."

student (curious)

Wait. Raises an error when you create an instance? Not at type-check time?

teacher (serious)

Actually — at class definition time. The moment your interpreter finishes reading your class body, Python checks: does this class implement all abstract methods from the ABC? If not, you cannot instantiate it. You'll get a TypeError before any code runs.

student (surprised)

That is different from Protocol. Protocol raises errors at type-check time if the type checker is running, but at runtime it doesn't care.

teacher (neutral)

Exactly. ABC is stricter at runtime. It also supports something Protocol cannot: shared state and inherited init methods that subclasses must call with super().

student (excited)

Oh — that is the thing. Subclasses calling super().init().

teacher (focused)

Protocol cannot enforce that. Protocol only checks method signatures. It does not care if you call super().init. ABC can require it in the base class, and subclasses that do not call it will... well, they can still instantiate, but their parent's initialization code is skipped. ABC does not prevent it — it just makes it possible to structure.

student (thinking)

So ABC is for when you need a base class with shared initialization state. Protocol is for when you just need a structural contract.

teacher (encouraging)

That is the exact decision tree. If your contract includes "all subclasses must run the parent init" or "all subclasses share this attribute," use ABC. Otherwise, Protocol.

student (focused)

Let me think about our order notification system. We have EmailNotifier, SMSNotifier, and we're adding SlackNotifier. All three need to track the order_id from the base class. Should that be an ABC?

teacher (serious)

Yes. Because all three need to share the same order_id storage. If one of them forgets to call super().init(order_id), it will not have the order_id attribute. ABC makes that structure explicit.

student (curious)

But how does ABC enforce that they call super().init?

teacher (neutral)

It doesn't enforce it in the way you might think. What ABC does is this: define an abstractmethod. If a subclass does not implement it, you get a TypeError when you try to instantiate the subclass. So if you define abstract send_notification(), every subclass must implement send_notification() or you cannot create an instance.

student (thinking)

But there is no way to make ABC check that super().init() was actually called?

teacher (serious)

Correct. That is why the pattern works best when the abstract method itself makes the call. If your EmailNotifier.send_notification() calls super().init() — no wait, that is wrong. Let me be precise.

student (confused)

I'm confused.

teacher (focused)

The ABC pattern is: the base class has init with the shared state. The subclass overrides send_notification() but must call super().init() in its own init. There is no requirement — it is just the pattern that works.

student (thinking)

So if EmailNotifier has its own init, and it does not call super().init(), the order_id will not be set?

teacher (neutral)

Right. And then when send_notification() tries to use self.order_id, it crashes with AttributeError. There is no magic enforcement. But the pattern guides the design: subclasses know they need to call super().init.

student (amused)

That feels like the type system relying on developers reading documentation.

teacher (encouraging)

Welcome to Python. We have a type checker now, and you should use it. But the core language still trusts that you read the code. ABC makes the structure more explicit than Protocol does.

student (focused)

Okay, so let me picture the order notifier ABC. OrderNotifier has order_id from init. EmailNotifier and SMSNotifier both override send_notification() to send notifications. They both call super().init(order_id) to get the parent's initialization.

teacher (neutral)

Exactly. And here is the next part: what if someone writes a BadNotifier that does not inherit from OrderNotifier?

student (thinking)

With Protocol, it wouldn't matter. As long as BadNotifier has send_notification(), it satisfies the Protocol.

teacher (serious)

Right. With ABC, you cannot use it in code that expects OrderNotifier. Only subclasses of OrderNotifier count. That is the membership card.

student (curious)

So which one should the function parameter accept?

teacher (focused)

That is your design choice. If you want to accept any class that can send notifications — Protocol. If you want to restrict to only OrderNotifier subclasses that share the order_id state — ABC.

student (excited)

For our system, we want the order_id enforcement. So ABC.

teacher (serious)

Good. And here is the abstract method part: define send_notification() as an @abstractmethod on the ABC. If someone tries to create a subclass and forgets to implement send_notification(), Python raises TypeError immediately.

student (thinking)

But wait — they can still try to subclass it without implementing send_notification()?

teacher (neutral)

They can try. But the moment they try to create an instance of their incomplete subclass, they get TypeError: Can't instantiate abstract class BadNotifier with abstract method send_notification.

student (surprised)

Oh! So it is not that you cannot create the class. You can create the class definition. But you cannot create instances of it.

teacher (focused)

Right. The error happens at instantiation, not at class definition. Some people call this "definition time" because the interpreter is reading the class body, but technically it is at instantiation time.

student (thinking)

So the pattern is: mark send_notification() as @abstractmethod on OrderNotifier. When EmailNotifier is defined, if it does not implement send_notification(), that is okay. But the moment someone tries to do EmailNotifier(), Python says no.

teacher (encouraging)

Exactly right. And that error message is so clear that developers cannot miss it. "Abstract method send_notification not implemented." They immediately know what to do.

student (focused)

And for the order_id, the subclass just calls super().init(order_id) in its own init?

teacher (neutral)

Yes. EmailNotifier.init() accepts email as a parameter, calls super().init(order_id) to set the parent state, then stores self.email. Now EmailNotifier has both order_id from the parent and email from itself.

from abc import ABC, abstractmethod

class OrderNotifier(ABC):
    def __init__(self, order_id: str):
        self.order_id = order_id  # shared state all subclasses get

    @abstractmethod
    def send_notification(self, message: str) -> str: ...

class EmailNotifier(OrderNotifier):
    def __init__(self, order_id: str, email: str):
        super().__init__(order_id)   # parent sets self.order_id
        self.email = email           # EmailNotifier adds self.email

    def send_notification(self, message: str) -> str:
        return f"Email to {self.email}: {message}"

# EmailNotifier works — both attrs available
n = EmailNotifier("ORD-001", "alice@example.com")
print(n.order_id, n.email)   # ORD-001 alice@example.com

# IncompleteNotifier fails — no send_notification implementation
class IncompleteNotifier(OrderNotifier): pass
# IncompleteNotifier("ORD-002")  # TypeError: Can't instantiate abstract class
student (excited)

That is cleaner than Protocol. The ABC makes the structure explicit — order_id is always present, send_notification() is always present, and subclasses that do not implement send_notification() cannot be instantiated.

teacher (serious)

That is why ABC is still used in production code everywhere. The structure is enforced. The state is shared. Subclasses cannot accidentally skip the initialization.

student (curious)

And if you wanted to add an @abstractproperty? Like notification_type that each subclass must define?

teacher (focused)

You would mark it with @property and @abstractmethod. The type checker and the ABC both enforce that subclasses implement it.

student (thinking)

So the comparison is: Protocol if you just need a duck-typing contract. ABC if you need shared state, explicit membership, and runtime errors on incomplete subclasses.

teacher (proud)

That is the exact decision. You now understand why your codebase has both.

student (focused)

Tomorrow is dataclasses, right? I have a feeling ABC and dataclasses together are going to be powerful.

teacher (serious)

Tomorrow the type system meets data construction. You're going to see how @dataclass can work with ABC, how it auto-generates init, and what happens when you have both. You've earned it.