I would like to be able to provide a function with a tuple of types which would then be used to deserialize the returned data from a transaction. The function would then return the instances of those types, if the transaction had succeeded. For example:
T = TypeVar('T')
class Base:
@classmethod
def deserialize(cls: Type[T], bts: bytes) -> T:
return cls(**json.loads(bts))
@classmethod
def transaction(cls, *types):
items = self.db.transact([t.generate_load_operation() for t in types])
items = [t.deserialize(item) for item in items]
# how do I type-hint transaction, so that it would imply that
# it will always return a tuple (or a list) of instances of classes
# contained in variable types?
return items
class A(Base):
pass
class B(Base):
pass
a_inst, b_inst = Base.transaction(A, B)
How should I go about annotating transaction so that the type-checker could properly infer the types of values returned from it?
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With