How to preserve type hints despite variable type annotation during assignment

97 Views Asked by At

I am writing a Lazy class that works like partial, but has some extra functionalities. The goal is to pre-initialize an object by wrapping it in the Lazy class and later finish the initialization by calling to_eager, where the user can provide some additional arguments. The issue is with preserving type hints in IDE.

Here is the code

from typing import Callable, Generic, ParamSpec, Type, TypeVar


T = TypeVar("T")
P = ParamSpec("P")


class Lazy(Generic[T, P]):  # Any makes it ok to asign to any other type

    def __init__(
        self, cls: Type[T] | Callable[P, T], *args: P.args, **kwargs: P.kwargs
    ):
        self.cls = cls
        self.args = args
        self.kwargs = kwargs

    def to_eager(self, *args: P.args, **kwargs: P.kwargs) -> T:
        assert not args
        kwg = {**self.kwargs, **kwargs}
        return self.cls(*self.args, **kwg)


class SomeClass:
    def __init__(self, y: int = 1):
        self.y = y


l = Lazy(SomeClass, y=5)
l.to_eager()  # here VSCode hints me with (y: int -> 1)

When I try to call to_eager(), I get a nice hint about possible args for the original class.

enter image description here

Now the issue is, if this is combined with variable type assignment

l: Lazy = Lazy(SomeClass, y=5)
l.to_eager()  # type hint is gone, I get (... -> Unknown)

This can be improved a bit by

l: Lazy[SomeClass, ...] = Lazy(SomeClass, y=5)
l.to_eager()  # I get (... -> SomeClass)

but this does not contain the args.

My question is - how to preserve the args type hints in this case while also doing the variable type assignment? The type assignment is needed eg when I combine this with a dataclass. Maybe there is some configuration for pyright to not do type widening? Or perhaps vscode has some useful option for this?

What I tried so far:

  1. Pass P as the second argument in Lazy[SomeClass, P], but getting warning that P has no meaning in this context and also no type hints from VSCode
1

There are 1 best solutions below

3
chepner On

Lazy by itself is equivalent to Lazy[Any, Any], overriding the inferred type of Lazy[SomeClass, int]. It's the laziness (pun intended) of not specifying a precise type hint that prevents the editor from providing an argument hint.

... itself is recognized as shorthand for types.EllipsisType.


What you would want is for the editor to support some notion of type-variable binding so that you can write one "type" whose value is bound by the actual argument passed to Lazy, something like

l: Lazy[Cls, T] = Lazy(SomeClass, y=5)

and have Cls and T bound (perhaps just for this assignment) to SomeClass and int, respectively.