Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

tuple length type narrowing #2474

Closed
GBeauregard opened this issue Oct 22, 2021 · 8 comments
Closed

tuple length type narrowing #2474

GBeauregard opened this issue Oct 22, 2021 · 8 comments
Labels
addressed in next version Issue is fixed and will appear in next published version enhancement request New feature or request

Comments

@GBeauregard
Copy link

Describe the bug
pyright should support type narrowing based on tuple length to avoid the false positive in the following code snippet:

from typing import Union

MultiTuple = Union[tuple[object], tuple[object, object]]

def process_tuples(tup: MultiTuple) -> object:
    if len(tup) == 1:
        return tup[0]
    else:
        first, second = tup
        return second
  /home/gregory/Downloads/testcase/pytest/tuplesize.py:9:9 - error: Tuple size mismatch: expected 2 but received 1 (reportGeneralTypeIssues)

This came up in the rich codebase, and there's a corresponding mypy issue with an open PR: python/mypy#1178

Expected behavior
pyright should be able to type narrow tuples based on len()

VS Code extension or command-line
pyright 1.1.180 Linux commandline

@GBeauregard
Copy link
Author

This one is particularly nasty in the rich codebase because the len(..)>1 is a large union of signatures that's quite unwieldy to cast

@erictraut
Copy link
Collaborator

erictraut commented Oct 22, 2021

This isn't a supported type narrowing pattern. For a full list of supported type narrowing patterns, see this documentation.

Each new type narrowing pattern requires custom code and can slow down overall analysis, so we do not add new patterns unless we see signal that it's a commonly-used case that will help a significant number of pyright users. I haven't seen this use case very frequently, and it's not currently supported in mypy. So, for now, I don't think we can justify adding this. We can revisit in the future if we get additional signal.

In the meantime, I recommend using a user-defined type guard defined in PEP 647. I am the author of that PEP, and one of the examples I provided specifically covers your use case. Search for is_two_element_tuple.

@erictraut erictraut added the as designed Not a bug, working as intended label Oct 22, 2021
@GBeauregard
Copy link
Author

For tracking purposes, here's the open PR for mypy that adds this type of type narrowing support: python/mypy#10367 It seems upstream was interested in merging but it got lost between releases so we'll see what happens. I do think there's value in the len narrowing pattern at least for tuples.

@GBeauregard
Copy link
Author

Is there some trick or workaround to TypeGuard length 1 tuples? I suspect you're aware of the pitfalls by your choice to do the two element case in the PEP. It seems to me this isn't really expressible. This is needed for rich since the not-length-1 case is large union of types so I think unless we want to make 4 or 5 typeguards we actually do have to use a cast.

@erictraut
Copy link
Collaborator

erictraut commented Oct 22, 2021

_T = TypeVar("_T")

def is_two_element_tuple(val: tuple[_T, ...]) -> TypeGuard[tuple[_T, _T]]:
    return len(val) == 2

def process_tuples(tup: MultiTuple) -> object:
    if not is_two_element_tuple(tup):
        return tup[0]
    else:
        first, second = tup
        return second

@GBeauregard
Copy link
Author

GBeauregard commented Oct 22, 2021

I think we're fairly confident in IRC this isn't possible with TypeGuard. Your proposed solution has a number of problems with tuple unions, for example you're only checking the length 2 case when I need to separate out the length 1 tuple case from an inferred Tuple[Unknown, ...] (due to an isinstance tuple check w/ an object) - and I need to separate this in addition to a few specific sized cases where further these tuple cases don't uniformly have the same type.

See https://github.com/willmcgugan/rich/blob/e6fec11cae662d26c22ed54340681cada272fc3c/rich/repr.py#L65 for my solution where the signature of arg is being narrowed from Union[object, Tuple[object], Tuple[str, object], Tuple[str, object, object]] by starting with an isinstance to tuple.

@erictraut
Copy link
Collaborator

If you provide a minimal self-contained example, I can help find a solution.

@GBeauregard
Copy link
Author

I wrote out a long post with an example and lessons, but I've decided it's all redundant except:

Apologies for burying the lead for what I really wanted from the start. To make a long story short my main problem is I didn't internalize the part of the PEP that TypeGuard only narrows in the positive case leading to confusion when I was reveal_type'ing the negative case. My main lesson is TypeGuard is more like assert and less like branching isinstance checks.

I appreciate your time and I'll update if len() ever becomes a supported narrowing pattern in mypy (it looks they also want it to work for literals which could be interesting). It's not needed for my use case, but I can see value in the ability to narrow in both branches in a way that a len() narrowing could provide (but I suppose you could double TypeGuard). Studying my example at least gave appreciation for the complication involved in implementing this enhancement request.

@erictraut erictraut added addressed in next version Issue is fixed and will appear in next published version enhancement request New feature or request and removed as designed Not a bug, working as intended labels Jan 1, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
addressed in next version Issue is fixed and will appear in next published version enhancement request New feature or request
Projects
None yet
Development

No branches or pull requests

2 participants