The circular import problem in Python. Some module foo imports module bar, but bar also imports foo. On itself, it's not necessarily a problem. Python allows it. Depending on how both modules interact, you might not even notice there is cycle in the dependency chain.

However, when you have a problem, some serious hair-pulling might ensue.

There are enough places around the web that elaborate on this issue, play the blame game ("bad design!") and offer possible solutions. These solutions have varying degrees of ugliness or architectural itchiness: rethink your coupling/dependencies, introduce abstract interfaces, merge modules, split modules, use local imports, defer imports, etc. Sometimes a cleaner design or better decoupling might indeed get you out of the circular import hole. But sometimes there is an inherent circular dependency and the only way out are ugly hacks. But we digress.

Circular imports and type hinting

Since I started using type hinting more, I noticed that it is easier to get into circular import troubles. It's not so unexpected since class type hints typically require you to import more. Also, they have to be imported at top level so you can not leverage tricks with local or deferred imports.

Example

When you have a circular import issue only because of type hinting, there is lesser known solution I want to show here.

Let's take this simple artificial example with two modules that need each other: connection.py defines an interface to something like a REST API, and it can create some kind of entity in this API, called Thing:

# connection.py
from thing import Thing

class ApiConnection:
    def get_thing(self) -> Thing:
        return Thing(connection=self)

This Thing (defined in thing.py) keeps a reference to the connection so that operations on a Thing can be send to the API.

# thing.py
from connection import ApiConnection

class Thing:
    def __init__(self, connection: ApiConnection):
        self._conn = connection

No surprise that the circular dependency here will cause failure, resulting in a classic circular import stack trace like:

  File "main.py"
    from connection import ApiConnection
  File "connection.py"
    from thing import Thing
  File "thing.py"
    from connection import ApiConnection
ImportError: cannot import name 'ApiConnection' from 'connection'

In thing.py, the import of the connection module is only there for the ApiConnection type hint. That means that if you don't care too much about that type hint, you could drop it an break the cycle of doom.

Conditional import for type hints

A lesser know solution for this case is to use a conditional import that is only active in "type hinting mode", but doesn't interfere at run time. The typing.TYPE_CHECKING constant makes this easily possible. In our example we change thing.py to:

# thing.py
from typing import TYPE_CHECKING
if TYPE_CHECKING:
    from connection import ApiConnection

class Thing:
    def __init__(self, connection: 'ApiConnection'):
        self._conn = connection

The code will now execute properly as there is no circular import issue anymore. Type hinting tools on the other hand should still be able to resolve the ApiConnection type hint in Thing.__init__. My current IDE (PyCharm) for example picks it up just fine for code intelligence features.

Unfortunately the type hint has to be specified as a "forward reference" string which is bit uglier than a normal type hint. Since Python 3.7 however, thanks to
PEP563 Postponed Evaluation of Annotations it is possible to specify the type hint without the quotes, but that requires an additional import first:

from __future__ import annotations