Is there a way to partially influence the parametrization of a type variable from an `__init__`/`__new__` argument?


I’m trying to find a solution to type check django fields, in a way that doesn’t rely on the current plugin. The signature of Field contains a null: bool argument, which when set to True, allow None as a model instance value for this field.

The current implementation of django-stubs is to add types to the __set__/__get__ methods of the Field class. Nullability is currently handled in the plugin, and I’m trying to support it natively. I’ve been able to implement the following (this post title says partially because I’m just adding | None to the type variables):

Code sample in pyright playground

from typing import Generic, TypeVar, Literal, overload

_ST = TypeVar("_ST")
_GT = TypeVar("_GT")

class Field(Generic[_ST, _GT]):
    def __new__(
        null: Literal[True],
    ) -> Field[_ST | None, _GT | None]: ...
    def __new__(
        null: Literal[False] = ...,
    ) -> Field[_ST, _GT]: ...

    # For demonstration purposes:
    def get_type(self) -> _GT: ...
    def set_type(self) -> _ST: ...

class IntegerField(Field[float | int | str, int]):

a = IntegerField(null=True)
reveal_type(a.get_type())  # Type of "a.get()" is "int"   + None if null=True
reveal_type(a.set_type())  # Type of "a.set()" is "float | int | str" + None if null=True

Unfortunately, the return type of the __new__ method is incorrect:

reveal_type(a)  # Type of "a" is "Field[float | int | str | None, int | None]", should be `IntegerField`

I could use Self as a return type instead, but Self can’t be used with type arguments (so I can’t use -> Self[_ST, _GT]).

Considering this, I’ve tried going the old way (used before Self was a thing) (playground), but without success.

Using __init__(self: Field[..., ...] instead of hacking with __new__ leads to the same result, i.e. nullability ins’t taken into account.

I found this to be related in some way: Parametric self and default arguments · python/typing · Discussion #1197 · GitHub, although it differs a bit from what I have. Seems like I’m also hitting the need for higher kinded types.

I’m wondering if there’s a solution to this. Thanks in advance!

Edit: to be clear, this can still be done by explicitly writing the __new__ definitions on each subclass (see this issue, which is closely related and lead to a working solution using __init__ with pyright). What I am asking here is if there’s a way to make it possible to have the same behavior without these explicit overloads on each subclass. I’m fine with a “it requires higher kinded types for it to be supported”, just want to make sure I’m not missing a possible solution.