python / mypy

Optional static typing for Python
https://www.mypy-lang.org/
Other
18.22k stars 2.78k forks source link

Let "Literal" accept another Literal type variable #10026

Open xuhdev opened 3 years ago

xuhdev commented 3 years ago

Feature

Currently Literal doesn't accept another Literal or Final[Literal]. For example:

from typing_extensions import Final, Literal

s: Final[Literal['ccc']] = 'ccc'
s1: Literal[s] = s

outputs:

a.py:4: error: Parameter 1 of Literal[...] is invalid
a.py:4: error: Variable "a.s" is not valid as a type
a.py:4: note: See https://mypy.readthedocs.io/en/latest/common_issues.html#variables-vs-type-aliases
Found 2 errors in 1 file (checked 1 source file)

Pitch

This is particularly useful when there are some defined constants and people refer to those constants rather than the actual value. For example, some of these socket constants: https://docs.python.org/3/library/socket.html#socket.BDADDR_ANY

Zac-HD commented 2 years ago

I'm not sure this is a bug: the PEP notes that "", and mypy is perfectly happy if you omit the Final:

from typing import Literal

var = Literal[Literal[1]] = 1

And in Literal[s], s is a name, not a literal value.

ZeeD commented 2 years ago

while this may be not a bug, I thik this design decision limits the usefulness of Literals - and Finals let's say I define a pair of constants

from typing import Final, Literal

MY_CONST_A: Final = 1
MY_CONST_B: Final = 2

from now on I would like to avoid using directly the values 1 and 2 and instead refer to the constants MY_CONST_A and MY_CONST_B.

let's say I want to define a function that can return either of that consts, what the return type should be? or similarly, I want to set the type of a value that can be set to one of the two values; what is it's type?

def foo() -> Literal[MY_CONST_A, MY_CONST_B]:
    return MY_CONST_A if ... else MY_CONST_B

is not supported by mypy, and I need to set Literal[1,2] as return type to make the signature work

erictraut commented 2 years ago

This proposal would require an update to PEP 586, so this discussion should probably be moved to the typing-sig forum rather than mypy's issue tracker. (For full transparency, I'm one of the primary contributors to pyright, Microsoft's Python type checker.)

If we were to adopt a change like this, I think we'd need to place some additional constraints on the constants. Not only would they need to be marked Final, but they'd also need to be assigned an expression that is unambiguously a literal value — in other words, expressions that are allowed today within a Literal. That excludes more complex expressions.

For example, I would propose that none of these constants should be allowed in a Literal.

MY_CONST_C: Final = my_func()
MY_CONST_D: Final = 1 + 2
MY_CONST_E: Final = 1 if a else 2
MY_CONST_F: Final = (1, 2, 3)[0]
MY_CONST_G: Final = ...

The problem with more complex expressions is that different type checkers can infer different types. If we constrain it to literal values only, there's no ambiguity or inconsistency between type checkers.

DevilXD commented 2 years ago

I'm not sure if it's helpful or not, but PEP 586 already mentions this:

Literal may also be parameterized by other literal types, or type aliases to other literal types. For example, the following is legal:

ReadOnlyMode         = Literal["r", "r+"]
WriteAndTruncateMode = Literal["w", "w+", "wt", "w+t"]
WriteNoTruncateMode  = Literal["r+", "r+t"]
AppendMode           = Literal["a", "a+", "at", "a+t"]

AllModes = Literal[ReadOnlyMode, WriteAndTruncateMode,
                   WriteNoTruncateMode, AppendMode]

This feature is again intended to help make using and reusing literal types more ergonomic.

That being said, I just ran into this issue while trying to specify a variable type as Literal[logging.NOTSET, logging.DEBUG], caused most likely by those being inferred by MyPy as int rather than Literal[0] and Literal[10] respectively. Regarding the type being int instead of Literal, I've proposed a typeshed PR: https://github.com/python/typeshed/pull/6610

Having that PR accepted, MyPy would still be expected to accept Literal type aliases like so:

NOTSET: Literal[0]
DEBUG: Literal[10]

# currently valid
logging_level: Literal[Literal[0], Literal[10]]
# currently invalid
logging_level: Literal[NOTSET, DEBUG]
karolyjozsa commented 1 year ago

What if we take a different approach, and the Type parameterization is extended with Literals? I.e. if we could get this working?

MY_CONST_A: Final = 1
MY_CONST_B: Final = 2
def foo() -> Type[MY_CONST_A] | Type[MY_CONST_B]:
    return MY_CONST_A if ... else MY_CONST_B