Python Remote Debugging with VS Code and debugpy

Python Remote Debugging with VS Code and debugpy

Debugging is the process of identifying, analyzing and removing errors in the software. It is a process that can start at any stage of the software development, even as early as the software has been written.

Sometimes, remote debugging is necessary. In the simplest terms, remote debugging is debugging an application running in a remote environment like production and staging. To perform remote debugging, you need to be able to remotely connect to a running application from your local development environment.

This article will focus on a debugging strategy known as forward analysis for Python remote debugging. We’ll cover the different tools and libraries such as debugpy that will allow you to debug a live application remotely. We’ll go over the setup and configuration steps for remote debugging in Visual Studio Code, one of the most accessible and powerful IDEs available.

Note that setting up this Python remote debugging workflow takes some effort, especially when applied to real-world production applications. If you want to minimize the hassle and establish a much smoother production debugging experience from VS Code or PyCharm, consider setting up a Lightrun account.

Debugging Basics

There are a couple of strategies you can employ when debugging issues.

  • Backward analysis: Tracing the problem from an error log or message in order to identify the region of the code with the failure.
  • Forward analysis: Tracing the problem using breakpoints at different steps of your program’s execution.

From there, there’s a basic workflow to follow.

  1. Identify the error: Error reports might be specific or vague depending on the part of the software, environment, and stage of development they show up on. The first step to effective debugging is to make sure you identify the actual error.
  2. Find the error location: Once you have identified the error correctly, you need to narrow down the code’s location to where the bug is present.
  3. Analyze the error: Now that you have a better idea of where the error might be located, your next step is to analyze the conditions and the underlying cause to solve the issue.
  4. Fix and validate: Finally, you need to take steps to change the code and validate that your solution is correct. Typically, this is done by writing unit tests that validate the conditions of the original software defect.

Prerequisites

For this tutorial, you will need Python 3, venv, Docker Compose, debugpy, and the Flask web framework. You will also need Visual Studio Code (VS Code), preferably with the official Python extension installed. Feel free to install other VS Code Python extensions as you see fit.

You’ll use a simple sudoku script as your debugging target. To download the demo application, follow this link. Alternatively, you can also use your own target Python 3 application, as the libraries you’ll use should be compatible.

Python Remote Debugging Architecture

Remote Debugging Diagram

At a high level, a remote debugging process requires:

  1. A connection between the server and your local development environment.
  2. A debugging package installed on the remote application compatible with the IDE.
  3. An IDE configured to allow for Python remote debugging.
  4. Matching code between the IDE and the version deployed on the server.

Remote debugging has several benefits, but mainly it allows you to inspect and debug the application directly on the system. In many situations, this might be the only option available to developers to trace a software defect.

Start with debugging your sudoku solver locally to get familiar with the debugging tools and interface. Later, you’ll modify that initial configuration for Python remote debugging.

Setting Up Local Debugging with VS Code

The official Python extension provided by VS Code supports debugging several types of Python applications, from simple scripts like your sudoku solver to more complex applications like Flask and Django.

Create the Initial Configuration

With the sudoku solver script open, click the Run tab on the sidebar.

Run view

A dialog asks you to customize the Run and Debug configuration.

Run Configuration

Click create a launch.json file to open a dialog where you will select the debug configuration.

Debug Configuration

Out of the box, Visual Studio Code gives you a couple of presets to start, including Django and Flask. Go ahead and select the Python file configuration from the dropdown menu. VS Code will immediately create and open a sample launch.json configuration file.

Additionally, the sidebar will change to show the following sections:

Python Debugger pane in VS Code

  • Variables: variable values and expression evaluation from the selected stack frame in the Call Stack section.
  • Watch: the list of expressions to watch, helpful in setting conditional breakpoints.
  • Call Stack: the order in which methods, modules, and functions are getting called.
  • Breakpoints: defined stopping or pausing places during application execution.

Let’s take a closer look and define a couple of breakpoints in your file:

Breakpoints

Finally, go ahead and run the program through the debug configuration.

Debug options

The program will initialize and stop at your first breakpoint on line 9 and correctly show the call stack that leads to the program’s execution. Additionally, you can inspect all the variables and their state at the current step in the program execution.

How to Set Up Python Remote Debugging

Now, with a better understanding of your IDE’s overall layout and debugging capabilities, you can move forward and set up for Python remote debugging. In order to properly illustrate the capabilities and value of remote debugging, you need a more complex application than a simple sudoku solver. Fortunately, Microsoft has a sample Flask project that you can leverage for your tutorial.

The Flask Sample Project has a simple web app and a matching Docker file that will allow you to simulate the Python remote debugging scenario. If you’d like to set up and build this application from scratch, feel free to visit the official tutorial.

Step 1: Application and Docker Setup

Start by checking out the repository and setting up the sample project:

git clone git@github.com:microsoft/python-sample-vscode-flask-tutorial.git 
cd python-sample-vscode-flask-tutorial
python3 -m venv env
source ./env/bin/activate
python3 -m pip install --upgrade pip
pip install -r requirements.txt
code .

The above Bash snippet performs the following actions:

  • Clone the official VS Code Flask tutorial.
  • Create a Python Virtual Environment.
  • Activate the virtual environment.
  • Make sure pip is up to date.
  • Install required libraries specified in requirements.txt.
  • Open VS Code. This will automatically load the Flask project with the active directory set to python-sample-vscode-flask-tutorial.

Note that on the first run, VS Code will ask you to select the relevant Python interpreter. Make sure to select the one from your virtual environment.

Virtual env

Next, make a few changes inside the python-sample-vscode-flask-tutorial project to simulate a remotely running Python application with Docker. Open the Dockerfile and uncomment the last three lines of the file.

# Pull a pre-built alpine Docker image with NGINX and Python 3 installed
FROM tiangolo/uwsgi-nginx:python3.8-alpine-2020-12-19

# Set the port on which the app runs; make both values the same.
#
# IMPORTANT: When deploying to the Azure App Service, go to the App Service on the Azure 
# portal, navigate to the Applications settings blade, and create a setting named
# WEBSITES_PORT with a value that matches the port here (the Azure default is 80).
# You can also create a setting through the App Service Extension in VS Code.
ENV LISTEN_PORT=5000
EXPOSE 5000

# Indicate where uwsgi.ini lives
ENV UWSGI_INI uwsgi.ini

# Tell NGINX where static files live. Typically, developers place static files for
# multiple apps in a shared folder, but for the purposes of this tutorial you can use the one
# app's folder. Note that when multiple apps share a folder, you should create subfolders
# with the same name as the app underneath "static", so there aren't any collisions
# when all those static files are collected together.
ENV STATIC_URL /hello_app/static

# Set the folder where uwsgi looks for the app
WORKDIR /hello_app

# Copy the app contents to the image
COPY . /hello_app

# If you have additional requirements beyond Flask (which is included in the
# base image), generate a requirements.txt file with pip freeze and uncomment
# the next three lines.
COPY requirements.txt /
RUN pip install --no-cache-dir -U pip
RUN pip install --no-cache-dir -r /requirements.txt

Next create a docker-compose.yml that will allow you to launch your Docker container.

./python-sample-vscode-flask-tutorial/docker-compose.yml

# docker-compose.yml
version: "3.4"

services:
  flask-server:
    image: flask
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - 5000:5000
    volumes:
      - .:/hello_app:cached

To verify that you can run your Flask application, execute the following command and visit localhost:5000:

docker-compose up

If everything is working correctly, you should see the following in your browser:

Flask Application

Step 2: Debugger Setup

Now that you have the sample Flask application up and running, set up VS Code for remote debugging.

As before, create a launch configuration for attaching to your Python remote debugger. You can do this by adding or modifying your launch.json file as follows:

./python-sample-vscode-flask-tutorial/.vscode/launch.json

{
  "configurations": [
        {
            "name": "Python: Attach",
            "type": "python",
            "request": "attach",
            "localRoot": "${workspaceFolder}",
            "remoteRoot": "/hello_app",
            "port": 9000,
            "secret": "my_secret",
            "host": "localhost"
        },
  ]
}

Make sure the localRoot and remoteRoot mappings are correct to your project setup and system. These mappings are used to link the files being executed on the remote server with your local copy.

Install debugpy

Next, install and set up debugpy, a Python module that allows you to create a debugger for your Python applications. To install debugpy, make sure to add debugpy to requirements.txt:

./python-sample-vscode-flask-tutorial/requirements.txt

Flask
debugpy

Instantiate debugpy Inside Your Application

Create the following file inside your hello_app application:

./python-sample-vscode-flask-tutorial/hello_app/debugger.py

from os import getenv

def initialize_debugger():
    if getenv("DEBUG") == "True":
        import multiprocessing

        if multiprocessing.current_process().pid > 1:
            import debugpy

            debugpy.listen(("0.0.0.0", 9000))
            print("Debugger is ready to be attached, press F5", flush=True)
            debugpy.wait_for_client()
            print("Visual Studio Code debugger is now attached", flush=True)

Instantiate the Debugger

./python-sample-vscode-flask-tutorial/hello_app/webapp.py

# Entry point for the application.
from . import app    # For application discovery by the 'flask' command. 
from . import views  # For import side effects of setting up routes. 
from . import debugger  # For import side effects of setting up debugger.

# Time-saver: output a URL to the VS Code terminal so you can easily Ctrl+click to open a browser
# print('https://127.0.0.1:5000/hello/VSCode')


./python-sample-vscode-flask-tutorial/hello_app/__init__.py

from flask import Flask  # Import the Flask class
from .debugger import initialize_debugger

initialize_debugger()

app = Flask(__name__)    # Create an instance of the class for your use

Expose the Debugging Port

Finally make sure your debugging port is exposed on your Docker container. Open docker-compose.yml and add the following line:

# docker-compose.yml
version: "3.4"

services:
  flask-server:
    image: flask
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - 5000:5000
      - 9000:9000
    volumes:
      - .:/hello_app:cached

This will allow you to attach your remote debugging session to the container.

Launch the Container with a Debugger Enabled

Next, launch your Docker container in debug mode. The main thing you’re doing here is passing the DEBUG variable, which will then instantiate your debugpy listener.

docker-compose run --rm -e FLASK_APP=hello_app/webapp.py -e FLASK_ENV=development -e DEBUG=True --service-ports flask-server flask run --host 0.0.0.0

Finally, the last step is to attach to a debug session directly from Visual Studio Code. The following is a GIF of the debugger working against the Python remote application:

Remote Debugger

In summary, this is what’s happening:

  • In a terminal, run the Docker command with the DEBUG variable set to true.
  • When prompted, the debugger can now be attached; run the Python Attach launch configuration.
  • Add a breakpoint to the line you want to debug by clicking to the left of its number.
  • Call the corresponding route (localhost:5000/hello/flask in the GIF).
  • The debugger stops the code at your breakpoint.

Conclusion

As you’ve probably realized, a powerful Python remote debugging setup can take some effort to incorporate effectively. Trying to replicate the exact conditions that a customer experiences can add another layer of difficulty.

If you want to remotely debug your Python application with much less friction, take a look at Lightrun: our next-gen remote debugger for your production environment. With Lightrun, you can inject logs without changing code or redeploying, and add snapshots: breakpoints that don’t stop your production application. Lightrun supports Python, Java, and Node.js applications, integrates with VS Code, and you can start using it today!

Black Friday 2024: What to Expect

Black Friday, the traditional kickoff to the holiday shopping season, is set to make waves in 2024 with projected sales reaching an impressive $10.8 billion—a 9.9% increase from last year according to Statistics.blackfriday analysts. According to the same team, Cyber Monday sales in 2024 are expected to reach $13.2 billion—a 6.1% increase from 2023.

Both events in sum are expected to generate $24 billion in sales. A major contributor to this growth is the increasing number of consumers who prefer to shop online. As consumer behaviors evolve to favor online shopping, retailers are expected to invest more in their online infrastructure, user experience, and platform reliability to meet this projected demand.

More traffic means more potential revenue, but it also means more queries, more transactions, and more potential problems. As an online retailer, you need to be well-prepared from a technical standpoint to ensure that you don’t miss out on any potential sales.

Amazon Prime Day 2018: A Prime Example For How Things Can Go Wrong

