4

I have a function, which has a signature like this:

def func(**kwargs):

The user of that function will call the function with zero or one keyword arguments. If he passes one argument, the name will be foo_id, bar_id, baz_id etc., but I don't know the exact name he will use. The value of the passed argument will be some interger. I still want to take that argument's value and use it.

Currently I'm doing it like this, but I was wondering would there be a cleaner way to achieve this:

def func(**kwargs):
    if kwargs:
        target_id = list(kwargs.values())[0]
    else:
        target_id = None

    # use target_id here, no worries if it's None

I'm using Python 3.8, so backwards compatibility is not an issue.

ruohola
  • 21,987
  • 6
  • 62
  • 97
  • What kind of API has un-predefined keyword arguments? Sounds like an oxymoron to me. – martineau Dec 16 '19 at 17:19
  • @martineau Way to complex to fully explain here. But in short: this will be used in a base class for django graphene Mutations, which then will take one argument each. This argument is for consistency sake always the name of the model and `_id`. So no one actually calls this Python function, but the caller is the user of the graphql api. – ruohola Dec 16 '19 at 17:26
  • Hmm, I understand you've left details out, but it still sounds like it could be an [XY problem](https://meta.stackexchange.com/questions/66377/what-is-the-xy-problem) with the real issue being with the the class hierarchy's design. – martineau Dec 16 '19 at 17:40
  • @martineau This (like any other problem) could be tackled in a million different ways. But doing it with an abstract base class, which all schemas that need the functionality, derive to their own Mutations, gives me very little duplicate code and an extremely clean, consistent, and easy to use API. I'm quite pleased with this solution. – ruohola Dec 16 '19 at 17:44

2 Answers2

6

Here we are

def func(**kwargs):
    target_id = next(iter(kwargs.values()), None)

    print(target_id)


func(name_id='name')
func(value_id='value')
func(test_id='test')
func()

Outputs

python test.py
name
value
test
None
Alexandr Shurigin
  • 3,921
  • 1
  • 13
  • 25
2

Since the dictionary has only one item, and you don't need to keep it in the dictionary, the cleanest way is to use the dict.popitem method. This returns both the argument name and its value as a pair.

def func(**kwarg):
    if kwarg:
        name, value = kwarg.popitem()
        # ...
    else:
        # ...

Since the caller should supply at most one argument, I recommend explicitly raising an error if it is called with more arguments:

def func(**kwarg):
    if len(kwarg) > 1:
        raise TypeError(f'Expected at most 1 keyword arg, got {len(kwarg)}.')
    elif kwarg:
        name, value = kwarg.popitem()
        # ...
    else:
        # ...
kaya3
  • 47,440
  • 4
  • 68
  • 97
  • 2
    `...will call the function with zero or one keyword arguments.` – Mark Dec 16 '19 at 16:41
  • 1
    @MarkMeyer Good catch, fixed. – kaya3 Dec 16 '19 at 16:43
  • very pythonic way :D – Alexandr Shurigin Dec 16 '19 at 16:44
  • 1
    I would think it would be more pythonic to put `popitem` in a `try` block and catch the `KeyError` if you need to. Of course that's a tiny detail. – Mark Dec 16 '19 at 16:44
  • 3
    not advocating this but `name, value = (kwarg or {None:None}).popitem()` – Chris_Rands Dec 16 '19 at 16:45
  • Regarding if/else vs. try/catch, my opinion is that *"explicit is better than implicit"*, so if/else is better here because the condition explicitly shows under what circumstances one will happen vs. the other. Try/except requires the reader to infer what the condition is from what exception could be thrown by the code in the try block, and if you write `except KeyError:` explicitly then the reader may still have to look up what error `popitem` throws on an empty dictionary to be sure it's correct. – kaya3 Dec 16 '19 at 16:50