Generic TypeGuard inferred as Never when passed as Callable with recursive Union type
Nobody has claimed this yet.
- Dominant language
- Python
- Stars
- 20.6k
- Forks
- 3.3k
- PR merge metrics
- PR metrics pending
Description
Bug Report
I'm trying to make a function called ensure that takes a type guard function and an argument and returns the argument if the type guard function returns true:
def ensure(guard: Callable[[X], TypeGuard[Y]], x: X) -> Y:
if guard(x):
return x
raise TypeError(f'{x} is not the right type')
This works fine for simple Union types, but fails for recursive Union types. Both scenarios work fine in Pylance.
To Reproduce
I created a Gist at:
https://mypy-play.net/?mypy=latest&python=3.12&gist=b48835cb0b0a7a3b5933e10593107dd3
that repros the issue. (It is also a valid pytest test.)
The core of the issue is shown by:
Y = TypeVar('Y')
Foo: TypeAlias = list['Foo'] | str | int
def is_str(o: Foo) -> TypeGuard[str]:
return isinstance(o, str)
def ensure(guard: Callable[[Foo], TypeGuard[Y]], x: Foo) -> Y:
if guard(x):
return x
raise TypeError(f'{x} is not the right type')
def test_ensure_is_str(s: Foo) -> None:
# Pylance: list[Foo] | str | int; mypy: Union[builtins.list[...], builtins.str, builtins.int]
reveal_type(s)
# mypy: error: Need type annotation for "x" [var-annotated]
# mypy: error: Argument 1 to "ensure" has incompatible type "Callable[[Foo], TypeGuard[str]]"; expected "Callable[[Foo], TypeGuard[Never]]" [arg-type]
x = ensure(is_str, s)
reveal_type(x) # Pylance: str; mypy: Any
# error: Argument 1 to "ensure" has incompatible type "Callable[[Foo], TypeGuard[str]]"; expected "Callable[[Foo], TypeGuard[Never]]" [arg-type]
reveal_type(ensure(is_str, s)) # Pylance: str; mypy: Never
assert x == s
In this example, mypy can't deduce that x is a str because it expects ensure to take a function returning TypeGuard[Never].
Expected Behavior
mypy should be able to:
- correctly deduce the argument type to
ensure - deduce that
xis astr
Actual Behavior
main.py:32: note: Revealed type is "Union[builtins.list[...], builtins.str, builtins.int]"
main.py:35: error: Need type annotation for "x" [var-annotated]
main.py:35: error: Argument 1 to "ensure" has incompatible type "Callable[[Foo], TypeGuard[str]]"; expected "Callable[[Foo], TypeGuard[Never]]" [arg-type]
main.py:36: note: Revealed type is "Any"
main.py:38: note: Revealed type is "Never"
main.py:38: error: Argument 1 to "ensure" has incompatible type "Callable[[Foo], TypeGuard[str]]"; expected "Callable[[Foo], TypeGuard[Never]]" [arg-type]
Your Environment
- Mypy version used: 1.14.1
- Mypy command-line flags: none
- Mypy configuration options from
mypy.ini(and other config files): none - Python version used: 3.11, 3.12
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with the linked mypy-play reproduction, which is also described as a valid pytest test, using the reported Python 3.11 or 3.12 setup. Trace generic inference for ensure’s Callable[[Foo], TypeGuard[Y]] when Foo is recursive, then add regression coverage showing str rather than Never and verify the reported diagnostics are resolved.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- devtools
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Stale
- Clarity
- Clearly specified
- Newbie friendliness
- 45/100