python / python/mypy

(🐞) Failure to narrow lambdas when using `Protocol` with `__call__`

Offen
#16,797 1 Kommentar 2 Reaktionen 0 zugewiesene Personen Auf GitHub ansehen

Dieses Issue hat noch niemand übernommen.

bug
Vorherrschende Sprache
Python
Sterne
20.6k
Forks
3.3k
PR-Merge-Kennzahlen
PR-Kennzahlen ausstehend

Beschreibung

Bug Report

Mypy fails to narrow lambdas and detect errors when using a Protocol with a __call__ method, in situations where using the equivalent Callable formulation results in narrowing and error detection.

To Reproduce

In the following snippet, type narrowing is performed on the lambda function passed to the validator argument of validate10, resulting in the implementation typo being caught by Mypy:

from typing import Callable, TypeVar

InstanceT = TypeVar("InstanceT")

def validate10(instance: InstanceT, validator: Callable[[InstanceT, int], bool]) -> bool:
    return validator(instance, 10)

class MyClass:
    def validate(self, value: int) -> bool:
        return value >= 10

instance = MyClass()
validate10(instance, lambda self, value: self.validatez(value)) # Mypy error
# "MyClass" has no attribute "validatez"; maybe "validate"?

The following snippet is essentially equivalent to the above, but using Protocol in place of Callable. The type of the lambda function is no longer narrowed, and the implementation typo is not caught:

from typing Protocol, TypeVar

InstanceT = TypeVar("InstanceT")
InstanceT_contra = TypeVar("InstanceT_contra", contravariant=True)
class Validator(Protocol[InstanceT_contra]):

    def __call__(self, instance: InstanceT_contra, value: int, /) -> bool:
        ...

def validate10(instance: InstanceT, validator: Validator[InstanceT]) -> bool:
    return validator(instance, 10)

class MyClass:
    def validate(self, value: int) -> bool:
        return value >= 10

instance = MyClass()
validate10(instance, lambda self, value: self.validatez(value)) # No error

I would expect the narrowing behaviour in the two snippets to be the same (or close enough).
Please note that the issue persists even when the type variable is removed:

class MyClass:
    def validate(self, value: int) -> bool:
        return value >= 10

class Validator(Protocol):

    def __call__(self, instance: MyClass, value: int, /) -> bool:
        ...

def validate10(instance: MyClass, validator: Validator) -> bool:
    return validator(instance, 10)

instance = MyClass()
validate10(instance, lambda self, value: self.validatez(value)) # No error

Pylance/Pyright (strict) detects the typo in all cases.

Your Environment

  • Mypy version used: 1.7.1
  • Mypy command-line flags: --strict
  • Python version used: 3.12

Beitragsleitfaden

Beitragsleitfaden öffnen

Erste Schritte

  1. Lies das ganze Issue und danach den Beitragsleitfaden des Projekts.
  2. Schreib ins Issue, dass du es übernimmst — das erspart doppelte Arbeit.
  3. Forke das Repository und arbeite in einem Branch.
  4. Öffne einen Pull Request, der die Issue-Nummer nennt.

Rechercherichtung

Beginne mit den Callable- und Protocol-Reproduktionen rund um validate10, Validator und die an validator übergebene lambda und vergleiche ihre abgeleiteten Parametertypen unter --strict. Fertig ist es, wenn die auf Protocol basierenden Fälle die lambda wie der Callable-Fall eingrenzen und das falsch geschriebene Attribut validatez melden, auch wenn die Typvariable entfernt wird.

Vom Indexierungsmodell aus dem Issue-Text verfasst.

Bewertung

Tech-Stack
python
Bereich
devtools
Issue-Typ
Bug
Schwierigkeit
4/5
Geschätzter Aufwand
3-5 Tage
Aktivitätsstatus
Veraltet
Klarheit
Größtenteils klar
Anfängerfreundlichkeit
35/100

Neue Issues direkt in Ihr Postfach

Eine kurze Übersicht über anfängerfreundliche GitHub-Issues.