Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Python dynamic type hinting

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?

like image 225
Andrey Cizov Avatar asked Feb 27 '18 21:02

Andrey Cizov


1 Answers

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.

like image 165
MisterMiyagi Avatar answered Sep 27 '22 17:09

MisterMiyagi