Skip to content

TypeGuard typechecking on filter is too strict #12682

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

Open
SmedbergM opened this issue Apr 27, 2022 · 9 comments
Open

TypeGuard typechecking on filter is too strict #12682

SmedbergM opened this issue Apr 27, 2022 · 9 comments
Labels
bug mypy got something wrong topic-overloads topic-type-context Type context / bidirectional inference topic-typeguard TypeGuard / PEP 647

Comments

@SmedbergM
Copy link

Bug Report

NB: This may be more properly an issue in Typeshed, or require changes in that project to fix, but our team encountered it via MyPy.

The type annotations on builtins.filter cause correct and idiomatic usages to fail to typecheck:

To Reproduce

# filter-mypy.py
path_parts: List[Optional[str]] = ["foo", None, "bar"]

"/".join(filter(None, path_parts))
"/".join(filter(bool, path_parts))
"/".join(filter(lambda s: isinstance(s, str), path_parts))

Expected:

$> mypy filter-mypy.py
Success: no issues found in 1 source file

Actual result:

$> mypy filter-mypy.py
filter-mypy.py:6: error: Argument 1 to "filter" has incompatible type "Type[bool]"; expected "Callable[[Optional[str]], TypeGuard[str]]"
filter-mypy.py:7: error: Argument 1 to "filter" has incompatible type "Callable[[Any], bool]"; expected "Callable[[Optional[str]], TypeGuard[str]]"
Found 2 errors in 1 file (checked 1 source file)

Your Environment

  • Mypy version used:
$> mypy --version
mypy 0.950 (compiled: yes)
  • Python version used:
$> python --version
Python 3.10.4
  • Operating system and version: Ubuntu 20.04

The following is what my editor (VSCode) pulls up when I investigate the type hints for filter, though I doubt this is the same file that MyPy itself is consulting:

class filter(Iterator[_T], Generic[_T]):
    @overload
    def __init__(self, __function: None, __iterable: Iterable[_T | None]) -> None: ...
    @overload
    def __init__(self, __function: Callable[[_S], TypeGuard[_T]], __iterable: Iterable[_S]) -> None: ...
    @overload
    def __init__(self, __function: Callable[[_T], Any], __iterable: Iterable[_T]) -> None: ...

Note that both offending lines conform to the third type signature, but fail the second.

@SmedbergM SmedbergM added the bug mypy got something wrong label Apr 27, 2022
@SmedbergM
Copy link
Author

NB: the new behavior was introduced in python/typeshed#5661

@JelleZijlstra
Copy link
Member

This seems like a mypy bug, not a typeshed bug. It's picking the wrong overload.

@hauntsaninja
Copy link
Collaborator

hauntsaninja commented Apr 28, 2022

Yeah, mypy isn't happy with this on 0.942 either, I think the bug is a type context related.

@devhl-labs
Copy link

Any word on getting this fixed? Maybe the same issue - #12996

@Zandwhich
Copy link

Has a workaround been identified in the meantime?

@ilevkivskyi
Copy link
Member

Although error message is bad (mypy sometimes randomly chooses "best" overload match to report error if none of overload matches), it is technically correct (assuming that best inferred type of the first argument in second and third call is Callable[[Any], bool]). To illustrate, this crashes at runtime:

from typing import Any, List, Optional

path_parts: List[Optional[str]] = ["foo", None, "bar"]

def always(x: Any) -> bool:
    return True

"/".join(filter(always, path_parts))

So to support this we will need both:

  • Special-case bool constructor to be a typeguard against None
  • Special-case lambda x: isinstance(x, ...) to infer a type with TypeGuard

Both are possible but non-trivial (and still a bit hacky).

@anden-akkio
Copy link

anden-akkio commented Aug 12, 2023

I was personally getting this error whenever I was attempting to filter a list[str] down to just the elements that contained a substring. However, my problem was that substring was from a dict.get() that could resolve to None, so this was actually a good catch from mypy.

Narrowing the type with an assertion was enough to stop mypy from producing this error.

table_name = self.table_specifier.specifier.get("table")
if not table_name:
  raise ...
filtered = list(filter(lambda x: x in <>))

@ffissore
Copy link

Another example snippet that triggers the issue.

Here mypy is happy

from typing_extensions import reveal_type

non_none_strings = list(filter(lambda x: x is not None, [None, "value"]))
reveal_type(non_none_strings)

the revealed type is builtins.list[Union[builtins.str, None]]. Since I need to join the non-none strings, I specified the type

from typing_extensions import reveal_type

non_none_strings: list[str] = list(filter(lambda x: x is not None, [None, "value"]))
reveal_type(non_none_strings)

print(", ".join(non_none_strings))

which makes mypy complain with

asd.py:3: error: Argument 1 to "filter" has incompatible type "Callable[[Any], bool]"; expected "Callable[[str | None], TypeGuard[str]]"  [arg-type]
asd.py:4: note: Revealed type is "builtins.list[builtins.str]"

The workaround I had to use was to NOT pass the lambda at all

from typing_extensions import reveal_type

non_none_strings: list[str] = list(filter(None, [None, "value"]))
reveal_type(non_none_strings)

print(", ".join(non_none_strings))

@gszabo
Copy link

gszabo commented Dec 14, 2023

In my use-case I needed the first item of a list of dicts where a particular key equals a particular value. At first I used next and filter like this:

item = next(filter(lambda v: v["my-key"] == "my-value", list_of_dicts), None)
if item is None:
    ...
else:
    ...

I got 3 mypy errors for the same line:

Argument 1 to "filter" has incompatible type "Callable[[Any], Any]"; expected "Callable[[Any], TypeGuard[Any]]"  [arg-type]
Argument 1 to "filter" has incompatible type "Callable[[Any], Any]"; expected "Callable[[Any], TypeGuard[dict[Any, Any] | None]]"  [arg-type]
Value of type "dict[Any, Any] | None" is not indexable  [index]

My workaround was to use next with a generator expression:

item = next((item for item in list_of_dicts if item["my-key"] == "my-value"), None)
if item is None:
    ...
else:
    ...

real-yfprojects added a commit to real-yfprojects/sphinx-polyversion that referenced this issue Jun 11, 2024
As reported in python/mypy#12682, mypy didn't recognise `bool` as a
`TypeGuard[T]` for `Optional[T]`. However it doesn't flag using `None`
as a typeguard in `filter` which behaves exactly the same.

* sphinx_polyversion/git.py
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug mypy got something wrong topic-overloads topic-type-context Type context / bidirectional inference topic-typeguard TypeGuard / PEP 647
Projects
None yet
Development

No branches or pull requests

9 participants