Alex Lin Wang 王帅

Interface Stubs

September 1, 2025 • 4 min read
Learn how to resolve circular import dependencies using Python's Abstract Base Classes (ABC). Create clean module boundaries with interface stubs that break dependency cycles while maintaining type safety.

Imagine you are building two separate modules, but within each one you declare two different dataclasses as such:

@dataclass
class Order:
    user: User
    item: str

@dataclass
class User:
    name: str
    
    def create_order(self, item: str) -> Order:
        return Order(self, item)

However, the only problem is that, if these are in different modules, you'll have a circular import issue due to each class relying on definitions present in the other.

To resolve this, create interface stubs with abstract classes for the core data structures and then use them as type hints for the actual concrete class. For the above example, you may create a new file interface.py which contains this implementation:

import abc

class IOrder(metaclass=abc.ABCMeta):
    user: 'IUser'
    item: str

class IUser(metaclass=abc.ABCMeta):
    name: str
    
    @abc.abstractmethod
    def create_order(self, item: str) -> IOrder:
        ...

Then you can have the actual concrete class implementations to use said interfaces to allow for modularization in separate files. First, define Order as:

from dataclasses import dataclass
from interface import IOrder, IUser

@dataclass
class Order(IOrder):
    user: IUser
    item: str

Then define the User in a separate file as such:

from dataclasses import dataclass
from interface import IUser, IOrder

@dataclass
class User(IUser):
    name: str
    
    def create_order(self, item: str) -> IOrder:
        return IOrder(self, item)

Here are a list of benefits:

  • Breaks circular dependencies
  • Improves testability with mocking interfaces
  • Reduces dependency bloat
  • More clearly documented + modularized

This pattern is particularly useful in larger codebases where you have complex relationships between modules. By defining abstract interfaces, you create a contract that both modules can depend on without directly importing each other. This makes your code more maintainable and easier to test, as you can mock the interfaces during unit testing.

Questions or feedback? Feel free to reach out!