Debugging in Python, part 5: Working through multiple bugs
MP 144: What happens when you fix a bug, only to find another bug right away?
Note: This post is part of an ongoing series about debugging in Python. The posts in this series will only be available to paid subscribers for the first 6 weeks. After that they will be available to everyone. Thank you to everyone who supports my ongoing work on Mostly Python.
We often think of debugging as fixing a bug, when in reality there's usually a number of bugs we have to work through before things start working again. In the previous examples in this series, fixing one bug brought the project to a working state again. In this post, we'll see what happens when fixing one bug leaves you with a project that's still broken.
Fixing a bug
We'll stick with the Dice Battle example one more time. I've introduced more than one bug in the project; here's what happens when we run it now:
$ python dice_battle.py File "dice_battle.py", line 18 for _ in range(num_battles): IndentationError: unexpected indent
There's an IndentationError
in the main file. Let's take a look at that section of code:
... # Simulate some battles between players A and B. num_battles = 10 results = {...} for _ in range(num_battles): a_result, b_result = utils.battle() print(f"\nPlayer A: {a_result}") print(f"Player B: {b_result}") ...
Here, the for
statement is indented along with the body of the loop. Let's unindent the first line of the loop:
... # Simulate some battles between players A and B. num_battles = 10 results = {...} for _ in range(num_battles): a_result, b_result = utils.battle() ...
That should fix the issue. Let's try running the program again:
$ python dice_battle.py Traceback (most recent call last): File "dice_battle.py", line 7, in <module> import utils File "utils.py", line 3, in <module> from die import Die File "die.py", line 1, in <module> import randaom ModuleNotFoundError: No module named 'randaom'
Oh no, now there's an even longer traceback! Did we mess something up?
It's quite possible that an attempted fix for one bug actually caused another problem. But it's also possible that fixing one bug revealed another issue in the project. In this case, there's no reason to think our fix caused a problem. The IndentationError
we saw previously is gone, and we know the first line of a for
loop shouldn't be indented along with the loop's body. Let's try to fix this new bug, and see what happens.
Fixing the second bug
We've looked at resolving the ModuleNotFoundError
previously. Here, there's a typo in the statement that imports the random
module, in die.py. Let's spell it correctly:
import random import os from dataclasses import dataclass ...
Now let's run the program again:
$ python dice_battle.py Traceback (most recent call last): File "dice_battle.py", line 19, in <module> a_result, b_result = utils.battle() ^^^^^^^^^^^^^^ File "utils.py", line 10, in battle a_result = die.roll() ^^^^^^^^^^ File "die.py", line 19, in roll return random.ranint(1, self.num_sides) ^^^^^^^^^^^^^ AttributeError: module 'random' has no attribute 'ranint'. Did you mean: 'randint'?
Oh no! There's another error! Again, it's good to ask ourselves if the changes we just made might have caused this error. Since randaom
isn't a module, and fixing that typo made the ModuleNotFoundError
go away, that's quite unlikely. Let's keep debugging.
Fixing the third bug
This is an AttributeError
, which shows up in line 19 of die.py. Recent versions of Python have gotten a lot better at identifying likely causes of errors in our projects, and its suggestion here is quite likely correct. Line 19 calls random.ranint()
, a function that doesn't exist. But there's a function random.randint()
, which might be what we meant to use.
Let's look at the relevant section of code:
class Die: ... def roll(self): """Roll the die.""" return random.ranint(1, self.num_sides)
This is the roll()
method. It's supposed to return a random integer between 1 and the number of sides the current Die
object has. The randint()
function does exactly that.
Let's fix that typo:
class Die: ... def roll(self): """Roll the die.""" return random.randint(1, self.num_sides)
And let's run the program again:
$ python dice_battle.py Player A: 6 Player B: 1 Player A won! ... Player A: 2 Player B: 2 Tie! Summary: Player A won 4 battles. Player B won 4 battles. There were 2 tied battles.
That's great! The program is working again!
Before celebrating, let's run the project's tests and see if there are any other issues that come up:
$ pytest ... tests/e2e_tests/test_basic_behavior.py . [100%] ========== 1 passed in 0.03s ==========
Everything looks good for this small project! In a real-world situation, we could move on to adding new features, or whatever the current focus of new work is.
Practicing with multiple bugs
I'm developing py-bugger as this series evolves, and the goal is to help people practice debugging in increasingly realistic ways. Previously, we've looked at usage like this:
$ py-bugger -e ModuleNotFoundError
That's good, but you'll know exactly what kind of error you'll have to debug if you run this command.
In the latest version of py-bugger (0.4.1), the -e
argument is optional. Running the py-bugger
command without -e
will introduce a random bug into your project. You can use the --num-bugs
argument, but at the moment that will introduce multiple bugs that all induce the same kind of exception.
If you want to introduce multiple random bugs, you can make successive py-bugger calls, without the -e
argument:
$ py-bugger Added bug. All requested bugs inserted. $ py-bugger --ignore-git-status Added bug. All requested bugs inserted. $ py-bugger --ignore-git-status Added bug. All requested bugs inserted.
Each time you call py-bugger, it will look for a clean Git status before introducing bugs. You can override that behavior with --ignore-git-status
.
Note: You may find that py-bugger crashes when run more than once against the same project. If py-bugger introduces a bug that makes the project unable to be parsed successfully, it may crash the next time it runs. There's currently an open issue about this; if you see any other crashing behavior please consider reporting it.
Conclusions
When we teach debugging, we often show the process of fixing one bug. It can then be discouraging to work on a real project, only to find that fixing one bug can often lead to the immediate appearance of another bug.
The good news is that fixing that first bug wasn't wasted work. Real-world debugging often requires persistence; if you're willing to keep addressing each new traceback that shows up, you'll probably be able to bring your projects back to a working state.
In the next post we'll see what happens when a bug appears in a project that includes third-party libraries.