Contents
  1. Why Paths Break During Debugging
  2. The Role of launch.json
  3. The Two Fields That Solve Most Path Problems
  4. Building Paths in Code with pathlib
  5. Passing Arguments
  6. Using // Comments to Organise Configurations
  7. A Complete Reference Configuration
  8. What You Can Do Now
← All posts

Python Debugging in VSCode: File Paths, Venvs, and launch.json

When Python's debugger cannot find your modules or reads the wrong file paths, the problem is almost always the working directory mismatch. launch.json and pathlib are where you fix it.

Running a Python file directly with the play button in VSCode and running it through the debugger are not the same thing. The debugger launches Python in its own process with its own working directory. When that process disagrees with your file’s assumed location, you get ModuleNotFoundError, wrong file paths, or relative paths that resolve to somewhere entirely unexpected. The fix lives in .vscode/launch.json and in how you write paths inside your code.

Why Paths Break During Debugging

Python resolves relative file paths from the working directory of the process (cwd). When you run a script from the terminal at your project root, this is set correctly by the shell. When VSCode launches the debugger, it makes its own assumptions, and those assumptions are often wrong.

The most common symptoms:

  • ModuleNotFoundError for a module that imports fine in the terminal
  • A relative path like ./data/input.csv resolving to the wrong directory
  • The debugger picking up the system Python instead of your virtual environment

All three have the same root cause: the debugger process is not starting from where you think it is.

The Role of launch.json

.vscode/launch.json controls exactly how VSCode launches your debugger: which file to run, what arguments to pass, and what directory to treat as the working directory.

Create this file at .vscode/launch.json in your project root. VSCode picks it up automatically.

{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Run main",
      "type": "debugpy",
      "request": "launch",
      "program": "${workspaceFolder}/src/main.py",
      "cwd": "${workspaceFolder}",
      "console": "integratedTerminal"
    }
  ]
}

The Two Fields That Solve Most Path Problems

program is the absolute path to the file being debugged. Always use ${workspaceFolder} as the base. It resolves to the root directory of the folder you have open in VSCode, regardless of where you are on the filesystem.

"program": "${workspaceFolder}/src/main.py"

cwd sets the working directory of the debugger process. Setting it to ${workspaceFolder} means any relative path in your code resolves from the project root, which is the same behaviour you get when running from the terminal there.

"cwd": "${workspaceFolder}"

Without cwd set explicitly, VSCode defaults to the directory of the file being debugged. If your script is at src/pipeline/run.py, the working directory becomes src/pipeline/, and any relative path expecting the project root silently resolves to the wrong place.

Building Paths in Code with pathlib

Even with cwd set correctly in launch.json, hardcoded string paths in your code are fragile. A string like "./data/input.csv" breaks silently on a different OS, in a different working directory, or when the project is moved.

pathlib.Path is Python’s standard library solution. It represents paths as objects and overloads the / operator to join path segments cleanly.

from pathlib import Path

# Anchor to the current file's location
BASE_DIR = Path(__file__).parent

# Build paths from there
data_path   = BASE_DIR / "data" / "input.csv"
output_path = BASE_DIR / "output" / "results.json"
config_path = BASE_DIR.parent / "config" / "settings.toml"

Path(__file__) is always the path to the file being executed. .parent moves up one directory. The / operator joins segments, equivalent to os.path.join() but readable and chainable.

This matters during debugging because __file__ is anchored to the file’s location, not the working directory. A path built from Path(__file__).parent resolves correctly whether you run the script from the terminal, the play button, or the debugger.

from pathlib import Path

# If this file is at src/main.py, .parent.parent is the project root
BASE_DIR = Path(__file__).parent.parent

input_file = BASE_DIR / "data" / "raw" / "input.csv"
output_dir = BASE_DIR / "output"
output_dir.mkdir(parents=True, exist_ok=True)

with open(input_file) as f:
    data = f.read()

cwd in launch.json controls where string paths resolve. Path(__file__) controls where pathlib paths resolve. Use pathlib inside your code and cwd to anchor the debugger process.

Passing Arguments

Scripts that accept command-line arguments use the args field. Each flag and its value is a separate string in the array:

"args": ["--mode", "dry-run", "--limit", "100"]

This is equivalent to running:

python src/main.py --mode dry-run --limit 100

For file paths passed as arguments, use relative paths anchored to cwd:

"args": [
  "--input", "./data/raw/input.csv",
  "--output", "./data/processed/"
]

Using // Comments to Organise Configurations

A project with multiple scripts or run modes accumulates many configurations quickly. launch.json supports // comments, which standard JSON does not allow but VSCode’s parser accepts. Use them to group configurations into labelled sections so the debugger dropdown stays readable.

{
  "version": "0.2.0",
  "configurations": [

    // ═══════════════════════════════════════════════
    // DEVELOPMENT
    // ═══════════════════════════════════════════════
    {
      "name": "Main (dry-run)",
      "type": "debugpy",
      "request": "launch",
      "program": "${workspaceFolder}/src/main.py",
      "args": ["--mode", "dry-run"],
      "cwd": "${workspaceFolder}",
      "console": "integratedTerminal"
    },
    {
      "name": "Main (real)",
      "type": "debugpy",
      "request": "launch",
      "program": "${workspaceFolder}/src/main.py",
      "args": ["--mode", "run"],
      "cwd": "${workspaceFolder}",
      "console": "integratedTerminal"
    },

    // ═══════════════════════════════════════════════
    // UTILITIES
    // ═══════════════════════════════════════════════
    {
      "name": "Data Validator",
      "type": "debugpy",
      "request": "launch",
      "program": "${workspaceFolder}/src/utils/validate.py",
      "args": ["--input", "./data/raw/"],
      "cwd": "${workspaceFolder}",
      "console": "integratedTerminal"
    },
    {
      "name": "Report Generator",
      "type": "debugpy",
      "request": "launch",
      "program": "${workspaceFolder}/src/reports/generate.py",
      "args": ["--output-dir", "./output/reports/"],
      "cwd": "${workspaceFolder}",
      "console": "integratedTerminal"
    }

  ]
}

A Complete Reference Configuration

{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Run main (dry-run)",
      "type": "debugpy",
      "request": "launch",
      "program": "${workspaceFolder}/src/main.py",
      "args": ["--mode", "dry-run", "--limit", "10"],
      "cwd": "${workspaceFolder}",
      "console": "integratedTerminal"
    }
  ]
}
FieldPurpose
nameLabel shown in the debugger dropdown
typeAlways debugpy for Python
requestAlways launch to start a new process
programAbsolute path to the file to run
argsCommand-line arguments as an array
cwdWorking directory for the debugger process
consoleUse integratedTerminal to see print output

What You Can Do Now

Take any Python project you have open in VSCode and do the following:

Step 1. Create .vscode/launch.json with one entry for the main script you run:

{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Run",
      "type": "debugpy",
      "request": "launch",
      "program": "${workspaceFolder}/src/main.py",
      "cwd": "${workspaceFolder}",
      "console": "integratedTerminal"
    }
  ]
}

Step 2. Open any file in your project that builds a file path with a string and replace it with pathlib:

# Before
data_path = "./data/input.csv"

# After
from pathlib import Path
data_path = Path(__file__).parent.parent / "data" / "input.csv"

Step 3. Press F5 in VSCode. Select your configuration from the dropdown. If it runs correctly in the debugger with no path errors, both fixes are working. If it still breaks, check that cwd is ${workspaceFolder} and that your pathlib path climbs the right number of .parent levels to reach the file you need.

← All posts