Debugging#

Everyone knows that debugging is twice as hard as writing a program in the first place.

— Brian Kernighan

Slides/PDF#

Debugging with print()#

Logical errors often only surface dynamically and cannot be found by static code analysis using lint tools. Here you have to trace the actual program flow. This is called debugging. The simplest form is print debugging, where you flood the code with print() statements.

We’ll extend our division function with plenty of print() statements. For example, it’s common to print the input parameters, print errors and warnings, and also log the results.

def division(numerator, denominator):
    print(f"Debug: Eingabe Zaehler: {numerator}")
    print(f"Debug: Eingabe Nenner: {denominator}")
    if not isinstance(denominator, (int, float)):
        print(f"Error: Nenner nicht vom Datentyp `int` oder `float`, sondern vom Typ {type(denominator)}")
        raise ValueError(f"Nenner nicht vom Datentyp `int` oder `float`, sondern vom Typ {type(denominator)}")
    elif not isinstance(numerator, (int, float)):
        print(f"Error: Zaehler nicht vom Datentyp `int` oder `float`, sondern vom Typ {type(numerator)}")
        raise ValueError(f"Zaehler nicht vom Datentyp `int` oder `float`, sondern vom Typ {type(numerator)}")
    elif not denominator:
        print("Warning: Division durch 0")
        return None
    else:
        result = numerator / denominator
        print(f"Info: Das Ergebnis von {numerator}/{denominator} = {result}")
        return result

Now we can clearly understand exactly what happened, especially in the event of an error. For example, in the case of division by zero.

division(10, 0)
Debug: Eingabe Zaehler: 10
Debug: Eingabe Nenner: 0
Warning: Division durch 0

However, even when things are correct, we still have a lot of output. That can be very distracting, because you can miss real errors very quickly. For example, we perform ten divisions, one of which was a division by zero.

for denominator in range(-2, 8):
    division(10, denominator)
Debug: Eingabe Zaehler: 10
Debug: Eingabe Nenner: -2
Info: Das Ergebnis von 10/-2 = -5.0
Debug: Eingabe Zaehler: 10
Debug: Eingabe Nenner: -1
Info: Das Ergebnis von 10/-1 = -10.0
Debug: Eingabe Zaehler: 10
Debug: Eingabe Nenner: 0
Warning: Division durch 0
Debug: Eingabe Zaehler: 10
Debug: Eingabe Nenner: 1
Info: Das Ergebnis von 10/1 = 10.0
Debug: Eingabe Zaehler: 10
Debug: Eingabe Nenner: 2
Info: Das Ergebnis von 10/2 = 5.0
Debug: Eingabe Zaehler: 10
Debug: Eingabe Nenner: 3
Info: Das Ergebnis von 10/3 = 3.3333333333333335
Debug: Eingabe Zaehler: 10
Debug: Eingabe Nenner: 4
Info: Das Ergebnis von 10/4 = 2.5
Debug: Eingabe Zaehler: 10
Debug: Eingabe Nenner: 5
Info: Das Ergebnis von 10/5 = 2.0
Debug: Eingabe Zaehler: 10
Debug: Eingabe Nenner: 6
Info: Das Ergebnis von 10/6 = 1.6666666666666667
Debug: Eingabe Zaehler: 10
Debug: Eingabe Nenner: 7
Info: Das Ergebnis von 10/7 = 1.4285714285714286

Debugging with logging#

Therefore, for more complex programs, one typically uses a logging package. These allow print statements to be assigned to categories and filtered by them. In the Python library logging, the categories are: debug, info, warning, error, and critical.

import logging

log = logging.getLogger("meinlog")

def division(numerator, denominator):
    log.debug(f"Eingabe Zaehler: {numerator}")
    log.debug(f"Eingabe Nenner: {denominator}")
    if not isinstance(denominator, (int, float)):
        log.error(f"Nenner nicht vom Datentyp `int` oder `float`, sondern vom Typ {type(denominator)}")
        raise ValueError(f"Nenner nicht vom Datentyp `int` oder `float`, sondern vom Typ {type(denominator)}")
    elif not isinstance(numerator, (int, float)):
        log.error(f"Zaehler nicht vom Datentyp `int` oder `float`, sondern vom Typ {type(numerator)}")
        raise ValueError(f"Zaehler nicht vom Datentyp `int` oder `float`, sondern vom Typ {type(numerator)}")
    elif not denominator:
        log.warning("Division durch 0")
        return None
    else:
        result = numerator / denominator
        log.info(f"Das Ergebnis von {numerator}/{denominator} = {result}")
        return result

If we call the function now, we only see the division-by-zero warning.

log.setLevel(logging.WARNING)
for denominator in range(-2, 8):
    division(10, denominator)
Division durch 0

We can, if needed, raise the log level, as we do during troubleshooting. For example, we want to receive all debug messages.

log.setLevel(logging.DEBUG)
for denominator in range(-2, 8):
    division(10, denominator)
Division durch 0

Moreover, logging can automatically include additional information. We can already see in the log above that it’s not only the level (INFO, DEBUG, WARNING) but also the name of the logger (meinlog). We can customize this format to, for example, also output the timestamp, which is especially important for understanding when something happened.

sh = logging.StreamHandler()
formatter = logging.Formatter('%(asctime)s  %(name)s  %(levelname)s: %(message)s')
sh.setFormatter(formatter)
log.addHandler(sh)
log.setLevel(logging.INFO)

for denominator in range(-2, 8):
    division(10, denominator)
2026-01-22 14:46:53,965  meinlog  INFO: Das Ergebnis von 10/-2 = -5.0
2026-01-22 14:46:53,965  meinlog  INFO: Das Ergebnis von 10/-1 = -10.0
2026-01-22 14:46:53,966  meinlog  WARNING: Division durch 0
2026-01-22 14:46:53,966  meinlog  INFO: Das Ergebnis von 10/1 = 10.0
2026-01-22 14:46:53,966  meinlog  INFO: Das Ergebnis von 10/2 = 5.0
2026-01-22 14:46:53,966  meinlog  INFO: Das Ergebnis von 10/3 = 3.3333333333333335
2026-01-22 14:46:53,966  meinlog  INFO: Das Ergebnis von 10/4 = 2.5
2026-01-22 14:46:53,966  meinlog  INFO: Das Ergebnis von 10/5 = 2.0
2026-01-22 14:46:53,966  meinlog  INFO: Das Ergebnis von 10/6 = 1.6666666666666667
2026-01-22 14:46:53,967  meinlog  INFO: Das Ergebnis von 10/7 = 1.4285714285714286

In practice, logging is widely used, especially in cloud applications. Since they don’t have screens, errors must be searched for in the logs. As long as everything is fine, an application runs, for example, at the INFO log level, with only a small amount of output. When an error occurs, the server is set to the DEBUG log level, and one then looks in the detailed logs for information to narrow down the error.

Debugging via Debug Interfaces#

Many integrated development environments (IDEs) offer to run the debugger directly. These debuggers allow the dynamic execution of the code to be interrupted. The goal is to pause execution just before the error occurs, in order to observe the error behavior closely.

There are typically two forms of interruption supported:

  • Pausing at specific lines of code with the help of breakpoints.

  • Pausing on specific exceptions.

Debugging Jupyter Notebooks in VS Code#

The debugging interfaces look somewhat different depending on the IDE, but they offer similar features. This Jupyter Notebook was written in VSCode, which we will treat as the first example.

In most IDEs you can usually click to the left of a line to set a breakpoint . We will set a breakpoint on line 11 for the warning output.

Then the code is executed in a special debugging environment that allows you to interrupt the execution. In our notebook in VSCode, this is started by the symbol lectures/images/debug_vscode_2.png.

