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.