[{"data":1,"prerenderedAt":1577},["ShallowReactive",2],{"doc:\u002Fpyqgis-fundamentals-environment-setup\u002Fsetting-up-pycharm-for-qgis\u002Fconfigure-vs-code-for-pyqgis-development":3},{"id":4,"title":5,"body":6,"description":1566,"extension":1567,"meta":1568,"navigation":1572,"path":1573,"seo":1574,"stem":1575,"__hash__":1576},"docs\u002Fpyqgis-fundamentals-environment-setup\u002Fsetting-up-pycharm-for-qgis\u002Fconfigure-vs-code-for-pyqgis-development\u002Findex.md","Configure VS Code for PyQGIS Development",{"type":7,"value":8,"toc":1553},"minimark",[9,13,22,31,157,162,192,196,199,230,319,355,362,366,369,531,555,559,562,630,645,772,793,916,920,923,930,1196,1208,1218,1235,1255,1259,1262,1278,1282,1288,1376,1380,1450,1454,1466,1470,1484,1494,1511,1517,1521,1549],[10,11,5],"h1",{"id":12},"configure-vs-code-for-pyqgis-development",[14,15,16,17,21],"p",{},"VS Code does not know where QGIS is, and QGIS does not know VS Code exists. Bridging them takes three settings and one launch configuration — after which imports resolve, completion works, and a breakpoint set in the editor stops inside the running application. Getting it wrong produces the familiar experience of red squiggles under every ",[18,19,20],"code",{},"qgis"," import while the code runs perfectly.",[14,23,24,25,30],{},"This recipe belongs to ",[26,27,29],"a",{"href":28},"\u002Fpyqgis-fundamentals-environment-setup\u002Fsetting-up-pycharm-for-qgis\u002F","Setting Up PyCharm for QGIS",", which covers the same ground for the other common editor. It covers selecting the right interpreter per platform, adding the QGIS Python paths so analysis resolves, launching a standalone script with the environment set, and attaching to a running QGIS for plugin work.",[14,32,33],{},[34,35,40,44,48,55,64,74,80,86,91,95,99,105,109,112,115,118,121,124,127,132,136,139,142,145,148,151],"svg",{"viewBox":36,"role":37,"ariaLabel":38,"xmlns":39},"0 0 760 304","img","Three separate concerns in an editor setup: which interpreter runs the code, which paths the analyser reads for completion, and which process the debugger attaches to","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg",[41,42,43],"title",{},"Three things that all look like \"the interpreter\"",[45,46,47],"desc",{},"Selecting an interpreter decides what runs a script. Extra analysis paths decide what the language server can see for completion and error checking, and are configured separately. The debugger target is a third choice: a script launched by the editor, or the QGIS process already running.",[49,50],"rect",{"x":51,"y":51,"width":52,"height":53,"fill":54},"0","760","304","#f6f3ea",[56,57,63],"text",{"x":58,"y":59,"style":60,"fill":61,"textAnchor":62},"380","28","text-anchor:middle;font-size:14px;font-weight:bold;font-family:sans-serif","#17211d","middle","Configure all three, or two of them will look broken",[49,65],{"x":66,"y":67,"width":68,"height":69,"rx":70,"fill":71,"stroke":72,"style":73},"20","56","228","200","10","#eff3ff","#2563eb","stroke-width:2.5",[56,75,79],{"x":76,"y":77,"style":78,"fill":72,"textAnchor":62},"134","84","text-anchor:middle;font-size:11.5px;font-weight:bold;font-family:sans-serif","1 · interpreter",[56,81,85],{"x":76,"y":82,"style":83,"fill":84,"textAnchor":62},"112","text-anchor:middle;font-size:10.5px;font-family:sans-serif","#2f3b35","what runs the code",[56,87,90],{"x":76,"y":88,"style":89,"fill":84,"textAnchor":62},"138","text-anchor:middle;font-size:10px;font-family:monospace","python.defaultInterpreter",[56,92,94],{"x":76,"y":93,"style":83,"fill":84,"textAnchor":62},"164","the QGIS Python,",[56,96,98],{"x":76,"y":97,"style":83,"fill":84,"textAnchor":62},"182","or a conda env",[56,100,104],{"x":76,"y":101,"style":102,"fill":103,"textAnchor":62},"216","text-anchor:middle;font-size:10px;font-family:sans-serif","#b45309","wrong → imports fail at run",[49,106],{"x":107,"y":67,"width":68,"height":69,"rx":70,"fill":108,"stroke":103,"style":73},"266","#fdf2e2",[56,110,111],{"x":58,"y":77,"style":78,"fill":103,"textAnchor":62},"2 · analysis paths",[56,113,114],{"x":58,"y":82,"style":83,"fill":84,"textAnchor":62},"what completion sees",[56,116,117],{"x":58,"y":88,"style":89,"fill":84,"textAnchor":62},"python.analysis.extraPaths",[56,119,120],{"x":58,"y":93,"style":83,"fill":84,"textAnchor":62},"the QGIS python dir",[56,122,123],{"x":58,"y":97,"style":83,"fill":84,"textAnchor":62},"and plugins dir",[56,125,126],{"x":58,"y":101,"style":102,"fill":103,"textAnchor":62},"wrong → red squiggles only",[49,128],{"x":129,"y":67,"width":68,"height":69,"rx":70,"fill":130,"stroke":131,"style":73},"512","#edf8e9","#15803d",[56,133,135],{"x":134,"y":77,"style":78,"fill":131,"textAnchor":62},"626","3 · debug target",[56,137,138],{"x":134,"y":82,"style":83,"fill":84,"textAnchor":62},"what stops at a breakpoint",[56,140,141],{"x":134,"y":88,"style":89,"fill":84,"textAnchor":62},"launch.json",[56,143,144],{"x":134,"y":93,"style":83,"fill":84,"textAnchor":62},"launch a script, or",[56,146,147],{"x":134,"y":97,"style":83,"fill":84,"textAnchor":62},"attach to QGIS",[56,149,150],{"x":134,"y":101,"style":102,"fill":103,"textAnchor":62},"wrong → breakpoints ignored",[56,152,156],{"x":58,"y":153,"style":154,"fill":155,"textAnchor":62},"286","text-anchor:middle;font-size:11px;font-family:sans-serif","#59645f","a plugin needs the third; a standalone script needs the first two",[158,159,161],"h2",{"id":160},"prerequisites","Prerequisites",[163,164,165,178,185],"ul",{},[166,167,168,172,173,177],"li",{},[169,170,171],"strong",{},"QGIS 3.34 LTR"," (bundled Python 3.12) or newer installed, or a ",[26,174,176],{"href":175},"\u002Fpyqgis-fundamentals-environment-setup\u002Fvirtual-environments-for-gis\u002Fuse-pyqgis-with-conda-and-mamba\u002F","conda environment"," containing QGIS.",[166,179,180,181,184],{},"VS Code with the ",[169,182,183],{},"Python"," extension. Pylance provides the analysis; the older Jedi backend also works but resolves the QGIS bindings less well.",[166,186,187,188,191],{},"For debugging inside QGIS, the ",[18,189,190],{},"debugpy"," package importable by the QGIS Python.",[158,193,195],{"id":194},"point-at-the-right-interpreter","Point at the right interpreter",[14,197,198],{},"Which Python to select depends on the platform, and the file to find is not always obvious.",[14,200,201,202,205,206,209,210,213,214,217,218,221,222,225,226,229],{},"On ",[169,203,204],{},"Linux"," with a package-managed QGIS, the system ",[18,207,208],{},"python3"," already has the bindings on its path, so the system interpreter works. On ",[169,211,212],{},"Windows",", QGIS ships its own: ",[18,215,216],{},"C:\\Program Files\\QGIS 3.34\\apps\\Python312\\python.exe",", reached through the ",[18,219,220],{},"python-qgis.bat"," wrapper that sets the environment first. On ",[169,223,224],{},"macOS",", it is inside the application bundle at ",[18,227,228],{},"QGIS.app\u002FContents\u002FMacOS\u002Fbin\u002Fpython3",".",[231,232,237],"pre",{"className":233,"code":234,"language":235,"meta":236,"style":236},"language-json shiki shiki-themes github-dark","{\n  \"python.defaultInterpreterPath\": \"\u002Fusr\u002Fbin\u002Fpython3\",\n  \"python.analysis.extraPaths\": [\n    \"\u002Fusr\u002Fshare\u002Fqgis\u002Fpython\",\n    \"\u002Fusr\u002Fshare\u002Fqgis\u002Fpython\u002Fplugins\",\n    \"${workspaceFolder}\"\n  ],\n  \"python.analysis.typeCheckingMode\": \"basic\"\n}\n","json","",[18,238,239,248,265,274,282,290,296,302,313],{"__ignoreMap":236},[240,241,244],"span",{"class":242,"line":243},"line",1,[240,245,247],{"class":246},"s95oV","{\n",[240,249,251,255,258,262],{"class":242,"line":250},2,[240,252,254],{"class":253},"sDLfK","  \"python.defaultInterpreterPath\"",[240,256,257],{"class":246},": ",[240,259,261],{"class":260},"sU2Wk","\"\u002Fusr\u002Fbin\u002Fpython3\"",[240,263,264],{"class":246},",\n",[240,266,268,271],{"class":242,"line":267},3,[240,269,270],{"class":253},"  \"python.analysis.extraPaths\"",[240,272,273],{"class":246},": [\n",[240,275,277,280],{"class":242,"line":276},4,[240,278,279],{"class":260},"    \"\u002Fusr\u002Fshare\u002Fqgis\u002Fpython\"",[240,281,264],{"class":246},[240,283,285,288],{"class":242,"line":284},5,[240,286,287],{"class":260},"    \"\u002Fusr\u002Fshare\u002Fqgis\u002Fpython\u002Fplugins\"",[240,289,264],{"class":246},[240,291,293],{"class":242,"line":292},6,[240,294,295],{"class":260},"    \"${workspaceFolder}\"\n",[240,297,299],{"class":242,"line":298},7,[240,300,301],{"class":246},"  ],\n",[240,303,305,308,310],{"class":242,"line":304},8,[240,306,307],{"class":253},"  \"python.analysis.typeCheckingMode\"",[240,309,257],{"class":246},[240,311,312],{"class":260},"\"basic\"\n",[240,314,316],{"class":242,"line":315},9,[240,317,318],{"class":246},"}\n",[14,320,321,324,325,328,329,331,332,335,336,339,340,342,343,346,347,350,351,354],{},[169,322,323],{},"Breakdown:"," ",[18,326,327],{},"extraPaths"," is what fixes the squiggles — it tells the language server where to find ",[18,330,20],{}," and ",[18,333,334],{},"processing"," without affecting what actually runs. The ",[18,337,338],{},"plugins"," directory matters because ",[18,341,334],{}," lives there rather than in the main python directory, which is why ",[18,344,345],{},"import processing"," resolves for the runtime and not for the analyser until this is set. Including ",[18,348,349],{},"${workspaceFolder}"," lets a plugin's own modules resolve when the plugin package is the workspace root. ",[18,352,353],{},"typeCheckingMode: \"basic\""," is a good middle setting: it catches real mistakes without flagging every dynamically typed Qt call.",[14,356,357,358,361],{},"These belong in ",[18,359,360],{},".vscode\u002Fsettings.json"," inside the project rather than in user settings, so the paths travel with the repository and a colleague on a different platform can override only what differs.",[158,363,365],{"id":364},"launch-a-standalone-script","Launch a standalone script",[14,367,368],{},"A script that initialises QGIS itself needs the environment set before Python starts.",[231,370,372],{"className":233,"code":371,"language":235,"meta":236,"style":236},"{\n  \"version\": \"0.2.0\",\n  \"configurations\": [\n    {\n      \"name\": \"PyQGIS: run script\",\n      \"type\": \"debugpy\",\n      \"request\": \"launch\",\n      \"program\": \"${file}\",\n      \"console\": \"integratedTerminal\",\n      \"justMyCode\": false,\n      \"env\": {\n        \"QT_QPA_PLATFORM\": \"offscreen\",\n        \"PYTHONPATH\": \"\u002Fusr\u002Fshare\u002Fqgis\u002Fpython:\u002Fusr\u002Fshare\u002Fqgis\u002Fpython\u002Fplugins\"\n      }\n    }\n  ]\n}\n",[18,373,374,378,390,397,402,414,426,438,450,462,475,484,497,508,514,520,526],{"__ignoreMap":236},[240,375,376],{"class":242,"line":243},[240,377,247],{"class":246},[240,379,380,383,385,388],{"class":242,"line":250},[240,381,382],{"class":253},"  \"version\"",[240,384,257],{"class":246},[240,386,387],{"class":260},"\"0.2.0\"",[240,389,264],{"class":246},[240,391,392,395],{"class":242,"line":267},[240,393,394],{"class":253},"  \"configurations\"",[240,396,273],{"class":246},[240,398,399],{"class":242,"line":276},[240,400,401],{"class":246},"    {\n",[240,403,404,407,409,412],{"class":242,"line":284},[240,405,406],{"class":253},"      \"name\"",[240,408,257],{"class":246},[240,410,411],{"class":260},"\"PyQGIS: run script\"",[240,413,264],{"class":246},[240,415,416,419,421,424],{"class":242,"line":292},[240,417,418],{"class":253},"      \"type\"",[240,420,257],{"class":246},[240,422,423],{"class":260},"\"debugpy\"",[240,425,264],{"class":246},[240,427,428,431,433,436],{"class":242,"line":298},[240,429,430],{"class":253},"      \"request\"",[240,432,257],{"class":246},[240,434,435],{"class":260},"\"launch\"",[240,437,264],{"class":246},[240,439,440,443,445,448],{"class":242,"line":304},[240,441,442],{"class":253},"      \"program\"",[240,444,257],{"class":246},[240,446,447],{"class":260},"\"${file}\"",[240,449,264],{"class":246},[240,451,452,455,457,460],{"class":242,"line":315},[240,453,454],{"class":253},"      \"console\"",[240,456,257],{"class":246},[240,458,459],{"class":260},"\"integratedTerminal\"",[240,461,264],{"class":246},[240,463,465,468,470,473],{"class":242,"line":464},10,[240,466,467],{"class":253},"      \"justMyCode\"",[240,469,257],{"class":246},[240,471,472],{"class":253},"false",[240,474,264],{"class":246},[240,476,478,481],{"class":242,"line":477},11,[240,479,480],{"class":253},"      \"env\"",[240,482,483],{"class":246},": {\n",[240,485,487,490,492,495],{"class":242,"line":486},12,[240,488,489],{"class":253},"        \"QT_QPA_PLATFORM\"",[240,491,257],{"class":246},[240,493,494],{"class":260},"\"offscreen\"",[240,496,264],{"class":246},[240,498,500,503,505],{"class":242,"line":499},13,[240,501,502],{"class":253},"        \"PYTHONPATH\"",[240,504,257],{"class":246},[240,506,507],{"class":260},"\"\u002Fusr\u002Fshare\u002Fqgis\u002Fpython:\u002Fusr\u002Fshare\u002Fqgis\u002Fpython\u002Fplugins\"\n",[240,509,511],{"class":242,"line":510},14,[240,512,513],{"class":246},"      }\n",[240,515,517],{"class":242,"line":516},15,[240,518,519],{"class":246},"    }\n",[240,521,523],{"class":242,"line":522},16,[240,524,525],{"class":246},"  ]\n",[240,527,529],{"class":242,"line":528},17,[240,530,318],{"class":246},[14,532,533,324,535,538,539,542,543,546,547,550,551,554],{},[169,534,323],{},[18,536,537],{},"justMyCode: false"," is worth setting deliberately — with the default ",[18,540,541],{},"true",", stepping into QGIS's own Python (the Processing framework, for instance) is impossible, and a good deal of debugging in this area is exactly that. ",[18,544,545],{},"QT_QPA_PLATFORM=offscreen"," lets the script run without a display, which is what you want for anything destined for a server. ",[18,548,549],{},"${file}"," runs whatever is open; a fixed ",[18,552,553],{},"program"," path is better once the entry point settles.",[158,556,558],{"id":557},"attach-to-a-running-qgis","Attach to a running QGIS",[14,560,561],{},"Plugin code runs inside the QGIS process, so the debugger has to attach rather than launch.",[231,563,567],{"className":564,"code":565,"language":566,"meta":236,"style":236},"language-python shiki shiki-themes github-dark","# paste into the QGIS Python console once per session\nimport debugpy\ndebugpy.configure(python=\"\u002Fusr\u002Fbin\u002Fpython3\")\ndebugpy.listen((\"127.0.0.1\", 5678))\nprint(\"waiting for the debugger to attach…\")\n","python",[18,568,569,575,584,600,617],{"__ignoreMap":236},[240,570,571],{"class":242,"line":243},[240,572,574],{"class":573},"sjoCn","# paste into the QGIS Python console once per session\n",[240,576,577,581],{"class":242,"line":250},[240,578,580],{"class":579},"snl16","import",[240,582,583],{"class":246}," debugpy\n",[240,585,586,589,592,595,597],{"class":242,"line":267},[240,587,588],{"class":246},"debugpy.configure(",[240,590,566],{"class":591},"s9osk",[240,593,594],{"class":579},"=",[240,596,261],{"class":260},[240,598,599],{"class":246},")\n",[240,601,602,605,608,611,614],{"class":242,"line":276},[240,603,604],{"class":246},"debugpy.listen((",[240,606,607],{"class":260},"\"127.0.0.1\"",[240,609,610],{"class":246},", ",[240,612,613],{"class":253},"5678",[240,615,616],{"class":246},"))\n",[240,618,619,622,625,628],{"class":242,"line":284},[240,620,621],{"class":253},"print",[240,623,624],{"class":246},"(",[240,626,627],{"class":260},"\"waiting for the debugger to attach…\"",[240,629,599],{"class":246},[14,631,632,324,634,637,638,641,642,644],{},[169,633,323],{},[18,635,636],{},"listen()"," opens a port and returns immediately; QGIS stays responsive. ",[18,639,640],{},"debugpy.configure(python=...)"," is needed when the QGIS interpreter is not the one debugpy would pick for its helper process, which is the usual case on Windows and macOS. Calling ",[18,643,636],{}," twice in one session raises, so wrap it in a guard if it goes into a plugin's startup rather than being pasted by hand.",[231,646,648],{"className":233,"code":647,"language":235,"meta":236,"style":236},"{\n  \"name\": \"PyQGIS: attach to QGIS\",\n  \"type\": \"debugpy\",\n  \"request\": \"attach\",\n  \"connect\": { \"host\": \"127.0.0.1\", \"port\": 5678 },\n  \"justMyCode\": false,\n  \"pathMappings\": [\n    {\n      \"localRoot\": \"${workspaceFolder}\",\n      \"remoteRoot\": \"${env:HOME}\u002F.local\u002Fshare\u002FQGIS\u002FQGIS3\u002Fprofiles\u002Fdefault\u002Fpython\u002Fplugins\u002Fmy_plugin\"\n    }\n  ]\n}\n",[18,649,650,654,666,677,689,716,727,734,738,750,760,764,768],{"__ignoreMap":236},[240,651,652],{"class":242,"line":243},[240,653,247],{"class":246},[240,655,656,659,661,664],{"class":242,"line":250},[240,657,658],{"class":253},"  \"name\"",[240,660,257],{"class":246},[240,662,663],{"class":260},"\"PyQGIS: attach to QGIS\"",[240,665,264],{"class":246},[240,667,668,671,673,675],{"class":242,"line":267},[240,669,670],{"class":253},"  \"type\"",[240,672,257],{"class":246},[240,674,423],{"class":260},[240,676,264],{"class":246},[240,678,679,682,684,687],{"class":242,"line":276},[240,680,681],{"class":253},"  \"request\"",[240,683,257],{"class":246},[240,685,686],{"class":260},"\"attach\"",[240,688,264],{"class":246},[240,690,691,694,697,700,702,704,706,709,711,713],{"class":242,"line":284},[240,692,693],{"class":253},"  \"connect\"",[240,695,696],{"class":246},": { ",[240,698,699],{"class":253},"\"host\"",[240,701,257],{"class":246},[240,703,607],{"class":260},[240,705,610],{"class":246},[240,707,708],{"class":253},"\"port\"",[240,710,257],{"class":246},[240,712,613],{"class":253},[240,714,715],{"class":246}," },\n",[240,717,718,721,723,725],{"class":242,"line":292},[240,719,720],{"class":253},"  \"justMyCode\"",[240,722,257],{"class":246},[240,724,472],{"class":253},[240,726,264],{"class":246},[240,728,729,732],{"class":242,"line":298},[240,730,731],{"class":253},"  \"pathMappings\"",[240,733,273],{"class":246},[240,735,736],{"class":242,"line":304},[240,737,401],{"class":246},[240,739,740,743,745,748],{"class":242,"line":315},[240,741,742],{"class":253},"      \"localRoot\"",[240,744,257],{"class":246},[240,746,747],{"class":260},"\"${workspaceFolder}\"",[240,749,264],{"class":246},[240,751,752,755,757],{"class":242,"line":464},[240,753,754],{"class":253},"      \"remoteRoot\"",[240,756,257],{"class":246},[240,758,759],{"class":260},"\"${env:HOME}\u002F.local\u002Fshare\u002FQGIS\u002FQGIS3\u002Fprofiles\u002Fdefault\u002Fpython\u002Fplugins\u002Fmy_plugin\"\n",[240,761,762],{"class":242,"line":477},[240,763,519],{"class":246},[240,765,766],{"class":242,"line":486},[240,767,525],{"class":246},[240,769,770],{"class":242,"line":499},[240,771,318],{"class":246},[14,773,774,324,776,779,780,783,784,788,789,229],{},[169,775,323],{},[18,777,778],{},"pathMappings"," is the setting that makes breakpoints actually bind. The editor knows the file as ",[18,781,782],{},"~\u002Fdev\u002Fmy_plugin\u002Fdialog.py"," while QGIS loaded it from the profile's plugins directory; without a mapping, the debugger attaches successfully and every breakpoint stays hollow. Where the plugin is developed ",[785,786,787],"em",{},"in place"," — the profile directory symlinked to the repository — the mapping is unnecessary, and that arrangement is worth setting up for exactly this reason. More on the mechanics in ",[26,790,792],{"href":791},"\u002Fpyqgis-fundamentals-environment-setup\u002Fsetting-up-pycharm-for-qgis\u002Fdebug-qgis-plugin-with-debugpy\u002F","debugging a QGIS plugin with debugpy",[14,794,795],{},[34,796,799,802,805,808,824,827,832,837,845,850,854,857,861,868,873,877,883,887,891,895,899,903,906,909,912],{"viewBox":797,"role":37,"ariaLabel":798,"xmlns":39},"0 0 760 288","The editor and QGIS as separate processes connected by a debug adapter port, with a path mapping translating between the repository location and the installed plugin location",[41,800,801],{},"Attaching across two processes",[45,803,804],{},"VS Code runs in one process and QGIS in another. The debugpy library inside QGIS listens on a local port, and the editor connects to it. Because the editor sees files in the repository and QGIS loaded them from the profile plugins directory, a path mapping is required for breakpoints to bind.",[49,806],{"x":51,"y":51,"width":52,"height":807,"fill":54},"288",[809,810,811],"defs",{},[812,813,820],"marker",{"id":814,"viewBox":815,"refX":816,"refY":817,"markerWidth":818,"markerHeight":818,"orient":819},"dbgArrow","0 0 10 10","8","5","7","auto-start-reverse",[821,822],"path",{"d":823,"fill":84},"M0 0 L10 5 L0 10 z",[56,825,826],{"x":58,"y":59,"style":60,"fill":61,"textAnchor":62},"Two processes, two views of the same file",[49,828],{"x":829,"y":830,"width":107,"height":831,"rx":70,"fill":71,"stroke":72,"style":73},"24","60","180",[56,833,836],{"x":834,"y":835,"style":78,"fill":72,"textAnchor":62},"157","88","VS Code",[49,838],{"x":839,"y":840,"width":841,"height":842,"rx":817,"fill":843,"stroke":72,"style":844},"48","106","218","46","#dbeafe","stroke-width:1.6",[56,846,849],{"x":834,"y":847,"style":848,"fill":84,"textAnchor":62},"126","text-anchor:middle;font-size:9.5px;font-family:monospace","~\u002Fdev\u002Fmy_plugin\u002F",[56,851,853],{"x":834,"y":852,"style":848,"fill":84,"textAnchor":62},"142","dialog.py",[56,855,856],{"x":834,"y":831,"style":102,"fill":84,"textAnchor":62},"breakpoint set here",[56,858,860],{"x":834,"y":859,"style":102,"fill":155,"textAnchor":62},"208","localRoot",[49,862],{"x":863,"y":864,"width":865,"height":67,"rx":816,"fill":866,"stroke":867,"style":73},"318","122","124","#eef7f4","#0f766e",[56,869,872],{"x":58,"y":870,"style":871,"fill":867,"textAnchor":62},"146","text-anchor:middle;font-size:10.5px;font-weight:bold;font-family:sans-serif","port 5678",[56,874,190],{"x":58,"y":875,"style":876,"fill":84,"textAnchor":62},"166","text-anchor:middle;font-size:9.5px;font-family:sans-serif",[242,878],{"x1":879,"y1":880,"x2":881,"y2":880,"stroke":84,"style":882},"294","150","312","stroke-width:2;marker-end:url(#dbgArrow)",[242,884],{"x1":885,"y1":880,"x2":886,"y2":880,"stroke":84,"style":882},"448","466",[49,888],{"x":889,"y":830,"width":890,"height":831,"rx":70,"fill":130,"stroke":131,"style":73},"472","264",[56,892,894],{"x":893,"y":835,"style":78,"fill":131,"textAnchor":62},"604","QGIS process",[49,896],{"x":897,"y":840,"width":101,"height":842,"rx":817,"fill":898,"stroke":131,"style":844},"496","#e8efe6",[56,900,902],{"x":893,"y":847,"style":901,"fill":84,"textAnchor":62},"text-anchor:middle;font-size:9px;font-family:monospace","…\u002Fprofiles\u002Fdefault\u002Fpython\u002F",[56,904,905],{"x":893,"y":852,"style":901,"fill":84,"textAnchor":62},"plugins\u002Fmy_plugin\u002Fdialog.py",[56,907,908],{"x":893,"y":831,"style":102,"fill":84,"textAnchor":62},"the file actually executing",[56,910,911],{"x":893,"y":859,"style":102,"fill":155,"textAnchor":62},"remoteRoot",[56,913,915],{"x":58,"y":914,"style":154,"fill":155,"textAnchor":62},"268","without pathMappings the breakpoint stays hollow and never binds",[158,917,919],{"id":918},"tasks-linting-and-the-rest-of-the-loop","Tasks, linting and the rest of the loop",[14,921,922],{},"Two more pieces turn a working setup into a comfortable one.",[14,924,925,926,929],{},"A ",[169,927,928],{},"task"," for packaging saves remembering the incantation, and VS Code will run it from the command palette or a keybinding.",[231,931,933],{"className":233,"code":932,"language":235,"meta":236,"style":236},"{\n  \"version\": \"2.0.0\",\n  \"tasks\": [\n    {\n      \"label\": \"package plugin\",\n      \"type\": \"shell\",\n      \"command\": \"python\",\n      \"args\": [\"-m\", \"zipfile\", \"-c\", \"dist\u002Fmy_plugin.zip\", \"my_plugin\u002F\"],\n      \"group\": \"build\",\n      \"problemMatcher\": []\n    },\n    {\n      \"label\": \"run tests\",\n      \"type\": \"shell\",\n      \"command\": \"pytest\",\n      \"args\": [\"-q\", \"tests\u002F\"],\n      \"group\": \"test\",\n      \"options\": {\n        \"env\": {\n          \"QT_QPA_PLATFORM\": \"offscreen\",\n          \"PYTHONPATH\": \"\u002Fusr\u002Fshare\u002Fqgis\u002Fpython:\u002Fusr\u002Fshare\u002Fqgis\u002Fpython\u002Fplugins\"\n        }\n      },\n      \"problemMatcher\": [\"$python\"]\n    }\n  ]\n}\n",[18,934,935,939,950,957,961,973,984,996,1030,1042,1050,1055,1059,1070,1080,1091,1107,1118,1126,1134,1146,1156,1162,1168,1181,1186,1191],{"__ignoreMap":236},[240,936,937],{"class":242,"line":243},[240,938,247],{"class":246},[240,940,941,943,945,948],{"class":242,"line":250},[240,942,382],{"class":253},[240,944,257],{"class":246},[240,946,947],{"class":260},"\"2.0.0\"",[240,949,264],{"class":246},[240,951,952,955],{"class":242,"line":267},[240,953,954],{"class":253},"  \"tasks\"",[240,956,273],{"class":246},[240,958,959],{"class":242,"line":276},[240,960,401],{"class":246},[240,962,963,966,968,971],{"class":242,"line":284},[240,964,965],{"class":253},"      \"label\"",[240,967,257],{"class":246},[240,969,970],{"class":260},"\"package plugin\"",[240,972,264],{"class":246},[240,974,975,977,979,982],{"class":242,"line":292},[240,976,418],{"class":253},[240,978,257],{"class":246},[240,980,981],{"class":260},"\"shell\"",[240,983,264],{"class":246},[240,985,986,989,991,994],{"class":242,"line":298},[240,987,988],{"class":253},"      \"command\"",[240,990,257],{"class":246},[240,992,993],{"class":260},"\"python\"",[240,995,264],{"class":246},[240,997,998,1001,1004,1007,1009,1012,1014,1017,1019,1022,1024,1027],{"class":242,"line":304},[240,999,1000],{"class":253},"      \"args\"",[240,1002,1003],{"class":246},": [",[240,1005,1006],{"class":260},"\"-m\"",[240,1008,610],{"class":246},[240,1010,1011],{"class":260},"\"zipfile\"",[240,1013,610],{"class":246},[240,1015,1016],{"class":260},"\"-c\"",[240,1018,610],{"class":246},[240,1020,1021],{"class":260},"\"dist\u002Fmy_plugin.zip\"",[240,1023,610],{"class":246},[240,1025,1026],{"class":260},"\"my_plugin\u002F\"",[240,1028,1029],{"class":246},"],\n",[240,1031,1032,1035,1037,1040],{"class":242,"line":315},[240,1033,1034],{"class":253},"      \"group\"",[240,1036,257],{"class":246},[240,1038,1039],{"class":260},"\"build\"",[240,1041,264],{"class":246},[240,1043,1044,1047],{"class":242,"line":464},[240,1045,1046],{"class":253},"      \"problemMatcher\"",[240,1048,1049],{"class":246},": []\n",[240,1051,1052],{"class":242,"line":477},[240,1053,1054],{"class":246},"    },\n",[240,1056,1057],{"class":242,"line":486},[240,1058,401],{"class":246},[240,1060,1061,1063,1065,1068],{"class":242,"line":499},[240,1062,965],{"class":253},[240,1064,257],{"class":246},[240,1066,1067],{"class":260},"\"run tests\"",[240,1069,264],{"class":246},[240,1071,1072,1074,1076,1078],{"class":242,"line":510},[240,1073,418],{"class":253},[240,1075,257],{"class":246},[240,1077,981],{"class":260},[240,1079,264],{"class":246},[240,1081,1082,1084,1086,1089],{"class":242,"line":516},[240,1083,988],{"class":253},[240,1085,257],{"class":246},[240,1087,1088],{"class":260},"\"pytest\"",[240,1090,264],{"class":246},[240,1092,1093,1095,1097,1100,1102,1105],{"class":242,"line":522},[240,1094,1000],{"class":253},[240,1096,1003],{"class":246},[240,1098,1099],{"class":260},"\"-q\"",[240,1101,610],{"class":246},[240,1103,1104],{"class":260},"\"tests\u002F\"",[240,1106,1029],{"class":246},[240,1108,1109,1111,1113,1116],{"class":242,"line":528},[240,1110,1034],{"class":253},[240,1112,257],{"class":246},[240,1114,1115],{"class":260},"\"test\"",[240,1117,264],{"class":246},[240,1119,1121,1124],{"class":242,"line":1120},18,[240,1122,1123],{"class":253},"      \"options\"",[240,1125,483],{"class":246},[240,1127,1129,1132],{"class":242,"line":1128},19,[240,1130,1131],{"class":253},"        \"env\"",[240,1133,483],{"class":246},[240,1135,1137,1140,1142,1144],{"class":242,"line":1136},20,[240,1138,1139],{"class":253},"          \"QT_QPA_PLATFORM\"",[240,1141,257],{"class":246},[240,1143,494],{"class":260},[240,1145,264],{"class":246},[240,1147,1149,1152,1154],{"class":242,"line":1148},21,[240,1150,1151],{"class":253},"          \"PYTHONPATH\"",[240,1153,257],{"class":246},[240,1155,507],{"class":260},[240,1157,1159],{"class":242,"line":1158},22,[240,1160,1161],{"class":246},"        }\n",[240,1163,1165],{"class":242,"line":1164},23,[240,1166,1167],{"class":246},"      },\n",[240,1169,1171,1173,1175,1178],{"class":242,"line":1170},24,[240,1172,1046],{"class":253},[240,1174,1003],{"class":246},[240,1176,1177],{"class":260},"\"$python\"",[240,1179,1180],{"class":246},"]\n",[240,1182,1184],{"class":242,"line":1183},25,[240,1185,519],{"class":246},[240,1187,1189],{"class":242,"line":1188},26,[240,1190,525],{"class":246},[240,1192,1194],{"class":242,"line":1193},27,[240,1195,318],{"class":246},[14,1197,1198,1200,1201,1203,1204,1207],{},[169,1199,323],{}," Setting the environment on the ",[785,1202,928],{}," rather than globally keeps the terminal clean for everything else, and it is the same pair of variables the launch configuration needs — worth extracting into a shell profile if it appears a third time. ",[18,1205,1206],{},"problemMatcher: [\"$python\"]"," makes pytest failures clickable in the problems panel, which is a small thing that changes how often the tests get run. The packaging task is deliberately the same command CI will run, so a package that builds locally builds there.",[14,1209,1210,1211,610,1214,1217],{},"For ",[169,1212,1213],{},"linting",[18,1215,1216],{},"ruff"," is the pragmatic choice, and one configuration line prevents most of the noise PyQGIS code generates:",[231,1219,1223],{"className":1220,"code":1221,"language":1222,"meta":236,"style":236},"language-toml shiki shiki-themes github-dark","[tool.ruff.lint]\nignore = [\"N802\", \"N803\"]     # Qt method names are camelCase by convention\n","toml",[18,1224,1225,1230],{"__ignoreMap":236},[240,1226,1227],{"class":242,"line":243},[240,1228,1229],{},"[tool.ruff.lint]\n",[240,1231,1232],{"class":242,"line":250},[240,1233,1234],{},"ignore = [\"N802\", \"N803\"]     # Qt method names are camelCase by convention\n",[14,1236,1237,1239,1240,610,1243,1246,1247,1250,1251,1254],{},[169,1238,323],{}," Qt's naming conventions collide with PEP 8, and a plugin overriding ",[18,1241,1242],{},"initGui",[18,1244,1245],{},"unload"," or ",[18,1248,1249],{},"processAlgorithm"," cannot rename them. Silencing those two rules project-wide is better than scattering ",[18,1252,1253],{},"noqa"," comments, and it leaves the rest of the naming checks doing useful work on your own code.",[158,1256,1258],{"id":1257},"reload-without-restarting","Reload without restarting",[14,1260,1261],{},"Plugin development is a loop of edit, reload, test, and restarting QGIS each time is intolerable.",[14,1263,1264,1265,1268,1269,1272,1273,1277],{},"Install the ",[169,1266,1267],{},"Plugin Reloader"," plugin from the official repository, set it to your plugin, and bind its action to a key. It unloads and re-imports the plugin package, which picks up edits to any module inside it. What it does not pick up is changes to compiled resources or to ",[18,1270,1271],{},"metadata.txt",", and it can leave a stale module cached if the plugin imports something from outside its own package — see ",[26,1274,1276],{"href":1275},"\u002Fqgis-plugin-development\u002Fplugin-boilerplate-structure\u002Freload-qgis-plugin-without-restart\u002F","reloading a QGIS plugin without restarting"," for the details and the cases where a restart is genuinely required.",[158,1279,1281],{"id":1280},"qgis-version-compatibility","QGIS version compatibility",[14,1283,1284,1285,1287],{},"The examples target ",[169,1286,171],{}," (Python 3.12).",[1289,1290,1291,1306],"table",{},[1292,1293,1294],"thead",{},[1295,1296,1297,1301,1303],"tr",{},[1298,1299,1300],"th",{},"QGIS version",[1298,1302,183],{},[1298,1304,1305],{},"Notes",[1307,1308,1309,1328,1342,1352,1366],"tbody",{},[1295,1310,1311,1315,1318],{},[1312,1313,1314],"td",{},"3.16 LTR",[1312,1316,1317],{},"3.7",[1312,1319,1320,1321,1323,1324,1327],{},"Paths as described; ",[18,1322,190],{}," works, ",[18,1325,1326],{},"ptvsd"," was the older name.",[1295,1329,1330,1333,1336],{},[1312,1331,1332],{},"3.22 LTR",[1312,1334,1335],{},"3.9",[1312,1337,1338,1339,229],{},"Windows bundled Python moves to ",[18,1340,1341],{},"apps\\Python39",[1295,1343,1344,1347,1349],{},[1312,1345,1346],{},"3.28 LTR",[1312,1348,1335],{},[1312,1350,1351],{},"Profile directory layout unchanged.",[1295,1353,1354,1357,1360],{},[1312,1355,1356],{},"3.34 LTR",[1312,1358,1359],{},"3.12",[1312,1361,1362,1363,229],{},"Baseline for this page; Windows path is ",[18,1364,1365],{},"apps\\Python312",[1295,1367,1368,1371,1373],{},[1312,1369,1370],{},"3.40+",[1312,1372,1359],{},[1312,1374,1375],{},"Qt6 builds may require a matching PyQt stub package for completion.",[158,1377,1379],{"id":1378},"troubleshooting","Troubleshooting",[163,1381,1382,1394,1409,1419,1427,1441],{},[166,1383,1384,324,1391,1393],{},[169,1385,1386,1387,1390],{},"Red squiggles under ",[18,1388,1389],{},"import qgis"," but the code runs.",[18,1392,117],{}," is not set. It is separate from the interpreter.",[166,1395,1396,1401,1402,1405,1406,1408],{},[169,1397,1398,1400],{},[18,1399,345],{}," never resolves."," The ",[18,1403,1404],{},"python\u002Fplugins"," directory was not added; ",[18,1407,334],{}," is not in the main python directory.",[166,1410,1411,1414,1415,1418],{},[169,1412,1413],{},"The script fails to import QGIS at runtime."," The interpreter is wrong, or ",[18,1416,1417],{},"PYTHONPATH"," is not set in the launch config.",[166,1420,1421,324,1424,1426],{},[169,1422,1423],{},"The debugger attaches but breakpoints stay hollow.",[18,1425,778],{}," is missing or wrong. Compare the paths QGIS reports for the loaded module.",[166,1428,1429,324,1432,1435,1436,1438,1439,229],{},[169,1430,1431],{},"Cannot step into QGIS's own Python.",[18,1433,1434],{},"justMyCode"," is ",[18,1437,541],{},". Set it ",[18,1440,472],{},[166,1442,1443,1449],{},[169,1444,1445,1448],{},[18,1446,1447],{},"debugpy.listen"," raises on a second run."," It can only be called once per process. Guard it, or restart QGIS.",[158,1451,1453],{"id":1452},"conclusion","Conclusion",[14,1455,1456,1457,1459,1460,1462,1463,1465],{},"Set the interpreter, the analysis paths and the debug target separately — they are three different settings that all present as \"VS Code cannot find QGIS\". Keep them in the workspace's ",[18,1458,360],{}," so they travel with the repository, set ",[18,1461,1434],{}," to ",[18,1464,472],{},", and map the paths when attaching to a plugin loaded from the profile directory.",[158,1467,1469],{"id":1468},"frequently-asked-questions","Frequently Asked Questions",[14,1471,1472,1475,1476,1479,1480,229],{},[169,1473,1474],{},"Do I need QGIS type stubs?","\nNot for basic completion, which works from the shipped ",[18,1477,1478],{},".pyi"," files and the bindings themselves. Stubs help with strict type checking — see ",[26,1481,1483],{"href":1482},"\u002Fpyqgis-fundamentals-environment-setup\u002Fsetting-up-pycharm-for-qgis\u002Fenable-pyqgis-autocompletion-with-type-stubs\u002F","enabling PyQGIS autocompletion with type stubs",[14,1485,1486,1489,1490,229],{},[169,1487,1488],{},"Can I use the QGIS Python console instead?","\nFor quick experiments, yes, and it is often faster. The editor earns its place for anything with more than one file, and for breakpoints — see ",[26,1491,1493],{"href":1492},"\u002Fpyqgis-fundamentals-environment-setup\u002Fqgis-python-console-basics\u002Frun-and-save-scripts-in-qgis-python-editor\u002F","running and saving scripts in the QGIS Python editor",[14,1495,1496,1499,1500,1503,1504,1506,1507,229],{},[169,1497,1498],{},"Does this work with a remote QGIS in Docker?","\nYes — ",[18,1501,1502],{},"debugpy.listen((\"0.0.0.0\", 5678))"," inside the container, the port published, and ",[18,1505,778],{}," translating the container path to the host one. See ",[26,1508,1510],{"href":1509},"\u002Fpyqgis-fundamentals-environment-setup\u002Fheadless-qgis-and-server-automation\u002Frun-pyqgis-in-docker-container\u002F","running PyQGIS in a Docker container",[14,1512,1513,1516],{},[169,1514,1515],{},"Should the plugin live in the profile directory or the repository?","\nThe repository, with a symlink from the profile directory pointing at it. That removes the path mapping, keeps version control clean, and means a reload picks up edits immediately.",[158,1518,1520],{"id":1519},"related","Related",[163,1522,1523,1528,1533,1538,1544],{},[166,1524,1525,1527],{},[26,1526,29],{"href":28}," — the guide this recipe belongs to",[166,1529,1530],{},[26,1531,1532],{"href":791},"Debug a QGIS Plugin with debugpy",[166,1534,1535],{},[26,1536,1537],{"href":1482},"Enable PyQGIS Autocompletion with Type Stubs",[166,1539,1540],{},[26,1541,1543],{"href":1542},"\u002Fpyqgis-fundamentals-environment-setup\u002Fsetting-up-pycharm-for-qgis\u002Fbest-ide-for-qgis-plugin-development\u002F","Best IDE for QGIS Plugin Development",[166,1545,1546],{},[26,1547,1548],{"href":1275},"Reload a QGIS Plugin Without Restarting",[1550,1551,1552],"style",{},"html pre.shiki code .sjoCn, html code.shiki .sjoCn{--shiki-default:#9AA79F}html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"title":236,"searchDepth":250,"depth":250,"links":1554},[1555,1556,1557,1558,1559,1560,1561,1562,1563,1564,1565],{"id":160,"depth":250,"text":161},{"id":194,"depth":250,"text":195},{"id":364,"depth":250,"text":365},{"id":557,"depth":250,"text":558},{"id":918,"depth":250,"text":919},{"id":1257,"depth":250,"text":1258},{"id":1280,"depth":250,"text":1281},{"id":1378,"depth":250,"text":1379},{"id":1452,"depth":250,"text":1453},{"id":1468,"depth":250,"text":1469},{"id":1519,"depth":250,"text":1520},"Set up Visual Studio Code for QGIS plugin and script work — pointing the interpreter at the QGIS Python, extra analysis paths, launch configs, and attaching the debugger to a running QGIS.","md",{"slug":12,"type":1569,"breadcrumb":1570,"datePublished":1571,"dateModified":1571},"article","VS Code Setup","2026-08-27",true,"\u002Fpyqgis-fundamentals-environment-setup\u002Fsetting-up-pycharm-for-qgis\u002Fconfigure-vs-code-for-pyqgis-development",{"title":5,"description":1566},"pyqgis-fundamentals-environment-setup\u002Fsetting-up-pycharm-for-qgis\u002Fconfigure-vs-code-for-pyqgis-development\u002Findex","1ntwbIrWuI4WwOnhoHwGGPeP8GSQAsKcsEBLj40XEUY",1787823360565]