Skip to main content

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.