Type arithmetic inconsistency
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
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.
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.
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.
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.