Why is there more than one way to end a while loop?
MP #16: Choosing an approach when there's more than one option
Perl was a popular programming language in the late 1990s through the early 2000s, when the web was really taking shape. The motto of Perl at the time was “There’s more than one way to do it”, which is why you may have seen the acronym TMTOWTDI. This motto was part of Perl’s popularity; people loved the expressiveness of Perl, and the opportunity it gave them to come up with unique solutions to common problems.1
This expressiveness was ultimately part of the reason Perl began to decline in popularity. It turns out that while unique solutions to common problems are fun to write, they’re not always fun to maintain. People would write some code for a project, put it aside for a while, and come back to it some time later only to find themselves confused by what they had written earlier. Or, they’d work on a project with other people, and find it hard to come to agreement on a common approach.
This was in sharp contrast to one of Python’s core ideas, captured in the original Zen of Python:2
There should be one — and preferably only one — obvious way to do it.
This principle tries to encourage Python programmers to write the simplest, most obvious code that gets the job done. If you and I are writing Python code to accomplish the same task, our code should look similar. This relates to the fact that code is read more often than it’s written. Code that’s more readable tends to be more maintainable as well.
All of this is nice, but it’s not always clear how to act on this advice when you’re new to Python. There are many areas of Python where there’s more than one way to do something. People often write to ask about these areas of Python:
How do I choose which approach to use when there’s more than one way to do something?
I’m going to focus this discussion on the most recent version of this question that someone asked, about how to choose among the various ways you can end a while
loop.
Why offer multiple ways to do something?
Whenever you see that there’s more than one way to do something in Python, it’s helpful to keep in mind that there’s almost always a good reason for this. In many of these situations, it won’t be obvious why there are multiple options available.
Some of the reasons for supporting multiple approaches include:
There are multiple use cases, of which your situation is only one.
More efficient approaches have been developed since the language was first written.
There’s a lot of code running around the world that still uses older approaches. (This is known as backward compatibility.)
It can be difficult to justify the importance of different approaches in introductory learning resources, because often times the use cases that differentiate the multiple approaches are too complex to write up in simple examples. Focusing on a simple structure like a while
loop is a nice way to look at the larger question of why multiple approaches exist.
None of this is specific to Python; this issue comes up in every language that sees wide adoption in a variety of different fields, over a period of decades. It’s the ultimate fate of any successful programming language.
Ending a while
loop: three different approaches
We’ll look at a simple example just to see the three ways you can end a while
loop. Then we’ll look at a slightly longer example which has a clear reason for preferring one specific approach.
Putting the condition in the while
statement
The simplest approach is to put the condition that ends the loop directly in the while
statement itself. Let’s write a short loop that prints the first ten square numbers:
# ten_squares.py
x = 1
while x < 11:
square = x**2
print(square)
x += 1
Here’s the output:
1
4
9
...
100
One advantage of this approach is that it’s really clear. You can read the while
statement itself, and see what condition will cause the loop to end.
Using the break
statement
We can also write an infinite loop, and place a break
statement inside the loop:
# ten_squares_break.py
x = 1
while True:
square = x**2
print(square)
x += 1
if x > 10:
break
The output is identical to the previous loop.
Whenever you see a loop that starts with while True
, look inside the loop for a condition that will cause the loop to end. Note that the if
block can be placed at the end of the loop as you see here, or at the beginning of the loop. This lets you control whether the loop always runs at least once, or checks the condition before running the first time.
Using a flag
We can also set a flag that controls when the loop stops running:
# ten_squares_flag.py
x = 1
active = True
while active:
square = x**2
print(square)
x += 1
if x > 10:
active = False
This is a perfect example of why simple examples can be good for showing syntax, but really bad at explaining why you’d use a certain approach. Functionally, this is just like the previous example using while True
. But, it’s more complex without any real purpose.
The main advantage of using a flag is that code anywhere in the project can set the flag. You won’t see that in really simple examples like this. So let’s look at an example that shows more clearly why this approach is available.
Using a flag across multiple methods
Let’s consider an example where multiple methods in a class are called from inside a while
loop, and any of those methods may need to cause the loop to stop running.
Imagine we’re writing code to control an assembly line that builds computers. In this simplified version of the assembly line, we’ll simulate three steps in building a computer: Add a hard drive, add some RAM, and add a GPU. However, we’ll throw in a little twist. All the steps are likely to succeed, but when one of them fails the whole assembly line must be stopped.
We’ll start with a method that checks the success of any step in the assembly process:
# assembly_line.py
"""Simulate a computer assembly line."""
from random import random
class AssemblyLine:
def __init__(self):
self.line_running = False
def check_success(self):
"""Randomly decide if a step was successful."""
if random() < 0.99:
return True
return False
We’re building a class called AssemblyLine
. The __init__()
method defines one attribute, self.line_running
. When an AssemblyLine
object is initialized, it starts off in an inactive state.
The method check_success()
uses the random()
function, which returns a number between 0 and 1. There’s a 99% chance that number will be less than 0.99, so this method will return True
99% of the time. Whenever a component is added to a computer on the assembly line, there’s a 1% chance the step will fail and we’ll need to halt the assembly line.
Now we’ll write three methods that simulate adding components to a computer. Each method will call check_success()
, and set the line_running
attribute to False
only if the step fails:
class AssemblyLine:
def __init__(self):
...
def check_success(self):
...
def add_hard_drive(self):
if self.check_success():
print(" Added hard drive.")
else:
print(" Failed to add hard drive.")
self.line_running = False
def add_ram(self):
if self.check_success():
print(" Added RAM.")
else:
print(" Failed to add RAM.")
self.line_running = False
def add_gpu(self):
if self.check_success():
print(" Added GPU.")
else:
print(" Failed to add GPU.")
self.line_running = False
Each of these methods prints a success message if they succeed. If they fail, they print a failure message and set the line_running
flag to False
.
Now let’s write a method to start the assembly line:
class AssemblyLine:
...
def start_line(self):
"""Start the line, and keep building
until something fails.
"""
self.line_running = True
num_started = 0
while self.line_running:
num_started += 1
print(f"Building computer number {num_started}:")
self.add_hard_drive()
self.add_ram()
self.add_gpu()
# If the loop ended, the line must have stopped.
print("\nThe line has stopped due to a failure.")
print(f"Built {num_started-1} computers.")
assembly_line = AssemblyLine()
assembly_line.start_line()
The method start_line()
changes the value of line_running
to True
. It also initializes a variable called num_started
, so we can track how many computers enter production.
The while
loop is controlled by the flag self.line_running
. Note that this flag is never modified in the body of the while
loop. It’s only set to False
when one of the individual steps fails.
When the loop has finished running, the method prints a message stating that the assembly line has been stopped, and shows how many computers were built before the stoppage:
$ python assembly_line.py
Building computer number 1:
Added hard drive.
Added RAM.
Added GPU.
Building computer number 2:
Added hard drive.
Added RAM.
Added GPU.
...
Building computer number 23:
Failed to add hard drive.
Added RAM.
Added GPU.
The line has stopped due to a failure.
Built 22 computers.
In this run, 22 computers were built before a failed hard drive installation stopped the line.
The code that runs an actual assembly line would of course be much more complicated, but the mechanism for managing control of the line could be implemented quite similar to what you see here. A single small loop delegates control out to individual steps, and any of those steps can hit the big button that stops the entire assembly line.
Flags in larger projects
Flags are often used in larger projects such as games. The codebase for games tends to span multiple files, and many things can happen that would cause the game to be over. Maybe the player loses all their ships, or their health runs out. Maybe they run out of time, or maybe someone else wins the game.
In a game project, you can have a single flag for the overall game’s state, such as game_active
. This one variable controls the main game loop. Anywhere in the code base, any block of code can set game_active
to False
and the game will end.
A slight variation is to have a loop that implements while True
, but the game_active
flag controls what is done inside the main loop. For example, here’s the main loop from the Alien Invasion game in Python Crash Course:
while True:
self._check_events()
if self.game_active:
self.ship.update()
self._update_bullets()
self._update_aliens()
self._update_screen()
self.clock.tick(60)
This makes sure the code that controls game play only runs when the game is active, but the code that checks for events such as starting a new game always runs.
Conclusion
Most mature programming languages offer a number of different ways to do simple things, because those simple things end up being used in a wide variety of projects of varying degrees of complexity. When you’re learning the syntax of a language, you might use more complex structures than necessary just to see how they work. When you’re working on real-world projects, use the simplest structures that let you solve the problems you care to solve.
Resources
You can find the code files from this post in the mostly_python GitHub repository.
There was much more to Perl’s popularity. For example it was an excellent language for processing text, which was an important feature in a web-focused language in the early days of the internet.
If you haven’t heard of the Zen of Python before, try running import this
in a Python terminal session.