Skip to content

Circular import under if TYPE_CHECKING breaks subclass initialization signature #12259

Description

@jwodder

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.pyfrom .widgetsimportBlueWidget, RedWidget, Widget, WidgetSpecclassApp:
defmake_widget(self, spec: WidgetSpec) ->Widget:
ifspec.color=="red":
returnRedWidget(app=self, spec=spec)
elifspec.color=="blue":
returnBlueWidget(app=self, spec=spec)
else:
raiseValueError(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.pyfrom __future__ importannotationsfromtypingimportTYPE_CHECKING, OptionalimportattrifTYPE_CHECKING:
from ..appimportApp@attr.defineclassWidgetSpec:
color: strflavor: strnessness: Optional[int]
@attr.defineclassWidget:
app: Appspec: WidgetSpec

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

# src/foobar/widgets/blue.pyimportattrfrom .baseimportWidget@attr.defineclassBlueWidget(Widget):
nessness: int=attr.field(init=False)
def__attrs_post_init__(self) ->None:
ifself.spec.nessnessisNone:
raiseValueError("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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugmypy got something wrong

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions