Syntactic sugar to encourage use of named arguments

Continuing my pattern of “that’s scary, this instead?”:

n = SimpleNamespace()
n.x = 1
n.y = 2

func(**vars(n))

Surely if you’re using identifiers which mirror arguments of a function, you’re already planning to use those variables for that function, so why not encapsulate them in a way which makes them easy to expand to kwargs? If it’s to have those names in the line that calls the function, then maybe some magic using locals?

def expandlocal(*names: list[str], _loc=locals()):
    return {n:_loc[n] for n in names}

x = 1
y = 2

func(**expandlocal('x','y'))

Yes, expandlocal needs to be defined in the same scope as the variables you want to use in this case. It is straightforward (though maybe stinky) to use inspect to get the previous frame’s locals.

2 Likes

Might be a bit late to suggest this now given the PEP but taking off on @vovavili 's suggestion of ellipsis: one way might just be to use a single ellipsis to refer to all variable names that match param names.

The callee:

def exp(alpha, beta, delta, epsilon, gamma):
    return alpha**beta + delta*epsilon - gamma

The caller:

def callexp():
    a = 2
    beta = 5
    atled = 8
    epsilon = 9
    gamma = 7
    #return exp(alpha=a, beta=beta, delta=atled, epsilon=epsilon, gamma=gamma)
    #return exp(alpha=a, delta=atled, beta=beta, epsilon=epsilon, gamma=gamma)
    return exp(alpha=a, delta=atled, ...)

The first return statement is how we write now.

The second return statement is just a shuffling of args so the ones with different names to the callee params come first. Result is the same as the first return statement.

The third return statement is a suggestion using a single ellipsis which will match all callee params to similarly named variables in the caller.

To me, this feels

  1. more intuitive as to its meaning as even in normal language ellipsis suggests ‘and everything else’
  2. more ‘pythonic’ perhaps given we now use ellipsis at least in type hints
  3. less verbose than rewriting param names in suggestions like beta=, epsilon=, gamma=
  4. avoids personal preferences of prepending or appending special characters and whether those count as ‘ugly’
1 Like
  1. Happens to already be legal syntax with a different meaning. Sorry. (Unless you’re going to mandate at least one non-matching kwarg before it, which would feel very weird.)
1 Like

Apart from the backward compatibility problem @Rosuav points out, I don’t think it’s possible to get this working. What you want is that for all kwargs accepted by the callee that exist as local variables, we should pass that kwarg. But we may not even know what kwargs the callee is taking, since it might accept **kwargs. Also, this suggestion would make it very difficult to tell at a glance whether a local variable is in use. And it may have very unexpected effects if an unrelated local variable just so happens to share the name of a kwarg to a function.

7 Likes

@Rosuav Thanks for the feedback. I didn’t remember having come across the syntax before. Most source code I read and use usually have args or **kwargs spelt out.

@Jelle Thanks for this. I do find though that even now I don’t know what **kwargs the callee is taking until I read the callee function code. I agree that an unrelated local similarly-named variable would be a bug.

It’s just that ... happens to be the special value Ellipsis, and is thus a perfectly valid function parameter :slight_smile: This is usually a good thing, though - for example, I could do something like this:

do_stuff(...)

and it’s syntactically legal, but carries the notion that it’s not “done” yet. Great for half-done code, placeholders, and doctests.

3 Likes

I’m curious how folks react to this; it appears to exactly hit the goals of this idea, while also being clear that you intend to use particular variables with a specific function and protecting against spelling errors. It could use a little more intelligence when interleaving positional and keyword args.

class 'Partial' or 'Closure' or 'SimpleArgspace' or ...
from typing import Callable
import inspect

class Partial:
    def __init__(self, func: Callable):
        self.__dict__['_func'] = func
        self.__dict__['_kwargs'] = inspect.getargs(func.__code__).args
        self.__dict__['_argvals'] = {}

    def __setattr__(self, name: str, value):
        if name in self._kwargs:
            self.__dict__['_argvals'][name] = value
        else:
            raise TypeError(f'"{name}" not an argument for function "{self._func.__name__}"')

    def __call__(self, *args, **kwargs):
        return self._func(*args, **(self._argvals | kwargs))

def f(*,x,y,z):
    return x*y*z

p = Partial(f)
p.x = 1
p.y = 2

print(p(z=3))
1 Like

Not really, no. It’s a massive amount of cognitive overhead for what should simply be the passing of arguments. This proposal is a simple shorthand, not a massive wrapper that requires changing the way you do everything. Also, I’m not seeing how you can even do what the proposal is providing, and avoid writing a name twice when passing a value as its own existing name. So… no, it doesn’t hit the goals of the idea, not at all.

