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.