October 2019

Volume 34 Number 10

[The Working Programmer]

Python: Flow Control

By Ted Neward | October 2019

Ted NewardIn the last article (msdn.com/magazine/mt833510), I took a start down the path toward Python, getting it installed and paying homage to the Gods of Computer Science. Obviously, there’s not much you can do at that point, so it’s time to dive deeper into Python by examining its flow control structures, like if/then statements, loops and exceptions.

One thing before I begin, though: I never really touched on the subject of editors. While most readers of this magazine will be accustomed to using Visual Studio for all editing purposes, building solutions and projects and such, Python doesn’t really fit that model. And certainly, while you could use Visual Studio simply as a text editor, it’s generally pretty much a heavyweight for that sort of use. (As my videogaming sons like to put it, “There’s no kill like OVERkill.”) As a result, Visual Studio Code is likely to be the editor of choice for most folks. However, the Anaconda distribution that I discussed last time (which ships as part of the Visual Studio installer) comes with another option, a more IDE-ish experience called Spyder. It’s certainly not as extensive as Visual Studio is, but it makes for a nice “halfway point” between the Visual Studio Code editor-first experience and the Visual Studio IDE-everything experience. (For those who are fans of the JetBrains suite of tools, it also makes PyCharm, which has a certain amount of popularity among the Python crowd.) As always, the choice is up to you—most Python projects have no dependencies on any particular editor or environment.

Armed thusly with your editor of choice, let’s proceed.

Branching

