In many cases we have some group of valid strings and they end up becoming magic strings in the code difficult to find, change, maintain, type hint…
First step is usually to define constants for them.
But it becomes difficult to group them and get type annotations for functions that expect one string from a group.
If we could use attributes then it becomes much easier to group them, use and maintain them (we get autocomplete, can find references, etc.) but it is not obvious which python object is the right one for the job.
Enum is a good candidate, but it forces us to use the .value if we want/have to use the strings (and not the Enum type).
Literal seems to be the right for the job of limiting values in a function parameter, but I cannot figure a way to link it to the strings (without having to type the strings twice).
Example of what I’m trying to describe:
class Mode(???): # should this be Enum? dataclass? named tuple? constants grouped somehow? ... # The goal is to be able to later do something like Mode.read # It should ideally be immutable/frozen read = 'r' write = 'w' # Could we refer to Mode to keep it DRY and easier to maintan whenever we add options? AnyMode = Literal['r', 'w'] def f(mode: AnyMode) ... f(Mode.read)
Using Enum comes close
from enum import Enum from typing import Literal class Mode(Enum): read = 'r' write = 'w' def f(mode: Literal['r', 'w']): # Could be a third party function we cannot change. # If we can change it, then we would like to refer to Mode instead # of having to type the same strings again. if mode in ('r', 'w'): print('mode is ok') else: print('mode is NOT ok') # We are forced to use the .value all the time (not desired) f(Mode.read.value) # mode is ok f(Mode.write.value) # mode is not ok # wrap f to avoid having to use .value all the time ? def f_bis(mode: Mode): return f(mode.value) # We don't need to use .value but we had to wrap the function f_bis(Mode.read) # mode is ok f_bis(Mode.write) # mode is ok ModeLit = Literal[Mode.read, Mode.write] # this works but refers to Enum objects, not strings ModeLit = Literal[Mode.read.value, Mode.write.value] # IDE complains about this
Using dataclasses? Feels like a lot of code and repetitions for something simple…
ReadModeType = Literal['r'] WriteModeType = Literal['w'] ValidModeTypes = Literal[ReadModeType, WriteModeType] @dataclasses.dataclass(frozen=True, eq=True) class _Mode: read: ReadModeType = "r" write: WriteModeType = "w" Mode = _Mode() # have to instantiate it to have it frozen ModeType = Literal[Mode.read, Mode.write] # Literal does not like this def f(mode: ValidModeTypes): if mode in [Mode.read, Mode.write]: print('Mode is ok') else: print('Mode is not ok') f('r') # type check ok f('oops') # type check NOT ok f(Mode.read) # type check ok f(Mode.write) # type check ok