Functools.partial extension to support specific positional arguments

Maybe the problem is in the DSL part? What if functools.partial could bind positional arguments in standard library functions like it can with user-defined ones without raising a TypeError, would that be a problem?

seconds_to_minutes = partial(divmod, y=60)

To be clear, in my previous comment I wasn’t advocating for including better-partial (as it is currently implemented) in the standard library, rather for functions created via def to support a placeholder-like partial application syntax as a language feature.

I work a lot in a functional framework and I frequently run into situations where something like the syntax in better-partial would be useful.

2 Likes

Maybe a good solution for the specific case in Functools.partial extension to support specific positional arguments - #3 by AndersMunch (which is the one that led me here) is to actually support kwargs in that function (and possibly others in the stdlib that surprisingly don’t support them).

I say that because the natural way of solving this was (for me):

strptime_tpo = partial(datetime.strptime, format="%d/%m/%Y")

And then the failure was:

TypeError: strptime() takes no keyword arguments

Which is itself kinda weird, as almost any function in python supports keyword args automatically.

1 Like

I follow the rationale for leaving functools.partial alone, but
there is a specific awkwardness that I haven’t seen highlighted:

Suppose I have a function:

def foo(a, b):
    ...

As a caller I can choose foo(3,4) or foo(a=3, b=4) based on terseness vs clarity.

However, I cannot choose between partial(foo, 3) and partial(foo, a=3).
The latter result cannot be called positionally, so it hasn’t returned a
full-fledged partially bound version of foo. This isn’t obvious.

4 Likes

I don’t think that’s correct. It has, but one that’s consistent with Python’s rules about positional vs. keyword arguments. (Namely, that you can’t mix them out of order, and positional args have to come first.) What partial doesn’t do is give you a pass to break those rules.

You missed several more possibilities, though, and the rules for which of them you can and can’t use inform the semantics of partial as well.

These are OK:

  1. foo(b=4, a=3)
  2. foo(3, b=4)

These are not:

  1. foo(4, a=3)
  2. foo(b=4, 3)

The second is a SyntaxError (“positional argument follows keyword argument”).

The first is a TypeError, and as it turns out, the same as your g = partial(foo, a=3); g(4) because positional arguments always come first.

A partial with a keyword argument supplied doesn’t slot that argument in as the first positional argument, it slots it in as the first keyword argument.

That’s also what makes this perfectly OK. It doesn’t violate the ordering rules. (It only seems like it does, until you understand what’s going on.):

>>> h = partial(foo, b=4)
>>> h(3)

That works because it’s equivalent to foo(3, b=4), NOT foo(b=4, 3). Which is why g = partial(foo, a=3) => g(4) => foo(4, a=3) breaks the rules.

(And, returning to your original claim, with h = partial(foo, b=4) you can choose between h(3) and h(a=3).)

…I suppose I can’t disagree that this behavior is “not obvious”, but not everything in the language will be. And it is documented (emphasis added):

functools.partial(func, /, *args, **keywords)

Return a new partial object which when called will behave like func called with the positional arguments args and keyword arguments keywords. If more arguments are supplied to the call, they are appended to args. If additional keyword arguments are supplied, they extend and override keywords.