`ParamSpec` inferred overly wide from protocol
Nobody has claimed this yet.
- Dominant language
- Python
- Stars
- 20.6k
- Forks
- 3.3k
- PR merge metrics
- PR metrics pending
Description
Bug Report
When implementing a protocol generic wrt a ParamSpec type inference ends up inferring Any for the param spec in certain cases. Writing code around the way the type checker works we can force it to unify against a properly inferred param spec, but this limits our ability to write nice APIs.
To Reproduce
from typing import Protocol, reveal_type
class Context: pass
class Namer[**P](Protocol):
def name_for(self, *args: P.args, **kwargs: P.kwargs) -> str: ...
def execute_on(
self, ctx: Context, *args: P.args, **kwargs: P.kwargs
) -> None: ...
class Impl0:
@staticmethod
def name_for(x: int, y: str) -> str:
return 'Test'
@staticmethod
def execute_on(ctx: Context, x: int, y: str):
pass
class Impl1:
@staticmethod
def name_for(y: str) -> str:
return 'Test'
@staticmethod
def execute_on(ctx: Context, x: int, y: str):
pass
class UseImplFirst[**P, T]:
def __init__(self, impl: T, *args: P.args, **kwargs: P.kwargs) -> None:
self.impl = impl
self.args = args
self.kwargs = kwargs
def __call__(self: UseImplFirst[P, Namer[P]]) -> None:
pass
def useImplSecond[**P](impl: Namer[P], *args: P.args, **kwargs: P.kwargs):
pass
def useImplThird[**P](wrapper: UseImplFirst[P, Namer[P]]) -> None:
pass
__testImpl0: Namer[[int, str]] = Impl0
UseImplFirst(Impl0, 0, '0')()
useImplSecond(Impl0, 0, '0')
useImplThird(UseImplFirst(Impl0, 0, '0'))
__testImpl1: Namer[[int, str]] = Impl1 # Should fail (Fails)
UseImplFirst(Impl1, 1, '1')() # Should fail (Succeeds)
useImplSecond(Impl1, 1, '1') # Should fail (Succeeds)
useImplThird(UseImplFirst(Impl1, 1, '1')) # Should fail (Succeeds)
Expected Behavior / Actual Behavior
See the snippet above.
Woraround
The following forces mypy to infer the ParamSpec first and unify against the protocol implementation later:
class UseImpl[**P]:
def __init__(self, *args: P.args, **kwargs: P.kwargs):
self.args = args
self.kwargs = kwargs
def __call__(self, impl: Namer[P]) -> None:
pass
__testImpl0: Namer[[int, str]] = Impl0
UseImpl(0, '0')(Impl0)
__testImpl1: Namer[[int, str]] = Impl1 # Should fail (Fails)
UseImpl(1, '1')(Impl1) # Should fail (fails)
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 by running the reproducer from the issue and comparing the mypy 1.12.0 and 1.20.2 playgrounds. Use the expected failures for Impl1 and the working workaround as the completion criteria, then add regression coverage in the relevant type-inference tests once the implementation area is identified.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- compilers
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 45/100