python / python/mypy

Generic TypeGuard inferred as Never when passed as Callable with recursive Union type

Open
#18,427 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bug
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 x is a str

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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.