0

In C# it's possible to "suggest" which type a generic function uses by manually supplying it, instead of letting the compiler guess it from context.

So a method T func<T>() would return a string if used like var x = func<String>().

I'm looking for the same effect for type hinting purposes in VSCode.
The reason being, that I have a lookup function that returns a tkinter.Widget, but depending on which Widget it looks up, it could be any variant of that.

While just having the method type hinted as -> tkinter.Widget: works for the majority of properties, I'd like to be able to specifically tell VSCode which type to expect to get specific properties too.
I've found out that I can "force" a type onto a variable, by using widget: tkinter.Label = getWidget() to tell VSCode that the result is to be treated as a tkinter.Label, but at this point I'm simply curious if an approach closer to C#'s, maybe with an optional named parameter, exists as well.

I've found some posts about typing.TypeVar, but I'm not fully getting yet how that works nor if it's even relevant to what I'm looking for.

Edit: code example from C#

public class JsonConfig
{
    public static T Load<T>(string jsonFile = null) { ... }
}

This is a snippet from a class I wrote to easily use Newtonsoft.Json for (de)serializing my own simple settings classes.
The useful part about C#'s Generics is that this method can accept any class I throw at it at runtime, as long as the underlying (de)serializer can make sense of it.
In my case this meant any simple datatype up to Dictionary<T,T> and List<T> and any structure that can be constructed with those.

If I call the method like

var data = ConfigInstance.Load<List<String>>();

then the IDE knows that the expected result ending up in the variable must be List<String> and from there on treats the variable as such for intellisense etc.

In VSCode Python the closest I got to this behaviour is setting a default return type like

def Load(jsonFile: str = None) -> tkinter.Widget: ...

and if needed "forcing" the variable I'm loading into to take the expected derived type

frame: tkinter.Frame = Load()

At this point I'm just wondering if there's another approach to this, maybe by providing the expected return type with a named variable, or however Pylance does type hinting in VSCode.

