Type Hints: Solve Circular Dependency

Type hints: solve circular dependency

You can use a forward reference by using a string name for the not-yet-defined Client class:

class Server():
def register_client(self, client: 'Client')
pass

As of Python 3.7, you can also postpone all runtime parsing of annotations by adding the following __future__ import at the top of your module:

from __future__ import annotations

at which point the annotations are stored as string representations of the abstract syntax tree for the expression; you can use typing.get_type_hints() to resolve those (and resolve forward references as used above).

See PEP 563 -- Postponed Evaluation of Annotations for details; this behaviour will be the default in Python 4.0.

Type-hints for two interdependent classes with circular dependencies

You can use forward references, i.e. use service: "HandlerService" instead of service: HandlerService.

Note that in future python versions, this will become obsolete, as the annotation evaluation order will not be done at function definition time, but after all of them have been defined. You can start using this behavior with a from __future__ import annotations import in python 3.7, and later. See PEP563 for details.

Python type hinting without cyclic imports

There isn't a hugely elegant way to handle import cycles in general, I'm afraid. Your choices are to either redesign your code to remove the cyclic dependency, or if it isn't feasible, do something like this:

# some_file.py

from typing import TYPE_CHECKING
if TYPE_CHECKING:
from main import Main

class MyObject(object):
def func2(self, some_param: 'Main'):
...

The TYPE_CHECKING constant is always False at runtime, so the import won't be evaluated, but mypy (and other type-checking tools) will evaluate the contents of that block.

We also need to make the Main type annotation into a string, effectively forward declaring it since the Main symbol isn't available at runtime.

If you are using Python 3.7+, we can at least skip having to provide an explicit string annotation by taking advantage of PEP 563:

# some_file.py

from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from main import Main

class MyObject(object):
# Hooray, cleaner annotations!
def func2(self, some_param: Main):
...

The from __future__ import annotations import will make all type hints be strings and skip evaluating them. This can help make our code here mildly more ergonomic.

All that said, using mixins with mypy will likely require a bit more structure then you currently have. Mypy recommends an approach that's basically what deceze is describing -- to create an ABC that both your Main and MyMixin classes inherit. I wouldn't be surprised if you ended up needing to do something similar in order to make Pycharm's checker happy.

Type hints with circular dependencies across files

The problem is that Line isn't a name for a type at all within point.py, and Point isn't a name for a type within line.py. Putting it in quotes doesn't help; that just delays when the string is resolved. It still has to eventually resolve to a type, so you're just delaying things to a point where they resolve to a NameError, which doesn't help thing.

If you do an import line in Point and import point in Line, then point.Point and line.Line become types, which solves that problem. But of course it creates a new problem: a circular import.


In some cases, as explained in PEP 484, you can resolve that just by doing a conditional "static-typing-only" import, like this:

import typing
if typing.TYPE_CHECKING:
import line

… and then using 'line.Line' in the type annotation.

See the docs on TYPE_CHECKING for more details. (In particular, if you need compatibility with Python 3.5 before 3.5.2, this will give you a NameError instead of False at runtime, which is a pain… but not many people need to run on 3.5.1.)


If that doesn't solve the problem, you need some scheme to avoid the circular import, the same way you would for a runtime circular import.

For example, you can go with the traditional "interface" solution where the first type depends on the second, but the second doesn't depend on the first, it depends only on a superclass for the first.

Typically, this superclass would be an ABC, to indicate that it's serving only as an interface to some real class defined elsewhere. (And that way, you know, and Python and your static checker can enforce, that anything that types as the ABC must be an instance of one of the concrete subclasses of that ABC—of which there's only one.)

# pointbase.py
import abc
class PointBase(abc.ABC):
@abc.abstractmethod
def method_that_does_not_need_line(self):
pass

# point.py
import pointbase
import line
class Point(pointbase.PointBase):
def method_that_does_not_need_line(self):
do_stuff()
def method_that_does_need_line(self, line: line.Line):
do_stuff(line)

# line.py
import pointbase
class Line:
def method_that_needs_point(self, point: pointbase.PointBase):
do_stuff(point)


Related Topics



Leave a reply



Submit