Chapter 5 - Debugging (Python)
Here's a concise walkthrough of the main ideas in Chapter 5, each with a small example.
Raising exceptions
You can stop your program on purpose with raise when something is wrong, so that the error can be handled elsewhere.
Example: enforce a positive age.
def set_age(age):
if age <= 0:
raise Exception("Age must be positive.")
print("Age set to", age)
set_age(25) # OK
set_age(0) # Raises Exception
Assertions (sanity checks)
assert is used to state "this must be true here"; if it's false, Python raises AssertionError and the program should crash, revealing a programmer bug early.
Example: list must be sorted.
numbers = [1, 3, 5, 7]
assert numbers[0] <= numbers[-1], "List is not sorted!"
If you accidentally reversed it:
numbers = [7, 5, 3, 1]
assert numbers[0] <= numbers[-1], "List is not sorted!" # AssertionError
Logging: seeing what the program is doing
The logging module lets you record messages about what your program is doing (with timestamps and levels) instead of sprinkling print() everywhere.
Basic setup and a simple log:
import logging
logging.basicConfig(
level=logging.DEBUG,
format="%(asctime)s - %(levelname)s - %(message)s"
)
logging.debug("Start of program")
x = 5
logging.info(f"x is {x}")
logging.debug("End of program")
Using logging to debug code
By logging values inside loops or functions, you can see where logic goes wrong.
Example: factorial with a bug and logs:
import logging
logging.basicConfig(level=logging.DEBUG, format="%(levelname)s - %(message)s")
def factorial(n):
logging.debug(f"Start factorial({n})")
total = 1
for i in range(n + 1): # Bug: starts at 0
total *= i
logging.debug(f"i={i}, total={total}")
logging.debug(f"End factorial({n})")
return total
print(factorial(5)) # Shows 0 and logs reveal i starts at 0
Fix:
for i in range(1, n + 1):
...
Logging to a file
You can send log messages to a file instead of the screen using the filename argument to basicConfig.
Example:
import logging
logging.basicConfig(
filename="my_program.log",
level=logging.DEBUG,
format="%(asctime)s - %(levelname)s - %(message)s"
)
logging.info("Program started")
Why logging is better than print
Logging can be turned on/off or filtered by importance (DEBUG, INFO, WARNING, ERROR, CRITICAL) without editing each call, unlike print().
Example: only show errors and above.
import logging
logging.basicConfig(level=logging.ERROR, format="%(levelname)s - %(message)s")
logging.debug("Debug detail") # Hidden
logging.info("Some info") # Hidden
logging.error("Something broke") # Shown
Disabling logging
You can disable log output globally with logging.disable(level), often after development.
Example: turn all logging off:
import logging
logging.disable(logging.CRITICAL)
logging.error("This will not be shown")
logging.critical("Nor this")
Using a debugger (Mu example)
A debugger lets you run your program line by line, inspecting variables at each step, which is great for understanding what's really happening.
Simple buggy adder:
print("Enter first number:")
first = input() # '5'
print("Enter second number:")
second = input() # '3'
print("The sum is " + first + second) # "53", not 8
Stepping through in the debugger shows first and second are strings, so you know to convert them:
total = int(first) + int(second)
Breakpoints
A breakpoint is a marker on a line where the debugger should pause so you don't have to step through every line from the start.
Conceptual example:
total = 0
for i in range(1, 1001):
total += i
if i == 500:
print("Halfway")
# Breakpoint here to inspect total when i == 500
When run under the debugger, execution runs normally, pauses on the line with the breakpoint, and you can inspect i and total.
Overall idea of the chapter
The chapter's main message is: use exceptions, assertions, logging, and a debugger together to find bugs faster and earlier, instead of guessing from code alone.