Optional kwarg in function invocation

I wish I could optionally pass keyword arguments to a function call, for example:

foo(x=1, y=2, z=3 if bar)

I’ve no idea what good (human-readable) syntax for this would be, ideas are welcome! :slight_smile:

Today I do this, but find it quite unwieldly:

foo(
    x=yada.yada.yada,
    y=yada.yada.yada,
    **{"timeout": timeout} if timeout else {},
    **{"max_age": max_age} if max_age else {},
)

[edit: typos in closing braces]

While on the subject, I’m a bit surprised in the parsing difference between dictionary literals and (dict) keyword arguments:

# OK
dict(a=1, **{"b": 2} if z else {})

# SyntaxError
{"a": 1, **{"b": 2} if z else {}}

# Work-around
{"a": 1, **({"b": 2} if z else {})}
1 Like

I would do the following, which I find more readable than your approach - but it’s a matter of personal preference.

args = dict(x=yada.yada.yada, z=yada.yada.yada)
if timeout:
    args['timeout'] = timeout
if max_age:
    args['max_age'] = max_age
foo(**args)

Also, I’d look at re-designing foo (or the parts of the system around it) to not need such a complex signature.

I don’t think this need is sufficiently common, or the available options are so bad, that this warrants dedicated syntax.

2 Likes

I also use the approach of @pf_moore
The original proposal works better with type checking though.
I’m not sure how is easily to extend the current Python grammar.
I know @guido and @pablogsal work on PEG parser, maybe it allows to implement the proposal much simpler.

Your suggested solution starts to remove the explicitness of the link between the function call and its arguments, as x= and y= are already 5 lines away from the function call. This can get out of hand for functions (that are out of my control) with many parameters, even getting off-screen. In my work, I encounter this problem almost daily, and the solution I employ is to create multiple different functions with smaller signatures which call the original function with different pre-filled arguments

My general pattern here is
kwargs = {}
if timeout: kwargs[‘timeout’] = timeout
if max_arg: args[‘max_age’] = max_age
foo(x=yada.yada.yada, z=yada.yada.yada, **kwargs)

that is - only the extra optional args get the special But you can also do:
foo(x=yada.yada.yada, z=yada.yada.yada,
timeout=timeout if timeout else None,
max_age=max_age if max_age else None)
(replace None with whatever sentinel value foo uses for not-supplied-parameters).

If foo really detects actually-passed vs passed-with-some-value, then thats perhaps a thing to change. Because using None as a sentinel allows that last example to collapse down to:
foo(x=yada.yada.yada, z=yada.yada.yada,
timeout=timeout,
max_age=max_age)
and still be semantically sensible.