I’m not sure in practice how easy it is to take this into account though. I assume that this is what Mike means by analysing the whole program graph and it implies that when reaching an error the type checker should backtrack and see if a compatible narrower type could have been chosen somewhere. As far as I know neither mypy or pyright ever does this: type propagation only applies in the forward direction. Apart from the runtime and complexity difficulties of backtracking I imagine it would be more difficult for a programmer to understand because later code affects the types retroactively and the location at which a type checker should report an error would be ambiguous.
In general using the inferred type to infer new types reduces the need for annotations and even makes it possible to do things that cannot be expressed with annotations like:
class B:
pass
class C(B):
def call_c_method(self):
pass
def func(b: B) -> B:
if isinstance(b, C):
a = [b] # list[C]
a[0].call_c_method()
else:
a = [b] # list[B]
return a[0]
In my experience this kind of inference is more useful and avoids a lot of boilerplate like introducing new variables a1, a2 etc just so they can have different types in each branch. Here pyright can fully understand func and infer and check all of the types without needing any annotations in the body of the function. On the other hand there are no annotations that would satisfy mypy in this case: instead the code has to be rewritten somehow.
I find this situation frequently when adding annotations in a large existing codebase: I can annotate function parameters and return values in a few places and then pyright understands the bodies of functions by inferring the types of all local and intermediate objects. Annotating the signature of a function though causes mypy to start checking the body and then requiring annotations or code changes.
The issue in this thread is really just that inference with Literal types is ambiguous because they aren’t really intended to be nominal types like enums whose type must be strictly preserved. Instead they are supposed to be literal values:
from typing import Literal, TypeAlias
Days: TypeAlias = Literal[0, 1, 2, 3, 4, 5, 6]
def next(day: Days) -> Days:
return (day + 1) % 7