Skip to content

ParamSpec inferred overly wide from protocol #21384

@iglosiggio

Description

@iglosiggio

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)

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugmypy got something wrongtopic-inferenceWhen to infer types or require explicit annotationstopic-paramspecPEP 612, ParamSpec, Concatenate

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions