Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

typing: Dynamically Create Literal Alias from List of Valid Values

I have a function which validates its argument to accept only values from a given list of valid options. Typing-wise, I reflect this behavior using a Literal type alias, like so:

from typing import Literal


VALID_ARGUMENTS = ['foo', 'bar']

Argument = Literal['foo', 'bar']


def func(argument: 'Argument') -> None:
    if argument not in VALID_ARGUMENTS:
        raise ValueError(
            f'argument must be one of {VALID_ARGUMENTS}'
        )
    # ...

This is a violation of the DRY principle, because I have to rewrite the list of valid arguments in the definition of my Literal type, even if it is already stored in the variable VALID_ARGUMENTS. How can I create the Argument Literal type dynamically, given the VALID_ARGUMENTS variable?

The following things do not work:

from typing import Literal, Union, NewType


Argument = Literal[*VALID_ARGUMENTS]  # SyntaxError: invalid syntax

Argument = Literal[VALID_ARGUMENTS]  # Parameters to generic types must be types

Argument = Literal[Union[VALID_ARGUMENTS]]  # TypeError: Union[arg, ...]: each arg must be a type. Got ['foo', 'bar'].

Argument = NewType(
    'Argument',
    Union[
        Literal[valid_argument]
        for valid_argument in VALID_ARGUMENTS
    ]
)  # Expected type 'Type[_T]', got 'list' instead

So, how can it be done? Or can't it be done at all?

like image 515
jonathan.scholbach Avatar asked Oct 25 '20 09:10

jonathan.scholbach


People also ask

What is a type alias?

A type alias is defined by assigning the type to the alias. In this example, Vector and list [float] will be treated as interchangeable synonyms: Type aliases are useful for simplifying complex type signatures.

What is a literal type in mypy?

From the mypy documentation: " Literal types may contain one or more literal bools, ints, strs, bytes, and enum values. However, literal types cannot contain arbitrary expressions: types like Literal [my_string.trim ()], Literal [x > 3], or Literal [3j + 4] are all illegal."

What are the alias types of anystr?

These type aliases correspond to the return types from re.compile () and re.match (). These types (and the corresponding functions) are generic in AnyStr and can be made specific by writing Pattern [str], Pattern [bytes], Match [str], or Match [bytes].

How do you indicate that a value is dynamically typed?

Use Any to indicate that a value is dynamically typed. Initially PEP 484 defined Python static type system as using nominal subtyping. This means that a class A is allowed where a class B is expected if and only if A is a subclass of B.


1 Answers

Go the other way around, and build VALID_ARGUMENTS from Argument:

Argument = typing.Literal['foo', 'bar']
VALID_ARGUMENTS: typing.Tuple[Argument, ...] = typing.get_args(Argument)

It's possible at runtime to build Argument from VALID_ARGUMENTS, but doing so is incompatible with static analysis, which is the primary use case of type annotations. Building VALID_ARGUMENTS from Argument is the way to go.

I've used a tuple for VALID_ARGUMENTS here, but if for some reason you really prefer a list, you can get one:

VALID_ARGUMENTS: typing.List[Argument] = list(typing.get_args(Argument))
like image 127
user2357112 supports Monica Avatar answered Sep 18 '22 05:09

user2357112 supports Monica