CodingNic

Debugging, Testing, and Modules

Debugging Python

Debugging, Testing, and Modules 20 min read

Debugging Python

Objectives

By the end of this chapter, you should be able to:

  • Identify and understand common Python errors
  • Catch errors with try and except
  • Set breakpoints in your code
  • Use the logging module instead of scattered print() calls

๐Ÿ’ก Why this matters: Every program you write will eventually break. Knowing what each error actually means turns a wall of red text into information you can act on immediately.

Common Built-In Errors

Error Happens when Example
NameError A variable isn’t defined test โ†’ NameError: name 'test' is not defined
KeyError A dictionary doesn’t have the key you asked for {}["foo"] โ†’ KeyError: 'foo'
AttributeError A value doesn’t have the attribute or method you called "awesome".foo โ†’ AttributeError: 'str' object has no attribute 'foo'
IndexError A list index is out of range ["hello"][2] โ†’ IndexError: list index out of range
ValueError The type is right, but the value isn’t valid int("foo") โ†’ ValueError: invalid literal for int() with base 10: 'foo'
TypeError Python can’t combine two incompatible types "awesome" + [] โ†’ TypeError: can only concatenate str (not "list") to str
ZeroDivisionError You divide a number by zero 5 / 0 โ†’ ZeroDivisionError: division by zero

Raising Your Own Errors

The raise keyword lets you throw an error deliberately. This is useful when you’re writing your own validation, so bad input fails loudly instead of causing confusing problems later:

python
def set_age(age):
    if age < 0:
        raise ValueError("age cannot be negative")
    return age

set_age(-5)
text
Traceback (most recent call last):
  ...
ValueError: age cannot be negative

Catching Errors with try/except

Wrap code that might fail in a try block, and handle the specific error in except:

python
try:
    foobar
except NameError as err:
    print(err)

You could catch every possible error with a bare except:, but avoid this:

python
try:
    nice + []
except:
    print("Something went wrong!")

The problem: catching everything means you can never tell what actually went wrong. Always except a specific error type, or a tuple of a few, if more than one is possible:

python
try:
    # do some stuff
except (NameError, ValueError) as e:
    # do some other stuff

Running Cleanup Code with finally

Sometimes you need a block of code to run no matter what, whether the try block succeeds or raises an error. finally guarantees that:

python
try:
    file = open("data.txt")
    risky_operation(file)
except FileNotFoundError:
    print("Couldn't find that file.")
finally:
    print("This always runs, error or not.")

finally is especially useful for cleanup, like closing a file or a network connection, that has to happen either way. In practice, a with block (from the file I/O module) handles this automatically for files, but finally is the general-purpose tool for any cleanup that isn’t file-specific.

Setting Breakpoints

To pause execution and inspect your code interactively, drop this line in:

python
import pdb; pdb.set_trace()

Since Python 3.7, the built-in breakpoint() function does the same thing without an import:

python
breakpoint()

Inside the debugger prompt ((Pdb)), a few commands cover most of what you need: n runs the next line, c continues execution until the next breakpoint (or the end), and q quits the debugger entirely.

Beyond print(): the logging Module

print() is fine for quick checks, but it doesn’t scale: you can’t easily turn it off, filter by severity, or tell where in your program a message came from. The built-in logging module solves all three:

python
import logging

logging.basicConfig(level=logging.INFO)

logging.debug("Detailed info, useful while diagnosing a problem")
logging.info("Confirmation that things are working as expected")
logging.warning("Something unexpected happened, but the program is still working")
logging.error("A more serious problem: some functionality has failed")
logging.critical("A serious error: the program itself may not be able to continue")

Each function logs at a different level, in increasing severity: DEBUG, INFO, WARNING, ERROR, CRITICAL. basicConfig(level=logging.INFO) sets the minimum level that actually gets shown. Here, debug() calls are silently skipped, while info() and everything more severe are printed:

text
INFO:root:Confirmation that things are working as expected
WARNING:root:Something unexpected happened, but the program is still working
ERROR:root:A more serious problem: some functionality has failed
CRITICAL:root:A serious error: the program itself may not be able to continue

Change the level once (say, to WARNING) and every debug()/info() call across your whole program goes quiet without deleting a single line of code. A scattered pile of print() statements can’t do that.

Try It

  1. Trigger three different errors on purpose (an undefined variable, a missing dictionary key, an out-of-range list index) and read each traceback.
  2. Wrap one of them in a try/except that catches only that specific error and prints a friendlier message instead.
  3. Add a finally block to that same try/except and confirm it runs whether or not the error actually happens.
  4. Drop a breakpoint() into a short script and step through it, using c to continue.
  5. Replace a few print() calls in a short script with logging calls at different levels, then change basicConfig’s level and see which messages disappear.

Recap

  • Different errors point at different problems: NameError (undefined name), KeyError (missing key), AttributeError (missing attribute), IndexError (bad index), ValueError (right type, wrong value), TypeError (incompatible types), ZeroDivisionError (division by zero).
  • try/except catches specific errors so you can respond to them; a bare except: hides what actually went wrong, so avoid it.
  • finally runs no matter what, success or error, which makes it the right place for cleanup code.
  • breakpoint() (or import pdb; pdb.set_trace()) pauses execution so you can inspect your code as it runs.
  • logging gives you severity levels (DEBUG through CRITICAL) and a single place to control what gets shown: a real upgrade over scattered print() calls.

Next lesson: organizing code into modules, and importing them.