pyright icon indicating copy to clipboard operation
pyright copied to clipboard

Type arithmetic inconsistency

Open MathiasSven opened this issue 4 months ago • 4 comments

Describe the bug This came up while annotating a list of flags. The in-place OR operator does not behave as expected when it comes to Literals. This is perhaps understandable if extending the type level arithmetic to those comes with unforeseen consequences or unwarranted complexity, however what is more puzzling is the change in behaviour of a[0] | two when it is inside a for loop.

Code

from typing import Literal

two: Literal[2] = 2

a: list[Literal[0, 1, 2, 3]] = []

reveal_type(a[0] | two)      # Literal[2, 3]
a[0] = a[0] | two            # OK
for _ in [...]:
    reveal_type(a[0] | two)  # int
    a[0] = a[0] | two        # ERROR

b: list[Literal[0, 1, 2, 3]] = []
b[0] |= two                  # ERROR

VS Code extension or command-line Command-line, version 1.1.406

MathiasSven avatar Oct 05 '25 17:10 MathiasSven

Pyright does not perform "literal math" inside of loops because there's generally no way to determine statically how many times the loop will be performed. That explains the first error in your example.

Pyright also doesn't currently perform literal math for augmented assignment operations. This could theoretically be added, but it would be an enhancement request.

For more information about literal math and its limitations, refer to this documentation.

erictraut avatar Oct 05 '25 17:10 erictraut

Upon further inspection, pyright does support literal math for augmented assignment operations. I'm not sure off the top of my head why it's not working in your example above. Reopening.

erictraut avatar Oct 05 '25 17:10 erictraut

Reviewing the history of this implementation, literal math for augmented assignments is currently applied only when the target is a local variable — not an index expression, an attribute access expression, a variable that has a global or nonlocal binding, or a variable that is captured from an outer scope. Applying it in these situations is generally unsafe. I added this restriction in response to this bug report.

I think I could safely relax this restriction specifically for index expressions and attribute access expressions, but I'll need to think about it more.

erictraut avatar Oct 05 '25 18:10 erictraut

Ah yes, apologies, when I stumbled across this I hadn't thought about that because Bitwise OR is idempotent, but for most operations this would not be the case. Additionally, for this use case, enum.Flag is more appropriate and avoids this issue.

Regarding the augmented assignment issue, I installed pyright 1.1.353 to see if I could modify the example so it would fail with either index or attribute access, and perhaps this could be an issue?

# pyright: strict

from typing import reveal_type

def test():
    class A:
        v = 0
    A.v = 0

    def foo():
        A.v += 1

    reveal_type(A.v)
    A.v += 1
    reveal_type(A.v)
    foo()
    reveal_type(A.v)
    return A.v

a = test()
reveal_type(a)
print(a)
❯ npx pyright
/tmp/tmp.SUz16mTJvW/main.py
  /tmp/tmp.SUz16mTJvW/main.py:13:17 - information: Type of "A.v" is "Literal[0]"
  /tmp/tmp.SUz16mTJvW/main.py:15:17 - information: Type of "A.v" is "Literal[1]"
  /tmp/tmp.SUz16mTJvW/main.py:17:17 - information: Type of "A.v" is "Literal[1]"
  /tmp/tmp.SUz16mTJvW/main.py:21:13 - information: Type of "a" is "Literal[1]"
0 errors, 0 warnings, 4 informations

At runtime print(a) shows 2.

MathiasSven avatar Oct 05 '25 18:10 MathiasSven