Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mypy does not correctly narrow indexing operations when using Literal or Final keys #7905

Open
Michael0x2a opened this issue Nov 8, 2019 · 3 comments

Comments

@Michael0x2a
Copy link
Collaborator

Consider the following program:

from typing_extensions import Literal, TypedDict
from enum import Enum

class Key(Enum):
    X = 1
    Y = 2
    Z = 3

class MyDict(TypedDict):
    key: Literal[Key.X, Key.Y]
    blah: int

KEY: Literal["key"] = "key"

d: MyDict

if d["key"] is Key.X:
    reveal_type(d["key"])  # note: Revealed type is 'Literal[Key.X]'

if d[KEY] is Key.X:
    reveal_type(d[KEY])  # note: Revealed type is 'Literal[Key.X, Key.Y]'

Mypy is currently capable of narrowing expressions like d["key"], which we can see in the first expression.

So, it's natural to assume that mypy would be able to do the same for the second since the two programs are theoretically identical -- but we can't.

The root cause has to do with the "literal" subsystem (which is not to be confused with the Literal types subsystem) here: https://github.com/python/mypy/blob/master/mypy/literals.py#L65

The index in the second example is a NameExpr, which causes the if statement to evaluate to false and return a LITERAL_NO. This then makes the narrowing logic rule out d[KEY] as a candidate for narrowing in https://github.com/python/mypy/blob/master/mypy/checker.py#L3775.

I'm not really sure what the best way of fixing this would be. The natural solution would be to also pass along the expression type into the literal(...) function, but that seems very annoying to do. I'm also not entirely sure whether this is even a sound narrowing: I'm not very familiar with the "literals" subsystem, or how it's meant to interact with Literal types.

@ilevkivskyi
Copy link
Member

Does this happen often? If not, I would propose to not worry about this for now (mostly because I am not sure what to do either).

@ilevkivskyi
Copy link
Member

Related #7339 (even stronger version of this).

Michael0x2a added a commit to Michael0x2a/mypy that referenced this issue Nov 9, 2019
This diff adds support for the following pattern:

```python
from typing import Enum, List
from enum import Enum

class Key(Enum):
    A = 1
    B = 2

class Foo:
    key: Literal[Key.A]
    blah: List[int]

class Bar:
    key: Literal[Key.B]
    something: List[str]

x: Union[Foo, Bar]
if x.key is Key.A:
    reveal_type(x)  # Revealed type is 'Foo'
else:
    reveal_type(x)  # Revealed type is 'Bar'
```

In short, when we do `x.key is Key.A`, we "propagate" the information
we discovered about `x.key` up one level to refine the type of `x`.

We perform this propagation only when `x` is a Union and only when we
are doing member or index lookups into instances, typeddicts,
namedtuples, and tuples. For indexing operations, we have one additional
limitation: we *must* use a literal expression in order for narrowing
to work at all. Using Literal types or Final instances won't work;
See python#7905 for more details.

To put it another way, this adds support for tagged unions, I guess.

This more or less resolves python#7344.
We currently don't have support for narrowing based on string or int
literals, but that's a separate issue and should be resolved by
python#7169 (which I resumed work
on earlier this week).
Michael0x2a added a commit to Michael0x2a/mypy that referenced this issue Nov 9, 2019
This diff adds support for the following pattern:

```python
from typing import Enum, List
from enum import Enum

class Key(Enum):
    A = 1
    B = 2

class Foo:
    key: Literal[Key.A]
    blah: List[int]

class Bar:
    key: Literal[Key.B]
    something: List[str]

x: Union[Foo, Bar]
if x.key is Key.A:
    reveal_type(x)  # Revealed type is 'Foo'
else:
    reveal_type(x)  # Revealed type is 'Bar'
```

In short, when we do `x.key is Key.A`, we "propagate" the information
we discovered about `x.key` up one level to refine the type of `x`.

We perform this propagation only when `x` is a Union and only when we
are doing member or index lookups into instances, typeddicts,
namedtuples, and tuples. For indexing operations, we have one additional
limitation: we *must* use a literal expression in order for narrowing
to work at all. Using Literal types or Final instances won't work;
See python#7905 for more details.

To put it another way, this adds support for tagged unions, I guess.

This more or less resolves python#7344.
We currently don't have support for narrowing based on string or int
literals, but that's a separate issue and should be resolved by
python#7169 (which I resumed work
on earlier this week).
Michael0x2a added a commit to Michael0x2a/mypy that referenced this issue Nov 9, 2019
This diff adds support for the following pattern:

```python
from typing import Enum, List
from enum import Enum

class Key(Enum):
    A = 1
    B = 2

class Foo:
    key: Literal[Key.A]
    blah: List[int]

class Bar:
    key: Literal[Key.B]
    something: List[str]

x: Union[Foo, Bar]
if x.key is Key.A:
    reveal_type(x)  # Revealed type is 'Foo'
else:
    reveal_type(x)  # Revealed type is 'Bar'
```

In short, when we do `x.key is Key.A`, we "propagate" the information
we discovered about `x.key` up one level to refine the type of `x`.

We perform this propagation only when `x` is a Union and only when we
are doing member or index lookups into instances, typeddicts,
namedtuples, and tuples. For indexing operations, we have one additional
limitation: we *must* use a literal expression in order for narrowing
to work at all. Using Literal types or Final instances won't work;
See python#7905 for more details.

To put it another way, this adds support for tagged unions, I guess.

This more or less resolves python#7344.
We currently don't have support for narrowing based on string or int
literals, but that's a separate issue and should be resolved by
python#7169 (which I resumed work
on earlier this week).
@Michael0x2a
Copy link
Collaborator Author

Yeah, it's somewhat low priority. I mostly added this issue because it came up while I was working on another PR and wanted something to link to.

Michael0x2a added a commit that referenced this issue Nov 13, 2019
This diff adds support for the following pattern:

```python
from typing import Enum, List
from enum import Enum

class Key(Enum):
    A = 1
    B = 2

class Foo:
    key: Literal[Key.A]
    blah: List[int]

class Bar:
    key: Literal[Key.B]
    something: List[str]

x: Union[Foo, Bar]
if x.key is Key.A:
    reveal_type(x)  # Revealed type is 'Foo'
else:
    reveal_type(x)  # Revealed type is 'Bar'
```

In short, when we do `x.key is Key.A`, we "propagate" the information
we discovered about `x.key` up to refine the type of `x`.

We perform this propagation only when `x` is a Union and only when we
are doing member or index lookups into instances, typeddicts,
namedtuples, and tuples. For indexing operations, we have one additional
limitation: we *must* use a literal expression in order for narrowing
to work at all. Using Literal types or Final instances won't work;
See #7905 for more details.

To put it another way, this adds support for tagged unions, I guess.

This more or less resolves #7344.
We currently don't have support for narrowing based on string or int
literals, but that's a separate issue and should be resolved by
#7169 (which I resumed work
on earlier this week).
@AlexWaygood AlexWaygood added the topic-final PEP 591 label Mar 28, 2022
@AlexWaygood AlexWaygood added the topic-type-narrowing Conditional type narrowing / binder label May 3, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants