pythonmypypython-typingtype-alias

Reusing type hints


I'm trying to reuse type hints from a dataclass in my function signature - that is, without having to type the signature out again.

What would be the best way of going about this?

from dataclasses import dataclass
from typing import Set, Tuple, Type

@dataclass
class MyDataClass:
    force: Set[Tuple[str, float, bool]]

# I've had to write the same type annotation in the dataclass and the
# function signature - yuck
def do_something(force: Set[Tuple[str, float, bool]]):
    print(force)

# I want to do something like this, where I reference the type annotation from
# the dataclass. But, doing it this way, pycharm thinks `force` is type `Any`
def do_something_2(force: Type["MyDataClass.force"]):
    print(force)

Solution

  • What would be the best way of going about this?

    PEP 484 gives one clear option for this case

    Type aliases

    Type aliases are defined by simple variable assignments: (...) Type aliases may be as complex as type hints in annotations -- anything that is acceptable as a type hint is acceptable in a type alias:

    Applied to your example this would amount to (Mypy confirms this as correct)

    from dataclasses import dataclass
    
    Your_Type = set[tuple[str, float, bool]]
    
    
    @dataclass
    class MyDataClass:
        force: Your_Type
    
    
    def do_something(force: Your_Type):
        print(force)
    

    The above is written using Python 3.9 onward Generic Alias Type. The syntax is more concise and modern since typing.Set and typing.Tuple have been deprecated.



    Now, fully understanding this in terms of the Python Data Model is more complicated than it may seem:

    3.1. Objects, values and types

    Every object has an identity, a type and a value.

    Your first attempt of using Type would give an astonishing result

    >>> type(MyDataClass.force)
    
    AttributeError: type object 'MyDataClass' has no attribute 'force'
    

    This is because the builtin function type returns a type (which is itself an object) but MyDataClass is "a Class" (a declaration) and the "Class attribute" force is on the Class not on the type object of the class where type() looks for it. Notice the Data Model carefully on the difference:

    • Classes

      These objects normally act as factories for new instances of themselves

    • Class Instances

      Instances of arbitrary classes

    If instead you checked the type on an instance you would get the following result

    >>> init_values: set = {(True, "the_str", 1.2)}
    
    >>> a_var = MyDataClass(init_values)
    
    >>> type(a_var)
    <class '__main__.MyDataClass'>
    
    >>> type(a_var.force)
    <class 'set'>
    

    Now lets recover the type object (not the type hints) on force by applying type() to the __anotations__ on the Class declaration object (here we see the Generic Alias type mentioned earlier). (Here we are indeed checking the type object on the class attribute force).

    >>> type(MyDataClass.__annotations__['force'])
    <class 'typing._GenericAlias'>
    

    Or we could check the annotations on the Class instance, and recover the type hints as we are used to seeing them.

    >>> init_values: set = {(True, "the_str", 1.2)}
    >>> a_var = MyDataClass(init_values)
    >>> a_var.__annotations__
    
    {'force': set[tuple[str, float, bool]]}
    

    I've had to write the same type annotation in the dataclass and the function signature -

    For tuples annotations tend to become long literals and that justifies creating a purpose variable for conciseness. But in general explicit signatures are more descriptive and it's what most API's go for.

    The typing Module

    Fundamental building blocks:

    Tuple, used by listing the element types, for example Tuple[int, int, str]. The empty tuple can be typed as Tuple[()]. Arbitrary-length homogeneous tuples can be expressed using one type and ellipsis, for example Tuple[int, ...]. (The ... here are part of the syntax, a literal ellipsis.)