7

How can I get the literal value out of a Literal[] from typing?

from typing import Literal, Union

Add = Literal['add']
Multiply = Literal['mul']
Action = Union[Add,Multiply]

def do(a: Action):
    if a == Add:
        print("Adding!")
    elif a == Multiply:
        print("Multiplying!")
    else:
        raise ValueError

do('add')

The code above type checks since 'add' is of type Literal['add'], but at runtime, it raises a ValueError since the string 'add' is not the same as typing.Literal['add'].

How can I, at runtime, reuse the literals that I defined at type level?

LudvigH
  • 1,769
  • 17
  • 33
  • Isn't `Literal` just a hack for the specific use case of type annotations though? Why would you not simply use the string `'add'` here? – tripleee Dec 03 '19 at 19:28
  • With the code above, a static analyzer can tell me that exactly the strings `'add'` and `'mul'` are the allowed parameter values to `do`. Compare with example `AllModes` in [PEP-586](https://www.python.org/dev/peps/pep-0586/). I don't think I could do that so easily with just the strings. – LudvigH Dec 03 '19 at 19:34
  • But a `Literal` is not a value, it is a type constraint which permits a set of values. In your example, `Literal['add', 'mul']` would accept both of these values, and there is obviously no way from the constraint alone to know which of these the it is going to be passed, if indeed it is going to be passed a valid literal at all, or anything whatsoever. – tripleee Dec 03 '19 at 19:39
  • Yes. But I want to make a `if..elif..` statement to check what value was passed, and since I think it is good practice to not have a string written at several place, I would like to not write `'add'`, but rather `Add.value` or something similar. A similar effect is accomplished with enums, of course, but means that the user of `do` would have to import the specific enum to pass parameters. – LudvigH Dec 03 '19 at 20:30
  • I can see that this seems to be what you want, but I don't understand why you insist on this design. A dict of string: function would seem to be a more traditional as well as more workable design. If you want to create a literal out of the dictionary's keys, that will then be completely trivial. – tripleee Dec 04 '19 at 05:50
  • So in summary, it is impossible to get the value out of the typing.Literal at runtime, and your recommendation is to type the same string literal at two places: in dict keys and in the typing.Literal? – LudvigH Dec 04 '19 at 06:25
  • `Literal[ops.keys()]` doesn't require any retyping. I haven't investigated whether it is in fact impossible; again, I would recommend either clarifying why you think it is useful, or trying a different approach. – tripleee Dec 04 '19 at 06:26
  • PEP-586: Literal may be parameterized with literal ints, byte and unicode strings, bools, Enum values and None. .... The following parameters are intentionally disallowed by design: Arbitrary expressions like Literal[3 + 4] or Literal["foo".replace("o", "b")]. ....so that won't work, by what I understand. – LudvigH Dec 04 '19 at 06:32
  • Works here, in the sense that I can type that in without errors, though I can imagine it might throw off a lexical analyzer. – tripleee Dec 04 '19 at 06:35
  • 1
    `myliteral.__args__` contains the args used to initialize it, so I suppose you could dig them out from there. – tripleee Dec 04 '19 at 06:37
  • Would you like to hint at that in an answer, so I can mark it accepted? I just found the `typing.get_args` and `typing.get_origin`, and they might help a little bit. The conclusion still seems to hold: there is no good way to do this i python at the moment. – LudvigH Dec 04 '19 at 07:15

1 Answers1

9

The typing module provides a function get_args which retrieves the arguments with which your Literal was initialized.

>>> from typing import Literal, get_args
>>> l = Literal['add', 'mul']
>>> get_args(l)
('add', 'mul')

However, I don't think you gain anything by using a Literal for what you propose. What would make more sense to me is to use the strings themselves, and then maybe define a Literal for the very strict purpose of validating that arguments belong to this set of strings.

>>> def my_multiply(*args):
...    print("Multiplying {0}!".format(args))
...
>>> def my_add(*args):
...    print("Adding {0}!".format(args))
...
>>> op = {'mul': my_multiply, 'add': my_add}
>>> def do(action: Literal[list(op.keys())]):
...    return op[action]

Remember, a type annotation is essentially a specialized type definition, not a value. It restricts which values are allowed to pass through, but by itself it merely implements a constraint -- a filter which rejects values which you don't want to allow. And as illustrated above, its argument is a set of allowed values, so the constraint alone merely specifies which values it will accept, but the actual value only comes when you concretely use it to validate a value.

tripleee
  • 139,311
  • 24
  • 207
  • 268
  • Is that `Litera[list...]` valid Python? mypy gives me `error: Invalid type: Literal[...] cannot contain arbitrary expressions` – huyz Dec 05 '20 at 21:27
  • The `get_args` approach won't work out of the box if one combined multiple literals like `Literal[Literal[Literal[1, 2, 3], "foo"], 5, None]` (source: [PEP 586](https://www.python.org/dev/peps/pep-0586/)). For that, one needs to call `get_args` multiple times – Intrastellar Explorer Jan 12 '21 at 01:43
  • 1
    @huyz I guess I didn't test at the time, and you seem to be right; neither `Literal[lst]` nor `Literal[*lst]` works for me in Python 3.8.2. – tripleee Jan 12 '21 at 05:34