Debugging and Logging

Chapter Outline

Chapter 6: Debugging and Logging

Debugging is an essential part of software development. Almost every piece of software that is written has errors or various kinds. It is the programmer's job to identify and fix pieces of code that make some software malfunction. This process is called debugging.

Most programming languages offer a debugger that allow the programmer to set breakpoints, to halt program execution at certain points within the program, and inspect the current state of the program variables. This allows the programmer to identify whether some variables may have incorrect values at certain point of the execution.

The debugger also allows the programmer to step though the code line by line, or step over, or out of certain sections of the code to ensure that the code execution order is correct. The Python Debugger is the pdb.

6.1 Debugging with pdb and IDE Tools

The Python Debugger, or pdb lets you pause program execution and step through code interactively.

import pdb

def debug_demo():
    a = 10
    b = 0
    pdb.set_trace()
    return a / b

debug_demo()

The program pauses after the set_trace() function is executed:

> /.../pdb_demo.py(7)debug_demo()
-> return a / b
(Pdb) 

Code stepping using PDB

  • n – next line
  • s – step into function
  • p var – print variable
  • q – quit debugger

IDE Debugging

Most modern IDEs (e.g., PyCharm, VS Code) natively support:

  • Breakpoints
  • Step over/into
  • Variable watching
  • Call stack inspection

Instead of running the python program you need to debug the program.

6.2 Logging in Python

Python’s built-in logging module provides a flexible framework for emitting log messages from your code. It's far superior to using print() statements for:

  • Debugging
  • Tracking errors
  • Auditing system events
  • Application monitoring in production

Basic Logging Example

import logging

logging.basicConfig(level=logging.INFO)
logging.info("App started")

Output:

INFO:root:App started

Log Levels and Usage

Level Use Case Example
DEBUG Internal state for developers
INFO Successful operations
WARNING Recoverable problems or deprecations
ERROR Serious problems that need attention
CRITICAL Application is unusable

Changing Output Format

logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(levelname)s - %(message)s'
)

Output:

2023-07-17 16:21:10,200 - INFO - App started

Writing Logs to a File

logging.basicConfig(
    filename='app.log',
    level=logging.DEBUG,
    format='%(asctime)s - %(levelname)s - %(message)s'
)

Logging in Functions

def divide(a, b):
    if b == 0:
        logging.error("Attempted to divide by zero")
        return None
    return a / b

divide(10, 0)

Logging Exceptions

try:
    1 / 0
except ZeroDivisionError:
    logging.exception("Something went wrong!")

Output:

2023-07-17 01:26:40,389 - ERROR - Something went wrong!
Traceback (most recent call last):
  File "/.../basic_logging.py", line 11, in <module>
    1 / 0
    ~~^~~
ZeroDivisionError: division by zero

6.3 Refactoring the Calculator: Fault-Tolerant Version

Here’s a refactored CLI calculator with logging and error handling.

calculator.py

import logging

logging.basicConfig(
    filename='calculator.log',
    level=logging.INFO,
    format='%(asctime)s - %(levelname)s - %(message)s'
)

def add(a, b): return a + b
def subtract(a, b): return a - b
def multiply(a, b): return a * b

def divide(a, b):
    if b == 0:
        logging.error("Division by zero attempted.")
        raise ValueError("Cannot divide by zero.")
    return a / b

def main():
    print("CLI Calculator (With Logging and Exception Handling)")
    try:
        operation = input("Choose operation (add/subtract/multiply/divide): ").strip().lower()
        num1 = float(input("Enter first number: "))
        num2 = float(input("Enter second number: "))

        if operation == "add":
            result = add(num1, num2)
        elif operation == "subtract":
            result = subtract(num1, num2)
        elif operation == "multiply":
            result = multiply(num1, num2)
        elif operation == "divide":
            result = divide(num1, num2)
        else:
            logging.warning(f"Invalid operation: {operation}")
            return

        logging.info(f"{operation}({num1}, {num2}) = {result}")
        print(f"Result: {result}")

    except ValueError as ve:
        logging.error(f"Value error: {ve}")
        print(f"Value error: {ve}")
    except Exception as e:
        logging.critical(f"Unexpected error: {e}", exc_info=True)
        print("An unexpected error occurred.")

if __name__ == "__main__":
    main()

Check your understanding

Test your knowledge of Debugging and Logging in Python