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).