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
.
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.

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