BloodyRain2k
  • 177
  • 1
  • 3
  • 11
  • Could you please provide a [MRE] demonstrating the behavior you are _currently_ experiencing and show what you would like instead? A small code snippet can often explain more than paragraphs of prose. In general, what you describe is possible, but what approach works (if any) depends on the specifics of your function. – Daniil Fajnberg Jun 23 '23 at 19:41
  • [This](https://medium.com/@steveYeah/using-generics-in-python-99010e5056eb) may be of use – Narish Jun 23 '23 at 19:51

1 Answers1

1

Generic functions

There is no syntax in Python for explicitly binding type variables in generic functions in the way you showed with C#.

Generic functions in Python are simply function, where one or more parameters and optionally the return type are annotated with type variables (directly or indirectly via parameterized generic types).

One example from the relevant section of PEP 484 is this:

from typing import Sequence, TypeVar

T = TypeVar('T')      # Declare type variable

def first(l: Sequence[T]) -> T:   # Generic function
    return l[0]

The type variable T is then implicitly bound, when you call that function. Calling first([10, 20, 30]) a static type checker will infer the argument type to be a subtype of Sequence[int], thus collapsing T to int and infer the return type as int accordingly. (It is worth noting that how exactly this inference is done depends on the type checker. There are edge cases, where this can be contentious.)

One common way of emulating the notation you have in mind is by adding a parameter to the function that represents the type you want. So in your example, you could have something like this:

from tkinter import Label, Widget
from typing import TypeVar

W = TypeVar("W", bound=Widget)


def load_widget(cls: type[W]) -> W:
    ...


widget = load_widget(Label)

A type checker will then infer widget to be of type Label. The upper bound set on the type variable simply sets up a contract that whatever type we pass to load_widget must be a subtype of Widget.


Generic classes

It is a bit different with classes though. User-defined generic types can be subscripted with square brackets to pass a type argument. In the first function example above we already saw that with the generic Sequence type. Since Python 3.9 the built-in container types like list or dict allow subscription to create generic alias types.

You can do the same with custom generic classes:

from tkinter import Label, Widget
from typing import Generic, TypeVar

W = TypeVar("W", bound=Widget)


class SomethingWithWidgets(Generic[W]):
    def load_widget(self) -> W:
        ...


thing: SomethingWithWidgets[Label]
...  # actually initialize `thing` somehow
widget = thing.load_widget()

This annotation SomethingWithWidgets[Label] can be explicit or again inferred somehow from the constructor of thing. In this example I deliberately left out instantiation just to show that once the type of thing is fully specified as SomethingWithWidgets[Label], the type checker will always infer the output of thing.load_widget() as being of type Label.

But it is important to note that by default passing such a type argument at runtime has no noticable effect on the objects involved. In other words, just because that Label type is passed as a type argument to the generic class' __class_getitem__ that will not make the load_widget method magically spit out a Label instance.


Runtime vs. static analysis

Most of the typing constructs in Python were designed with static type checkers in mind and thus have minimal runtime implications. But there are increasingly many libraries out there that deliberately leverage the typing machinery to achieve very significant runtime effects. But since this is not what it was designed for, that code can seem fairly complicated/obscure.

Examples/discussions:


Typical approaches/solutions

What solution is appropriate depends on how you want that load_widget function of yours to work exactly. If all it gets as an argument is some path to a file and then depending on the contents of that file it can return either a Label or some other kind of Widget, there is no way to annotate that statically.

You could go the route of explicitly passing the desired class as an additional argument to the function as I showed above. (You could then also raise an error inside it, if the type does not match.) Then you would have a deterministic return type based on that argument.

There is also the option of overloading your function signature. For example, instead of passing the class itself to the function, you could define some literal value that the caller will have to pass to get a specific type of widget out of the function:

from tkinter import Label, Widget
from typing import Literal, overload


@overload
def load_widget(file: str, widget_type: Literal["label"]) -> Label: ...


@overload
def load_widget(file: str, widget_type: None = None) -> Widget: ...


def load_widget(file: str, widget_type: str | None = None) -> Widget:
    ...


widget1 = load_widget("foo.json", "label")
widget2 = load_widget("bar.json")

Here widget1 will be inferred as being of type Label, whereas widget2 will just be inferred as being of type Widget.

If you want the function to remain completely dynamic, you always have the option of simply casting its output as a specific type, whenever you call it. This has essentially no runtime cost, but will be clear to any type checker.

from tkinter import Label, Widget
from typing import cast


def load_widget(file: str) -> Widget:
    ...


widget = cast(Label, load_widget("foo.json"))

Emulating your desired syntax (just for fun)

If you want to get fancy, you can hack together a class of callables that will allow you to emulate the syntax you desire.

from __future__ import annotations
from tkinter import Label, Widget
from typing import Generic, TypeVar

W1 = TypeVar("W1", bound=Widget)
W2 = TypeVar("W2", bound=Widget)


class WidgetLoader(Generic[W1]):
    def __init__(self, widget_type: type[W1]) -> None:
        self.widget_type = widget_type

    def __getitem__(self, item: type[W2]) -> WidgetLoader[W2]:
        return WidgetLoader(item)

    def __call__(self, file: str) -> W1:
        ...  # actual logic that depends on `widget_type`


load_widget = WidgetLoader(Widget)

Now, load_widget can be called like this:

widget = load_widget[Label]("foo.json")

widget will be inferred as being of type Label.

This is the closest I could get to the syntax you showed. This simplistic implementation is questionable of course. For one thing, each call to __getitem__ will create a new object, which is entirely unnecessary. You would have to cache that call somehow.

All this effort just for some fancy syntax? I think at that point you might as well just explicitly pass the type to a regular function.

Daniil Fajnberg
  • 12,753
  • 2
  • 10
  • 41