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.