Parameter handling

Robert Crowther Jan 2022
Last Modified: Feb 2023

Like many languages, Python can pass parameters. The only mild difference is that no type statement is required,

All examples with Python 3. Adjust to you environment e.g. Debians may require ‘python3’.

def a(b, c):
    print('{}, {}'.format(b, c))

a(1, 2)
$ 1, 2

Fifty coder pages will tell you, Python can also pass loose arguments (’vargs’?) using an asterix argument, But Python 3 can’t,

def a(b, c, *):
    print('{}, {}'.format(b, c))
$ SyntaxError: named arguments must follow bare *

The error message explains what is rejected. No way to access this list of args unless it is named. The lone star has no purpose, Can we access [psitional arguments with a lone star?

def a(*, surplus=9):
    print(surplus)

a(1, 2, surplus=8)
$ TypeError: a() takes 0 positional arguments but 2 positional arguments (and 1 keyword-only argument) were given

a(surplus=8)
$ 9

No. Star arguments must be named.

Five hundred code pages will tell you, the name of the arg list can be anything, but by convention, it is ‘args’. Now it is named, it will work,

def a(b, c, *args):
    print('{}, {}, {}'.format(b, c, args))

a(1, 2, 8, 10)
$ 1, 2, (8, 10)

Since Python has no differentiation, if an argument is mandatory, you should probably signal this by writing explicitly (but see discussion further down),

def a(a, b, c, *args):
    # 'c' is not optional...
    print('{}, {}, {}, {}'.format(a, b, c, args))

a(1, 2, 8, 10)
$ 1, 2, 8, (10,)

As with any other language I can think of, simple args are passed by their position.

Args can be also passed, as some other languages can, with named (or ‘default’) arguments,

def a(b=77, c=99):
    print('{}, {}'.format(b, c))

a()
$ 77, 99

a(b=1, c=2)
$ 1, 2

Here’s a twist. Values can be passed by position to named arguments,

bb=1
cc=2

a(bb, cc)
$ 1, 2

Though there are limits,

a(b=bb, cc)

$ SyntaxError: positional argument follows keyword argument

a(bb, b=cc)

TypeError: a() got multiple values for argument 'b'

So, from the above, we know named args must be positioned after args (likely for easy, unambiguous parsing),

def a(b=77, c):
    print('{}, {}'.format(b, c))

$ SyntaxError: non-default argument follows default argument

Default args can also have a varg, signalled by double ’**’, but if naked, like the single ’*’, this errors,

def a(b=77, c=99, **):
    print('{}, {}'.format(b, c))

$ SyntaxError: invalid syntax

Five thousand code pages will tell you, default args can accessed, like args, by naming, and that the name of the varg list can be anything but, by convention, it is ‘kwargs’. Delivery is by a dict,

def a(b=77, **kwargs):
    print('{}, {}'.format(b, kwargs))

a(b=1, z=99)
$ 1, {z=99}

What if we add a keyworded value to the call?

a(23, z=99)
$ 23, {'z': 99}

That worked.

As with args, named args can be extracted by making them explicit in the function declaration,

def a(b=77, z=99, **kwargs):
    print('{}, {}, {}'.format(b, z, kwargs))

a(b=1, z=99)
$ 1, 99, {}

Note how the keyworded args are not in the ‘kwargs’ dict—explicitly naming has ‘consumed’ the argument or, the value positions has overrides the ‘kwargs’ catch‐all.

Positioned values still work even if the value is named, the name ‘x’ is ignored for the argument position,

a(1, x=77, y=99)

$ 1, 77, {'y': 99}

The catch‐all of a ‘**kwargs’ leads to a Python idiom. Often, functions are provided with *args or, especially, **kwargs if the coder feels the functionality of the method may be extended or overridden,

def a(b, *args, c=77, **kwargs):
    print('{}, {}, {}, {}'.format(b, args, c, kwargs))

a(1, 2, 3, c=5, d=6)
$ 1, (2, 3), 5, {'d': 6}

Here’s the oddity again—must have been decided somewhere—a named value can pass into a positional arg,

def a(b, **kwargs):
    print('{}, {}'.format(b, kwargs))

a(b=4)
$ 4, {}

And here’s another Python idiom. Let’s say there is a complex function type (this is simple for illustration, but assume it passes four positional and three default arguments),

def a(**kwargs):
    print('{}'.format(kwargs))

and you wish to wrap/override it,

def b(**kwargs):
    print('{}'.format(kwargs))
    # you may want to do something with kwarg 'q' here...
    a(**kwargs)

To provide new and handle additional information, you can extract the new kwargs you were interested in. Rather than the above,

def b(q=22, **kwargs):
    # do something with 'q'
    print('{}, {}'.format(q, kwargs))
    a(**kwargs)

b(q=77, b=3, c=4)

$ 77, {'b': 3, 'c': 4}
{'b': 3, 'c': 4}

Note how the value named ‘q’ was extracted in method ‘b’ so did not arrive in method ‘a’ (unless reapplied to ‘a’). You will see this kind of code frequently in the __init__() methods of classes.

References

All has been considered and discussed e.g.

https://www.python.org/dev/peps/pep-3102/