[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


More information about the Tutor mailing list