Skip to content

Narrowing unions with in/__contains__ #8940

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

Closed
sfoster1 opened this issue Jun 3, 2020 · 4 comments
Closed

Narrowing unions with in/__contains__ #8940

sfoster1 opened this issue Jun 3, 2020 · 4 comments

Comments

@sfoster1
Copy link

sfoster1 commented Jun 3, 2020

It seems like you should be able to narrow a union of literals to a subset, or a union of unions of literals to just one of the unions, by checking if a value with such a union is in a container of one of the subset/sub unions (see the code example for something hopefully more coherent).

This is really useful for, say, a deserialized JSON object that has different subsets of keys that map to differently-shaped objects, and you want to pass them to functions that take the appropriate parameters.

  • Are you reporting a bug, or opening a feature request?

Really could be either - this is behavior I'm surprised by, but it could be not implemented rather than working incorrectly

  • Minimal repro showing the issue:

Playground gist link: https://mypy-play.net/?mypy=latest&python=3.8&gist=18262b0bbd541a22d4f48c5bb628f00e
Code:

from typing import Union, Dict, List, Literal

firstset = Union[Literal['a'], Literal['b'], Literal['c']]
secondset = Union[Literal['d'], Literal['e'], Literal['f']]

firstdict: Dict[firstset, int] = {'a': 2}
seconddict: Dict[secondset, float] = {'d': 3.0}

firstlist: List[firstset] = ['a', 'b', 'c']
secondlist: List[secondset] = ['d', 'e', 'f']

def printit(key: Union[Literal['a'], Literal['b'], Literal['c'],
                       Literal['d'], Literal['e'], Literal['f']]):
    if key in firstdict:
        reveal_type(key) # Union[Literal['a'], Literal['b'], Literal['c'], Literal['d'], Literal['e'], Literal['f']]'
    if key in seconddict:
        reveal_type(key) # Union[Literal['a'], Literal['b'], Literal['c'], Literal['d'], Literal['e'], Literal['f']]'

    if key in firstlist:
        reveal_type(key) # Union[Literal['a'], Literal['b'], Literal['c'], Literal['d'], Literal['e'], Literal['f']]'
    if key in secondlist:
        reveal_type(key) # Union[Literal['a'], Literal['b'], Literal['c'], Literal['d'], Literal['e'], Literal['f']]'

    if key == 'a' or key == 'b' or key == 'c':  # this one works
        reveal_type(key)  # 'Union[Literal['a'], Literal['b'], Literal['c']]' 
  • What is the actual behavior/output?
test.py:16: note: Revealed type is 'Union[Literal['a'], Literal['b'], Literal['c'], Literal['d'], Literal['e'], Literal['f']]'
test.py:18: note: Revealed type is 'Union[Literal['a'], Literal['b'], Literal['c'], Literal['d'], Literal['e'], Literal['f']]'
test.py:21: note: Revealed type is 'Union[Literal['a'], Literal['b'], Literal['c'], Literal['d'], Literal['e'], Literal['f']]'
test.py:23: note: Revealed type is 'Union[Literal['a'], Literal['b'], Literal['c'], Literal['d'], Literal['e'], Literal['f']]'
test.py:26: note: Revealed type is 'Union[Literal['a'], Literal['b'], Literal['c']]'
  • What is the behavior/output you expect?
test.py:16: note: Revealed type is 'Union[Literal['a'], Literal['b'], Literal['c']]
test.py:18: note: Revealed type is 'Union[Literal['d'], Literal['e'], Literal['f']]
test.py:21: note: Revealed type is 'Union[Literal['a'], Literal['b'], Literal['c']]
test.py:23: note: Revealed type is 'Union[Literal['d'], Literal['e'], Literal['f']]
test.py:26: note: Revealed type is 'Union[Literal['a'], Literal['b'], Literal['c']]'
  • What are the versions of mypy and Python you are using?
    Do you see the same issue after installing mypy from Git master?
    Happens on 3.7 and 3.8, mypy 0.770 and whatever playground has as "latest"
  • What are the mypy flags you are using? (For example --strict-optional)
    Defaults

(FYI if you were reading this shortly after I posted it and were annoyed by me editing it a lot - I'm really sorry, I accidentally posted it early and needed to actually fill it out)

@emmatyping
Copy link
Member

I think this seems reasonable to support, but I'm not sure how hard it would be. Perhaps @Michael0x2a has a better idea.

@antonagestam
Copy link
Contributor

This shares some aspects with my proposal to make immutable containers work like literal unions: #8689

sfoster1 added a commit to sfoster1/mypy that referenced this issue Jun 8, 2020
mypy can already narrow unions in this case:

def b(a: Union[int, str]):
    if a == 2:
         reveal_type(a)  # int

and it can narrow optionals in this case:

def c(d: Optional[int], e: List[int]):
    if d in e:
        reveal_type(d)  # int

This pr allows it to narrow unions in that case:

def f(g: Union[int, str, bool], h: List[Union[int, str]]):
    if g in h:
        reveal_type(g)  # Union[int, str]
    else:
        reveal_type(g)  # bool

This is useful for (in my case) picking structures out of a tagged
union.

In terms of what still needs to be done
- This is my first time contributing to mypy so I'm sure I've missed
some edge cases
- I'm sure I haven't added quite enough tests - some pointers in that
direction would be quite welcome
- I would really like this to also work if the collection items are
scalar types like List[int] - I'm not sure if that's necessary for this
PR though

And any and all comments on the details or broad structure of the PR are
welcome of course.

Closes python#8940
@sfoster1
Copy link
Author

sfoster1 commented Jun 8, 2020

@ethanhs Opened a PR with something that at least begins to implement the issue - interested in your (or @Michael0x2a 's, or really anybody's) thoughts.

@antonagestam I think this would be quite similar to your feature request but would at least at the moment require extra annotation in your code, like

ALLOWED_METHODS: Final[FrozenSet[Union[Literal['POST']...]]] = frozenset({'POST'...})

which is obviously ugly, but I think the proper solution is probably to have your ALLOWED_METHODS = frozenset({... deduce to a final frozenset of a union of Literals rather than using the frozenset itself as a union.

@JelleZijlstra JelleZijlstra added the topic-type-narrowing Conditional type narrowing / binder label Mar 19, 2022
@AlexWaygood
Copy link
Member

Closing as a duplicate of #3229

@AlexWaygood AlexWaygood closed this as not planned Won't fix, can't repro, duplicate, stale Oct 21, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants