Wednesday, December 5, 2012

PP #1: The always-true condition

Python Pitfalls are quirks of the Python programming language that may trip up those new to the language or to programming. First in yet another series.

My experience with Python is that my code usually does what I expect it to—in fact, more so than with any other programming language I've used. But that doesn't mean it's impossible to write code that doesn't do what you expect! Far from it. I see code like this on Stack Overflow pretty much every day:

while True:
    command = raw_input("command> ").lower().strip()
    if command == "stop" or "exit":
    elif command == "help":
        print "quit, exit: exit the program"
        print "help:       display this help message" 
    elif command:
        print "Unrecognized command."

Can you guess the question? It's some variation of "No matter what command I enter, it always exits."

And the reason is always the same: a Boolean expression using or that is always truthy. (See PF #1: Truth and Consequences.) In this case, it's this line:

    if command == "quit" or "exit":

The programmer who wrote that line intended it to match either "quit" or "exit." Instead, it matches any command. The reason for this becomes obvious if we look at how Python evaluates the condition.

As described in PF #2: And and/or or, the value of a Boolean or operation is truthy if either operand is truthy. The operands of or here are:
  • command == "quit"
  • "exit"
The latter operand, being a string with nonzero length, is truthy in Python, and therefore the result of the or is always truthy. Therefore, this test matches everything and the suite under the if is always executed!

This error is particularly pernicious because it reads exactly how you would phrase it in English, and expresses the programmer's intent clearly. It's just that the meaning of or is more specific in Python than in English.

One way to write this test so that it works as expected is to explicitly tell Python to test command against both alternatives:

    if command == "quit" or command == "exit":

Of course, there's a shorter and clearer way to write this in Python:

    if command in ("quit", "exit"):

If you will be testing for a lot of alternatives and performance is important, use a set defined outside the loop. Testing for membership in an existing set is a lot faster than testing for membership in a tuple that's created new each time the test is executed.

exit_synonyms = set(["quit", "halt", "stop", 
                     "end", "whoa", "exit"])

while True:
    command = raw_input("command> ").lower().strip()
    if command in exit_synonyms:
    # and so on as above

No comments:

Post a Comment