Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Use of Generic and TypeVar

I'm not able to understand the use of Generic and TypeVar, and how they are related. https://docs.python.org/3/library/typing.html#building-generic-types

The docs have this example:

class Mapping(Generic[KT, VT]):
    def __getitem__(self, key: KT) -> VT:
        ...
        # Etc.

X = TypeVar('X')
Y = TypeVar('Y')

def lookup_name(mapping: Mapping[X, Y], key: X, default: Y) -> Y:
    try:
        return mapping[key]
    except KeyError:
        return default

Type variables exist primarily for the benefit of static type checkers. They serve as the parameters for generic types as well as for generic function definitions.

What can't I simply use Mapping with some existing type, like int, instead of creating X and Y?

like image 960
Abhijit Sarkar Avatar asked Aug 11 '21 09:08

Abhijit Sarkar


People also ask

What is the role of generic?

The generic role indicates an element's role is equivalent to that of the non-semantic <div> and <span> elements. The generic role is intended for use as the implicit role of generic elements in host languages for use by user agents only; not for use by developers.

What is TypeVar?

In short, a TypeVar is a variable you can use in type signatures so you can refer to the same unspecified type more than once, while a NewType is used to tell the type checker that some values should be treated as their own type.

What is generic type Python?

One major topic in the Python programming language that we've made use of but haven't really explained is the use of generic types. A generic type is a class or interface that can accept a parameter for the type of object that it stores. A great example is the List class that we are very familiar with.

Should you use type hints in Python?

Type hints work best in modern Pythons. Annotations were introduced in Python 3.0, and it's possible to use type comments in Python 2.7. Still, improvements like variable annotations and postponed evaluation of type hints mean that you'll have a better experience doing type checks using Python 3.6 or even Python 3.7.


2 Answers

Type variables are literally "variables for types". Similar to how regular variables allow code to apply to multiple values, type variables allow code to apply to multiple types.
At the same time, just like code is not required to apply to multiple values, it is not required to depend on multiple types. A literal value can be used instead of variables, and a literal type can be used instead of type variables – provided these are the only values/types applicable.

Since the Python language semantically only knows values – runtime types are also values – it does not have the facilities to express type variability. Namely, it cannot define, reference or scope type variables. Thus, typing represents these two concepts via concrete things:

  • A typing.TypeVar represents the definition and reference to a type variable.
  • A typing.Generic represents the scoping of types, specifically to class scope.

Notably, it is possible to use TypeVar without Generic – functions are naturally scoped – and Generic without TypeVar – scopes may use literal types.


Consider a function to add two things. The most naive implementation adds two literal things:

def add():
    return 5 + 12

That is valid but needlessly restricted. One would like to parameterise the two things to add – this is what regular variables are used for:

def add(a, b):
    return a + b

Now consider a function to add two typed things. The most naive implementations adds two things of literal type:

def add(a: int, b: int) -> int:
    return a + b

That is valid but needlessly restricted. One would like to parameterise the types of the two things to add – this is what type variables are used for:

T = TypeVar("T")

def add(a: T, b: T) -> T:
    return a + b

Now, in the case of values we defined two variables – a and b but in the case of types we defined one variable – the single T – but used for both variables! Just like the expression a + a would mean both operands are the same value, the annotation a: T, b: T means both parameters are the same type. This is because our function has a strong relation between the types but not the values.


While type variables are automatically scoped in functions – to the function scope – this is not the case for classes: a type variable might be scoped across all methods/attributes of a class or specific to some method/attribute.

When we define a class, we may scope type variables to the class scope by adding them as parameters to the class. Notably, parameters are always variables – this applies to regular parameters just as for type parameters. It just does not make sense to parameterise a literal.

#       v value parameters of the function are "value variables"
def mapping(keys, values):
    ...

#       v type parameters of the class are "type variables"
class Mapping(Generic[KT, VT]):
    ...

When we use a class, the scope of its parameters has already been defined. Notably, the arguments passed in may be literal or variable – this again applies to regular arguments just as for type arguments.

#       v pass in arguments via literals
mapping([0, 1, 2, 3], ['zero', 'one', 'two', 'three'])
#       v pass in arguments via variables
mapping(ks, vs)

#          v pass in arguments via literals
m: Mapping[int, str]
#          v pass in arguments via variables
m: Mapping[KT, VT]

Whether to use literals or variables and whether to scope them or not depends on the use-case. But we are free to do either as required.

like image 176
MisterMiyagi Avatar answered Nov 14 '22 23:11

MisterMiyagi


The whole purporse of using Generic and TypeVar (here represented as the X and Y variables) is when one wants the parameters to be as generic as possible. int can be used, instead, in this case. The difference is: the static analyzer will interpret the parameter as always being an int.

Using generics mean the function accepts any type of parameter. The static analyzer, as in an IDE for instance, will determine the type of the variables and the return type as the arguments are provided on function call or object instantiation.


mapping: Mapping[str, int] = {"2": 2, "3": 3}
name = lookup_name(mapping, "1", 1)

In the above example type checkers will know name will always be an int relying on the type annotations. In IDEs, code completion for int methods will be shown as the 'name' variable is used.

Using specific types is ideal if that is your goal. The function accepting only a map with int keys or values, and/or returning int in this case, for instance.

As X and Y are variable you can choose any name want, basically.

Below example is possible:

def lookup_name(mapping: Mapping[str, int], key: str, default: int) -> int:
    try:
        return mapping[key]
    except KeyError:
        return default

The types are not generic in the above example. The key will always be str; the default variable, the value, and the return type will always be an int. It's the programmer's choice. This is not enforced by Python, though. A static type checker like mypy is needed for that.

The Generic type could even be constrained if wanted:

import typing

X = typing.TypeVar("X", int, str) # Accept int and str
Y = typing.TypeVar("Y", int) # Accept only int

@MisterMiyagi's answer offers a thorough explanation on the use scope for TypeVar and Generic.

like image 21
Maicon Mauricio Avatar answered Nov 14 '22 22:11

Maicon Mauricio