I have the following structure for a package:
/prog
-- /ui
---- /menus
------ __init__.py
------ main_menu.py
------ file_menu.py
-- __init__.py
__init__.py
prog.py
These are my import/classes statements:
prog.py
:
from prog.ui.menus import MainMenu
/prog/ui/menus/__init__.py
:
from prog.ui.menus.file_menu import FileMenu
from prog.ui.menus.main_menu import MainMenu
main_menu.py
:
import tkinter as tk
from prog.ui.menus import FileMenu
class MainMenu(tk.Menu):
def __init__(self, master: tk.Tk, **kwargs):
super().__init__(master, **kwargs)
self.add_cascade(label='File', menu=FileMenu(self, tearoff=False))
[...]
file_menu.py
:
import tkinter as tk
from prog.ui.menus import MainMenu
class FileMenu(tk.Menu):
def __init__(self, master: MainMenu, **kwargs):
super().__init__(master, **kwargs)
self.add_command(label='Settings')
[...]
This will lead to a circular import problem in the sequence:
prog.py
-> __init__.py
-> main_menu.py
-> file_menu.py
-> main_menu.py
-> [...]
From several searches it was suggested to update the imports to such:
file_menu.py
import tkinter as tk
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from prog.ui.menus import MainMenu
class FileMenu(tk.Menu):
def __init__(self, master: 'MainMenu', **kwargs):
super().__init__(master, **kwargs)
self.add_command(label='Settings')
[...]
I've read the TYPE_CHECKING docs and the mypy docs on the usage, but I do not follow how using this conditional resolves the cycle. Yes, at runtime it works because it evaluates to False
so that is an "operational resolution", but how does it not reappear during type checking:
The TYPE_CHECKING constant defined by the typing module is False at runtime but True while type checking.
I don't know a great deal about mypy, thus I fail to see how once the conditional evaluates to True
that the issue will not reappear. What occurs differently between "runtime" and "type checking"? Does the process of "type checking" mean code is not executed?
Notes:
This is not a circular import dependency problem so dependency injection isn't needed
This is strictly a cycle induced by type hinting for static analysis
I am aware of the following import options (which work just fine):
Replace from [...] import [...]
with import [...]
Conduct imports in MainMenu.__init__
and leave file_menu.py
alone
Does the process of "type checking" mean code is not executed?
Yes, exactly. The type checker never executes your code: instead, it analyzes it. Type checkers are implemented in pretty much the same way compilers are implemented, minus the "generate bytecode/assembly/machine code" step.
This means your type checker has more strategies available for resolving import cycles (or cycles of any kind) than the Python interpreter will have during runtime since it doesn't need to try blindly importing modules.
For example, what mypy does is basically start by analyzing your code module-by-module, keeping track of each new class/new type that's being defined. During this process, if mypy sees a type hint using a type that hasn't been defined yet, substitute it with a placeholder type.
Once we've finished checking all the modules, check and see if there are still any placeholder types floating around. If so, try re-analyzing the code using the type definitions we've collected so far, replacing any placeholders when possible. We rinse and repeat until there are either no more placeholders or we've iterated too many times.
After that point, mypy assumes any remaining placeholders are just invalid types and reports an error.
In contrast, the Python interpreter doesn't have the luxury of being able to repeatedly re-analyze modules like this. It needs to run each module it sees, and repeatedly re-running modules could break some user code/user expectations.
Similarly, the Python interpreter doesn't have the luxury of being able to just swap around the order in which we analyze modules. In contrast, mypy can theoretically analyze your modules in any arbitrary order ignoring what imports what -- the only catch is that it'll just be super inefficient since we'd need lots of iterations to reach fixpoint.
(So instead, mypy uses your imports as suggestions to decide in which order to analyze modules. For example, if module A directly imports module B, we probably want to analyze B first. But if A imports B behind if TYPE_CHECKING
, it's probably fine to relax the ordering if it'll help us break a cycle.)