The *args and **kwargs arguments can break the robustness of a function or method. They make the signature fuzzy, and the code often starts to become a small argument parser where it should not, for example:
def fuzzy_thing(**kwargs):
if 'do_this' in kwargs:
print('ok i did this')
if 'do_that' in kwargs:
print('that is done')
print('ok')
>>> fuzzy_thing(do_this=1)
ok i did this
ok
>>> fuzzy_thing(do_that=1)
that is done
ok
>>> fuzzy_thing(what_about_that=1)
ok
If the argument list gets long and complex, it is tempting to add magic arguments. But this is more a sign of a weak function or method that should be broken into pieces or refactored.
When *args is used to deal with a sequence of elements that are treated the same way in the function, asking for a unique container argument such as an iterator is better, for example:
def sum(*args): # okay
total = 0
for arg in args:
total += arg
return total
def sum(sequence): # better!
total = 0
for arg in sequence:
total += arg
return total
For **kwargs, the same rule applies. It is better to fix the named arguments to make the method's signature meaningful, for example:
def make_sentence(**kwargs):
noun = kwargs.get('noun', 'Bill')
verb = kwargs.get('verb', 'is')
adjective = kwargs.get('adjective', 'happy')
return f'{noun} {verb} {adjective}'
def make_sentence(noun='Bill', verb='is', adjective='happy'):
return f'{noun} {verb} {adjective}'
Another interesting approach is to create a container class that groups several related arguments to provide an execution context. This structure differs from *args or **kwargs because it can provide internals that work over the values, and can evolve independently. The code that uses it as an argument will not have to deal with its internals.
For instance, a web request passed on to a function is often represented by an instance of a class. This class is in charge of holding the data passed by the web server, as shown in the following code:
def log_request(request): # version 1 print(request.get('HTTP_REFERER', 'No referer'))
def log_request(request): # version 2 print(request.get('HTTP_REFERER', 'No referer')) print(request.get('HTTP_HOST', 'No host'))
Magic arguments cannot be avoided sometimes, especially in metaprogramming. For instance, they are indispensable in the creation of decorators that work on functions with any kind of signature.
Let's discuss class names in the next section.