[OT-ish] Design principles: no bool arguments

S

Steven D'Aprano

One design principle often mentioned here (with a certain degree of
disagreement[1]) is the idea that as a general rule, you shouldn't write
functions that take a bool argument to switch between two slightly
different behaviours.

This is a principle often championed by the BDFL, Guido van Rossum.

Here's a Javascript-centric article which discusses the same idea, and gives
it a name: the Boolean Trap.

http://ariya.ofilabs.com/2011/08/hall-of-api-shame-boolean-trap.html

No doubt there are counter arguments as well. The most obvious to me is if
the flag=True and flag=False functions share a lot of code, it is poor
practice to implement them as two functions with two copies of almost
identical code.

My solution to this is a technical violation of the "Avoid Boolean Trap"
principle, but only in a private function:

def spam_on(arg):
_spam(arg, True)

def spam_off(arg):
_spam(arg, False)

def _spam(arg, flag):
do stuff
if flag:
a
else:
b
more stuff




[1] This is the Internet. There's *always* a certain amount of disagreement.
 
M

Maarten

One design principle often mentioned here (with a certain degree of
disagreement[1]) is the idea that as a general rule, you shouldn't write
functions that take a bool argument to switch between two slightly
different behaviours.

This is a principle often championed by the BDFL, Guido van Rossum.

Here's a Javascript-centric article which discusses the same idea, and gives
it a name: the Boolean Trap.

http://ariya.ofilabs.com/2011/08/hall-of-api-shame-boolean-trap.html

No doubt there are counter arguments as well. The most obvious to me is if
the flag=True and flag=False functions share a lot of code, it is poor
practice to implement them as two functions with two copies of almost
identical code.

A simple one: C and C-like languages only have arguments, not keyword-
parameters. That alone makes a world of difference.

Maarten
 
S

Stefan Behnel

Maarten, 25.08.2011 09:52:
One design principle often mentioned here (with a certain degree of
disagreement[1]) is the idea that as a general rule, you shouldn't write
functions that take a bool argument to switch between two slightly
different behaviours.

This is a principle often championed by the BDFL, Guido van Rossum.

Here's a Javascript-centric article which discusses the same idea, and gives
it a name: the Boolean Trap.

http://ariya.ofilabs.com/2011/08/hall-of-api-shame-boolean-trap.html

No doubt there are counter arguments as well. The most obvious to me is if
the flag=True and flag=False functions share a lot of code, it is poor
practice to implement them as two functions with two copies of almost
identical code.

A simple one: C and C-like languages only have arguments, not keyword-
parameters. That alone makes a world of difference.

Right. It's totally unreadable to find this in the code:

data1.merge_with(data2, true);

Requires you to either a) know the underlying signature by heart, or b)
look it up before understanding the code.

It's a lot harder to argue against this:

data1.merge_with(data2, overwrite_duplicates=True)

Stefan
 
T

Thomas 'PointedEars' Lahn

Stefan said:
Maarten, 25.08.2011 09:52:
One design principle often mentioned here (with a certain degree of
disagreement[1]) is the idea that as a general rule, you shouldn't write
functions that take a bool argument to switch between two slightly
different behaviours.

This is a principle often championed by the BDFL, Guido van Rossum.

Here's a Javascript-centric article which discusses the same idea, and
gives it a name: the Boolean Trap.

http://ariya.ofilabs.com/2011/08/hall-of-api-shame-boolean-trap.html

No doubt there are counter arguments as well. The most obvious to me is
if the flag=True and flag=False functions share a lot of code, it is
poor practice to implement them as two functions with two copies of
almost identical code.

A simple one: C and C-like languages only have arguments, not keyword-
parameters. That alone makes a world of difference.

The logic is flawed, for one because keyword arguments can be emulated.
For example in C, you can (and would) OR-combine binary flag constants:

open("foo", O_RDONLY);

instead of

open("foo", TRUE);

(assuming the latter function existed). Other approaches can be found in C
as well:

fopen("foo", "r");

In ECMAScript implementations like JavaScript (which I count as C-like), you
can define the API so that it accepts object references (as explained in the
article):

foo(bar, {baz: true});

instead of (or in addition to)

foo(bar, true);

(But this is a trade-off readability vs. runtime and memory efficiency, as
each time the function is called an object needs to be created, if it is not
cached, and an additional property access is necessary in the
function/method. Python has much the same problem, see below.)

And there can hardly be an argument that W3C DOM Level 3 Events init…Event()
methods are *unnecessarily* FUBAR. Even OMG IDL supports constants (as
showed by the HTMLElement interface of DOM Level 2 Core), and passing of
object instances to methods.
Right. It's totally unreadable to find this in the code:

data1.merge_with(data2, true);

Requires you to either a) know the underlying signature by heart, or b)
look it up before understanding the code.

It's a lot harder to argue against this:

data1.merge_with(data2, overwrite_duplicates=True)

Both variants work (even in Py3) if you only define

class Data(object):
def merge_with(self, bar, overwrite_duplicates):
pass

data1 = Data()
data2 = Data()

You have to define

class Data(object):
def merge_with(self, bar, **kwargs):
# do something with kwargs['overwrite_duplicates']
pass

data1 = Data()
data2 = Data()

so that

data1.merge_with(data2, True);

is a syntax error ("TypeError: merge_with() takes exactly 2 arguments (3
given)").

IOW, this advantage of Python in readability is not only caused by API
definition, but also by how the API is used. It might turn into a
disadvantage if key lookups make the code expensive memory- and runtime
wise.

And you will still have to know the underlying signature to name the
argument. Worse, with keyword arguments you *have to* look up the
documentation (i. e., it needs to be well-documented as well) to know its
name (as the compiler can only tell you "kwargs").

