Typing a Right-Side Partial Function -- Possible? Should it be?

:thread: Typing Right-Side Partial Function (rpartial)

A Couple Caveats

  • I’d classify myself as a typing enthusiast who knows just enough to be dangerous.
  • I’m not suggesting Python’s type system should express every edge case—or that it even should.
  • I’m not fully confident this is one of those edge cases. But it’s been an interesting one to explore.

:sparkles: Motivation

I’ve found funcy’s rpartial function particularly helpful over the years, and recently needed it in a project. I decided to bring it into my codebase and give it a modern refresh with proper typing.

If you’re unfamiliar, rpartial is like functools.partial, except it applies arguments from the right. It’s useful when you want to repeatedly call a function with the same secondary arguments, while varying the primary ones. Since arguments are often ordered from most to least important, I find rpartial more useful than partial in practice.

Here’s the original implementation from Alexander Schepanovski’s funcy:

def rpartial(func, *args, **kwargs):
    """Partially applies last arguments.
       New keyworded arguments extend and override kwargs."""
    return lambda *a, **kw: func(*(a + args), **dict(kwargs, **kw))

Usage:

def func(a, b, c): return a + b + c
rfunc = rpartial(func, 2, 3)
assert rfunc(1) == 6  # equivalent to func(1, 2, 3)

Typing this cleanly turned out to be surprisingly difficult.

:brain: What I Tried

Using Python 3.12+'s native generic syntax, I explored several approaches. Here are a few:

# The problem here is you can't use a `TypeVarTuple` with `ParamSpec`
def rpartial[Args, KwT, **P, R](
    func: Callable[Concatenate[Args, KwT, P], R], /, *args: Args, **kwargs: KwT
) -> Callable[P, R]:
    ...

This fails because:

  • Concatenate only supports positional types followed by a ParamSpec
  • Mixing TypeVarTuple (ideally *Args) and keyword unpacking (KwT) inside Concatenate is invalid
  • There’s no way to slice a ParamSpec from the right

I also tried a simpler formulation:

def rpartial[**P, R](func: Callable[P, R], *args: object, **kwargs: object) -> Callable[P, R]:
    ...

This works at runtime but loses type precision. Then I explored a positional-only variant using TypeVarTuple:

def rpartial[*Ts, *Us, R](func: Callable[[*Ts, *Us], R], *args: *Us) -> Callable[[*Ts], R]:
    ...

This would be ideal—but Python doesn’t allow more than one TypeVarTuple in a type expression.

:magnifying_glass_tilted_left: What’s Missing

We need a way to express:

“Given a function f(*Ts, *Us, **Kw1, **Kw2), fix *Us and **Kw2, and return a callable that accepts *Ts and **Kw1.”

This would enable:

  • Right-side partial application
  • Keyword argument fixing from the right
  • Type-safe currying and function slicing

:light_bulb: Possible Directions

If this is a problem worth solving, here are a few ideas:

  • Extend Concatenate to support TypeVarTuple + keyword unpacking
  • Introduce a new CallableTransform or CallableSlice construct
  • Allow slicing or partitioning of ParamSpec and TypeVarTuple

:speaking_head: Call for Feedback

Has anyone tackled this before? Are there known workarounds or proposals in flight? I’d love to hear from type checker maintainers, library authors, and typing enthusiasts.

For the curious, my current attempt at modernizing the original looks like this:

def rpartial[**P, R](func: Callable[P, R], *args: object, **kwargs: object) -> Callable[P, R]:
    def partial_right(*fargs: P.args, **fkwargs: P.kwargs) -> R:
        return func(*(fargs + args), **{**fkwargs, **kwargs})  # pyright: ignore[reportCallIssue]
    return partial_right
2 Likes

I haven’t come across funcy before - thanks for this.

I’ve not thought about this in detail, but a couple of questions spring to mind. Firstly, can downstream users/ callers create ad-hoc type annotations, for each callable returned by rpartial? Secondly, is there a type description for functools.partial (‘lpartial’)?

I proposed a solution here.

2 Likes

James–

partial is implemented differently. It’s actually a class that wraps the function. Here’s the stub from typeshed:

def total_ordering(cls: type[_T]) -> type[_T]: ...
def cmp_to_key(mycmp: Callable[[_T, _T], int]) -> Callable[[_T], SupportsAllComparisons]: ...
@disjoint_base
class partial(Generic[_T]):
    @property
    def func(self) -> Callable[..., _T]: ...
    @property
    def args(self) -> tuple[Any, ...]: ...
    @property
    def keywords(self) -> dict[str, Any]: ...
    def __new__(cls, func: Callable[..., _T], /, *args: Any, **kwargs: Any) -> Self: ...
    def __call__(self, /, *args: Any, **kwargs: Any) -> _T: ...
    def __class_getitem__(cls, item: Any, /) -> GenericAlias: ...

The class implementation isn’t functionally “pure”, but it does benefit from being easier to introspect using its keywords, args, and func properties.

If you actually implemented partial as a true higher order function, in that it returns a function, you could type it with Python’s type system today. You could use ParamSpec and Concatenate to mostly express how the function transforms. You can only do that for arguments – not keywords – and only because ParamSpec takes the “spot” of the arguments (i.e. the left side). As you can see in Neil’s link, there’re a few related problems that face similar issues. You run into this mess anytime you try to type an argument slice or modification, especially if keywords are involved – partials, currying, decorators, function caches. My example isn’t likely to impact most pythonistas but decorators definitely do.

Neil – thanks for responding with that – it’s an interesting approach. Disappointing to see it hasn’t gotten much traction. I see there is currently some high-level interest in solving this problem with the BDFL’s endorsement of new syntax.

1 Like

Do you have a way of typing the normal partial function without the special casing of pyright?

I just had this question yesterday in SO, and they told me that is impossible, so it would be a good start.