· tutorial · 5 min read

Sane Python Debugging in VSCode

A path towards a more reasonable dev experience

A path towards a more reasonable dev experience

Python in VSCode: An escape from dependency conflict hell to a smooth debugging experience

I don’t know about you, but one of the things I’ve found most maddening about working with Python is how package management works. I got spoiled, coming from the world of Ruby — bundler just always seemed to work. Hell, even working with the dreaded node_modules is less painful than debugging dependency conflicts in Python.

I’ve been using VS Code for the past few years and I’ve been impressed by the IDE experience/the ability to set breakpoints easily. That said, even when I get package management in Python working, I’ve often had to return to print debugging because I couldn’t get VSCode to launch with all the necessary dependencies.

No more.

If you’re:

  • tired of dealing with a terrible, complicated, and broken method to manage dependencies, using tools that don’t integrate easily together
  • tired of not knowing how to setup your launch.json so that you can easily activate an interactive debugging session
  • or both

this post is for you.

For the purposes of this tutorial, we’ll use starting up a fastapi server, but this approach can be broadened to other commands that you might run on the CLI.

The Holy Trinity: asdf, poetry, and VSCode

The proposed approach consists of three tools:

  1. asdf: A version manager for multiple languages
  2. poetry: A dependency management and packaging tool for Python
  3. VSCode: Standard Bearer code editor turned IDE, with powerful debugging capabilities. If you know, you know.

Step 1: Setting up asdf

First, we need to install asdf. Here, I’m using it to manage both versions of Python as well as poetry.

# Install asdf
git clone https://github.com/asdf-vm/asdf.git ~/.asdf --branch v0.10.2

# Add asdf to your shell
echo '. $HOME/.asdf/asdf.sh' >> ~/.bashrc
echo '. $HOME/.asdf/completions/asdf.bash' >> ~/.bashrc
source ~/.bashrc

# Install Python plugin
asdf plugin-add python

# Install the latest Python version
asdf install python latest
asdf global python latest

# Install poetry plugin
asdf plugin-add poetry https://github.com/asdf-community/asdf-poetry.git

# Install the latest poetry version
asdf install poetry latest
asdf global poetry latest

The benefit of managing languages and dependency managers with asdf is that we have the same entry point for both. No more hours of dealing with “oh, wait, how do I get the dependencies I installed with miniconda to talk to those I installed with pip?“. Now, your $PATH variable will point to the same place for both Python and dependencies.

As an added bonus, these versions can be set and managed on a per-project level using a .tool-versions file.

What a country, eh?!

Step 2: Configuring poetry

Now that we have poetry installed, we need to configure it to create virtual environments within our project directory. This configuration ensures that poetry creates a .venv directory inside your project, making it easier for VSCode to find and use the correct virtual environment.

Go ahead and create a poetry.toml file in your project root:

[virtualenvs]
in-project = true

Step 3: Setting up your project

With asdf and poetry configured, we can set up the project. This step is a boilerplate step, feel free to skip it if you’ve got a functioning fastapi project.

  1. Initialize your project with poetry:

    poetry init
    

    This will create a pyproject.toml file, which is where poetry will manage your project’s dependencies.

  2. Add your project dependencies:

    poetry add fastapi
    poetry add uvicorn
    

    These commands will add FastAPI and Uvicorn to your project and update the pyproject.toml file.

  3. Create your hello world server

    mkdir -p app && touch app/main.py && cat << EOF > app/main.py
    from fastapi import FastAPI
    
    app = FastAPI()
    
    @app.get("/")
    async def root():
        return {"message": "Hello World"}
    EOF
    

    This command will create the app/main.py file that fastapi looks for to serve the hello world program on http://localhost:8000.

Step 4: Configuring VSCode

By this point, I should be able to start a fastapi server from the command line by running poetry run fastapi dev app/main.py.

But I promised a good debugging experience. So, for the pièce de résistance - setting up VSCode for seamless debugging.

Create a .vscode/launch.json file in your project root:

{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "FastAPI Dev Server",
      "type": "debugpy",
      "request": "launch",
      "module": "fastapi",
      "args": ["dev", "app/main.py"],
      "jinja": true,
      "justMyCode": false,
      "env": {
        "PYTHONPATH": "${workspaceFolder}"
      },
      "envFile": "${workspaceFolder}/app/.env",
      "python": "${workspaceFolder}/.venv/bin/python"
    }
  ]
}

I think the most important pieces to pay attention to here are the "module", "args", and "python" arguments.

The "python" argument tells VS Code that it should use the python binary in the virtual environment. It also makes available dependencies that were installed in the virtual environment.

The "module" is the actual CLI command that I’m calling. So here, you could swap out fastapi for uvicorn, or presumably any other module that’s available on the CLI.

The "args" list represents what should come after the fastapi command. Say you wanted to throw additional CLI flags, you’d do it in the "args" list.

Step 5: Running and Debugging

To debug in VSCode:

  1. Set your breakpoints in your code.
  2. Press F5 or use the “Run and Debug” sidebar to start the “FastAPI Dev Server” configuration.
  3. VSCode will launch your FastAPI application and pause at your breakpoints, allowing you to step through your code, inspect variables, and debug like a pro.

The Payoff

By adopting this setup, we’ve gained several advantages:

  1. Version Consistency: asdf helps enforce the same Python and poetry versions across contributors machines.
  2. Dependency Isolation: poetry creates project-specific virtual environments, preventing conflicts between projects and making it easy to manage dependencies.
  3. Seamless Debugging: The VSCode configuration allows for interactive debugging directly from the IDE, no more print statement debugging!
  4. Reproducibility: The combination of asdf and poetry makes it trivial to recreate the development environment on any machine.

Wrapping up

Hopefully making these changes reduce your pain when it comes to dependency conflicts, environment mismatches, and print statement debugging. It certainly reduced mine.

Happy coding!

Back to Blog