-
-
Notifications
You must be signed in to change notification settings - Fork 2.8k
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
mypy
unable to narrow type of tuple elements in case
clause in pattern matching
#12364
Comments
mypy
unable to narrow type in case
clause in pattern matchingmypy
unable to narrow type of tuple elements in case
clause in pattern matching
Yeah, mypy doesn't narrow on tuples. I commented on it here: #12267 (comment) But apparently the worry is that it is too computationally expensive. EDIT: hmm, maybe it's not the same problem actually |
I just ran into this exact problem in a codebase I'm converting to mypy. I worked around the issue (for now) by adding a superfluous |
I believe the issue I came across fits here. For example: from datetime import date
from typing import Optional
def date_range(
start: Optional[date],
end: Optional[date],
fmt: str,
sep: str = "-",
ongoing: str = "present",
) -> str:
match (start, end):
case (None, None): # A
return ""
case (None, date() as end): # B
return f"{sep} {end.strftime(fmt)}" # C
case (date() as start, None):
return f"{start.strftime(fmt)} {sep} {ongoing}"
case (date() as start, date() as end):
return f"{start.strftime(fmt)} {sep} {end.strftime(fmt)}"
case _:
raise TypeError(f"Invalid date range: {start} - {end}")
print(date_range(date(2020, 1, 1), date(2022, 10, 13), "%Y-%m-%d")) (This not-quite-minimal working example should run as-is). After line To circumvent, I cast to While this works and line Another suspicion I have is that structural pattern matching in Python is intended more for matching to literal cases, not so much for general type checking we'd ordinarily use EDIT: More context here: |
I have what appears the same issue (mypy 1.2, Python 3.11): A value of type match token:
case str(x):
...
case 'define', def_var, def_val:
reveal_type(token)
...
case 'include', inc_path:
reveal_type(token)
...
case 'use', use_var, lineno, column:
reveal_type(token)
...
case _:
reveal_type(token)
assert_never(token) In each case the type revealed is the full union, without only a Also Also for some reason I don’t see any errors from mypy in VS Code on this file, but I see them in droves when running it myself. 🤔 |
I encountered a similar issue with mapping unpacking, I suppose it fits here: values: list[str] | dict[str, str] = ...
match values:
case {"key": value}:
reveal_type(value) produces
Interestingly, if I replace the first line by
Looks like mixing list and dict causes an issue here? |
Ran into this with a two boolean unpacking def returns_int(one: bool, two: bool) -> int:
match one, two:
case False, False: return 0
case False, True: return 1
case True, False: return 2
case True, True: return 3 Don't make me use three if-elses and double the linecount 😢 |
I see a a number of others have provided minimal reproducible examples so I am including mine as is just for another reference point. MyPy Version: 1.5.0 from enum import StrEnum
from typing import assert_never
from pydantic import BaseModel
class InvalidExpectedComparatorException(Exception):
def __init__(
self,
minimum: float | None,
maximum: float | None,
nominal: float | None,
comparator: "ProcessStepStatus.MeasurementElement.ExpectedNumericElement.ComparatorOpts | None",
detail: str = "",
) -> None:
"""Raised by `ProcessStepStatus.MeasurementElement.targets()` when the combination of an ExpectedNumericElement
minimum, maximum, nominal, and comparator do not meet the requirements in IPC-2547 sec 4.5.9.
"""
# pylint: disable=line-too-long
self.minimum = minimum
self.maximum = maximum
self.nominal = nominal
self.comparator = comparator
self.detail = detail
self.message = f"""Supplied nominal, min, and max do not meet IPC-2547 sec 4.5.9 spec for ExpectedNumeric.
detail: {detail}
nominal: {nominal}
minimum: {minimum}
maximum: {maximum}
comparator: {comparator}
"""
super().__init__(self.message)
class ExpectedNumericElement(BaseModel):
"""An expected numeric value, units and decade with minimum and maximum values
that define the measurement tolerance window. Minimum and maximum limits shall be in the
same units and decade as the nominal. When nominal, minimum and/or maximum attributes are
present the minimum shall be the least, maximum shall be the greatest and the nominal shall
fall between these values.
"""
nominal: float | None = None
units: str | None = None
decade: float = 0.0 # optional in schema but says default to 0
minimum: float | None = None
maximum: float | None = None
comparator: "ComparatorOpts | None" = None
position: list[str] | None = None
class ComparatorOpts(StrEnum):
EQ = "EQ"
NE = "NE"
GT = "GT"
LT = "LT"
GE = "GE"
LE = "LE"
GTLT = "GTLT"
GELE = "GELE"
GTLE = "GTLE"
GELT = "GELT"
LTGT = "LTGT"
LEGE = "LEGE"
LTGE = "LTGE"
LEGT = "LEGT"
expected = ExpectedNumericElement()
if expected.comparator is None:
comp_types = ExpectedNumericElement.ComparatorOpts
# If no comparator is expressed the following shall apply:
match expected.minimum, expected.maximum, expected.nominal:
case float(), float(), float():
# If both limits are present then the default shall be GELE. (the nominal is optional).
comparator = comp_types.GELE
case float(), float(), None:
# If both limits are present then the default shall be GELE. (the nominal is optional).
comparator = comp_types.GELE
case float(), None, float():
# If only the lower limit is present then the default shall be GE.
comparator = comp_types.GE
case None, float(), float():
# If only the upper limit is present then the default shall be LE.
comparator = comp_types.LE
case None, None, float():
# If only the nominal is present then the default shall be EQ.
comparator = comp_types.EQ
case None as minimum, None as maximum, None as nom:
raise InvalidExpectedComparatorException(
minimum, maximum, nom, expected.comparator, "No minimum, maximum or, nominal present."
)
case float() as minimum, None as maximum, None as nom:
raise InvalidExpectedComparatorException(
minimum, maximum, nom, expected.comparator, "Minimum without maximum or nominal."
)
case None as minimum, float() as maximum, None as nom:
raise InvalidExpectedComparatorException(
minimum, maximum, nom, expected.comparator, "Maximum without minimum"
)
case _:
# NOTE: Known issue with mypy here narrowing types for tuples so assert_never doesn't work
# https://github.com/python/mypy/issues/14833
# https://github.com/python/mypy/issues/12364
assert_never((expected.minimum, expected.maximum, expected.nominal))
# raise InvalidExpectedComparatorException(
# expected.minimum,
# expected.maximum,
# expected.nominal,
# expected.comparator,
# "Expected unreachable",
# ) |
Out of curiosity is this planned to be addressed at some point? Is there a roadmap doc somewhere? This seems to be quite a fundamental issue that prevents using |
Mypy is a volunteer-driven project. There is no roadmap other than what individual contributors are interested in. |
I tried to dig into this. If I got it right, there are several similar-but-distinct issues here:
|
Just got bitten by this again, @JelleZijlstra do you think you could take a look at the linked PR, or ping anyone who could? Can I do something to help? 🙏 |
…16905) Fixes #12364 When matching a tuple to a sequence pattern, this change narrows the type of tuple items inside the matched case: ```py def test(a: bool, b: bool) -> None: match a, b: case True, True: reveal_type(a) # before: "builtins.bool", after: "Literal[True]" ``` This also works with nested tuples, recursively: ```py def test(a: bool, b: bool, c: bool) -> None: match a, (b, c): case _, [True, False]: reveal_type(c) # before: "builtins.bool", after: "Literal[False]" ``` This only partially fixes issue #12364; see [my comment there](#12364 (comment)) for more context. --- This is my first contribution to mypy, so I may miss some context or conventions; I'm eager for any feedback! --------- Co-authored-by: Loïc Simon <[email protected]>
I'm using 1.11.1 and still having issues with this: import typing as t
a: str | None = 'hello'
b: str | None = 'world'
match a, b:
case None, None:
# do nothing
pass
case a, None:
t.assert_type(a, str)
case None, b:
t.assert_type(b, str)
case a, b:
t.assert_type(a, str)
t.assert_type(b, str) I get errors on all asserts. |
Yeah, this is falls in the case 2:
Which is indeed not solved. I think it would be better to split it in a new issue though. |
Bug Report
When using
match
on a tuple,mypy
is unable to apply type narrowing when acase
clause specifies the type of the elements of the tuple.To Reproduce
Run this code through
mypy
:Expected Behavior
mypy
exists withSucces
.I would expect
mypy
to derive the type from thecase
clause in the second function, just like it does in the first function. Knowing the first element of the matched tuple is of typeMyClass
it should allow for a call to.say_boo()
.Actual Behavior
mypy
returns an error:Your Environment
I made a clean environment with no flags or
ini
files to test the behavior of the example code given.mypy 0.940
none
mypy.ini
(and other config files):none
Python 3.10.2
macOS Monterrey 12.2.1
The text was updated successfully, but these errors were encountered: