Skip to content

Adding __init__ breaks type inference from __new__ #12045

Open

Description

@last-partizan

Hello, i'm trying to fix a bug in django type stubs, and i believe i've stubled upon bug in mypy.

Django has following syntax for defining foreign keys:

class Book(models.Model):
    author = models.ForeignKey(Author, null=True)

This means author should be Author | None.

Right now django-types are using @override for __init__ with differently typed self parameter:

https://github.com/sbdchd/django-types/blob/90fc353b7d05ec600a4807d8b29343a6538692b1/django-stubs/db/models/fields/related.pyi#L293

@overload
def __init__(
        self: ForeignKey[_M],
       ...
       null: Literal[False],
...
@overload
def __init__(
        self: ForeignKey[Optional[_M]],
        ...
       null: Literal[True],
...

And it works for mypy, but doesn't work for pyright. In mypy it works becouse it uses undocumented feature of typing self, as i discovered here:

Proper, and most concise way of doing this, would be overloading __new__ with args/kwargs and bool: Literal[...], and leaving full annotation with all arguments typed in __init__ (which i attempted here sbdchd/django-types#86).

But, when we try to do this: mypy would ignore overrides in __new__ and will always use non-optional case.

To Reproduce

To reproduce this without much clutter, we can use following code:

# related_field_minimal.pyi
from typing import Generic, Literal, Optional, Type, TypeVar, overload

class Model: ...

_M = TypeVar("_M", bound=Optional[Model])

class ForeignKey(Generic[_M]):
    @overload
    def __new__(
        cls,
        to: Type[_M],
        null: Literal[False] = ...,
    ) -> ForeignKey[_M]: ...
    @overload
    def __new__(
        cls,
        to: Type[_M],
        null: Literal[True],
    ) -> ForeignKey[Optional[_M]]: ...
    def __init__(self, to: Type[_M], null: bool) -> None: ...
# models.py
from typing import Optional
from related_field_minimal import ForeignKey, Model


class Test(Model):
    pass


class BookModel(Model):
    required_field = ForeignKey(to=Test, null=False)
    optional_f1 = ForeignKey(to=Test, null=True)
    optional_f2 = ForeignKey[Test](to=Test, null=True)
    optional_f3 = ForeignKey[Optional[Test]](to=Test, null=True)

reveal_type(BookModel.required_field)
reveal_type(BookModel.optional_f1)
reveal_type(BookModel.optional_f2)
reveal_type(BookModel.optional_f3)

When there is no __init__ in ForeignKey, inferred types are correct, both in mypy and pyright.

models.py:15: note: Revealed type is "related_field_minimal.ForeignKey[models.Test]"
models.py:16: note: Revealed type is "related_field_minimal.ForeignKey[Union[models.Test, None]]"
models.py:17: note: Revealed type is "related_field_minimal.ForeignKey[Union[models.Test, None]]"
models.py:18: note: Revealed type is "related_field_minimal.ForeignKey[Union[models.Test, None]]"

As soon as we add __init__ to our type definition in related_field_minimal.pyi, it becomes incorrect in mypy:

models.py:15: note: Revealed type is "related_field_minimal.ForeignKey[models.Test]"
models.py:16: note: Revealed type is "related_field_minimal.ForeignKey[models.Test]"
models.py:17: note: Revealed type is "related_field_minimal.ForeignKey[models.Test]"
models.py:18: note: Revealed type is "related_field_minimal.ForeignKey[Union[models.Test, None]]"

(pyright still handles this case correctly).

Your Environment

  • Mypy version used: 0.931
  • Mypy command-line flags: None
  • Mypy configuration options from mypy.ini (and other config files): None
  • Python version used: 3.10

Activity

  1. stereobutter commented on Apr 8, 2023

    @stereobutter

    I've also encountered this issue (using mypy 1.2.0).

    from typing import TypeVar, Generic, overload, Tuple, Self, Any
    
    T = TypeVar("T")
    T1 = TypeVar("T1")
    T2 = TypeVar("T2")
    
    class Box(Generic[T]):
        @overload
        def __new__(cls, arg: T1) -> "Box[T1]": ...
        @overload
        def __new__(cls, arg1: T1, arg2: T2) -> "Box[Tuple[T1, T2]]": ...
        def __init__(self: Self, *args: Any) -> None: ...
    
    reveal_type(Box(1))  # Revealed type is "Box[<nothing>]"

    I've found no way to make mypy happy when __init__ was defined. This makes it impossible to use inline type annotations (instead of .pyi stubs) because at runtime __init__ is obviously needed.

  2. ikonst commented on Apr 8, 2023

    @ikonst
    Contributor

    I've found no way to make mypy happy when __init__ was defined. This makes it impossible to use inline type annotations (instead of .pyi stubs) because at runtime __init__ is obviously needed.

    The same is normally possible by wrapping in if not TYPE_CHECKING:.

  3. Mr-Pepe commented on Dec 1, 2023

    @Mr-Pepe

    I've found no way to make mypy happy when __init__ was defined. This makes it impossible to use inline type annotations (instead of .pyi stubs) because at runtime __init__ is obviously needed.

    The same is normally possible by wrapping in if not TYPE_CHECKING:.

    However, this breaks autocomplete and linters because attributes set inside __init__ are not visible anymore.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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