Skip to content

Circular import under if TYPE_CHECKING breaks subclass initialization signature #12259

Closed
@jwodder

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

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