Debugging in Python, part 4: Bugs in multi-file projects

MP 143: How do you find a bug in a larger project?

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.


Debugging tends to be simpler when working in a single-file project, in part because the tracebacks are shorter. There's a small amount of information to work through when reading the traceback, and you can usually see where the issue is pretty quickly. You still have to do some thinking in order to figure out what's wrong with the code and how to fix it, but it doesn't usually take long to sort things out.

Most real-world projects, however, involve many .py files spread across multiple directories. In this post we'll look at a small project with a number of .py files, and see how a slightly longer traceback is structured. We'll continue to use py-bugger to introduce errors, so you can practice doing what you see here as much as you like.

Refactoring Dice Battle

In the last post we worked with a single-file project called dice_battle.py. That project had enough going on that it could be refactored into smaller parts. I won't walk through the entire refactoring process here, but I'll share a summary of the changes that were made:

  • Before making any changes, I added a single end-to-end test. This test only focuses on the project's external behavior; it doesn't rely on the internal implementation at all. This is a great way to start testing a new project. If the overall behavior of the project changes, the test will fail and we'll know it. But we're free to change the internal structure and implementation of the project without having to rewrite any tests. We also won't have to figure out if a bunch of unit tests failed because critical behavior in the project is wrong, or if they failed because we've changed how we're doing things internally.
  • Move the Die class to a separate file called die.py. This isolates the code specific to modeling a die, and it's a model that can grow if we want to model additional behavior related to dice.
  • Move some of the functionality to a utils.py file. This moves implementation code out of the project's main file, leaving that file much more readable. It also moves us closer to being able to write an effective set of unit tests. Unit tests become appropriate once we have smaller functions that carry out specific tasks.

The original single-file dice_battle.py was about 50 lines long. After refactoring, we're left with a much smaller main file that's easier to reason about:

This post is only available to paid subscribers at the moment. It will be available to everyone 6 weeks after posting. If you'd like to continue reading now, please support my work by signing up for a paid subscription.

Paid subscription options