Tutorial: How to Debug Python code in Visual Studio Code

Remember when finding Python bugs meant peppering your code with print statements? Well, those days are over. Visual Studio Code packs a punch when it comes to Python debugging. Think breakpoints that trigger only when your conditions match, real-time variable tracking, and the ability to freeze time in your code execution. Whether you’re hunting down race conditions or memory leaks, these tools will make your debugging sessions so much more productive.

Assuming you’re already set up with Python and VS Code, we’ll jump right into the debugging features. 

Setting up debug configurations

Let’s set up VS Code’s debugging properly. The launch.json file will serve as your control center for debugging configurations:this is where you tell VS Code exactly how to run your debug sessions.

Basic configuration

This configuration packs the essentials: it uses debugpy (VS Code’s Python debugger), launches your current file, and runs it in the integrated terminal.

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

Advanced settings

Want more powerful controls? Add these options to your configuration:

  • ‘args’: Pass command-line arguments to your script
  • ‘env’: Set environment variables for your debug session
  • ‘justMyCode’: Toggle between debugging only your code or including library code
  • ‘console’: Choose between integrated or external terminal

For example, if you’re debugging a Flask app that needs specific environment variables:

{
    "name": "Flask App Debug",
    "type": "debugpy",
    "request": "launch",
    "program": "${workspaceFolder}/app.py",
    "env": {
        "FLASK_ENV": "development",
        "PORT": "5000"
    }
}

VS Code’s debugger automatically uses your selected Python interpreter, but you can override this in launch.json if needed. This flexibility lets you debug across different virtual environments without switching your workspace interpreter.

Multiple configurations

You can maintain several debug configurations in your launch.json for different scenarios. For instance:

  • One for your main application
  • Another for running tests
  • A configuration for debugging with specific command-line arguments
  • A setup for remote debugging

Switching between these configurations is a breeze too. It’s all there on the debug toolbar.

Furthermore, you can share these debugging setups with your team as well. The launch.json lives in your .vscode folder and can be version-controlled.

Essential debugging tools and features

Let’s explore VS Code’s debugging capabilities that make print-statement debugging look like stone age technology.

Breakpoint management

VS Code offers sophisticated breakpoint controls that go beyond simple line stops:

def process_user_data(user_dict):
    # Basic line breakpoint: Click the left margin or press F9
    processed_data = {}
    
    # Conditional breakpoint: Right-click margin > Add Conditional Breakpoint
    # Example: Break only when user_dict contains more than 100 items
    for user_id, details in user_dict.items():
        processed_data[user_id] = details.copy()
        
    # Hit count breakpoint: Break after N iterations
    # Right-click margin > Add Conditional Breakpoint > Hit Count
    for item in processed_data.items():
        validate_item(item)

Debug controls

Master these keyboard shortcuts for precise code navigation:

  • Step Over (F10): Perfect for skipping library function calls while tracking your logic
  • Step Into (F11): Dive into function internals when the bug hides in the details
  • Step Out (Shift+F11): Return to the caller when you’ve found what you need
  • Continue (F5): Run until your next breakpoint hits

Variable inspection

VS Code provides three ways to monitor your program’s state:

The Variables pane reveals your program’s state in real-time:

  • Local variables: Track scope-specific changes
  • Global variables: Monitor application-wide state
  • Special variables: Direct access to Python internals like ‘__dict__’ and ‘self’

The Watch pane acts as your persistent monitor:

# Add these to your Watch pane
len(processed_data)  # Track collection size
user_dict.keys()     # Monitor dictionary structure
sum(x.get('score', 0) for x in processed_data.values())  # Complex expressions

The debug console becomes your Python REPL within the current context — test fixes, evaluate expressions, and modify variables without touching your source code. Use it to experiment with potential solutions before committing changes.

Advanced debugging techniques

When basic debugging tools aren’t enough, the IDE offers sophisticated features for complex Python applications. These tools shine particularly when dealing with production issues, multi-threaded code, or remote deployments.

Exception handling

While VS Code’s debugger can automatically catch unhandled exceptions, you are free to customize this behavior. Set exception breakpoints through the debug panel to pause execution when specific exceptions occur:

try:
    process_user_data(large_dataset)
except ValueError as e:
    # VS Code will break here if exception breakpoints are enabled
    logging.error(f"Data validation failed: {e}")
    raise  # Re-raise to maintain the stack trace

Remote debugging

For debugging code running on servers or containers, VS Code provides remote debugging capabilities. First, add the debugpy code at the entry point of your remote application (usually before your main logic starts):

import debugpy

# Configure debugpy to listen on all interfaces
debugpy.listen(('0.0.0.0', 5678))
print("Waiting for debugger attach...")
debugpy.wait_for_client()  # Pause execution until VS Code connects

The corresponding launch.json configuration needs to specify the connection details:

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

To start debugging:

  1. Run your Python application on the remote machine
  2. Start the debugger in the IDE using the “Remote Attach” configuration
  3. VS Code will connect to your remote application and enable full debugging capabilities

Performance analysis

You can transform the debug console at your disposal into a powerful diagnostic hub. Here’s how to leverage its features:

  • Monitor variable states across threads by using the Parallel Watch window. This shows you the same variable’s value across different threads simultaneously, perfect for catching race conditions.

For memory analysis, combine VS Code’s Variables pane with tracemalloc to get deep insights:

import tracemalloc
tracemalloc.start()
snapshot = tracemalloc.take_snapshot()
top_stats = snapshot.statistics('lineno')
  • Want to identify performance bottlenecks? VS Code’s built-in timing features shine here. Set strategic breakpoints and watch execution times between them, helping you spot slow code segments without extra instrumentation.
  • For analyzing call stacks in multi-threaded applications use the Parallel Stacks window. It visualizes thread interactions and call stacks, making deadlock detection almost intuitive. You can even save these stack views as graphics for documentation or team discussions

These advanced features transform VS Code from a simple debugger into a comprehensive diagnostic tool, especially valuable when dealing with complex Python applications in production environments.

Parting thoughts

VS Code’s debugging arsenal makes Python bug hunting precise and methodical. From breakpoints to performance profiling, these features help developers navigate through complex code paths. Yet the most challenging bugs are those that slip through into production, often in edge cases we didn’t anticipate during development. This is where Qodo comes in. Qodo’s AI scans your codebase during development, identifying potential issues and suggesting optimizations before bugs have a chance to surface.

Remember: the best debugging is the one you never have to do. By combining VS Code’s powerful debugging features with Qodo’s predictive analysis, you won’t just be fixing bugs — you will be preventing them.