Is there really such a (practical) difference between some_argname = ... and p.some_argname = ...? I personally like the intentional scoping and the spelling check it provides.

If I’m not mistaken, something like Partial lets you write the name one fewer time than what was originally proposed?

some_argname = ... # first
func(some_argname=) # second

vs

p = Partial(func) # state your intentions
p.some_argname = ... # write the arg name once, use it directly
p()

Thoughts?

For my use cases at least, usage of Partial would still require writing the name multiple times, E.g.,

def target_fn(*, a, b, c):
  ...

def current(*, a, b):
  return target_fn(a=a, b=b, c=get_c())

def current_kwargs(**kwargs):
  return target_fn(**kwargs, c=get_c())

def proposed(*, a, b):
  c = get_c()
  # return target_fn(*, a, b, c)
  return target_fn(a=, b=, c=)

def partial_object(*, a, b):
   p = Partial(target_fn)
   p.a = a  # "a" still written twice
   p.b = b
   return p(c=get_c())

Okay, that’s interesting. I can’t think of many places where function signatures are coupled like that other than matplotlib or similar gui/rendering libraries that have a lot of configuration options which are packed into kwargs.

Maybe an unstated (or I missed it) advantage is being more able to write a function’s full signature rather than burying things in kwargs? That makes IDE hover docs and completions more effective. In that case, though, being able to pass the whole argument bundle to the inner function call (like standard use of kwargs) would be nicer than repeating all the arg names in the call?

Just FYI, this proposal is not at the conceptual stage at this point, as there is a PEP PR (preview) and even a reference implementation. I think time would be better spent engaging with that.

5 Likes

PEP 736 links to this discussion thread, but as best as I can tell this discussion includes many posts unrelated to the specific PEP proposal.

Would it make sense to start a thread specific to the merits or criticisms of PEP 736?

2 Likes

Agree, after seeing this posted in reddit, that was where I went to look.

FWIW, I think this style is ugly, but I agree with the PEP that’s subjective. And I’d use it enough that it would seem second nature, although it would probably confuse a new programmer. However, I much prefer the rejected syntax of

f(a, b, *, x)

It feels like it has a lot more “conceptual integrity” with the use of * in function definitions, to borrow a phrase from Fred Brooks. If I saw it in a calling signature without knowing about this PEP, there’s a good chance I would understand it anyways. Not so for the proposed syntax.

I’m also uncertain about the objections in the PEP:

The * could easily be missed in a long argument list
Long argument lists would end up expanded, as in the example post, where it’s pretty clear.

It is unclear whether keyword arguments for which the value was not elided may follow the *. If so, then their relative position will be inconsistent

I think it’s clear that non-elided arguments could follow a *, in part because relative position of kwargs doesn’t matter.

6 Likes

One issue with doing so is that people who’ve expressed reservations in this thread might not bother to repeat their points in a new one[1]. That could lead to a false sense of support for the proposal and end up making it harder for people to understand the status.


  1. I know I can’t be bothered reiterating my dislike of the proposal just to make sure my view is represented in the right place. ↩︎

3 Likes

It’s one of the most practical ideas I think. Really cool!
It will have an huge impact on “brand new” comers to Python I think.

My concern is from my perspective this thread clearly has no consensus, but were PEP 736 submitted to the SC it could be argued that the parts of this thread that have no consensus are not related to the specifics of PEP 736.

If that concern is not valid, I withdraw my request for a new thread.

That’s not a problem. Threads don’t have to. That’s why we have a document, the proposal itself, which lists the relevant points (without constantly reiterating the same old arguments, as inevitably happens in a lengthy thread), and promotes what the PEP author wishes to see happen; and more importantly, why we have a Steering Council that makes the final decision. We do not require consensus in this or any other thread. Obviously consensus is hugely helpful, but with many proposals, there have been dissenting voices, either saying that the entire proposal should be rejected, or preferring a different variant. That’s fine and normal.

If the PEP author wants a new thread for a fresh round of discussion, one can easily be created, but otherwise, this thread is fine IMO.

2 Likes

Agreed. From my experience as packaging PEP-delegate, consensus in a discussion thread is a useful indicator, as is lack of consensus. Making it easy to see the community discussion is important to inform my decision, but it doesn’t (nor should it) prevent me from making a decision that goes against the perceived consensus.

In this case, “triggers a whole load of questions around related but out of scope matters” would be very useful information to me, and having a new thread where such questions were curated out of existence would be actively harmful to the way I review PEPs. Of course, the SC may approach things differently, so this is just my view.

1 Like