Q4 is a critical time for businesses, and developers are often under immense pressure to ensure that their systems are up and running. The last thing you want is for your website to crash during the busiest shopping season of the year. Poor performance means lost revenue, and problems can quickly escalate into a PR nightmare.

You’ve likely heard about the infamous Amazon Prime Day crash in 2018, which reportedly cost the company an estimated $100 million in lost sales. Despite the charm of the “Dogs of Amazon” error pages, they couldn’t mitigate the damage. Cute error messages are no substitute for a robust, well-prepared platform.

Some unofficial post-mortems of the Prime Day crash suggest that the root cause was a failure in the auto-scaling system, which left Amazon scrambling to manually add server capacity. The internal system “Sable,” Amazon’s e-commerce storage and computation platform, struggled under the surge—which led to a failure across the platform. Some experts speculate that misconfigurations or software bugs in scaling systems magnified the issue, forcing Amazon to temporarily shut down international traffic to the site according to CNBC.

In all cases, the lesson is clear: impredictable issues can arise, and you need to be prepared.

If you google “How to prepare for Black Friday for developers,” you’ll find a plethora of articles that will tell you to “test your system,” “monitor it,” “add autoscalability to your infrastructure,” and “secure your code.” While these may be good pieces of advice, they touch only the surface of the problem as they are not shifting left enough to prevent the issues from happening in the first place.

In sports, for example, achieving peak performance isn’t the result of a single training session but rather a dedicated regimen of practice, evaluation, and adjustment over time. Similarly, preparing for Black Friday is not a one-time event, but rather a continuous process that requires iterative improvements based on feedback. The earlier this feedback is received, the better.

Shift-Left Everything: The Early Bird Catches the Worm

Performance should be a first-class citizen in your development process. That’s why it should not be an afterthought, but rather a core part of your development cycle. A software development lifecycle includes several stages, such as planning, coding, testing, and deployment. You should be thinking about performance from the early stages of development. The problem is that performance testing is often associated with the staging and production environments—which is too late in the development cycle.

By shifting performance testing to the left, you can identify and fix performance issues early in the development process. This approach allows you to catch performance bottlenecks before they become critical issues.

Performance problems can be triggered by a variety of factors, such as bad capacity planning, downsized infrastructure, inefficient code, slow database queries, extreme edge cases, or code that doesn’t scale well. All these issues can be identified early in the SDLC if you have the right tools and processes in place.

For example, with Lightrun, you can dynamically add a log to a specific function suspected of causing slow database queries, without halting or redeploying the application. This enables you to pinpoint the exact query, analyze its execution time, and resolve the issue during the development phase, long before it reaches staging or production.

Using Lightrun’s snapshot feature, you can also capture the state of your application at a specific point in time. Let’s say you have an issue that only occurs under certain conditions (e.g., a specific event or input). You can capture the state of your application after reproducing the issue using a snapshot that can be added directly from the Lightrun IDE plugin. This snapshot not only captures the trace of the application but also the state of the variables at that point in time. This allows you to deeply analyze the issue from the inside and fix it accordingly. You can also export the snapshot and share it with your team members for further analysis. All of this happens from the comfort of your IDE while you’re coding. The best part is that you capture logs and traces from any environment, including your development, testing, staging, and production environments without any code changes on the remote server.

Another crucial, often overlooked practice you should consider is observability.

Observability is the ability to understand what’s happening inside your system based on external outputs. An e-commerce platform is responsible for different types of operations, including user authentication, product search, transaction processing, and more. Observability allows you to monitor these operations and understand how they are performing from the user’s perspective. Using logs, metrics, and traces, developers can gain useful and actionable understanding of their applications’ and infrastructure’s behavior.

Nowadays, observability is a key practice, but its implementation is often not quite right in many organizations. First, not understanding what to “observe” can lead to a lot of noise and irrelevant data – a sheer volume of data makes it challenging to identify the root cause of a problem. Second, observability is often associated with the production environment, which is a late phase in the SDLC. The first problem creates a siloed approach to observability where developers are less likely to use the data to improve their code. The second problem leaves developers out of the loop when it comes to understanding how their code behaves in production.

Instead of this approach, observability should be a part of the development process from the beginning. But here’s the catch: if developers have to wait for a new deployment on the testing or staging environment to add a log or a metric, a new bottleneck is created. A solution to this, besides having a continuous deployment pipeline, is to use a tool that allows them to instrument their code on the fly without the need to redeploy their applications. This is also known as “dynamic instrumentation” and it’s a key feature of Lightrun.

According to a 2018 Stripe report, developers spend an average of 17.3 hours per week on maintenance and debugging issues. This amounts to $300 billion in lost developer productivity every year. By shifting left observability, performance testing, and debugging, you can reduce this number and save valuable time and resources.

Debugging in Production: No Longer a Meme

Shifting left observability, performance testing, and debugging is the secret sauce to a successful Black Friday. Small incremental changes with continuous feedback and early feedback are the ingredients to a successful recipe. However, incidents can still happen, and when they do, you need to be able to debug in production as close to the problem as possible.

Some years ago when someone mentioned “debugging in production“, it was often seen as a joke or the worst-case scenario. Today, it’s a regular practice for many organizations. The reason is simple: the production environment is the most complex and the most realistic environment. If a bug or a performance issue escapes your team despite all the early stages of testing, the early feedback, and the continuous improvement, you need to be able to debug it in production. Chances are that the issue is an edge case that only occurs in production.

Traditionally, debugging in production was a scary and unsafe process. The fastest way to do it was SSHing into the server and adding a print statement in the code. This is not only unsafe but also inefficient. It’s unsafe because you’re modifying the code in the production environment, which can lead to unexpected results. It’s inefficient because you need to redeploy the application to see the changes, which can take time and may disrupt the user experience.

Here’s when Lightrun once again comes to the rescue. A developer can add a line of log to the code in the live production environment without halting the application or redeploying it. Imagine the following scenario: you have a performance issue that only occurs in production. You can add a log to the suspected function or line of code, capture the logs, and analyze them in real-time to identify the root cause of the problem. Similarly, you can add a snapshot to capture the state of the application at a specific point in time and under specific conditions.

All you have to do is to install the Lightrun plugin in your IDE – available for IntelliJ, PyCharm, WebStorm, VS Code and select web IDEs – connect it to your production environment, and start debugging. It’s that simple.

Reduce the Mean Time to Resolution (MTTR)

It may be too late to prevent all performance issues from happening, but you can still reduce the time it takes to resolve them. The Mean Time to Resolution (MTTR) is a key KPI that measures the average time it takes to resolve an issue. The longer it takes to resolve an issue, the more revenue you lose.

To understand how to calculate the MTTR, let’s consider the following formula:

MTTR = (Total time to resolve issues) / (Total number of repairs

If you have a high MTTR, it means that you’re spending too much time resolving issues. Serious engineering teams aim to reduce the MTTR as much as possible. In general, there’s no good or bad value for the MTTR; this depends on the context of the organization, the criticality of the application, and the complexity of the issues. But as a rule of thumb, if your team is spending more than 3 hours per issue, you may want to consider improving your processes. This is especially true during Black Friday when every minute counts. According to ECDB, as an example, online shopping peak hours in some countries (e.g., Germany, UK) are between 8 pm and 10 pm. If your MTTR is 3 hours and your application crashes at 8 pm, your team will only start to resolve the issue at 11 pm. This means that you were down during all the peak hours.

Lightrun can help you reduce your MTTR. For example, Lightrun capabilities have enabled InsideTracker developers to diagnose and resolve application issues in real-time, saving valuable debugging time. Their MTTR improved by 50%, resulting in dozens of hours saved each month. Prior to using Lightrun, InsideTracker developers were unable to troubleshoot or access remote Kubernetes environments from their local machines in an easy, straightforward way. This led to long and inefficient debugging sessions that required hotfixes and redeployments taking hours.

What’s Next?

Lightrun provides a suite of powerful features that help developers quickly identify and resolve issues in production environments but before reaching production, Lightrun can also help you shift-left observability, testing, and debugging.

The tool offers a range of features that can help you improve your development process, including:

Lightrun empowers not only developers but also DevOps, SREs, Platform Engineers, and other stakeholders to improve their development process and as a result, catch issues early, reduce the MTTR, and improve the overall performance of their applications.

Ready for Black Friday 2024? If not, Lightrun is here to help: