# [Tutor] Defining variable arguments in a function in python

Steven D'Aprano steve at pearwood.info
Sun Dec 30 05:39:06 EST 2018

```On Sun, Dec 30, 2018 at 12:07:20AM -0500, Avi Gross wrote:

[...]
> Or on a more practical level, say a function wants an input from 1 to 10.
> The if statement above can be something like:
>
> >>> def hello(a, *n, **m) :
> 	if not (1 <= a <= 10) : a=5
> 	print(a)
> 	print(*n)
>
>
> >>> hello(1,2,3)
> 1
> 2 3
> >>> hello(21,2,3)
> 5
> 2 3
> >>> hello(-5,2,3)
> 5
> 2 3

This design is an example of "an attractive nuisance", possibly even a
"bug magnet". At first glance, when used for mickey-mouse toy examples
like this, it seems quite reasonable:

hello(999, 1, 2)  # I want the default value instead of 999

but thinking about it a bit more deeply, and you will recognise some
problems with it.

First problem:

How do you know what value to pass if you want the default? Is 999 out
of range? How about 11? 10? Who knows? If you have to look up the docs
to know what counts as out of range, you might as well read the docs to
find out what the default it, and just pass that:

hello(5, 1, 2)  # I want the default value 5

but that kind of defeats the purpose of a default. The whole point of a
default is that you shouldn't need to pass *anything at all*, not even a
placeholder.

(If you need a placeholder, then you probably need to change your
function parameters.)

But at least with sentinels like None, or Ellipsis, it is *obvious*
that the value is probably a placeholder. With a placeholder like 11 or
999, it isn't. They look like ordinary values.

Second problem:

Most of the time, we don't pass literal values to toy functions. We do
something like this example:

for number, widget_ID, delivery_date in active_orders:
submit_order(number, widget_ID, delivery_date)

Can you see the bug? Of course you can't. There's no obvious bug. But
little do you know, one of the orders was accidentally entered with an
out-of-range value, let's say -1, and instead of getting an nice error
message telling you that there's a problem that you need to fix, the
submit_order() function silently replaces the erroneous value with the
default.

The bug here is that submit_order() function exceeds its authority.

The name tells us that it submits orders, but it also silently decides
to change invalid orders to valid orders using some default value. But
this fact isn't obvious from either the name or the code. You only learn
this fact by digging into the source code, or reading the documentation,
and let's be honest, nobody wants to do either of those unless you
really have to.

So when faced with an invalid order, instead of getting an error that
you can fix, or even silently skipping the bad order, the submit_order()
function silently changes it to a valid-looking but WRONG order that you
probably didn't want. And that costs real money.

The risk of this sort of bug comes directly from the design of the
function. While I suppose I must acknowledge that (hypothetically)
there could be use-cases for this sort of design, I maintain that in
general this design is a bug magnet: responsibility for changing
out-of-range values to in-range values belongs with the caller, not
the called function.

The caller may delegate that responsibility to another:

for number, widget_ID, delivery_date in active_orders:
number = validate_or_replace(number)
submit_order(number, widget_ID, delivery_date)

which is fine because it is explicit and right there in plain sight.

This then allows us to make the submit_order() far more resiliant: if it
is passed an invalid order, it can either fail fast, giving an obvious
error, or at least skip the invalid order and notify the responsible
people.

--
Steven
```