def division(numerator, denominator):
    log.debug(f"Eingabe Zaehler: {numerator}")
    log.debug(f"Eingabe Nenner: {denominator}")
    if not isinstance(denominator, (int, float)):
        log.error(f"Nenner nicht vom Datentyp `int` oder `float`, sondern vom Typ {type(denominator)}")
        raise ValueError(f"Nenner nicht vom Datentyp `int` oder `float`, sondern vom Typ {type(denominator)}")
    elif not isinstance(numerator, (int, float)):
        log.error(f"Zaehler nicht vom Datentyp `int` oder `float`, sondern vom Typ {type(numerator)}")
        raise ValueError(f"Zaehler nicht vom Datentyp `int` oder `float`, sondern vom Typ {type(numerator)}")
    elif not denominator:
        log.warning("Division durch 0")
        return None
    else:
        result = numerator / denominator
        log.info(f"Das Ergebnis von {numerator}/{denominator} = {result}")
        return result

division(10, 0)
2026-01-22 14:46:53,970  meinlog  WARNING: Division durch 0

This starts the debugging mode. In this mode, the current line is highlighted and the current variables in memory are displayed.

Debugging in VS Code

In the debugging environment you can then step through it line by line by pressing the lectures/images/debug_vscode_2.png, and thereby trace how the program is executed and which variables change.

Quiz#

--- shuffleQuestions: true shuffleAnswers: true --- ### What is debugging? - [x] Finding and fixing bugs in the program code - [ ] The optimization of the program code - [ ] Writing documentation - [ ] Formatting source code ### Why isn't static code analysis alone sufficient? - [x] Because logical errors can occur at runtime - [ ] Because it doesn't detect security vulnerabilities - [ ] Because it makes the code slower - [ ] Because it doesn't work with Python ### What is `print` debugging? - [x] Inserting `print()` statements for runtime analysis - [ ] Removing all `print()` statements - [ ] An automated debugging method - [ ] A graphical debugger ### What is the purpose of `logging` in Python? - [x] Logging events and errors - [ ] Optimizing program execution - [ ] Creating user interfaces - [ ] Automatically testing functions ### Sort the following lines to correctly construct a complete `try-except` block 1. `def division(zaehler, nenner):` 2. ` print(f"Debug: Eingabe Zaehler: {zaehler}")` 3. ` print(f"Debug: Eingabe Nenner: {nenner}")` 4. ` if not isinstance(nenner, (int, float)):` 5. ` print(f"Error: Nenner nicht vom Datentyp `int` oder `float`, sondern vom Typ {type(nenner)}")` 6. ` raise ValueError(f"Nenner nicht vom Datentyp `int` oder `float`, sondern vom Typ {type(nenner)}")` 7. ` elif not isinstance(zaehler, (int, float)):` 8. ` print(f"Error: Zaehler nicht vom Datentyp `int` oder `float`, sondern vom Typ {type(zaehler)}")` 9. ` raise ValueError(f"Zaehler nicht vom Datentyp `int` oder `float`, sondern vom Typ {type(zaehler)}")` 10. ` elif not nenner:` 11. ` print("Warning: Division durch 0")` 12. ` return None` 13. ` else:` 14. ` ergebnis = zaehler / nenner` 15. ` print(f"Info: Das Ergebnis von {zaehler}/{nenner} = {ergebnis}")` 16. ` return ergebnis` ### What happens with a division by zero in the following code? ```python def division(zaehler, nenner): print(f"Debug: Eingabe Zaehler: {zaehler}") print(f"Debug: Eingabe Nenner: {nenner}") if not isinstance(nenner, (int, float)): ... elif not nenner: print("Warning: Division durch 0") return None ``` - [x] A warning is printed and `None` is returned. - [ ] A `ZeroDivisionError` is raised. - [ ] 0 is returned. - [ ] The denominator is automatically set to 1.