(🐞) Failure to narrow lambdas when using `Protocol` with `__call__`
Nobody has claimed this yet.
- Dominant language
- Python
- Stars
- 20.6k
- Forks
- 3.3k
- PR merge metrics
- PR metrics pending
Description
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
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 Callable and Protocol reproductions around validate10, Validator, and the lambda passed to validator, comparing their inferred parameter types under --strict. Done means the Protocol-based cases narrow the lambda like the Callable case and report the misspelled validatez attribute, including when the type variable is removed.
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
- Mostly clear
- Newbie friendliness
- 35/100