Why dict expansion allows dup keys but kwargs does not?

A quick demo:

>>> d1 = dict(a=1, b=2)
>>> d2 = dict(b=3, c=4)

# This works
>>> {**d1, **d2}
{'a': 1, 'b': 3, 'c': 4}

# This does not
>>> def fn(**kw): pass
...     
>>> fn(**d1, **d2)
Traceback (most recent call last):
  File "<python-input-4>", line 1, in <module>
    fn(**d1, **d2)
    ~~^^^^^^^^^^^^
TypeError: fn() got multiple values for keyword argument 'b'

I am wondering if this has been discussed before, or if there exists a good reason to justify such discrepancy (either on implementation side or user side). Please point me to those resources if you know any. Many thanks!

1 Like

I imagine for the same reason that kwarg dicts in general can’t duplicate other args, e.g. you can’t call fn(a=1, **{"a": 2}). One could imagine allowing overwrites there, but it’s much more confusing than it would be helpful, and there’s already the option to make the unified dictionary first and then pass it in.

That’s exactly my use case… I have a CSS-like dict object to describe the style of a text box. Sometimes I want to call a “draw” function with a few settings temporarily overridden. Those overrides should not be saved to the original dict.

i.e.

style = dict(fontSize=1, color=GREEN, align="center", verticalAlign="top", ...)

# Render in red only in special case
if special_case:
    render_text("some text", **style, color=RED) # Will cause error
else:
    render_text("some text", **style) # Normal settings

Currently my options are limited to:

  1. Copy the entire dict and change a few entries (not efficient)
  2. Dual dict expansion:
    render_text("some text", **{**style, "color": RED})
    

IMO both lacks elegancy.

You can use

render_text(**style | {'color': RED})
3 Likes

Why not factor out the render_text call and update the style in the different if/else clauses?

So something like:

style = dict(fontSize=1, color=GREEN, align="center", verticalAlign="top", ...)

if special_case:
    style["color"] = RED

render_text("some_text", **style)

Alternatively, this should also work and expresses (in my opinion) intent:

render_text("some_text", **style.update({color: RED})

I would probably still consider collecting non-standard attributes separately, like:

default_style = { ... }

overwrites = {}
# A bunch of `if/else` statements that add potential overwrites

render_text("some_text", **default_style.update(overwrites))

I’ve already mentioned such requirements in my previous post.

Typically UI/Video rendering happens in a loop. This means the same render call will be invoked many times. Each time with slightly different settings.

In your suggested approach, the original dict will be modified. This will impact consecutive frames which does not have the special case set to True - they should be rendered in green but they are red because special_case has been triggered in a previous iteration.

Following down your approach, we will need to manually restore color=GREEN when special_case == False. This induces multiple occurrences of a same constant value - it is considered a bad coding pattern in most cases.


For now I am happy with the solution provided by @bverheg . However it still incurs 2 dict copies (one for dict merge, another for argument expansion) each time I call the function.

I think it’s worth arguing to remove restrictions on duplicate keys for keyword arguments.

The dict expansion works, the difference is what happens with the expanded key/values afterwards.

In the first example it’s the same than doing…

>>> {'a': 1, 'b': 2, 'b': 3, 'c': 4}
{'a': 1, 'b': 3, 'c': 4}

… which is allowed.

But in the second case, it’s the same than doing…

>>> fn(a=1, b=2, b=3, c=4)
...
SyntaxError: keyword argument repeated: b

…which is not allowed (no matter how the arguments are “consumed” in the function declaration/initialization).

1 Like

As said before, this works:

fn(**d1 | d2)

A new contracted dict is constructed before it is fed to the function.