Search code examples
pythontype-hintingmypy

What's the correct way to use a TypeVar with a parameterized bound?


I occasionally run into a scenario like the following:

from typing import Generic, TypeVar

T = TypeVar('T')

class Widget(Generic[T]):
    content: T

class Jibbit(Generic[T]):
    element: T

class ThingHolder:
    thing: Widget | Jibbit

In the Python standard library, this situation arises in logging.handlers.QueueListener, where the QueueListener.queue attribute is equivalent to ThingHolder.thing above.

Now I want to convert ThingHolder so that it is parameterized by the type of the thing that it holds, so that I can differentiate between, for example, ThingHolder[Widget[int]] and ThingHolder[Jibbit[int]].

How do you spell this correctly with a TypeVar? If I write

Thing = TypeVar('Thing', bound=Widget | Jibbit)

then I get an error because I didn't specify a parameter for the two parameterized types.


Solution

  • It appears that you're supposed to parameterize the types in the bound= itself, and not attempt to parameterize the new type variable:

    Thing = TypeVar('Thing', bound=Widget[Any] | Jibbit[Any])
    
    
    class ThingHolder(Generic[Thing]):
        thing: Thing
    
        def __init__(self, thing: Thing) -> None:
            self.thing = thing
    

    I originally thought that this wouldn't work, because the "inner" type parameter isn't written anywhere in the definition. But it does in fact work:

    # OK
    w_int: Widget[int] = Widget(1)
    th_w_int: ThingHolder[Widget[int]] = ThingHolder(w_int)
    
    # OK
    w_str: Widget[str] = Widget("hello")
    th_w_str: ThingHolder[Widget[str]] = ThingHolder(w_str)
    
    # Errors!
    th_w_str = ThingHolder(w_int)
    th_w_int = ThingHolder(w_str)
    
    reveal_type(ThingHolder(Jibbit(None)))
    # __main__.ThingHolder[__main__.Jibbit[None]]
    
    reveal_type(ThingHolder(Jibbit([1,2,3])))
    # __main__.ThingHolder[__main__.Jibbit[builtins.list[builtins.int]]]
    

    I think it works because something like Widget[int] is indeed a subtype of Widget[Any] | Jibbit[Any], while something like list[int] is not. Clearly, Mypy is smart enough to track the "inner" types, even when they are not explicitly written in the class definition. Moreover, if you had the opportunity to inject your own type variable in the inner parameter, you might accidentally mess it up by using the wrong type variance for the inner parameter.