I've noticed, to my surprise, that in a function call, I could unpack a dict
with strings that weren't even valid python identifiers.
It's surprising to me since argument names must be identifiers, so allowing a function call to unpack a **kwargs
that has non-identifiers, with no run time error, doesn't seem healthy (since it could bury problems deeper that where they actually occur).
Unless there's an actual use to being able to do this, in which case my question becomes "what would that use be?".
Example code
Consider this function:
def foo(**kwargs):
first_key, first_val = next(iter(kwargs.items()))
print(f"{first_key=}, {first_val=}")
return kwargs
This shows that, within a function call, you can't unpack a dict
that has has integer keys, which is EXPECTED.
>>> t = foo(**{1: 2, 3: 4})
TypeError Traceback (most recent call last)
...
TypeError: foo() keywords must be strings
What is really not expected, and surprising, is that you can, on the other hand, unpack a dict
with string keys, even if these are not valid python identifiers:
>>> t = foo(**{'not an identifier': 1, '12': 12, ',(*&$)': 100})
first_key='not an identifier', first_val=1
>>> t
{'not an identifier': 1, '12': 12, ',(*&$)': 100}
CodePudding user response:
Looks like this is more of a kwargs
issue than an unpacking issue. For example, one wouldn't run into the same issue with foo
:
def foo(a, b):
print(a b)
foo(**{"a": 3, "b": 2})
# 5
foo(**{"a": 3, "b": 2, "c": 4})
# TypeError: foo() got an unexpected keyword argument 'c'
foo(**{"a": 3, "b": 2, "not valid": 4})
# TypeError: foo() got an unexpected keyword argument 'not valid'
But when kwargs
is used, that flexibility comes with a price. It looks like the function first attempts to pop out and map all the named arguments and then passes the remaining items in a dict
called kwargs
. Since all keywords are strings (but all strings are not valid keywords), the first check is easy - keywords must be strings
. Beyond that, it's up to the author to figure out what to do with remaining items in kwargs
.
def bar(a, **kwargs):
print(locals())
bar(a=2)
# {'a': 2, 'kwargs': {}}
bar(**{"a": 3, "b": 2})
# {'a': 3, 'kwargs': {'b': 2}}
bar(**{"a": 3, "b": 2, "c": 4})
# {'a': 3, 'kwargs': {'b': 2, 'c': 4}}
bar(**{1: 3, 3: 4})
# TypeError: keywords must be strings
Having said all that, there definitely is inconsistency but not a flaw. Some related discussions: