Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How does mypy use typing.TYPE_CHECKING to resolve the circular import annotation problem?

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

like image 876
pstatix Avatar asked May 01 '20 15:05

pstatix


People also ask

What is Type_checking Python?

Python is a dynamically typed language. This means that the Python interpreter does type checking only as code runs, and that the type of a variable is allowed to change over its lifetime.

How do I make MYPY ignore error?

Silencing errors based on error codes You can use a special comment # type: ignore[code, ...] to only ignore errors with a specific error code (or codes) on a particular line. This can be used even if you have not configured mypy to show error codes.

How do I use type ignore?

You can use the form # type: ignore[<code>] to only ignore specific errors on the line. This way you are less likely to silence unexpected errors that are not safe to ignore, and this will also document what the purpose of the comment is.

What is MYPY?

“Mypy is an optional static type checker for Python that aims to combine the benefits of dynamic (or 'duck') typing and static typing. Mypy combines the expressive power and convenience of Python with a powerful type system and compile-time type checking.” A little background on the Mypy project.


Video Answer


1 Answers

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.)

like image 171
Michael0x2a Avatar answered Sep 20 '22 21:09

Michael0x2a