Skip to content

(🎁) Support type variables in the bounds of other type variables #1226

Open

Description

@KotlinIsland
from typing import _T as T, TypeVar

L = TypeVar("L", bound=list[T])  # error: Type variable "typing._T" is unbound

def foo(l: L) -> L | T:  # error: A function returning TypeVar should receive at least one argument containing the same Typevar
    res = l[0]
    if res:
        return res
    return l

a: list[int] | list[str]

reveal_type(foo)  # "def [L <: list[T?], T] (l: L) -> L | T"
reveal_type(foo(a))  # list[int] | list[str]

Expected:

reveal_type(foo)  # "def [T, L <: list[T]] (l: L) -> L | T"
reveal_type(foo(a))  # list[int] | list[str] | int | str

Mypy should understand that T should be bound to foo from L

This is identical to generic TypeAliases:

L: TypeAlias = list[T]
a: L[int]

Here T is unbound, yet it's a valid and semantically sound expression.

Typescript example

The same idea could be represented in TypeScript as:

declare function foo<T, L extends T[]>(l: L): L | T

declare let a: number[] | string[]

let b = foo(a)

Although TS fails to infer the correct type here. (it infers as unknown)

basedmypy

This is partially supported in basedmypy

Activity

  1. erictraut commented on Jul 22, 2022

    @erictraut
    Collaborator

    This concept doesn't make sense. A TypeVar used within the bound of a TypeVar would have no defined scope. It should therefore be considered an error to use a TypeVar within a bound or constraint expression. PEP 484 isn't as clear as it could be on this point. I've attempted to remedy that omission in draft PEP 695.

  2. DetachHead commented on Jul 22, 2022

    @DetachHead

    how so? typescript supports it:

    declare function foo<T, L extends T[]>(l: L, t: T): T
    
    declare const a: number[]
    declare const b: number
    
    const c = foo(a, b) //inferred as foo<number, number[]>
  3. KotlinIsland commented on Jul 22, 2022

    @KotlinIsland
    ContributorAuthor

    @erictraut In my opinion this is identical to generic TypeAliases:

    L: TypeAlias = list[T]
    a: L[int]

    Here T is unbound, yet it's a valid and semantically sound expression.

    I've attempted to remedy that omission in draft PEP 695.

    class ClassC[T: dict[str, V]]: ...  # Type checker error: generic type
    # But
    class ClassC[V, T: dict[str, V]]: ...  # LGTM

    The second example is how I understand the example in the OP.

  4. AlexWaygood commented on Jul 22, 2022

    @AlexWaygood
    Member

    Things like this should be discussed in the python/typing issue tracker or on the typing-sig mailing list, not the mypy issue tracker. We'd need agreement on the spec before individual type checkers could start supporting this.

  5. KotlinIsland commented on Jul 23, 2022

    @KotlinIsland
    ContributorAuthor

    @AlexWaygood Could you transfer the issue to python/typing?

  6. transferred this issue frompython/mypyon Jul 23, 2022
  7. smheidrich commented on Dec 20, 2023

    @smheidrich
    Contributor

    As this is basically a subset of the functionality offered by higher-kinded types/TypeVars (HKTs) requested in #548, I'm wondering if it might make sense to have this as a first step before full HKT support?

    The example from the beginning

    L = TypeVar("L", bound=list[T])
    
    def foo(l: L) -> L | T: ...

    would, in the suggested HKT syntax, be written as

    L = TypeVar("L", bound=list[T], args=1)
    
    def foo(l: L[T]) -> L[T] | T: ...

    I think it might be confusing if both of these possibilities coexisted, so maybe the syntax for this feature could adopt the one from HKTs but put restrictions on it to limit it to non-full-HKT cases, i.e. making it forbidden to use L parametrized with anything other than T? But then again, if the entire PEP for this were to only make sense in anticipation of the subsequent HKT PEP, it might be better to skip it and go with full HKTs immediately? 🤔

  8. added
    topic: featureDiscussions about new features for Python's type annotations
    and removed on Jul 24, 2025
  9. RomanValov commented on Jan 16, 2026

    @RomanValov

    Here is my use case for "generic" bounds. Though in contrast to other examples in this and relevant issues (especially #548 ), my case would benefit from bound being simply another type variable rather then generic construct.

    I would like to implement decorator-based generic handler registry in my code. But crucial point for the registry is the possibility to apply decorator to functions with inexact (but compatible) call signatures. And preserve the signatures once being applied. It's where the construct with bound comes into play with bound being exact handler signature:

    from collections.abc import Callable
    
    from dataclasses import dataclass
    
    @dataclass(frozen=True)
    class Context[ValueT]:
        metric: ValueT
    
    type Handler = Callable[[Context[int], int], str]
    
    class Registry0:
        def __init__(self) -> None:
            self.handlers: list[Handler] = []
    
        def __call__[WrappedT: Handler](self, handler: WrappedT) -> WrappedT:
            self.handlers.append(handler)
            return handler
    
    class Registry1[HandlerT]:
        def __init__(self) -> None:
            self.handlers: list[HandlerT] = []
    
        def __call__(self, handler: HandlerT) -> HandlerT:
            self.handlers.append(handler)
            return handler
    
    class Registry2[HandlerT]:
        def __init__(self) -> None:
            self.handlers: list[HandlerT] = []
    
        def __call__[WrappedT: HandlerT](self, handler: WrappedT) -> WrappedT:
            self.handlers.append(handler)
            return handler
    
    register = Registry0()
    # register = Registry1[Handler]()
    # register = Registry2[Handler]()
    
    @register
    def basic_handler(ctx: Context[float], x: int) -> str:
        return extra_handler(ctx, x, 1)
    
    @register
    def extra_handler(ctx: Context[float], x: int, y: int = 0) -> str:
        return str(x + y)
    
    @register
    def value_handler(ctx: Context[int], x: int) -> str:
        return basic_handler(Context(1/x), ctx.metric)

    Compare Registry0, Registry1 and Registry2 in the snippet:

    Registry0 is non-generic implementation which perfectly handles (and preserves) non-exact function signatures of example handlers.

    Registry1 is an attempt to implement generic registry but it's too rigid and narrows function signatures after being applied. Hence handlers no longer being able to call each other as shown in the example.

    Registry2 would potentially resolve limitations of Registry1, but it requires type variable (of the method) to be bound with another type variable (of the class).

  10. changed the title [-](🎁) Support `TypeVar`s in the bounds of other `TypeVar`s[/-] [+](🎁) Support type variables in the bounds of other type variables[/+] on Apr 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    topic: featureDiscussions about new features for Python's type annotations

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions