Intermediate

Debugging in Python

The main goal is to familiarize yourself with three distinct tools for debugging Python applications, tools that won't guarantee you error-free code the first time, but will help you get...

Table of Contents

  1. Course Overview
  2. Command-line debug with pdb
  1. Advanced debugging with pdb and its allies
  1. Debugging with Visual Studio Code
  1. Advanced Debugging with Visual Studio Code
  1. Use PyCharm to debug Python

1. Course Overview

This course, titled Debugging in Python 3, is taught by Douglas Starnes, independent entrepreneur, author and speaker. The main goal is to familiarize yourself with three distinct tools for debugging Python applications, tools that won’t guarantee you error-free code the first time, but will help you get closer to it faster and more efficiently.

Topics covered in this course

The main themes covered are as follows:

  • pdb: Python’s built-in debugger, used at the command line. It works on the same principle as Git — you interact with it by typing commands.
  • Visual Studio Code with Python extension: a graphical interface for debugging Python applications, offering functionality impossible or very difficult to achieve with pdb.
  • PyCharm: A full-featured Python IDE from JetBrains, popular for debugging complex data structures and connecting to remote projects.
  • Add-on Utilities: Third-party tools like Pylint, ipdb, Black and isort that help reduce debugging time or improve code quality before even running the application.

Prerequisites

Before starting this course, you should be comfortable with:

  • The basics of Python 3 (syntax, functions, classes, modules)
  • The fundamental debugging workflow (understanding what a breakpoint, stack trace, etc. is)

What you will be able to do after this course

At the end of this course, you will master three different tools for debugging Python code, and you will be able to choose the most suitable according to your context (command line, IDE, remote environment, notebook, etc.).


2. Debugging at the command line with pdb

Module duration: 25m 43s

2.1 Before debugging with pdb

What is pdb?

pdb (Python Debugger) is a command-line debugger included in the Python standard library. This is a huge advantage: you don’t have to install anything, pdb is available wherever Python is installed. As it is a module, you must import it before using it.

Interacting with pdb is done in the same way as Git commands: you type textual commands to control the debugging session. At first glance this may seem cumbersome, but pdb offers exactly the same functionality as graphical debuggers in other IDEs.

Start debugger: pdb.set_trace()

The typical way to start pdb is to import the module and then call the set_trace() function at the place in the code where you want to start debugging:

import pdb; pdb.set_trace()

We often see the import and the call to set_trace() on the same line. This is perfectly legal in Python — multiple statements can be separated by a semicolon. PEP-8 discourages this practice in general, but for debugging it is commonly accepted because debug code is temporary anyway.

Here’s how this fits into a source file:

def get_investment_info(investment):
    import pdb; pdb.set_trace()
    price = get_current_price(investment["coin"])
    value = investment["quantity"] * price
    return value

The breakpoint() function (Python 3.7+)

Since Python 3.7, a new built-in function has been introduced:

def get_investment_info(investment):
    breakpoint()
    price = get_current_price(investment["coin"])
    value = investment["quantity"] * price
    return value

breakpoint() is equivalent to import pdb; pdb.set_trace() — it starts the debugger without requiring an explicit import. It seems cleaner, but it gives a false impression: it suggests that a breakpoint is placed there, which is not the case. For this reason, and because it is only available from Python 3.7, the course prefers to use set_trace().

Launch application with debugger

No special interpreter options are required. You simply run your file normally:

python app.py

When the interpreter reaches the call to set_trace(), pdb takes control and prints something like:

> /home/user/projet/app.py(35)get_investment_info()
-> price = get_current_price(investment["coin"])
(Pdb)
  • The first line indicates the file path, line number, and function in which execution is suspended.
  • The second line (preceded by ->) shows the code that will be executed in the next step.
  • (Pdb) is the prompt that waits for your commands.

2.2 Start a debug session

The demo application

The application used in the demos is deliberately simple: a cryptocurrency portfolio manager. The entry point creates a portfolio (dictionary list), loops through it and displays information about each investment. Prices are determined by the get_price() function:

# app.py

def get_current_price(coin):
    print(f"Getting price for {coin}")
    if coin == "bitcoin":
        return get_bitcoin_price()
    elif coin == "ethereum":
        return get_ethereum_price()
    elif coin == "solana":
        return get_solana_price()

def get_investment_info(investment):
    import pdb; pdb.set_trace()
    price = get_current_price(investment["coin"])
    total = investment["quantity"] * price
    print(f"{investment['coin']}: {investment['quantity']} @ ${price} = ${total:.2f}")

def main():
    portfolio = [
        {"coin": "bitcoin",  "quantity": 0.5},
        {"coin": "ethereum", "quantity": 5},
        {"coin": "solana",   "quantity": 50},
    ]
    for investment in portfolio:
        get_investment_info(investment)

if __name__ == "__main__":
    main()

2.3 Variable inspection commands

Once pdb has taken control, you can inspect the variable values ​​with the following commands:

OrderAliasesDescription
p <expr>Shows the value of the expression
pp <expr>Displays the value in pretty-print format (readable)
whatis <expr>Shows the variable type

Usage examples

(Pdb) p investment
{'coin': 'bitcoin', 'quantity': 0.5}

(Pdb) pp investment
{'coin': 'bitcoin', 'quantity': 0.5}

(Pdb) whatis investment
<class 'dict'>

(Pdb) p investment["coin"]
'bitcoin'

(Pdb) p investment["quantity"]
0.5

For simple structures, p and pp give the same result. For complex nested structures (list dictionaries, objects, etc.), pp produces much more readable formatting.


2.4 Execution control commands

These commands allow you to navigate the code during a pdb session:

OrderAliasesDescription
nextnExecute the current line and move on to the next one (without entering the called functions)
stepsExecutes the current line and enters the function if it is a call
continuecResumes execution until next breakpoint
returnrContinue until the return of the current function
quitqExits the debugger and ends execution

Difference between next and step

  • next (n): equivalent of “Step Over” in graphics IDEs. If the current line contains a function call, pdb executes this call in full and moves to the next line.
  • step (s): equivalent of “Step Into”. If the current line is a function call, pdb enters this function and positions itself on its first line.
(Pdb) n
> /home/user/projet/app.py(36)get_investment_info()
-> total = investment["quantity"] * price

(Pdb) s
--Call--
> /home/user/projet/app.py(5)get_current_price()
-> def get_current_price(coin):

2.5 Breakpoint management

The break command (aka b)

The break command has several functions depending on the arguments provided:

UseDescription
breakList all active breakpoints
break <line_number>Place a breakpoint on this line in the current file
break <function_name>Places a breakpoint at the entry of a function
break <file>:<line>Place a breakpoint in another file
break <line>, <condition>Sets a conditional breakpoint (see next section)

Examples

(Pdb) break 30
Breakpoint 1 at /home/user/projet/app.py:30

(Pdb) break
Num Type         Disp Enb   Where
1   breakpoint   keep yes   at /home/user/projet/app.py:30

(Pdb) break get_current_price
Breakpoint 2 at /home/user/projet/app.py:5

In the list of breakpoints, the columns indicate:

  • Num: the breakpoint identifier
  • Type: breakpoint (permanent) or tbreak (temporary, deleted after the first pass)
  • Available: keep (permanent) or del (temporary)
  • Enb: yes (enabled) or no (disabled)
  • Where: the file and the line

The clear command (aka cl)

The clear command deletes breakpoints:

UseDescription
clearRemove all breakpoints (requires confirmation)
clear <number>Delete the breakpoint with this identifier
clear <line>Remove the breakpoint on this line
(Pdb) clear 1
Deleted breakpoint 1 at /home/user/projet/app.py:30

(Pdb) clear
Clear all breaks? yes

Enable and disable breakpoints

Rather than deleting a breakpoint, you can temporarily disable it with disable and re-enable it with enable:

(Pdb) disable 2
Disabled breakpoint 2 at /home/user/projet/app.py:5

(Pdb) enable 2
Enabled breakpoint 2 at /home/user/projet/app.py:5

When a breakpoint is deactivated, execution no longer stops there but it remains in the list — you do not have to recreate it.

Temporary breakpoints: tbreak

The tbreak command creates a breakpoint that automatically deletes itself after being reached once:

(Pdb) tbreak 20
Breakpoint 3 at /home/user/projet/app.py:20
(Pdb) c
> /home/user/projet/app.py(20)get_current_price()
-> return 45000.0
Deleted breakpoint 3

2.6 Conditional breakpoints

A conditional breakpoint only suspends execution if a Python condition evaluates to True. This is very useful in loops or functions called many times: you only interrupt execution when a particular condition is met.

(Pdb) break 20, investment["coin"] == "ethereum"
Breakpoint 1 at /home/user/projet/app.py:20

(Pdb) break
Num Type         Disp Enb   Where
1   breakpoint   keep yes   at /home/user/projet/app.py:20
	stop only if investment["coin"] == "ethereum"

With this conditional breakpoint, execution only pauses when investment["coin"] is "ethereum". For "bitcoin" and "solana", the breakpoint is ignored.

You can also add a condition to an existing breakpoint with the condition command:

(Pdb) condition 1 investment["coin"] == "ethereum"
New condition set for breakpoint 1.

2.7 Evaluate Python expressions in pdb

The ! command (or simply type a Python expression) allows you to evaluate arbitrary Python code in the current application context. You can thus modify the state of the application during debugging:

(Pdb) !investment["coin"] = "solana"
(Pdb) p investment
{'coin': 'solana', 'quantity': 0.5}

This functionality is extremely powerful: it allows you to test corrections without restarting the application, to modify values ​​to simulate edge cases, or to test complex expressions in a real context.

(Pdb) !price = 99999.99
(Pdb) p price
99999.99

Warning: Changes to application state during debugging are real — they affect program execution.


2.8 Module summary

In this module, you learned to:

  • Prepare a Python application for debugging with pdb by adding a call to pdb.set_trace() or using breakpoint() (Python 3.7+)
  • Start the Python interpreter normally to start a debugging session
  • Inspect simple and complex variables with p, pp and whatis commands
  • Control execution with next, step, continue, return and quit
  • Manage breakpoints: set them, list them, delete them, activate/deactivate them, and create temporary ones
  • Create conditional breakpoints to fine-tune debug control
  • Evaluate Python expressions to change the state of the running application

3. Advanced debugging with pdb and its allies

Module duration: 26m 3s

3.1 Source code navigation commands

In this module, you will delve deeper into the pdb commands to navigate the code more precisely, in particular by entering nested functions and managing the stack trace.

The step command (aka s) — Step Into

The step command executes the current line. If this line contains a function call, pdb enters this function and positions itself on its first line, whether there is a breakpoint or not. This is the equivalent of “Step Into” in graphics debuggers.

(Pdb) s
--Call--
> /home/user/projet/app.py(5)get_current_price()
-> def get_current_price(coin):

The up (aka u) command — Step Out

When you have entered a function with step, you can move up one level in the stack trace with up. This is similar to “Step Out”, but with an important nuance: the function frame is not deleted — you simply move the cursor up in the stack trace. You can go back down if necessary.

(Pdb) up
> /home/user/projet/app.py(35)get_investment_info()
-> price = get_current_price(investment["coin"])

The down command (aka d) — Go back down the stack

Symmetrical with up, the down command goes down one level in the stack trace towards more recent frames.

(Pdb) down
> /home/user/projet/app.py(5)get_current_price()
-> def get_current_price(coin):

up and down accept a numeric argument to move multiple levels in a single command:

(Pdb) up 2     # remonte de 2 niveaux
(Pdb) down 3   # descend de 3 niveaux

3.2 Understanding and navigating the stack trace

The where command (aka w, bt)

The where command displays the complete stack trace:

(Pdb) where
  /home/user/projet/app.py(47)<module>()
-> get_investment_info(investment)
  /home/user/projet/app.py(35)get_investment_info()
-> price = get_current_price(investment["coin"])
  /home/user/projet/app.py(5)get_current_price()
-> if coin == "bitcoin":
> /home/user/projet/app.py(10)get_bitcoin_price()
-> return 45000.0

Reading the stack trace:

  • Frames are displayed oldest to newest (top to bottom)
  • Most recent (bottom) frame is usually the current frame
  • The chevron > indicates the current frame (the one you are in)
  • The thin arrow -> indicates the line on which this frame is suspended

In the example above, there are 4 frames:

  1. The main module (<module>) hanging on line 47
  2. get_investment_info hanging on line 35
  3. get_current_price hanging on line 5
  4. get_bitcoin_price (current frame >) hanging on line 10

After an up, the chevron moves to the middle frame:

(Pdb) up
> /home/user/projet/app.py(5)get_current_price()
-> if coin == "bitcoin":
(Pdb) where
  /home/user/projet/app.py(47)<module>()
-> get_investment_info(investment)
  /home/user/projet/app.py(35)get_investment_info()
-> price = get_current_price(investment["coin"])
> /home/user/projet/app.py(5)get_current_price()
-> if coin == "bitcoin":
  /home/user/projet/app.py(10)get_bitcoin_price()
-> return 45000.0

Note that the same 4 frames are present because get_bitcoin_price has not returned yet. When a function returns, its frame is removed from the stack trace.


3.3 Commands for displaying source code

The list command (aka l)

The list command displays the source code around the current line:

(Pdb) l
  8     def get_bitcoin_price():
  9         return 45000.0
 10  ->     return 45000.0
 11
 12     def get_ethereum_price():
 13         return 3000.0

By default, list displays 11 lines centered on the current line. You can pass arguments:

(Pdb) l 1, 20    # affiche les lignes 1 à 20
(Pdb) l .        # affiche la ligne courante et ses environs

The longlist command (aka ll)

longlist displays the entire current function (or module if you are at module level):

(Pdb) ll
  5     def get_current_price(coin):
  6         print(f"Getting price for {coin}")
  7         if coin == "bitcoin":
  8             return get_bitcoin_price()
  9         elif coin == "ethereum":
 10             return get_ethereum_price()
 11         elif coin == "solana":
 12  ->         return get_solana_price()

3.4 Third-party tools: Pylint

Presentation of Pylint

Pylint is a static Python code analyzer. It analyzes your code without executing it and identifies:

  • Code errors (unused imports, undefined variables, etc.)
  • Convention issues (non-compliance with PEP-8, incorrect naming, etc.)
  • Refactoring opportunities (redundant code, unnecessary return in elif, etc.)
  • Warnings (variable redefinition, bad practices, etc.)

In addition to analysis, Pylint can generate class diagrams and offers an optional spell checker. Plugins are available for popular frameworks like Django and Pydantic.

Installation and use

pip install pylint
pylint app.py

Pylint output example

************* Module app
app.py:1:0: C0304: Final newline missing (missing-final-newline)
app.py:8:4: C0103: Function name "get_Bitcoin_price" doesn't conform to snake_case naming style (invalid-name)
app.py:18:4: R1705: Unnecessary "elif" after "return", remove the leading "el" from "elif" (no-else-return)
app.py:30:4: W0621: Redefining name 'investment' from outer scope (line 45) (redefined-outer-name)
app.py:35:4: W0104: Statement seems to have no effect (pointless-statement)

-----------------------------------
Your code has been rated at 5.94/10

Message prefixes indicate their category:

  • C (Convention): style issues (PEP-8, naming, etc.)
  • R (Refactor): code that can be simplified
  • W (Warning): warnings about potential bugs
  • E (Error): probable errors
  • F (Fatal): errors preventing Pylint from analyzing the file further

Step-by-step correction with Pylint

C0304 — Final newline missing: Pylint recommends that the last line of a file be a blank line. Just add a blank line at the end of the file.

C0103 — Invalid function name: The function get_Bitcoin_price should be renamed to get_bitcoin_price (snake_case). Consider updating all places where it is called.

R1705 — Unnecessary elif after return: The following code contains unnecessary elifs:

# Avant (avec elif inutiles)
def get_current_price(coin):
    if coin == "bitcoin":
        return get_bitcoin_price()
    elif coin == "ethereum":
        return get_ethereum_price()
    elif coin == "solana":
        return get_solana_price()
    else:
        return 0.0
# Après (sans elif inutiles, plus propre)
def get_current_price(coin):
    if coin == "bitcoin":
        return get_bitcoin_price()
    if coin == "ethereum":
        return get_ethereum_price()
    if coin == "solana":
        return get_solana_price()
    return 0.0

W0621 — Redefined outer name: If a local variable in a function has the same name as a variable in the outer scope, Pylint reports this. The solution is to rename the local variable:

# Avant
def get_investment_info(investment):  # paramètre "investment"
    for investment in portfolio:      # réutilise "investment" — confus!
        ...

# Après
def get_investment_info(investment):
    for inv in portfolio:             # nom différent
        ...

After these corrections, the Pylint rating can go from 5.94 to 7+ out of 10.


3.5 Third-party tools: ipdb

Presentation of IPython and ipdb

IPython is an improved Python interpreter that adds features like code completion, automatic indentation, syntax highlighting, and better presentation of results. This is the basis of the Jupyter Notebook project.

ipdb brings all IPython improvements to the pdb debugger. The command interface remains the same — you use the same commands (n, s, c, b, etc.) — but the experience is enhanced by syntax highlighting and code completion.

Installation and use

pip install ipdb

To use ipdb instead of pdb, simply override the import:

# Remplacer :
import pdb; pdb.set_trace()

# Par :
import ipdb; ipdb.set_trace()

Advantages of ipdb over pdb

  • Syntactic coloring: the displayed code is colored, which improves readability
  • Autocompletion: Tab-completion in debug session
  • Better presentation of tracebacks: errors are easier to read
  • Integration with IPython: access to IPython magic commands if necessary

Use combined with the IPython REPL

IPython offers an enriched REPL accessible from the terminal:

pip install ipython
ipython
In [1]: def add(a, b):
   ...:     return a + b
   ...:

In [2]: add(1, 2)
Out[2]: 3

In [3]: import ipdb; ipdb.set_trace()

3.6 Other useful utilities

The course mentions several other tools that reduce the risk of bugs and debugging time:

ToolRole
BlackAutomatic code formatter that enforces PEP-8 style rules
isortOrganizes and sorts imports while respecting conventions
mypyChecks type annotations before execution (static type checking)

These tools can be integrated into editors (VS Code, PyCharm) or into CI/CD pipelines.

pip install black isort mypy

# Formater un fichier avec Black :
black app.py

# Trier les imports avec isort :
isort app.py

# Vérifier les types avec mypy :
mypy app.py

3.7 Module Summary

In this module, you learned to:

  • Enter functions with step and navigate the stack trace with up and down
  • Show and read full stack trace with where / w / bt
  • Show source code with list and longlist
  • Use Pylint to statically analyze code and improve its quality before even running it
  • Use ipdb to get a rich command line debugging experience (syntax highlighting, completion)
  • Know other useful tools: Black (formatting), isort (imports), mypy (type checking)

4. Debugging with Visual Studio Code

Module duration: 28m 42s

4.1 Configuring VS Code for Python

Visual Studio Code (VS Code) is a code editor developed by Microsoft. By default, its installation is minimalist — that’s intentional. Its strength lies in its extensibility through tens of thousands of extensions available in the VS Code Marketplace.

To debug Python applications in VS Code, you must install the Microsoft Python extension.

Install Python extension

Via the Extensions panel in VS Code:

  1. Click on the Extensions icon in the sidebar (or Ctrl+Shift+X / Cmd+Shift+X on macOS)
  2. Filter by “Most Popular”
  3. Select the Python extension from Microsoft (the most popular at the time of training)
  4. Click on the blue button Install

Important: Do not install the pre-release version of the Python extension. It may contain bugs that would interfere with starting Python debugging in VS Code.

Installation takes a few seconds and does not require restarting VS Code.


4.2 The Python extension for VS Code

Once the extension is installed, VS Code gains several capabilities for Python:

  • Run button: runs the Python file opened in the active tab
  • Debug button: launch the Python file in the debugger
  • No code addition in sources is necessary (unlike pdb)

The Python extension therefore advantageously replaces import pdb; pdb.set_trace(). You work directly on your source code, without modifying it.


4.3 The Run and Debug panel

The Run and Debug panel (accessible via Ctrl+Shift+D / Cmd+Shift+D) is the control center for debugging in VS Code. It is divided into several sections:

SectionDescription
VariablesShows all local and global variables in real time
WatchList of Python expressions to monitor continuously
Call StackEquivalent of the stack trace of pdb
BreakpointsList and management of all breakpoints set

A major advantage over pdb: all of this information is visible simultaneously and variable values ​​update in real time as you navigate through the code.


4.4 Managing breakpoints in VS Code

Set a breakpoint

Simply click in the gutter (the strip to the left of the line numbers) to place a breakpoint. A red circle appears. Clicking on this circle again removes it.

Alternatively, place the cursor on a line and press F9.

The Breakpoints section of the panel

The Breakpoints section lists all the breakpoints set. Each breakpoint is displayed with:

  • A checkbox to enable/disable
  • The file and line number
  • The possible condition

Disable a breakpoint: uncheck the box. The red circle in the editor turns gray. The breakpoint remains set but the execution will no longer stop there.

Reactivate a breakpoint: recheck the box.

Delete a breakpoint: right click on the breakpoint in the gutter > “Remove Breakpoint”, or direct click on the red circle.

Conditional breakpoints in VS Code

To create a conditional breakpoint:

  1. Right click in the gutter next to the desired line
  2. Select “Add Conditional Breakpoint…”
  3. Enter the Python condition in the field that appears
  4. Validate with Enter

For example, to stop only when the variable coin is equal to "ethereum":

coin == "ethereum"

The advantage over pdb is that the GUI makes defining and managing conditions much simpler and more visual.

Debug exceptions

The Breakpoints section offers an additional option: Caught Exceptions and Uncaught Exceptions. By checking these options, VS Code automatically stops on lines that throw exceptions — even without an explicit breakpoint. This is a feature not possible with pdb without additional code.


4.5 Inspection of variables and the Call Stack

The Variables section

The Variables section automatically displays all variables in the current scope:

  • Locals: local variables to the current function
  • Globals: module global variables

You can expand complex objects (dictionaries, lists, class instances) by clicking on the arrow next to their name, without having to type a command. The values ​​update in real time at each step.

The Call Stack section

The Call Stack is the graphical equivalent of the where command in pdb. It lists all active frames. By clicking on a frame in the Call Stack:

  • Editor jumps to corresponding line in code
  • The Variables section displays the variables in this specific frame

Compare with pdb: to do the same thing you had to use up/down then p/pp for each variable. With VS Code, it’s just a click away.

Example with multi-file application

For the advanced version of the application (divided into 4 files), the Call Stack allows you to easily navigate between the frames of different files:

# Fichiers de l'application refactorisée :
utilities.py    # fonctions utilitaires (dates, prix)
investment.py   # classe Investment
portfolio.py    # classe Portfolio (gère les investissements)
app.py          # point d'entrée

In the Call Stack, we can see:

summarize    portfolio.py:30
main         app.py:15
<module>     app.py:18

Clicking on main in the Call Stack takes you directly to app.py:15 and shows the variables of main.


4.6 Watch Expressions and Debug Console

Watch Expressions

The Watch section allows you to define arbitrary Python expressions whose value is displayed and updated in real time during the entire debug session. This is a significant improvement over the p and pp commands in pdb.

To add a watch expression:

  1. Click on the + in the Watch section
  2. Enter the desired Python expression
# Exemples de watch expressions utiles :
investment["coin"]
len(portfolio)
investment["quantity"] * price
portfolio.total_value()

Watch expressions are reevaluated at each debug step and their value is updated automatically.

The Debug Console

The Debug Console is an interactive panel that works like the p and pp commands of pdb, but in a much more user-friendly way. You can type any Python expression there:

> investment
{'coin': 'bitcoin', 'quantity': 0.5}

> investment["quantity"] * 45000
22500.0

> portfolio
[{'coin': 'bitcoin', 'quantity': 0.5}, {'coin': 'ethereum', 'quantity': 5}, ...]

The Debug Console also supports auto-completion (Tab) and syntax highlighting. Unlike pdb commands, you see the formatted result directly in the panel.


4.7 Module summary

In this module, you have:

  • Installed and configured the Python extension for Visual Studio Code
  • Used the Debug button to start a debug session without modifying the source code
  • Explored the Run and Debug panel: Variables, Watch, Call Stack, Breakpoints
  • Managed breakpoints graphically: set them, activate/deactivate, condition
  • Inspected variables in real time without typing commands
  • Navigated the Call Stack with a single click
  • Used Watch Expressions to monitor arbitrary expressions
  • Used Debug Console for ad hoc inspections

5. Advanced Debugging with Visual Studio Code

Module duration: 26m 13s

This module has a strong demonstration focus. It covers advanced VS Code features for Python debugging.

5.1 Logpoints

What is a logpoint?

A logpoint is a tool halfway between the breakpoint and the watch expression. Unlike a breakpoint, it does not suspend execution of the application. It logs a message in the Debug Console each time the corresponding line is executed.

This is a great alternative to print() calls scattered throughout the code. Advantages over print():

  • You do not need to modify the source code
  • You don’t have to remove them before deploying
  • Messages do not appear in application standard output
  • You can enable/disable them like breakpoints

How to create a logpoint

  1. Right click in the gutter next to the desired line
  2. Select “Add Logpoint…”
  3. A red diamond (instead of the circle of a breakpoint) appears in the gutter
  4. Enter the message in the field that appears — no need for quotes
  5. To insert the value of a Python expression, surround it with braces {}
# Exemple de message de logpoint :
Investment {investment['coin']}: quantity={investment['quantity']}, price={price}

The logpoint appears in the Breakpoints section of the Run and Debug panel, marked with the diamond symbol (♦).

Demonstration

In portfolio.py, function summarize(), we can place a logpoint on line 22. When debugging the application (by first opening app.py), the application runs without stopping, but the Debug Console displays:

Investment bitcoin: quantity=0.5, price=45000.0
Investment ethereum: quantity=5, price=3000.0
Investment solana: quantity=50, price=150.0
Investment cardano: quantity=100, price=2.5

5.2 Launch Configurations and launch.json

The multi-file problem

A drawback of VS Code is that the Debug button always launches the file currently open in the editor. If you have a multi-file application and you open a file that only contains a class definition, VS Code will “debug” that file, but since the code is legal with no side effects, nothing will happen.

The solution is to create a launch configuration.

Create launch.json file

  1. In the Run and Debug panel, click on the link “create a launch.json file”
  2. Select Python Debugger from the context menu
  3. Select Python File

VS Code automatically creates a .vscode/launch.json file:

{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Python Debugger: Current File",
            "type": "debugpy",
            "request": "launch",
            "program": "${file}",
            "console": "integratedTerminal"
        }
    ]
}

${file} means “the currently open file” — this does not fix the problem. Change the configuration to point to the entry point file:

{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Lancer l'application principale",
            "type": "debugpy",
            "request": "launch",
            "program": "${workspaceFolder}/refactored/app.py",
            "console": "integratedTerminal"
        }
    ]
}

With this configuration, no matter what file is open in the editor, VS Code will always start debugging from app.py.

Multiple configurations in launch.json

launch.json may contain multiple configurations. A drop-down menu at the top of the Run and Debug panel allows you to select the active configuration:

{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Lancer l'application principale",
            "type": "debugpy",
            "request": "launch",
            "program": "${workspaceFolder}/refactored/app.py",
            "console": "integratedTerminal"
        },
        {
            "name": "Attacher au serveur distant",
            "type": "debugpy",
            "request": "attach",
            "connect": {
                "host": "localhost",
                "port": 5678
            },
            "pathMappings": [
                {
                    "localRoot": "${workspaceFolder}",
                    "remoteRoot": "/home/user/projet"
                }
            ]
        }
    ]
}

5.3 Debug a web application (FastAPI)

Demo App Overview

The web application used in the demos is a REST API built with FastAPI. It exposes three endpoints for managing a cryptocurrency portfolio:

# server.py

from fastapi import FastAPI
from sqlmodel import Session, select
from models import Investment, engine
import uvicorn

app = FastAPI()

@app.get("/investments")
async def get_investments() -> list[Investment]:
    with Session(engine) as session:
        investments = session.exec(select(Investment)).all()
        return investments

@app.get("/investments/{coin}")
async def get_investment(coin: str) -> list[Investment]:
    with Session(engine) as session:
        investments = session.exec(
            select(Investment).where(Investment.coin == coin)
        ).all()
        return investments

@app.post("/investments")
async def new_investment(investment: Investment) -> Investment:
    with Session(engine) as session:
        session.add(investment)
        session.commit()
        session.refresh(investment)
        return investment

if __name__ == "__main__":
    uvicorn.run(app, host="0.0.0.0", port=8001)

Launch.json configuration for FastAPI

VS Code offers predefined configurations for FastAPI and Django. When you create a launch.json and select “FastAPI”:

{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Python Debugger: FastAPI",
            "type": "debugpy",
            "request": "launch",
            "module": "uvicorn",
            "args": [
                "server:app",
                "--host", "0.0.0.0",
                "--port", "8001"
            ],
            "jinja": true,
            "console": "integratedTerminal"
        }
    ]
}

Debugging workflow with FastAPI

  1. Set a breakpoint in an endpoint function (e.g., line 38 in new_investment)
  2. Select FastAPI configuration in the Run and Debug panel
  3. Click on Start Debugging
  4. Navigate to http://localhost:8001/docs for Swagger interface
  5. Extend the POST method, click “Try it out”, fill the JSON and send
  6. VS Code suspends execution at breakpoint in new_investment

5.4 Debugging in a Docker container

Workflow overview

Debugging in a Docker container follows a different workflow than previous demos. Rather than starting the application and the debugger simultaneously from VS Code, you:

  1. Start the Docker container normally
  2. Connect VS Code to the debugger afterward by “attaching” the session

Configure container with debugpy

debugpy is the implementation of the Debug Adapter Protocol (DAP) for Python — this is the protocol that VS Code uses to communicate with the Python debugger.

In the application source code (before the app code):

# server.py — ajout pour le débogage à distance

import debugpy

# Démarrer le serveur debugpy et écouter sur le port 5678
debugpy.listen(("0.0.0.0", 5678))

# Optionnel : attendre que VS Code se connecte avant de continuer
# debugpy.wait_for_client()

from fastapi import FastAPI
# ... reste du code

The Dockerfile

FROM python:3.11-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

EXPOSE 8001
EXPOSE 5678

CMD ["python", "server.py"]

Build and launch container

# Construire l'image
docker build -t crypto-api .

# Lancer le conteneur en exposant les deux ports
docker run -p 8001:8001 -p 5678:5678 crypto-api
  • Port 8001: client connection to REST API
  • Port 5678: connection from VS Code to the debugpy debugger

Launch.json configuration for Docker (Remote Attach)

Add a configuration in launch.json by clicking “Add Configuration” and selecting “Python Debugger: Remote Attach”:

{
    "name": "Python Debugger: Remote Attach",
    "type": "debugpy",
    "request": "attach",
    "connect": {
        "host": "localhost",
        "port": 5678
    },
    "pathMappings": [
        {
            "localRoot": "${workspaceFolder}",
            "remoteRoot": "/app"
        }
    ]
}

The pathMappings field is crucial: it allows VS Code to map local file paths to paths in the container.

Attach debugger

  1. Select the “Remote Attach” configuration in the Run and Debug panel
  2. Click on Start Debugging
  3. Set a breakpoint in server.py
  4. Send a request to the API (via curl, Postman or Swagger)
  5. VS Code suspends execution at breakpoint

5.5 Remote debugging via SSH

To debug an application running on a remote server (Linux), the workflow is similar to Docker debugging:

  1. On the remote server, add debugpy.listen() in the code and launch the application
  2. Create an SSH tunnel for port 5678:
    ssh -L 5678:localhost:5678 user@remote-server
    
  3. In VS Code, configure a “Remote Attach” launch configuration pointing to localhost:5678
  4. Log in and debug normally
{
    "name": "Attach SSH - Serveur distant",
    "type": "debugpy",
    "request": "attach",
    "connect": {
        "host": "localhost",
        "port": 5678
    },
    "pathMappings": [
        {
            "localRoot": "${workspaceFolder}",
            "remoteRoot": "/home/vagrant/projet"
        }
    ]
}

5.6 Debugging Jupyter Notebooks

This is functionality that pdb cannot offer. VS Code, with the right extensions, allows you to debug Jupyter Notebooks cells.

Necessary extensions

  • Jupyter Extension Pack from Microsoft (from the Extensions panel)
  • The ipykernel package installed in the Python environment
pip install ipykernel seaborn pandas jupyter

Create and configure a notebook in VS Code

  1. Open the Command Palette (Ctrl+Shift+P / Cmd+Shift+P)
  2. Type “Create new Jupyter notebook” and validate
  3. At the top right, click to select a kernel (Python environment with ipykernel)
  4. Save the notebook (essential step to activate debugging)

Example of notebook with bug

# Cellule 1 : Imports
import seaborn as sns
import pandas as pd

# Cellule 2 : Charger les données
tips = sns.load_dataset("tips")

# Cellule 3 : Taux de change
exchange_rates = {
    "AUD": 1.52,   # Dollar australien
    "CAD": 1.35,   # Dollar canadien
    "EUR": 0.91    # Euro
}

# Cellule 4 : Fonction de conversion
def convert_currency(amount, from_currency, to_currency):
    rate = exchange_rates[to_currency] / exchange_rates[from_currency]
    return amount * rate

# Cellule 5 : Ajouter des colonnes converties (avec potentiel bug)
for currency, rate in exchange_rates.items():
    tips[f"total_bill_{currency}"] = tips["total_bill"].apply(
        lambda x: x * rate
    )

Debug a cell

Rather than clicking on the “Run Cell” button (▶), click on the “Debug Cell” button (🐛) which appears to the left of the run button. VS Code starts a debugging session for this specific cell.

You can put breakpoints inside cells and use all the usual features of the VS Code debugger: Variables, Watch, Call Stack, etc.

Important: Debugging works between cells — variables defined in previous cells are accessible in the debugging session of subsequent cells.


5.7 Useful VS Code Extensions

VS Code has extensions that integrate into the editor to display recommendations from Pylint, Black and isort directly in the code, without going through the terminal.

Pylint Extension

Installation: search for “Pylint” in the Extensions panel and install the Microsoft version.

Once installed, the problems detected by Pylint appear directly in the editor:

  • Yellow wavy lines: Warnings
  • Blue wavy lines: convention suggestions
  • Problems tab: complete list of problems with navigation possibility

To suppress a Pylint warning on a specific line:

investment = get_investment_info(inv)  # pylint: disable=W0621

To disable a rule for an entire file, add at the top of the file:

# pylint: disable=missing-docstring

Extension Black Formatter

Installation: look for “Black Formatter” in the Extensions panel (Microsoft version).

Configuration in .vscode/settings.json to automatically format when saved:

{
    "[python]": {
        "editor.defaultFormatter": "ms-python.black-formatter",
        "editor.formatOnSave": true
    }
}

With this configuration, each time you save a Python file (Ctrl+S), Black automatically reformats the code according to its style rules.

Isort extension

Installation: search for “isort” in the Extensions panel (Microsoft version).

Configuration to sort imports on backup:

{
    "[python]": {
        "editor.codeActionsOnSave": {
            "source.organizeImports": "explicit"
        }
    }
}

isort classifies imports into three groups according to PEP-8:

# Imports standard library
import os
import sys
from datetime import datetime

# Imports tiers (packages installés)
import fastapi
import sqlmodel
from seaborn import load_dataset

# Imports locaux (votre propre code)
from investment import Investment
from portfolio import Portfolio
from utilities import get_current_price

5.8 Module Summary

In this advanced module, you saw:

  • Logpoints: log messages in the Debug Console without stopping execution and without modifying the source code — an excellent alternative to print()
  • Launch configurations: save debug configurations in .vscode/launch.json to always launch the right file and manage multiple scenarios (local, Docker, SSH)
  • Debugging web applications: use VS Code’s FastAPI/Django templates to easily start debugging REST APIs
  • Docker debugging: add debugpy in container, expose port 5678, and attach VS Code after startup
  • SSH debugging: SSH tunnel + “Remote Attach” configuration to debug on a remote server
  • Jupyter debugging: debug notebook cells with breakpoints, benefiting from all the features of VS Code
  • Useful extensions: Pylint (static analysis in the editor), Black (auto formatting), isort (organization of imports)

6. Use PyCharm to debug Python

Module duration: 14m 36s

6.1 PyCharm Editions

PyCharm is a Python IDE developed by JetBrains, the company behind ReSharper (for .NET). It is a high quality tool and a commercial product.

Community Edition (Free)

  • Downloadable for free from the JetBrains website
  • No limitations on the type of Python applications you can create
  • Requires configuring the IDE manually (Python environments, linters, etc.)
  • Ideal for most Python developers

Professional Edition (Paid)

  • Annual cost per subscription (individual or enterprise license)
  • Built-in support for Django, FastAPI, Flask and other web frameworks
  • Jupyter Notebook support
  • Remote Development: native connection to SSH, WSL servers, Docker containers
  • Project templates for frameworks
  • Integration with databases

For this course, the Community Edition is sufficient except for the remote debugging demo, which requires the Professional Edition.

Old Educational Edition

JetBrains formerly offered an “Educational Edition” of PyCharm, essentially the Community Edition with features for educational environments (course projects, etc.). These features have been integrated into current editions.


6.2 Basic debugging features in PyCharm

Notable difference with VS Code: entry point auto-detection

PyCharm automatically recognizes files containing an entry point (if __name__ == "__main__":). The first time it is run, PyCharm automatically creates a Run Configuration for this file. So no matter what file is open in the editor, PyCharm always launches app.py — unlike VS Code which requires manually creating a launch.json.

Debug interface

PyCharm’s interface is different from VS Code but covers the same functionalities:

  • Debug button (insect icon): separate from the Run button
  • Debug window: opens at the bottom, with tabs for variables, console, etc.
  • Frames: equivalent of the Call Stack
  • Variables: list of variables of the current frame, with navigation in complex structures

Set breakpoints

Like in VS Code, just click in the gutter. Breakpoints are represented by red circles.

# portfolio.py

class Portfolio:
    def summarize(self):
        total = 0.0
        for investment in self.investments:   # ← breakpoint sur cette ligne (ligne 30)
            price = get_current_price(investment.coin)
            total += investment.quantity * price
        return total

Debug Toolbar

The debugging toolbar in PyCharm offers the same buttons as VS Code:

ButtonShortcutAction
Step OverF8Execute the current line without entering functions
Step IntoF7Enters the function called on the current line
Step OutShift+F8Exit of current function
SummaryF9Continue to the next breakpoint
StopCtrl+F2Stops debugging session

The Frames section of PyCharm works like the Call Stack of VS Code. Click on a frame:

  • Show the source code at the corresponding line
  • Updates the Variables section with the variables of the selected frame

6.3 Inspecting complex data structures

A strong point of PyCharm compared to VS Code and pdb is its ability to inspect complex data structures in a very intuitive way.

Object navigation

In the Variables window, objects can be expanded recursively:

  • Class instances show all their attributes
  • Lists show each element with its index
  • Dictionaries show every key/value pair

Watch Expressions directly from variables

In PyCharm, you can add a watch expression directly by right-clicking a variable in the Variables section and selecting “Add to Watches”. It’s faster than with VS Code where you have to manually type the expression.

Conditional breakpoints in PyCharm

Right click on a breakpoint (red circle in the gutter) > Edit Breakpoint:

# Exemple de condition pour le breakpoint :
investment.coin == "ethereum"

PyCharm displays a widget directly in the editor to enter the condition, make it dependent on another breakpoint, or configure log actions.

Logpoints in PyCharm

In the breakpoint configuration widget (right click > Edit Breakpoint), by checking “Evaluate and log” and entering an expression, the breakpoint becomes a logpoint: it displays the result of the expression in the debugging console without suspending execution.


6.4 Remote debugging in PyCharm via SSH

Remote debugging is a feature of PyCharm Professional Edition.

Remote connection options

PyCharm supports several remote connection modes:

MethodDescription
SSHConnecting to a Linux Server via SSH
WSLWindows Subsystem for Linux (Windows only)
JetBrains SpaceJetBrains cloud service (source control, CI/CD)
DockerBeta at training time

Note: WSL is popular for Python development on Windows, combined with VS Code. Since WSL is only available on Windows, it is not covered in this course.

SSH connection in PyCharm

  1. When PyCharm opens, in the Remote Development section, click on SSH
  2. Click on the “New Project” drop-down menu > “Connect to Host”

Important: The remote host must be a Linux machine.

  1. Enter:
  • Host: IP address or host name of the server (ex: 192.168.56.10)
  • Username: the SSH user (ex: vagrant)
  1. Enter password when prompted
  2. PyCharm opens a new window connected to the remote server

The experience is as if you were working locally: editor, integrated terminal, debugger, everything works the same — but on the remote server.

# Exemple de configuration d'une machine Vagrant pour les démos :
# Vagrantfile
Vagrant.configure("2") do |config|
  config.vm.box = "ubuntu/focal64"
  config.vm.network "private_network", ip: "192.168.56.10"
  config.vm.provision "shell", inline: <<-SHELL
    apt-get update
    apt-get install -y python3 python3-pip
    pip3 install debugpy fastapi sqlmodel uvicorn
  SHELL
end

6.5 PyCharm Plugins

Like VS Code with its extensions, PyCharm is extensible via plugins available in the JetBrains Marketplace.

Pylint plugin for PyCharm

Installation:

  1. Go to File > Settings > Plugins (or PyCharm > Preferences > Plugins on macOS)
  2. Marketplace tab
  3. Search for “Pylint”
  4. Install the plugin and restart PyCharm

Once installed, Pylint integrates into the IDE. A “Pylint” tab appears in the bottom bar. You can:

  • Analyze the current file or the entire project
  • View issues listed with their category and code
  • Double-click on a problem to go directly to the affected line

Pylint recommendations also appear as marks in the editor (underlines), similar to VS Code.

Other useful plugins for PyCharm

PluginDescription
BlackPython Code Trainer
MypyChecking type annotations
isortAutomatic organization of imports
.env files supportColoring and support for .env files
DockerDocker integration in IDE

6.6 Module and course summary

PyCharm module summary

In this module, you discovered:

  • PyCharm, a commercial Python IDE from JetBrains, available in Community Edition (free) and Professional Edition (paid)
  • Entry point auto-detection: PyCharm automatically creates a launch configuration for files with if __name__ == "__main__":, avoiding the problem encountered in VS Code
  • Basic debugging features: breakpoints, stepping, variables, frames — similar to VS Code with a different interface
  • Navigating complex data structures: PyCharm facilitates recursive inspection of nested objects, lists and dictionaries
  • watch expressions: can be added directly from the Variables panel
  • Remote debugging via SSH (Professional Edition): transparent connection to a Linux server from PyCharm
  • Plugins: extension of PyCharm with tools like Pylint for static analysis

General course summary

This course introduced you to three complementary tools for debugging Python applications:

1. pdb — The standard library debugger

  • Available wherever Python is installed
  • Command line interface with text commands
  • Need to add import pdb; pdb.set_trace() in code
  • Ideal for GUI-less environments or for fast server-based debugging

2. Visual Studio Code — A modern, extensible editor

  • GUI that replaces most pdb commands
  • The Run and Debug panel centralizes variables, call stack, breakpoints and watch expressions
  • Advanced support: logpoints, launch configurations, Docker/SSH/Jupyter debugging
  • Recommended for daily development, including work on WSL

3. PyCharm — A complete Python IDE

  • Very comprehensive graphical debugging experience – Particularly strong for inspecting complex data structures
  • Integrated and Transparent Remote Development (Professional Edition)
  • Extension via plugins

Additional tools presented

ToolTypeRole
PylintStatic analyzerIdentify errors and improve code quality
ipdbImproved debuggerpdb with IPython (syntactic coloring, completion)
BlackTrainerAutomatically apply PEP-8 style rules
isortImport OrganizerSort and organize Python imports
mypyType checkerCheck type annotations before execution
debugpyDebug AdapterAllow VS Code to connect to remote Python

Next steps

To deepen your knowledge after this course, Pluralsight offers:

  • Working with Databases in Python 3 — to master data access with Python
  • Web Development with Bottle 0.12 Fundamentals — for web development with Python


Search Terms

debugging · python · foundations · data · analysis · engineering · analytics · pycharm · debug · command · aka · breakpoints · pdb · pylint · extension · launch.json · application · commands · expressions · remote · stack · breakpoint · conditional · container

Interested in this course?

Contact us to book it or get a custom training plan for your team.