Abstract Base Classes in Python: Enforce Contracts, Not Just Code
- 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.
- 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
TypeError on instantiation claiming abstract methods are not implemented — but you are sure you implemented them
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('_')])"@abstractmethod is not enforcing anything — subclass with missing methods instantiates without error
python -c "from your_module import YourBase; print(type(YourBase)); print(YourBase.__mro__)"grep -n 'class.*ABC\|ABCMeta\|from abc' your_module.pyisinstance() returns True for a class that does not implement the required methods
python -c "from your_module import YourABC, SuspectClass; print(issubclass(SuspectClass, YourABC)); print(SuspectClass in YourABC.__subclasses__())"grep -n 'register' your_module.pyProduction Incident
CryptoProcessor.charge() call returned None, which the calling code interpreted as a falsy result and silently skipped the payment confirmation step.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.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.Production Debug GuideCommon symptoms when ABCs are misused or missing in production Python systems.
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.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.
# ============================================================ # 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.
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'})
- 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.
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.
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__}")
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'})
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.
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.")
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.
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.isinstance() returns True even if no abstract methods exist on the registered class.register() exclusively for third-party code you cannot modify. Never for your own classes.register() deliberately does not provide.| Feature / Aspect | Abstract Base Classes (ABC) | Regular Inheritance | Duck Typing |
|---|---|---|---|
| Contract enforcement | At instantiation time — TypeError if any abstract method is missing, with a clear error message naming the exact method | No enforcement — missing methods silently return None (if base has a default) or raise AttributeError only at call time | No 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 timing | Earliest — at object creation time, before any business logic runs | Late — when the missing method is actually called in running code, which may be days after deployment | Latest — at call time in running code. Same timing as regular inheritance but with no base class to provide even a broken default. |
| isinstance() support | Yes — including virtual subclasses via register(). Supports logical type membership beyond the class hierarchy. | Yes — but only for classes that directly inherit from the base class | No — no class hierarchy to check against. You can only check for method existence via hasattr(). |
| Best use case | Frameworks, plugin systems, public APIs, team contracts where you cannot control who writes subclasses | Sharing concrete implementation across related classes where the base class itself is a valid, instantiable thing | Scripts, 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 default | N/A — no base class, no methods to share |
| Abstract property support | Yes — @property combined with @abstractmethod enforces configuration-style contracts | No built-in equivalent — you can use @property but there is no mechanism to require subclasses to override it | No equivalent |
| Python import required | from abc import ABC, abstractmethod | None — built-in language feature | None — 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
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
- 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 isregister()the correct choice?Mid-levelReveal - 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
- 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
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.
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.