mypy icon indicating copy to clipboard operation
mypy copied to clipboard

Avoid repetition with `Literal`

Open finite-state-machine opened this issue 1 year ago • 0 comments

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 a Final variable of an appropriate type (probably a Tuple) 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 a Literal (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 Literals, 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).

finite-state-machine avatar Jun 17 '23 12:06 finite-state-machine