There is no general way to do this: Variadic arguments such as *types
do not preserve order as far as static type checking is concerned. Variadic generics cover this behaviour but are only proposals so far.
One can either use a single annotation for variadic arguments, which degrades to the common base type, or use several annotations requiring to enumerate the most common cases.
Annotate the variadic arguments as a type variable (possibly bound via Base
). This will infer all types to the same, most common base type.
class Base:
@classmethod
def transaction(cls, *types: Type[T]) -> List[T]: ...
class A(Base): ...
class B(Base): ...
reveal_type(Base.transaction(A, A)) # builtins.list[mt.A*]
reveal_type(Base.transaction(A, B)) # builtins.list[mt.Base*]
This is sufficient when using similar types (e.g. just A
's or subclasses of A
's) and expecting only general features (e.g. just A
's). This will not be sufficient when using mixed type (e.g. A
and B
) since it will degenerate to the common base type (e.g. Base
).
Provide multiple @overload
signatures for a reasonable number of arguments, and use a variadic catch-all for huge number of arguments. This will infer the proper type for the specified cases, and use the most common base type otherwise.
class Base:
# explicitly enumerated types
@overload
@classmethod
def transaction(cls, t1: Type[T1], /) -> Tuple[T1]: ...
@overload
@classmethod
def transaction(cls, t1: Type[T1], t2: Type[T2], /) -> Tuple[T1, T2]: ...
# catch all number of types
@overload
@classmethod
def transaction(cls, *ts: Type[T]) -> Tuple[T, ...]: ...
# implementation
@classmethod
def transaction(cls, *types: Type[T]) -> Tuple[T, ...]: ...
class A(Base): ...
class B(Base): ...
reveal_type(Base.transaction(A)) # Revealed type is 'Tuple[mt.A*]'
reveal_type(Base.transaction(A, A)) # Revealed type is 'Tuple[mt.A*, mt.A*]'
reveal_type(Base.transaction(A, B)) # Revealed type is 'Tuple[mt.A*, mt.B*]'
reveal_type(Base.transaction(A, B, A)) # Revealed type is 'builtins.tuple[mt.Base*]'
The limit here is only how many cases one considers to be relevant. This mechanism is also used for the standard library.