Some more follow up :-)

First, I attached a file that contains six functions we care about and the behaviours of the four checkers + some of Jukka's proposals (previously linked). This assumes that things are subject to flow control and that checkers allow reassignments based on the declared type. I was going a little cross eyed, hopefully it helps clarify the state of things. (Apologies if I got something wrong!)

Next, the question was brought up as to why TypeScript behaves the way it does. I’m not particularly familiar with TypeScript, so I can’t claim to know for sure, but I did some digging:

tldr; I'm not convinced there's a good reason. It seems like they just went from always using the annotation to always using the annotation except for unions in order to have a better null story.
It looks like they implemented flow based narrowing in 2016, notably narrowing for unions:
Shortly after, they receive a complaint and discuss other narrowing. The variance narrowing issue gets brought up (along with optional properties, which I think don't matter to us):
It’s then mentioned as a thing they’re still talking about. But null is a reason for wanting to have something in place for unions:
It seems to still get brought up from time to time:

It's also worth asking why mypy currently doesn't abide by PEP 526. It's possible this is just a quirk of history, stemming from the evolution of type comments. mypy's behaviour appears to be the same as Proposal 4, except for not narrowing unions. I think that’s why Proposal 4 exists: it’s the most conservative change for mypy that would help with the original issue report in issue #2008.

Finally, some discussion of arguments against narrowing. Naturally these apply most to Proposal 3, which always narrows, but they do also apply to various status quos. These arguments are presented in Jukka's cons of Proposal 3 (I'd be curious if other arguments surface).

As discussed previously, I’m not fazed by the invariance argument (assuming you narrow to a compatible type).
I think I’ve also made my peace with the Sequence / disallowing local mutability argument, since it seems a little too treacherous to be relied on / you can always be explicit and cast / maybe use Final. I’d also want to see usage in real world code here.
I do think the interactions with Any are something that need some thought. However, if there isn’t a particularly easy way of doing things cleverly, I’m not that worried about inconsistency. People use Any to stop type checkers from trying to be too clever, so preserving the current behaviour of listening to the user in the presence of Any seems fine. I think users who would complain about not narrowing in the presence of Any would also complain about not narrowing at other times.

On Wed, 9 Sep 2020 at 11:17, Dimitris Vardoulakis <> wrote:

On Tue, Sep 8, 2020 at 7:00 PM Shantanu Jain <> wrote:
I found Jukka’s referenced comment to be valuable, so here’s a direct link: Eg, I believe pyright’s narrowing to Animal corresponds in part to Jukka’s Proposal 2 (or Proposal 4), which he mentions is TypeScript’s behaviour: when narrowing a Union, narrow to a member of the Union.

My view on the invariance issue is the most natural flow is to start with:
def g(x: Foo[Union[int, str]]): ...

def f() -> None:
    y = 1
    a = Foo(y)
Then get an error (with a very clear message / suggestion) and add:
def f() -> None:
    y = 1
    a: Foo[Union[int, str]] = Foo(y)
Which is fine in the presence of type narrowing, because obviously we shouldn’t narrow to an incompatible type. (It’s also my opinion that this is the type annotation that best helps describe what’s happening)

I don’t know that I’d be so quick to write off always narrowing (modulo Any), which I believe corresponds to Jukka’s Proposal 3. I’d be curious to know more about the significant number of false positives you’d expect; that seems contra my intuition and the results on the sample corpus Jukka described. It also feels wrong to mandate that all future type checkers can’t narrow in part because a majority of our current ones do not. It feels like if a type checker can prove something about a value, it should be fair game (and that as long as users can see that proof is possible, we’ll get bug reports).

Generally speaking, it’s surprising to me to make a usability argument for a distinction that most end users wouldn’t notice or weren’t making with intention. But maybe this is a case where if the behaviour is PEP-ed users will no longer find it surprising.

These are all good points. I remember that in Closure Compiler, we used to have the semantics where we keep the rhs type. We ran into issues occasionally where the user needed to use the value with the declared type, so they had to cast to that, which was annoying. We ended up changing the semantics to use the declared type. Unfortunately I can't remember the examples anymore though; just the generics one I already mentioned.

On Tue, 8 Sep 2020 at 09:28, dimvar--- via Typing-sig <> wrote:
I also prefer form (1) to respect the declared type and not use the rhs type. Keeping the declared type can avoid a spurious warning in cases where type invariance matters, e.g., with generics.

T = TypeVar('T')

class Foo(Generic[T]):
    def __init__(self, x: T) -> None:
        self.attr = x

def g(x: Foo[Union[int, str]]):
    return x

def f(x: int):
    y: Union[int, str] = 1
    a = Foo(y)
    g(a) # no warning

    y = 2
    b = Foo(y)
    g(b) # warning
Typing-sig mailing list --
To unsubscribe send an email to
Member address: