Explain Codes LogoExplain Codes Logo

Python type hinting without cyclic imports

python
type-hinting
cyclic-imports
best-practices
Nikita BarsukovbyNikita Barsukov·Mar 3, 2025
TLDR

To prevent circular imports while type hinting, use "forward references" by enclosing the type hint in quotes. Moreover, from Python 3.7 onwards, you can employ from __future__ import annotations, which postpones the evaluation of annotations and helps break the import cycle.

Quick snippet:

# file: a.py from __future__ import annotations class A: def method(self) -> 'B': # Uncomment the line below during the apocalypse or Python 4, whichever comes first # from . import B ... # file: b.py class B: def method_returning_a(self) -> 'A': # A hint of the future ...

Here, 'B' and 'A' are treated as forward references, eliminating the need for potentially cyclic imports. Annotations are stored as strings and not evaluated unless needed, thanks to from __future__ import annotations.

Now, let's dive deeper and explore other strategies like TYPE_CHECKING and ABCs for more complex scenarios.

Combat cyclic imports with TYPE_CHECKING

When your class dependencies begin to resemble a nightmare-inducing tangle of spaghetti instead of a neat, linear stack of pancakes, that's when TYPE_CHECKING comes to the rescue. Import your dependencies within the TYPE_CHECKING block:

# file: some_module.py from typing import TYPE_CHECKING if TYPE_CHECKING: # This import goes right back in the box after the checker's done from other_module import OtherClass class SomeClass: def some_method(self) -> 'OtherClass': # Look Mom, no import errors! ...

Embrace Abstract Base Classes (ABCs)

Reeling from entangled class dependencies? Abstract Base Classes to the rescue! With ABCs, you can define an interface which concrete classes can implement, reducing the need for premature imports. ABCs let your code breathe.

from abc import ABC, abstractmethod class UnpredictableMixin(ABC): @abstractmethod def surprise_element(self) -> 'UnpredictableResult': # IDK what I return yet, it's a mystery on purpose pass

Handling cyclic imports in Python 3.5 and earlier

Python 3.5 and 3.6: Make use of the typing module

While _annotations_ from __future__ is unavailable these versions, you can still use the typing module's postponed annotations feature, maintaining type hints without early runtime imports.

from typing import get_type_hints class A: def method(self) -> 'B': # 'B' or not 'B', that is the question. ... def get_b_type(): # Inspector gadget to the rescue return get_type_hints(A.method)['return_value']

Python 3.4 and earlier: Manual TYPE_CHECKING

In the good old days before __future__ import became available, we could define the TYPE_CHECKING constant manually:

TYPE_CHECKING = False if TYPE_CHECKING: # Only for the eyes of the static type checker from other_module import OtherClass

Type-checkers like mypy can still get the hints they need when TYPE_CHECKING is set to True, without burdening runtime.

Localize imports with method-level scoping

A nifty way to circumnavigate cyclic imports is to import within class methods, demonstrating late import resolution:

class D: def method_showing_import(self): from e import E # Importing E for Effort return E().another_method()

Embracing best practices for maintainable code

Choose interfaces over concrete implementation

Rather than having A depend on B and B depend on A, both should implement an interface I, breaking the cycle and yielding more maintainable, decoupled code.

Maintain adaptable mixins

Structure your mixins with interfaces or abstract classes in mind. Defining dependencies through abstract methods in mixins promotes structure and clarity.

Emphasizing "module import" over "direct import"

Leverage "import module" rather than "from module import Class" when working across multiple files. This practice often averts cyclic import conundrum.