The most basic branching construct is the classic if/then statement, which does exactly the same thing as in pretty much every other programming language ever invented: Test an expression and if the test yields a true result, execute a block of code; if the result is false, either bail entirely or execute an alternative “else” branch. Python adds only one wrinkle to this, which brings me to an important distinction of Python, that of significant whitespace. In Python, you don’t denote scope blocks using punctuation characters (like the “{“ and “}” in C-family languages such as C#), you simply indent a number of characters, typically four, like so:

# If-then
name = “Zoot”
if name == “Zoot”:
  print(“Naughty, naughty Zoot!”)
elif name == “Sir Robin”:
  print(“Brave, brave Sir Robin...”)
else:
  print(“I have no idea who you are”)

(Note that I may reduce the indentation in some code listings here to two characters for spacing in the magazine; but stylistically, Python is super-sensitive to whitespace and prefers four, so you should stick with that.) Take particular care to note the lack of parentheses around the expression, and the colon at the end of each test expression—the colon will be an indicator to you that Python expects the next line or sequence of lines to be indented. As a matter of fact, if you remove the “print” line from either of those blocks, and leave nothing in their place, Python will complain quite vigorously. If you don’t want code in an expected indented block, Python provides the “pass” statement, or a standalone “…,” to indicate a no-op statement:

# If-then
name = “Zoot”
if name == “Zoot”:
  pass
elif name == “Sir Robin”:
  ...
else:
  print(“I have no idea who you are”)

When this is executed, nothing will be printed, because it matches on the first expression but then passes on actually doing anything.

It’s also important to note that the Python mantra—“There’s only one way to do it”—holds here, as well, with respect to this sort of sequential testing of a value. Where other languages, like C#, Java, Ruby or F#, have some form of multiple-choice decision-making construct (usually denoted by the keyword “switch”), Python chooses instead to stick with the conceptually simpler “if-then-elif” collection. In other words, Python has no “switch” because a whole list of “elif”s can serve the same purpose. (Whether this is an advantage or drawback is open to debate and interpretation, of course.)

Looping

To loop, you have two constructs, while and for. While will continue to loop and execute a block until a test condition is determined to be false, and for iterates across a sequence, which is a generic way of referring to a variety of “iterable things.” The syntax for a while loop is pretty straightforward, with one interesting quirk that also applies to for. Have a look:

# While
count = 0
while count < 5:
  print(“Hello”)
  count = count + 1
else:
  print(“Done saying hello”)

Spot the quirk? As you might infer, the “else” block gets executed after the while loop ceases looping, regardless of how many times the loop was actually executed. Even if the count variable had started at 6, the else block would’ve fired. This is something Python offers that no other mainstream language does, and it provides a useful way to provide cleanup or other sorts of behavior that should always be executed, regardless of what actually happens with the loop.

The for loop operates similarly, but operates against a sequence, so let’s start with a list, which fundamentally is an array, but is dynamically resizable:

# For
numbers = [1, 2, 3, 5]
for n in numbers:
  if n == 3:
    continue
  print(n)
else:
  print(“3! From the Book of Armaments”)

As might be expected, this will recite King Arthur’s count down for the Holy Hand Grenade of Antioch.

Using a list might be acceptable for small numbers, but obviously if you want to count up to 100, having to construct a list of 100 elements ahead of time would be impractical. In some other languages, you have a particular flavor of the for-loop construct to set a starting point, ending point and iteration, but remember, in Python, there’s only one way to do it. (On top of which, that three-part for-loop construct is pretty cumbersome when you look back on it.) For these scenarios, Python provides a function, called range, to provide a sequence of numbers suitable for use in a loop:

ns = range(0,100)
for n in ns:
  print(n)

Running this snippet will also demonstrate that the results of the range function are bottom-inclusive, top-exclusive (meaning you’d see zerio, but not 100). For ranges that want to “step” by more than one, you can construct the range to jump by a set value by passing a third parameter to the range:

ns = range(0,100, 5)
for n in ns:
  print(n)

This will print the values five, 10, 15 and so on.

Regardless of the source of the iteration, the for-loop construct allows for manipulation of the loops via the break and continue keywords; the former arbitrarily exits the loop, and the latter causes the loop to begin again. Note as well that the range doesn’t have to start low and grow high; you can just as easily start with zero and “grow downward” by stepping by negative one each time, if so desired:

ns = range(0,-100,-1)
for n in ns:
  print(n)

But the real power of the range function comes into play when you realize that the underlying construct that the for-loop is operating off of is a sequence: an object that knows how to produce the next value expected. Many people will assume that this is similar to the iterator object that’s frequently used as part of C# (the IEnumerator interface), but in fact, this is a little bit more powerful than that. If you were to print the returned object from the range function, what you’d see isn’t the full list of values, but the object itself; that is to say, this snippet of code:

ns = range(0,10)
print(ns)

This code will print “range(0,10),” not “[0,1,2,3,4,5,6,7,8,9],” because no list has been constructed. The sequence object knows its lower range (0), its upper range (10), and how to produce each next successive value (add one to the current value) as requested, so it doesn’t need to construct the entire collection. This means the sequence object occupies the same amount of memory whether it’s a sequence from zero to 10 or from zero to 10 million. (This is often referred to as a “stream” in other languages.) Python makes use of sequences in a variety of ways throughout the platform, not just in for-loop constructs.

Exceptions

Finally, Python also supports exception-handling similar to the way C#, Java and C++ do: To signal an error condition that can be trapped and handled further up the call stack, Python allows you to “raise” an object instance (or a class, which Python will take to mean create an instance and then raise it) and immediately exit the current function, in the same way that you “throw” exceptions in C#:

raise Exception()

This will throw a rarely used generic exception type. I’ll talk more about how to create new exception types and so on after I cover how to create classes (and subclasses—anything “raise”d must inherit from a base class).

More to the point, Python allows you to catch these exceptions in much the same way that C#, C++ and Java do, by using the “try” keyword to establish a guarded block of code that, if an exception is raised, will transfer control to one of a series of “except” blocks, with a “finally” clause that will be guaranteed to fire regardless of how control exits the guarded block. What’s new is that Python also allows an “else” clause off of the “try” block, which fires in the event no exception is generated:

# Try/Catch
try:
  print(“Let’s do bad math”)
  impossible = 10 / 0
except ZeroDivisionError:
  print(“Can’t divide by zero, man”)
else:
  print(“That worked?!?”)
finally:
  print(“Finally clause”)

There are a few forms of exceptions that allow for catching multiple exception types and so on, but the core form is the same. As with C#/CLR exceptions, if the exception passes outside of the top-level function in the currently executing program, the thread will terminate and print a (hopefully helpful) message to the console. Exception types are objects, and custom exception types can be constructed for more precise error-handling logic in more complex programs, but doing so requires understanding how to construct objects in Python, which is still a bit out-of-scope for what I’ve covered so far.

Wrapping Up

For the most part, any experienced object-oriented developer familiar with the language constructs of Java, C#, C++ or any of their kin will find Python’s flow-control constructs to be easily digestible. If anything, Python’s insistence on rigid conformance to the “there’s only one way to do it” mantra makes it that much easier to bring the language into play; there’s no trying to defend when to use “switch/case” instead of a number of “if/else-if” statements, or trying to justify the use of a “do-while” against a “while.” In many respects, this makes Python a much easier language to take on as a first language, which is likely why Python is rapidly becoming the first language for many new programmers, as well as for data scientists who don’t want to program but “ju,st let me do stuff with my data.”

We’re hardly done here, though; Python, like any mainstream programming language, supports the idea of capturing behavior into a named construct—the function—and that’s where we’ll turn our collective heads next. In the meantime … happy coding!


Ted Neward is a Seattle-based polytechnology consultant, speaker and mentor. He has written a ton of articles, authored and co-authored a dozen books, and speaks all over the world. Reach him at ted@tedneward.com or read his blog at blogs.tedneward.com.

Thanks to the following technical expert for reviewing this article: Harry Pierson


Discuss this article in the MSDN Magazine forum