Python Classes Interfaces

In languages like Java, interfaces are a first-class construct. You define an interface, implement it in multiple classes, and swap those implementations in and out via configuration or dependency injection. Python doesn’t have a built-in interface keyword, but you can achieve the same clarity and safety using the abc (Abstract Base Classes) module. In this post, we’ll close that gap by showing you how to:

  1. Define a strict “interface” in Python
  2. Implement multiple concrete classes
  3. Dynamically select one implementation at runtime
  4. Even get isinstance checks to work as if the interface were real

Why You Might Want an “Interface” in Python

Python is wonderfully dynamic: any object with a display() method can be used interchangeably, whether or not it formally “implements” an interface. But there are times when you want:

  • Compile-time guarantees (or at least “fail early” guarantees) that certain methods exist
  • Centralized factories that pick one implementation based on config or environment
  • Cleaner, self-documenting code when multiple teams share the same API

Enter the abc module: with an abstract base class (ABC), you can declare methods that must be implemented, and Python will refuse to instantiate a subclass that doesn’t provide them.

The Core Code

Below is a small, self-contained example. We define:

  • Base – our abstract “interface”
  • A and B – two concrete implementations
  • Interface – a tiny factory that picks A or B by a simple condition and delegates all calls
from abc import ABC, abstractmethod

class Base(ABC):
    @abstractmethod
    def display(self) -> str:
        pass

class Interface(Base):
    def __init__(self, condition: str):
        if condition == "A":
            self._instance = A()
        elif condition == "B":
            self._instance = B()
        else:
            raise ValueError("Please choose 'A' or 'B'")
    
    def display(self) -> str:
        return self._instance.display()
    
    def __instancecheck__(self, instance):
        return isinstance(self._instance, instance)

class A(Base):
    def __init__(self):
        self.name = "Class A"
    
    def display(self) -> str:
        return f"This is {self.name}"

class B(Base):
    def __init__(self):
        self.name = "Class B"
    
    def display(self) -> str:
        return f"This is {self.name}"


if __name__ == "__main__":
    # Test valid conditions
    for condition in ["A", "B", "C"]:

        try:
            instance = Interface(condition)
            print(f"\nTesting {condition}:\tDisplay: {instance.display()}\tType: {type(instance)}\tIs Interface: {isinstance(instance, Interface)}\tIs Base: {isinstance(instance, Base)}\tIs A: {isinstance(instance._instance, A)}\tIs B: {isinstance(instance._instance, B)}")
        except ValueError as e:
            print(f"\nCaught expected error: {e}")
    
    # Try to instantiate Displayable directly (will fail)
    try:
        displayable = Base()
    except TypeError as e:
        print(f"\nCannot instantiate abstract class Displayable: {e}")
Python

Breaking It Down

Abstract Base Class

class Base(ABC):
    @abstractmethod
    def display(self) -> str: ...
Python

Marks Base as abstract.

Any subclass must implement display() or Python will raise a TypeError.

Concrete Implementations

class A(Base):
    def display(self): ...
class B(Base):
    def display(self): ...
Python

Both A and B supply display(), satisfying the contract.

Factory / Adapter

class Interface(Base):
    def __init__(..., condition):
        self._instance = A() or B()
    def display(self): return self._instance.display()
    def __instancecheck__(...): ...
