microsoft / pyright

Static Type Checker for Python

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Odd type narrowing for variable length tuples in value dependent if statements

runfalk opened this issue · comments

Describe the bug
I recently updated to 1.1.361 from 1.1.358 and got a new type error that passed previously.

I think I've narrowed it down narrowing of the tuple type in conditionals.

Code or Screenshots
I've managed to reproduce the same behaviour here.

import typing as t

def erased_tuple_range(l: int) -> tuple[int, ...]:
    return tuple(range(l))

triplet = erased_tuple_range(3)
first = triplet[0]
t.reveal_type(triplet)  # tuple[int, ...]
if first > 1 and len(triplet) > 1:
    # This seems correct to me
    t.reveal_type(triplet)  # tuple[int, int, *tuple[int, ...]]
else:
    # I'd expect this to still be tuple[int, ...] as we shouldn't be able to narrow this due to the `first > 1` check
    t.reveal_type(triplet)  # tuple[int, ...] | tuple[()] | tuple[int]

    # Slicing the triplet reveals the oddity I ran into. I'd expect this to still be
    # tuple[int, ...] as the if statement shouldn't allow us to narrow this down.
    t.reveal_type(triplet[1:])  # tuple[int, ...] | tuple[Unknown, ...] | tuple[()]

t.assert_type(erased_tuple_range(3)[1:], tuple[int, ...])

VS Code extension or command-line
Command line pyright.

The type narrowing is working as intended here.

The type tuple[int, ...] is equivalent to tuple[()] | tuple[int] | tuple[int, int] | .... In other words, it's an infinite union. For details, refer to the Python typing spec.

The part that is arguably incorrect here is in the evaluation of triplet[0] which is not strictly safe because the length of triplet is unknown and could be zero length. For example, if you call erased_tuple_range(0) rather than erased_tuple_range(3), your code will crash. Pyright doesn't currently catch this error, but it arguably should. If that's of interest to you, feel free to create a new enhancement request.

You can fix your code by adding an assert len(triplet) > 0 before the line first = triplet[0].

I'm going to close this issue because I don't think there's a bug in the type narrowing code.

Thank you for the answer. The assert indeed resolves the issue. The thing that still confuses me is that one of the members in the union after slicing is tuple[Unknown, ...], which is what caused the problem for me in the real code. I was misguided in the post above, but this seems like a real issue to me.

This is perhaps a better example of what I mean.

import typing as t

t.reveal_type(())  # tuple[()]
t.reveal_type(()[1:])  # tuple[Unknown, ...], but I would expect tuple[()]
assert () == ()[1:]  # OK

Ah, I see what you mean. I didn't realize (or perhaps knew at one point but forgot) that tuples allow out-of-range slices. Yeah, I can change that so pyright's behavior matches.

This is addressed in pyright 1.1.363, which I just published.