Skip to content

Circular import under if TYPE_CHECKING breaks subclass initialization signature #12259

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
jwodder opened this issue Feb 27, 2022 · 1 comment
Closed
Labels
bug mypy got something wrong

Comments

@jwodder
Copy link

jwodder commented Feb 27, 2022

MVCE repository: https://github.com/jwodder/mypy-bug-20220227 (Run tox -e typing to see the failed type-check)

Consider an App class with a method that creates Widget instances based on a spec. Widget is a base class, and the spec determines which child class gets instantiated.

# src/foobar/app.py
from .widgets import BlueWidget, RedWidget, Widget, WidgetSpec


class App:
    def make_widget(self, spec: WidgetSpec) -> Widget:
        if spec.color == "red":
            return RedWidget(app=self, spec=spec)
        elif spec.color == "blue":
            return BlueWidget(app=self, spec=spec)
        else:
            raise ValueError(f"Unsupported widget color: {spec.color!r}")

Now, the widgets keep a reference to the App, so in an attempt to avoid circular imports, the file containing the Widget definition uses an if TYPE_CHECKING: guard like so:

# src/foobar/widgets/base.py
from __future__ import annotations
from typing import TYPE_CHECKING, Optional
import attr

if TYPE_CHECKING:
    from ..app import App


@attr.define
class WidgetSpec:
    color: str
    flavor: str
    nessness: Optional[int]


@attr.define
class Widget:
    app: App
    spec: WidgetSpec

The RedWidget child class is empty, but the BlueWidget class adds an attribute:

# src/foobar/widgets/blue.py
import attr
from .base import Widget


@attr.define
class BlueWidget(Widget):
    nessness: int = attr.field(init=False)

    def __attrs_post_init__(self) -> None:
        if self.spec.nessness is None:
            raise ValueError("Blue widgets must be nessy")
        self.nessness = self.spec.nessness

Now, if we put this all together and run mypy, we get an erroneous error:

src/foobar/app.py:9: error: Unexpected keyword argument "app" for "BlueWidget" [call-arg]
                return BlueWidget(app=self, spec=spec)
                       ^
src/foobar/app.py:9: error: Unexpected keyword argument "spec" for "BlueWidget" [call-arg]
                return BlueWidget(app=self, spec=spec)
                       ^
Found 2 errors in 1 file (checked 6 source files)

Note that mypy only complains about the instantiation of BlueWidget, not RedWidget. Also note that the same error occurs if attrs is replaced with dataclasses.

I believe that this problem is caused by the circular import beneath the if TYPE_CHECKING: guard for some reason, as commenting it out and changing the Widget.app annotation to Any gets the type-checking to pass.

Your Environment

  • Mypy version used: both 0.931 and commit feca706

  • Mypy command-line flags: none

  • Mypy configuration options from mypy.ini (and other config files):

      [mypy]
      pretty = True
      show_error_codes = True
    
  • Python version used: 3.9.10

  • Operating system and version: macOS 11.6

@jwodder
Copy link
Author

jwodder commented Oct 2, 2023

As of mypy 1.5.1, this bug is no longer reproduceable.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug mypy got something wrong
Projects
None yet
Development

No branches or pull requests

2 participants