Python

    Chooses A or B based on a string.

    Delegates display() calls to the chosen object.

    Overrides __instancecheck__ so that isinstance(interface_obj, A) is True when appropriate—handy for legacy code or specialized logic.

    Error Handling

    Passing anything other than "A" or "B" to Interface results in a clear ValueError.

    Instantiating Base directly raises a TypeError, reminding you that it’s meant only as a base.

    Sample Output

    Testing A:      Display: This is Class A        Type: <class '__main__.Interface'>      Is Interface: True      Is Base: True   Is A: True      Is B: False
    
    Testing B:      Display: This is Class B        Type: <class '__main__.Interface'>      Is Interface: True      Is Base: True   Is A: False     Is B: True
    
    Caught expected error: Please choose 'A' or 'B'
    
    Cannot instantiate abstract class Displayable: Can't instantiate abstract class Base without an implementation for abstract method 'display'
    ShellSession

    Objective Judgement

    Pros

    Formalized Contracts
    By subclassing ABC and decorating methods with @abstractmethod, you get an explicit, enforceable contract. Anyone extending your code must implement the required methods or Python will refuse to instantiate the class—catching errors early.

    Clear Separation of Concerns

    • Interface (Base): defines what operations must exist.
    • Implementations (A, B, …): define how those operations work.
    • Factory/Adapter (Interface): handles which implementation to use.
      This separation makes it easier to reason about, test, and document each piece independently.

    Runtime Flexibility
    You can drive selection purely by configuration (environment variables, command-line flags, JSON/YAML configs, etc.), without touching application logic. Swapping out behavior can be as simple as changing one string in a config file.

    Plug-and-Play Extensibility
    Adding a new implementation (CD, etc.) is straightforward: subclass Base, implement display(), then hook it into your factory (or registry). This keeps your core code closed for modification but open for extension.

    Improved Tooling & Introspection
    IDEs and linters recognize @abstractmethod and can flag unimplemented methods. Documentation generators (Sphinx, pdoc, etc.) can automatically document your “interface” and list its concrete implementers.

    Cons

    Factory Maintenance Overhead
    Every time you add a new subclass, you have to remember to update the factory (Interface.__init__ or your registry mapping). Forgetting to do so can lead to runtime ValueErrors.

    Boilerplate for Small Projects
    For simple scripts or small codebases, this pattern can feel heavy—sometimes writing free-form duck-typed code is more concise and “Pythonic.”

    Surprising __instancecheck__ Behavior
    Overriding __instancecheck__ to make isinstance(interface_obj, A) return True can confuse readers who don’t expect an adapter to masquerade as its underlying object. Use it judiciously and document it clearly.

    Runtime, Not Compile-Time, Enforcement
    Python’s checks still happen at runtime, so you don’t get compile-time safety like in statically typed languages. You’ll only see missing-method errors when that code path is executed.

    Potential Performance Hit
    The indirection of delegating every call through self._instance adds a tiny overhead. In most apps it’s negligible, but in ultra-high-performance loops it can matter.

    Enhancement

    As your codebase grows, that simple if/elif chain in your factory can start to feel brittle. A common next step is to replace it with a registry—a plain dictionary that maps a string key to a concrete class. Instead of editing the factory every time you add a new subclass, you simply register it in one place. That keeps your instantiation logic clean, and makes it trivial to add or remove implementations without touching any conditional blocks.

    Once you’ve centralized your mapping, driving selection from configuration is a natural evolution. Read the desired key from a JSON or YAML file (or even an environment variable), and hand it straight to your factory. Suddenly swapping implementations is as easy as changing one value in a config file, and you can have different behaviors in development, staging, and production without a single code change.

    If you’re building a plugin ecosystem—say, letting third-party packages drop in their own handlers—look into Python’s entry-point system. By declaring an entry point group in your setup configuration, external packages can register new implementations that your application discovers at runtime. Your registry becomes self-populating, and new behavior can be added simply by installing another package.

    Finally, don’t forget to bolster all this with static checks and tests. If you’re using mypy, a typing.Protocol can let you ensure at “compile” time that each implementation really does provide display(), without the runtime overhead of an ABC. And in your test suite, write a single parametrized test that exercises every registered implementation against the same set of expectations. That way you’ll catch any missing methods or mis-registrations long before deployment, and keep your interface layer rock solid.

    Conclusion

    Although Python doesn’t have a built-in interface keyword, the abc module gives you powerful tools to enforce and document your APIs. By combining an abstract base class, a simple factory/adapter, and a dash of magic in __instancecheck__, you can get almost the same clarity and safety you’d expect in Java—along with all the dynamic goodness that makes Python so flexible. Give it a try in your next project!


    Posted

    in

    by

    Comments

    Leave a Reply

    Your email address will not be published. Required fields are marked *

    🧭