mypy
mypy copied to clipboard
Avoid repetition with `Literal`
Feature
We're often taught that good code is DRY ("don't repeat yourself") – it minimizes redundancy, and with it the possibility that two or more redundant forms won't agree as they're intended (e.g., one gets updated when the other does not).
It's often necessary to make the possible values of a Literal
available to code for use in conditionals. There doesn't seem to be a good way of doing that without repetition or ugly workarounds (like cast
).
- It would be helpful if
Literal
could dereference aFinal
variable of an appropriate type (probably aTuple
) as its parameter (see: attempt 1) - Alternatively, it would be helpful if a
Literal
could be treated like an iterable, tuple, or set (see: attempt 4) - Failing both of the above, mypy's handling of
typing.get_origin()
could accurately reflect function's return value when its input is aLiteral
(see: attempts 3, and related attempt 2) - Even a good, clean way of concisely and automatically verifying agreement between a
Literal
and a tuple or set would be most welcome (see: attempt 5)
Pitch
There isn't really a good way to avoid (error-prone) repetition when defining Literal
s, as in the following:
from __future__ import annotations
import typing
from enum import (
auto,
Enum,
)
from typing import (
AbstractSet,
Final,
Literal,
Tuple,
)
class SomeEnum(Enum):
ZERO = auto()
ONE = auto()
ALFA = auto()
BRAVO = auto()
# ATTEMPT ONE
LETTERS: Final = (SomeEnum.ALFA, SomeEnum.BRAVO)
Letter: Literal[LETTERS] # mypy issues error:
# "Parameter 1 of Literal[...] is invalid"
# [valid-type]
# ATTEMPT TWO
Letter2: Literal[SomeEnum.ALFA, SomeEnum.BRAVO]
LETTERS2: Final[Tuple[SomeEnum, ...]] = Letter2.__args__
# mypy issues error:
# "Item 'SomeEnum' of 'Literal[SomeEnum.ALFA, SomeEnum.BRAVO]'
# has no attribute '__args__'"
# [union-attr]
# ATTEMPT THREE
Letter3: Literal[SomeEnum.ALFA, SomeEnum.BRAVO]
LETTERS3: Final[AbstractSet[SomeEnum]] = frozenset(typing.get_origin(Letter3))
# mypy issues error:
# "Argument 1 to 'frozenset' has incompatible type
# 'Optional[Any]'; expected 'Iterable[Any]'"
# [arg-type]
# ATTEMPT FOUR
Letter4: Literal[SomeEnum.ALFA, SomeEnum.BRAVO]
LETTERS4: Final[AbstractSet[SomeEnum]] = frozenset(Letter4)
# obviously won't work without changes to `Literal` itself
# ATTEMPT FIVE (still requires repetition; also doesn't work)
Letter5: Literal[SomeEnum.ALFA, SomeEnum.BRAVO]
LETTERS5: Final[AbstractSet[SomeEnum]] = {SomeEnum.ALFA, SomeEnum.BRAVO}
assert set(typing.get_origin(Letter5)) == LETTERS5
# mypy issues error:
# "Argument 1 to 'set' has incompatible type
# 'Optional[Any]'; expected 'Iterable[Any]'"
# [arg-type]
My preference would be to accept the 1st or 4th forms, but I realize the third form is probably the easiest to implement (as it wouldn't likely require changes to any PEPs).