Home > database >  Why does unpacking non-identifier strings work on a function call?
Why does unpacking non-identifier strings work on a function call?

Time:07-15

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:

  1. Supporting (or not) invalid identifiers in **kwargs
  2. feature: **kwargs allowing improperly named variables
  • Related