So no doubt there are advantages to keyword arguments, but there are
disadvantages, too.
 
T

Terry Reedy

One design principle often mentioned here (with a certain degree of
disagreement[1]) is the idea that as a general rule, you shouldn't write
functions that take a bool argument to switch between two slightly
different behaviours.

This is a principle often championed by the BDFL, Guido van Rossum.

You missed the essential caveat to his rule. See below.
Here's a Javascript-centric article which discusses the same idea, and gives
it a name: the Boolean Trap.

http://ariya.ofilabs.com/2011/08/hall-of-api-shame-boolean-trap.html

This was mostly about defining parameters as positional-only (which
Python does not have) versus keyword-optional or keyword only. In
Python, callers always have the 'keyword = BOOL' option.
No doubt there are counter arguments as well. The most obvious to me is if
the flag=True and flag=False functions share a lot of code, it is poor
practice to implement them as two functions with two copies of almost
identical code.

My solution to this is a technical violation of the "Avoid Boolean Trap"
principle, but only in a private function:

def spam_on(arg):
_spam(arg, True)

def spam_off(arg):
_spam(arg, False)

def _spam(arg, flag):
do stuff
if flag:
a
else:
b
more stuff

As I think one of the comments pointed out, this now means that if I the
user need to compute whether to turn off or on, I now have to write

if expr: spam_on(arg)
elses: spam_off(arg)

(or put this on 4 lines if you prefer ;-) instead of

spam_switch(expr, arg)

so moving the conditional out of the function *pushes it onto every
user*. Note that naming the function 'switch' and putting the bool as
the first param makes it pretty obvious that True=='on', and False=='off'.

As I remember, Guido only recommendeds splitting if the boolean arg is
(would be) always (nearly) passed as a constant True or False. Of
course, I am not sure how one forsees that at the design stage, prior to
field use.
 
S

Stefan Behnel

Thomas 'PointedEars' Lahn, 25.08.2011 11:29:
Stefan said:
It's totally unreadable to find this in the code:

data1.merge_with(data2, true);

Requires you to either a) know the underlying signature by heart, or b)
look it up before understanding the code.

It's a lot harder to argue against this:

data1.merge_with(data2, overwrite_duplicates=True)
[...]
And you will still have to know the underlying signature to name the
argument. Worse, with keyword arguments you *have to* look up the
documentation (i. e., it needs to be well-documented as well) to know its
name (as the compiler can only tell you "kwargs").

Note that code is read more often than it gets written.

Stefan
 
T

Thomas 'PointedEars' Lahn

Stefan said:
Thomas 'PointedEars' Lahn, 25.08.2011 11:29:
Stefan said:
It's totally unreadable to find this in the code:

data1.merge_with(data2, true);

Requires you to either a) know the underlying signature by heart, or b)
look it up before understanding the code.

It's a lot harder to argue against this:

data1.merge_with(data2, overwrite_duplicates=True)
[...]
And you will still have to know the underlying signature to name the
argument. Worse, with keyword arguments you *have to* look up the
documentation (i. e., it needs to be well-documented as well) to know its
name (as the compiler can only tell you "kwargs").

Note that code is read more often than it gets written.

It is not clear to me why you completely ignored the rest of my
counter-argument. As it is, yours is a weak argument.
 
I

Ian Kelly

Both variants work (even in Py3) if you only define

class Data(object):
 def merge_with(self, bar, overwrite_duplicates):
   pass

data1 = Data()
data2 = Data()

You have to define

class Data(object):
 def merge_with(self, bar, **kwargs):
   # do something with kwargs['overwrite_duplicates']
   pass

data1 = Data()
data2 = Data()

so that

data1.merge_with(data2, True);

is a syntax error ("TypeError: merge_with() takes exactly 2 arguments (3
given)").

IOW, this advantage of Python in readability is not only caused by API
definition, but also by how the API is used.  It might turn into a
disadvantage if key lookups make the code expensive memory- and runtime
wise.

And you will still have to know the underlying signature to name the
argument.  Worse, with keyword arguments you *have to* look up the
documentation (i. e., it needs to be well-documented as well) to know its
name (as the compiler can only tell you "kwargs").

Note though that Python 3 adds actual keyword-only arguments, which
address all of your points:

class Data:
def merge_with(self, bar, *, overwrite_duplicates):
pass

Of course, in Python 2 that definition would be a syntax error, so you
can't really take advantage of it if you need compatibility.
 
T

Thomas 'PointedEars' Lahn

Ian said:
Thomas said:
Both variants work (even in Py3) if you only define [a named argument].
You have to define [a keyword argument, e.g. `kwargs'].

so that

data1.merge_with(data2, True);

is a syntax error ("TypeError: merge_with() takes exactly 2 arguments (3
given)").

IOW, this advantage of Python in readability is not only caused by API
definition, but also by how the API is used. It might turn into a
disadvantage if key lookups make the code expensive memory- and runtime
wise.

And you will still have to know the underlying signature to name the
argument. Worse, with keyword arguments you *have to* look up the
documentation (i. e., it needs to be well-documented as well) to know its
name (as the compiler can only tell you "kwargs").

Note though that Python 3 adds actual keyword-only arguments, which
address all of your points:

class Data:
def merge_with(self, bar, *, overwrite_duplicates):
pass

That's good to know. Thanks.
Of course, in Python 2 that definition would be a syntax error, so you
can't really take advantage of it if you need compatibility.

ACK.


Regards,
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

No members online now.

Forum statistics

Threads
473,992
Messages
2,570,220
Members
46,807
Latest member
ryef

Latest Threads

Top