0

Challenge: Define literals (and their types for function annotations), but only write the literal value once.

Real-life example: A port can be allocated a VLAN, or a list of VLANs, or can be defined as a trunk, or can be unallocated. We need constants for "trunk", "unallocated". The VLAN definition is class VlanId(int): ... We also need type definitions for type annotating functions, e.g.:

  • get_init_port_value() returning a single VLAN or "trunk" (no list of VLANs or "unallocated" is allowed)
  • get_port_status(port) returning current allocations on the given port

Try1: Defining the constant values and types separately. Works but ugly.

Trunk = Literal["trunk"]
TRUNK = "trunk"  # TRUNK type becomes Literal[“trunk”]
# NB, TRUNK: Trunk = “trunk” provides the exact same thing.
Unallocated = Literal["unallocated"]
UNALLOCATED = "unallocated"  # UNALLOCATED type becomes Literal[“unallocated”]
PortVlanAllocation = VlanId | list[VlanId] | Trunk | Unallocated
def get_init_port_value() -> VlanId | Trunk: ...
def get_port_status(port: Port) -> PortVlanAllocation: ...

Try2: Using Final type. Just like Try1, it works but ugly. And Type still does not work, just like Try3.

..
TRUNK: Final = "trunk"  # TRUNK type becomes Literal[“trunk”]
..

Try3: Getting the types from the defined constants. Does not work, because Type can only tell the type of a class.

..
PortVlanAllocation = VlanId | list[VlanId] | Type[TRUNK] | Type[UNALLOCATED]
..

Try4: Getting the types from the defined constants, again. Does not work, because Literal does not accept TRUNK and UNALLOCATED, even though they are literals.

..
PortVlanAllocation = VlanId | list[VlanId] | Literal[TRUNK] | Literal[UNALLOCATED]
..

Try5: Use enum for the non-VLAN allocations. Works, achieved that the literal string is written once only, and compact, but may not be the best.

class NonVlanAlloc(enum.Enum):
    TRUNK = "trunk"
    UNALLOCATED = "unallocated"
PortVlanAllocation = NonVlanAlloc | VlanId | list[VlanId]
def get_init_port_value() -> Literal[NonVlanAlloc.TRUNK] | VlanId: ...

I say it works, as mypy provides the expected results on the following valid and invalid get_init_port_value return statements:

return NonVlanAlloc.UNALLOCATED # Incompatible return value type (got "Literal[NonVlanAlloc.UNALLOCATED]", expected "Union[Literal[NonVlanAlloc.TRUNK], VlanId]")
return "trunk"  # Incompatible return value type (got "Literal['trunk']", expected "Union[Literal[NonVlanAlloc.TRUNK], VlanId]")
return NonVlanAlloc.TRUNK  # OK
return 42  # Incompatible return value type (got "Literal[42]", expected "Union[Literal[NonVlanAlloc.TRUNK], VlanId]")
return VlanId(2345789789)  # OK

This enum solution seems OK, but what if we do not have UNALLOCATED? Defining an enum for the single TRUNK would be a bit of an overkill, wouldn’t it?

I wonder whether there is a way to get the type of TRUNK (without separate explicit definition), i.e. what I wanted to achieve in Try3 and Try4 (and sort-of achieved in Try5). This is the inverse of Getting the literal out of a python Literal type, at runtime?, because

  • they declare the types and figure out the values,
  • while I am after declaring the values (constants) and figuring out their types (e.g. for function annotations).
Netcreator
  • 104
  • 11
  • 2
    maybe you're really after an [Enum](https://docs.python.org/3/library/enum.html)? – ti7 Jun 03 '23 at 04:17
  • 3
    The whole point of using `Literal` is that you can just use... a literal. If you want a global "constant" that is of that literal type, then I don't understand what is so onerous about writing another line – juanpa.arrivillaga Jun 03 '23 at 04:40
  • You can use `typing.get_args` to get `Literal` value from the type, if you don't want to repeat the string. – STerliakov Jun 03 '23 at 09:52
  • 1
    @SUTerliakov OP mentioned that and linked the corresponding post. But I still don't understand the point of all this. Just write the type definition and the constant assignment next to each other. Or use an Enum (seems more appropriate here anyway). This seems like an artificial problem. – Daniil Fajnberg Jun 03 '23 at 09:55
  • Well, if you have a 42-char eth address instead of UNALLOCATED string, repeating it twice might be troublesome. Though I'd use an alias like "owner" instead and lookup the address in a dictionary when needed. Or Enum, if there are several addresses - it fits such tasks very well. – STerliakov Jun 03 '23 at 10:44
  • Thanks for all the prompt comments. Having separate const and type means 2x declaration, plus 2x imports wherever I use it. Not onerous, but not according to the DRY principle. The `typing.get_args()` return type is `Any`, braking the main point of type-checking. Also, it is type->value and my question is about value->type. "Enums are not normal Python classes" (from docs), and actually it causes some type-checking issue in other parts of the code (the details cannot fit here). There might not be a solution, I am just picking your brains out there ... – Netcreator Jun 04 '23 at 00:54
  • Declaring a value, then trying to use the type from the value in the context of a type annotation, generally doesn’t work in Python. The only exception is when the value is a `type` object (e.g. a class). Forget about literals for a moment - try to think how you can declare a function object, then use its type as an annotation somewhere. (You can’t!) – dROOOze Jun 05 '23 at 04:19
  • PEP 586 discusses using [literal values as type hints](https://peps.python.org/pep-0586/#adding-more-concise-syntax), though their rationale for rejecting the idea is not, I think, explained clearly. They indicate using `Tuple[1,2]` as a hint for the literal tuple `(1, 2)`, which fails due to a conflict with its use as a generic type, but I don't see why you would use that instead of `(1,2)`. – chepner Jun 06 '23 at 13:42
  • I found that yes, there are other folks after such solution resulting in https://github.com/python/mypy/issues/10026 (my Try4), but "temporarily" undid it in Aug 2022. Then it remained undone and still pending. The last comment refers to enum as a "solution". – Netcreator Jun 09 '23 at 11:26

1 Answers1

0

while I am after declaring the values (constants) and figuring out their types (e.g. for function annotations).

Static Type annotations cannot be created after the fact. They are static. It's not always obvious because mypy has some logic to examine code, but mypy never actually executes your code. When you declare a variable the interpreter evaluates it at run-time. mypy doesn't know what value the variable holds at run-time because mypy isn't executing at run-time. The best mypy can do it determine the type.

On the other hand, Literal['foo'] can be evaluated at run-time.

This is why folks will implement a Literal first and then dynamically create the variable afterward. There is a pretty decent explanation here


This seems like a great place to adopt a WET approach. See DRY vs WET.

Meaning the following should not be thought of as taboo:

T_trunk = Literal["trunk"]
T_unalloc = Literal["unallocated"]

TRUNK: T_trunk = "trunk"
UNALLOCATED: T_unalloc = "unallocated"

That said, if you REALLY need to stay DRY and the strings are very large then using get_args should be considered acceptable since the annotation needs to be declared first.

from typing import get_args

T_trunk = Literal["trunk"]
T_unalloc = Literal["unallocated"]

TRUNK: T_trunk = get_args(T_trunk)[0]
UNALLOCATED: T_unalloc = get_args(T_unalloc)[0]
Marcel Wilson
  • 3,842
  • 1
  • 26
  • 55