Python type hinting without cyclic imports
For people struggling with cyclic imports when importing class only for Type checking: you will likely want to use a Forward Reference (PEP 484 - Type Hints):
When a type hint contains names that have not been defined yet, that definition may be expressed as a string literal, to be resolved later.
So instead of:
class Tree:
def __init__(self, left: Tree, right: Tree):
self.left = left
self.right = right
you do:
class Tree:
def __init__(self, left: 'Tree', right: 'Tree'):
self.left = left
self.right = right
The bigger issue is that your types aren't sane to begin with. MyMixin
makes a hardcoded assumption that it will be mixed into Main
, whereas it could be mixed into any number of other classes, in which case it would probably break. If your mixin is hardcoded to be mixed into one specific class, you may as well write the methods directly into that class instead of separating them out.
To properly do this with sane typing, MyMixin
should be coded against an interface, or abstract class in Python parlance:
import abc
class MixinDependencyInterface(abc.ABC):
@abc.abstractmethod
def foo(self):
pass
class MyMixin:
def func2(self: MixinDependencyInterface, xxx):
self.foo() # ← mixin only depends on the interface
class Main(MixinDependencyInterface, MyMixin):
def foo(self):
print('bar')
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.
Since Python 3.5, breaking your classes up into separate files is easy.
It's actually possible to use import
statements inside of a class ClassName:
block in order to import methods into a class. For instance,
class_def.py
:
class C:
from _methods1 import a
from _methods2 import b
def x(self):
return self.a() + " " + self.b()
In my example,
C.a()
will be a method which returns the stringhello
C.b()
will be a method which returnshello goodbye
C.x()
will thus returnhello hello goodbye
.
To implement a
and b
, do the following:
_methods1.py
:
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from class_def import C
def a(self: C):
return "hello"
Explanation: TYPE_CHECKING
is True
when the type checker is reading the code. Since the type checker doesn't need to execute the code, circular imports are fine when they occur within the if TYPE_CHECKING:
block. The __future__
import enables postponed annotations. This is an optional; without it you must quote the type annotations (i.e. def a(self: "C"):
).
We define _methods2.py
similarly:
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from class_def import C
def b(self: C):
return self.a() + " goodbye"
In VS Code, I can see the type detected from self.a()
when hovering:
And everything runs as expected:
>>> from class_def import C
>>> c = C()
>>> c.x()
'hello hello goodbye'
Notes on older Python versions
For Python versions ≤3.4, TYPE_CHECKING
is not defined, so this solution won't work.
For Python versions ≤3.6, postponed annotations are not defined. As a workaround, omit from __future__ import annotations
and quote the type declarations as mentioned above.