python / python/typing

Add PartialApplication

Open
#1,372 4 comments 7 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

topic: feature
Dominant language
Python
Stars
1.8k
Forks
302
Avg merge
23h
Merged PRs (30d)
8

Description

Pitch

Add typing.PartialApplication to facilitate the implementation of:

  • __get__, and
  • functools.partial,

both of which are practically impossible to natively (without plugins) annotate.

The __get__ method is currently handled internally by type checkers, and a MyPy plugin for partial has proved to be very difficult.

Proposal

I created a discussion, but I wanted to flesh this out as a new feature:

The idea is that PartialApplication takes three parameters:

  • a ParamSpec parameter P,
  • a tuple parameter T, and
  • a dictionary parameter D (defaulting to an empty dictionary).

It returns a new ParamSpec with all the arguments of P after removing

  • the first len(T) positional parameters,
  • the named keyword parameters from D.

It verifies that this removed parameters are all supertypes of the corresponding arguments, or else returns a type error.

Partial case study

An example with partial (might need some tweaks)

P = ParamSpec('P')
Q = ParamSpec('P')
R = TypeVar('R', covariant=True)

class partial(Generic[P, Q, R]):
  S: TypeAlias = PartialApplication(P, Q.args, Q.kwargs)
  def __init__(self, f: Callable[P, R], /, *args: Q.args, **kwargs: Q.kwargs): ...
  def __call__(self, /, *args: S.args, **kwargs: S.kwargs) -> R: ...

Thus, calling partial(f, ...) would check the parameters, and produce a __call__ method with the right signature.

JIT example

Consider trying to create a decorator jit that works with both bare functions and methods. The problem is that in the method case, it has to respond to __get__ and strip off the first argument. It seems that we can only do this with Concatenate:

from typing import Callable, Generic, Protocol, TypeVar, overload, Any

from typing_extensions import ParamSpec, Self, Concatenate

V_co = TypeVar("V_co", covariant=True)
U = TypeVar("U", contravariant=True)
P = ParamSpec("P")

class Wrapped(Protocol, Generic[P, V_co]):
    def __call__(self, /, *args: P.args, **kwargs: P.kwargs) -> V_co:
        ...

class WrappedMethod(Protocol, Generic[S, P, V_co]):
    def __call__(self: S, *args: P.args, **kwargs: P.kwargs) -> V_co:
        ...

    @overload
    def __get__(self, instance: None, owner: Any = None) -> Self:
        ...

    @overload
    def __get__(self, instance: S, owner: Any = None) -> Wrapped[P, V_co]:
        ...

# this overload can only be hit if there is a positional parameter.  It responds to `__get__` by
# throwing that parameter out.  
@overload
def jit(f: Callable[Concatenate[U, P], V_co]) -> WrappedMethod[U, P, V_co]:
    ...

@overload
def jit(f: Callable[P, V_co]) -> Wrapped[P, V_co]:
    ...

def jit(f: Callable[..., Any]) -> Any:
    ...

class X:
    @jit
    def f(self, x: int) -> None:
        pass

@jit
def g(x: int, y: float) -> None:
    pass

x = X()
x.f(3)
x.f(x=3)
g(3, 4.2)
g(x=3, y=4.2)  # Fails!
reveal_type(x.f)
reveal_type(g.__call__)

We can't seem to deal with the method case alongside the function case. Here's the proposed solution:

class Wrapped(Protocol, Generic[P, V_co]):
    def __call__(self, *args: P.args, **kwargs: P.kwargs) -> V_co:
        ...

    def __get__(self, instance: U, owner: Any = None
                ) -> Callable[PartialApplication[P, tuple[U]], V_co]:
        ...  # Much easier!

def jit(f: Callable[P, V_co]) -> Wrapped[P, V_co]:
    pass  # No overloads!

Contributor guide

No contributing guide indexed for this repository

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 proposed typing.PartialApplication behavior and compare it with the ParamSpec, Concatenate, get, functools.partial, typing, and typing_extensions concepts named in the issue. Read the linked discussion and MyPy issue for prior constraints; done means reaching agreement on the design and specifying behavior that supports both the partial and jit examples.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
tooling
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.