[{"data":1,"prerenderedAt":1310},["ShallowReactive",2],{"doc:\u002Fpyqgis-fundamentals-environment-setup\u002Fsetting-up-pycharm-for-qgis\u002Fdebug-qgis-plugin-with-debugpy":3},{"id":4,"title":5,"body":6,"description":1299,"extension":1300,"meta":1301,"navigation":241,"path":1306,"seo":1307,"stem":1308,"__hash__":1309},"docs\u002Fpyqgis-fundamentals-environment-setup\u002Fsetting-up-pycharm-for-qgis\u002Fdebug-qgis-plugin-with-debugpy\u002Findex.md","Debug a QGIS Plugin with debugpy",{"type":7,"value":8,"toc":1286},"minimark",[9,13,22,35,173,178,200,204,207,277,295,299,383,410,418,443,452,456,459,603,628,638,726,730,733,803,822,917,924,933,946,950,953,1012,1032,1036,1103,1109,1113,1176,1180,1192,1196,1205,1218,1227,1233,1239,1249,1253,1282],[10,11,5],"h1",{"id":12},"debug-a-qgis-plugin-with-debugpy",[14,15,16,17,21],"p",{},"Print statements work until the thing you need to inspect is a ",[18,19,20],"code",{},"QgsFeature"," inside a signal handler that fires forty times. Then what you want is a breakpoint: execution stopped, every variable visible, and the ability to step one line at a time. Getting that inside a running QGIS takes about ten minutes to set up once, and it changes how plugin development feels.",[14,23,24,25,30,31,34],{},"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",". It covers installing ",[18,32,33],{},"debugpy"," into the QGIS Python environment, starting a listener from the console, attaching from VS Code or PyCharm, mapping paths correctly, and debugging code that only runs in response to a click.",[14,36,37],{},[38,39,44,48,52,59,76,85,95,101,111,116,119,123,127,132,136,139,142,144,147,150,157,163,166,169],"svg",{"viewBox":40,"role":41,"ariaLabel":42,"xmlns":43},"0 0 760 262","img","Diagram of the debug attach architecture showing QGIS with debugpy listening on a local port and the editor connecting to it, with breakpoints flowing one way and variable state the other","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg",[45,46,47],"title",{},"How the debugger reaches into a running QGIS",[49,50,51],"desc",{},"QGIS runs with debugpy loaded, listening on a local port. The editor connects to that port as a client. Breakpoints set in the editor are sent into the running process, and when one is hit, execution pauses and the call stack and variables are sent back to the editor. QGIS keeps running the whole time; nothing is restarted.",[53,54],"rect",{"x":55,"y":55,"width":56,"height":57,"fill":58},"0","760","262","#f6f3ea",[60,61,62],"defs",{},[63,64,71],"marker",{"id":65,"viewBox":66,"refX":67,"refY":68,"markerWidth":69,"markerHeight":69,"orient":70},"dbgArrow","0 0 10 10","8","5","7","auto-start-reverse",[72,73],"path",{"d":74,"fill":75},"M0 0 L10 5 L0 10 z","#2f3b35",[77,78,84],"text",{"x":79,"y":80,"style":81,"fill":82,"textAnchor":83},"380","28","text-anchor:middle;font-size:14px;font-weight:bold;font-family:sans-serif","#17211d","middle","One process, paused on demand — no restart, no reload",[53,86],{"x":87,"y":88,"width":89,"height":90,"rx":91,"fill":92,"stroke":93,"style":94},"24","60","264","152","10","#eef7f4","#0f766e","stroke-width:2.5",[77,96,100],{"x":97,"y":98,"style":99,"fill":93,"textAnchor":83},"156","86","text-anchor:middle;font-size:12px;font-weight:bold;font-family:sans-serif","QGIS desktop",[53,102],{"x":103,"y":104,"width":105,"height":106,"rx":107,"fill":108,"stroke":109,"style":110},"46","100","220","34","6","#fffdf7","#59645f","stroke-width:1.5",[77,112,115],{"x":97,"y":113,"style":114,"fill":75,"textAnchor":83},"122","text-anchor:middle;font-size:11px;font-family:sans-serif","your plugin code",[53,117],{"x":103,"y":118,"width":105,"height":106,"rx":107,"fill":108,"stroke":109,"style":110},"142",[77,120,122],{"x":97,"y":121,"style":114,"fill":75,"textAnchor":83},"164","debugpy listening on 5678",[77,124,126],{"x":97,"y":125,"style":114,"fill":109,"textAnchor":83},"198","started from the console",[53,128],{"x":129,"y":88,"width":89,"height":90,"rx":91,"fill":130,"stroke":131,"style":94},"472","#eff3ff","#2563eb",[77,133,135],{"x":134,"y":98,"style":99,"fill":131,"textAnchor":83},"604","your editor",[53,137],{"x":138,"y":104,"width":105,"height":106,"rx":107,"fill":108,"stroke":109,"style":110},"494",[77,140,141],{"x":134,"y":113,"style":114,"fill":75,"textAnchor":83},"breakpoints in the same files",[53,143],{"x":138,"y":118,"width":105,"height":106,"rx":107,"fill":108,"stroke":109,"style":110},[77,145,146],{"x":134,"y":121,"style":114,"fill":75,"textAnchor":83},"stack, variables, step controls",[77,148,149],{"x":134,"y":125,"style":114,"fill":109,"textAnchor":83},"attaches to the port",[151,152],"line",{"x1":153,"y1":154,"x2":155,"y2":154,"stroke":131,"style":156},"292","112","466","stroke-width:2;marker-end:url(#dbgArrow)",[77,158,162],{"x":159,"y":160,"style":161,"fill":131,"textAnchor":83},"379","104","text-anchor:middle;font-size:10px;font-family:sans-serif","breakpoints in",[151,164],{"x1":155,"y1":165,"x2":153,"y2":165,"stroke":93,"style":156},"160",[77,167,168],{"x":159,"y":90,"style":161,"fill":93,"textAnchor":83},"state back out",[77,170,172],{"x":79,"y":171,"style":114,"fill":109,"textAnchor":83},"242","The files on both sides must be the same files — path mapping is where this usually goes wrong",[174,175,177],"h2",{"id":176},"prerequisites","Prerequisites",[179,180,181,189,192],"ul",{},[182,183,184,188],"li",{},[185,186,187],"strong",{},"QGIS 3.34 LTR"," (bundled Python 3.12) or newer.",[182,190,191],{},"VS Code, PyCharm Professional, or any editor supporting the Debug Adapter Protocol.",[182,193,194,195,199],{},"Permission to install a package into the QGIS Python environment — see ",[26,196,198],{"href":197},"\u002Fpyqgis-fundamentals-environment-setup\u002Fvirtual-environments-for-gis\u002Finstall-python-packages-into-qgis\u002F","Install Python Packages into the QGIS Environment"," if that is not straightforward on your platform.",[174,201,203],{"id":202},"install-debugpy-into-the-qgis-python","Install debugpy into the QGIS Python",[14,205,206],{},"The debugger must live in the Python interpreter QGIS is using, not in a separate virtual environment. Install it from the QGIS console:",[208,209,214],"pre",{"className":210,"code":211,"language":212,"meta":213,"style":213},"language-python shiki shiki-themes github-dark","import subprocess\nimport sys\n\nsubprocess.check_call([sys.executable, \"-m\", \"pip\", \"install\", \"--user\", \"debugpy\"])\n","python","",[18,215,216,228,236,243],{"__ignoreMap":213},[217,218,220,224],"span",{"class":151,"line":219},1,[217,221,223],{"class":222},"snl16","import",[217,225,227],{"class":226},"s95oV"," subprocess\n",[217,229,231,233],{"class":151,"line":230},2,[217,232,223],{"class":222},[217,234,235],{"class":226}," sys\n",[217,237,239],{"class":151,"line":238},3,[217,240,242],{"emptyLinePlaceholder":241},true,"\n",[217,244,246,249,253,256,259,261,264,266,269,271,274],{"class":151,"line":245},4,[217,247,248],{"class":226},"subprocess.check_call([sys.executable, ",[217,250,252],{"class":251},"sU2Wk","\"-m\"",[217,254,255],{"class":226},", ",[217,257,258],{"class":251},"\"pip\"",[217,260,255],{"class":226},[217,262,263],{"class":251},"\"install\"",[217,265,255],{"class":226},[217,267,268],{"class":251},"\"--user\"",[217,270,255],{"class":226},[217,272,273],{"class":251},"\"debugpy\"",[217,275,276],{"class":226},"])\n",[14,278,279,282,283,286,287,290,291,294],{},[185,280,281],{},"Breakdown:"," ",[18,284,285],{},"sys.executable"," inside the QGIS console is the QGIS Python interpreter, so this installs into the right environment regardless of what else is on the system — considerably more reliable than guessing the path. The ",[18,288,289],{},"--user"," flag avoids needing administrator rights and keeps the package out of the QGIS installation folder, which matters on Windows where that folder is often read-only and gets replaced on upgrade. On Linux, where QGIS commonly uses the system Python, the distribution's ",[18,292,293],{},"python3-debugpy"," package is an equally good route. Restart QGIS afterwards so the new package is importable.",[174,296,298],{"id":297},"start-listening-from-the-qgis-console","Start listening from the QGIS console",[208,300,302],{"className":210,"code":301,"language":212,"meta":213,"style":213},"import debugpy\n\ndebugpy.configure(python=r\"C:\u002FOSGeo4W\u002Fapps\u002FPython312\u002Fpython.exe\")   # Windows only\ndebugpy.listen((\"127.0.0.1\", 5678))\nprint(\"waiting for the debugger to attach\")\n",[18,303,304,311,315,352,368],{"__ignoreMap":213},[217,305,306,308],{"class":151,"line":219},[217,307,223],{"class":222},[217,309,310],{"class":226}," debugpy\n",[217,312,313],{"class":151,"line":230},[217,314,242],{"emptyLinePlaceholder":241},[217,316,317,320,323,326,329,332,336,340,343,345,348],{"class":151,"line":238},[217,318,319],{"class":226},"debugpy.configure(",[217,321,212],{"class":322},"s9osk",[217,324,325],{"class":222},"=",[217,327,328],{"class":222},"r",[217,330,331],{"class":251},"\"",[217,333,335],{"class":334},"sns5M","C:\u002FOSGeo4W\u002Fapps\u002FPython312\u002Fpython",[217,337,339],{"class":338},"sDLfK",".",[217,341,342],{"class":334},"exe",[217,344,331],{"class":251},[217,346,347],{"class":226},")   ",[217,349,351],{"class":350},"sjoCn","# Windows only\n",[217,353,354,357,360,362,365],{"class":151,"line":245},[217,355,356],{"class":226},"debugpy.listen((",[217,358,359],{"class":251},"\"127.0.0.1\"",[217,361,255],{"class":226},[217,363,364],{"class":338},"5678",[217,366,367],{"class":226},"))\n",[217,369,371,374,377,380],{"class":151,"line":370},5,[217,372,373],{"class":338},"print",[217,375,376],{"class":226},"(",[217,378,379],{"class":251},"\"waiting for the debugger to attach\"",[217,381,382],{"class":226},")\n",[14,384,385,282,387,390,391,394,395,398,399,402,403,406,407,409],{},[185,386,281],{},[18,388,389],{},"listen()"," opens a port and returns immediately, so QGIS stays usable while you attach — this is the important difference from ",[18,392,393],{},"wait_for_client()",", which blocks the whole application until the editor connects. Binding to ",[18,396,397],{},"127.0.0.1"," rather than ",[18,400,401],{},"0.0.0.0"," keeps the debug port off the network, which matters because a debug port is remote code execution by design. The ",[18,404,405],{},"configure()"," call is needed only on Windows, where debugpy otherwise cannot find an interpreter to launch its adapter with. Run this once per QGIS session; calling ",[18,408,389],{}," twice raises.",[14,411,412,413,417],{},"Where a bug happens during plugin ",[414,415,416],"em",{},"loading",", you do need to block:",[208,419,421],{"className":210,"code":420,"language":212,"meta":213,"style":213},"debugpy.listen((\"127.0.0.1\", 5678))\ndebugpy.wait_for_client()          # QGIS freezes here until the editor attaches\n",[18,422,423,435],{"__ignoreMap":213},[217,424,425,427,429,431,433],{"class":151,"line":219},[217,426,356],{"class":226},[217,428,359],{"class":251},[217,430,255],{"class":226},[217,432,364],{"class":338},[217,434,367],{"class":226},[217,436,437,440],{"class":151,"line":230},[217,438,439],{"class":226},"debugpy.wait_for_client()          ",[217,441,442],{"class":350},"# QGIS freezes here until the editor attaches\n",[14,444,445,447,448,451],{},[185,446,281],{}," Put these two lines at the top of the plugin's ",[18,449,450],{},"__init__.py",", attach from the editor, and QGIS resumes with the debugger already in place — the only way to catch an exception thrown before you could have run anything in the console. Remove them before shipping, or every user's QGIS will hang on startup.",[174,453,455],{"id":454},"attach-from-the-editor","Attach from the editor",[14,457,458],{},"VS Code needs a launch configuration:",[208,460,464],{"className":461,"code":462,"language":463,"meta":213,"style":213},"language-json shiki shiki-themes github-dark","{\n  \"name\": \"Attach to QGIS\",\n  \"type\": \"debugpy\",\n  \"request\": \"attach\",\n  \"connect\": { \"host\": \"127.0.0.1\", \"port\": 5678 },\n  \"pathMappings\": [\n    {\n      \"localRoot\": \"${workspaceFolder}\",\n      \"remoteRoot\": \"\u002Fhome\u002Fana\u002F.local\u002Fshare\u002FQGIS\u002FQGIS3\u002Fprofiles\u002Fdefault\u002Fpython\u002Fplugins\u002Fparcel_tools\"\n    }\n  ],\n  \"justMyCode\": true\n}\n","json",[18,465,466,471,485,496,508,535,544,550,563,574,580,586,597],{"__ignoreMap":213},[217,467,468],{"class":151,"line":219},[217,469,470],{"class":226},"{\n",[217,472,473,476,479,482],{"class":151,"line":230},[217,474,475],{"class":338},"  \"name\"",[217,477,478],{"class":226},": ",[217,480,481],{"class":251},"\"Attach to QGIS\"",[217,483,484],{"class":226},",\n",[217,486,487,490,492,494],{"class":151,"line":238},[217,488,489],{"class":338},"  \"type\"",[217,491,478],{"class":226},[217,493,273],{"class":251},[217,495,484],{"class":226},[217,497,498,501,503,506],{"class":151,"line":245},[217,499,500],{"class":338},"  \"request\"",[217,502,478],{"class":226},[217,504,505],{"class":251},"\"attach\"",[217,507,484],{"class":226},[217,509,510,513,516,519,521,523,525,528,530,532],{"class":151,"line":370},[217,511,512],{"class":338},"  \"connect\"",[217,514,515],{"class":226},": { ",[217,517,518],{"class":338},"\"host\"",[217,520,478],{"class":226},[217,522,359],{"class":251},[217,524,255],{"class":226},[217,526,527],{"class":338},"\"port\"",[217,529,478],{"class":226},[217,531,364],{"class":338},[217,533,534],{"class":226}," },\n",[217,536,538,541],{"class":151,"line":537},6,[217,539,540],{"class":338},"  \"pathMappings\"",[217,542,543],{"class":226},": [\n",[217,545,547],{"class":151,"line":546},7,[217,548,549],{"class":226},"    {\n",[217,551,553,556,558,561],{"class":151,"line":552},8,[217,554,555],{"class":338},"      \"localRoot\"",[217,557,478],{"class":226},[217,559,560],{"class":251},"\"${workspaceFolder}\"",[217,562,484],{"class":226},[217,564,566,569,571],{"class":151,"line":565},9,[217,567,568],{"class":338},"      \"remoteRoot\"",[217,570,478],{"class":226},[217,572,573],{"class":251},"\"\u002Fhome\u002Fana\u002F.local\u002Fshare\u002FQGIS\u002FQGIS3\u002Fprofiles\u002Fdefault\u002Fpython\u002Fplugins\u002Fparcel_tools\"\n",[217,575,577],{"class":151,"line":576},10,[217,578,579],{"class":226},"    }\n",[217,581,583],{"class":151,"line":582},11,[217,584,585],{"class":226},"  ],\n",[217,587,589,592,594],{"class":151,"line":588},12,[217,590,591],{"class":338},"  \"justMyCode\"",[217,593,478],{"class":226},[217,595,596],{"class":338},"true\n",[217,598,600],{"class":151,"line":599},13,[217,601,602],{"class":226},"}\n",[14,604,605,607,608,611,612,615,616,619,620,623,624,627],{},[185,606,281],{}," The path mapping is the part that everybody gets wrong first. The debugger reports file paths as the ",[414,609,610],{},"running process"," sees them, and your editor has the files open from wherever you edit them; unless the two are connected, breakpoints show as unverified hollow circles and never fire. If you develop directly in the plugins folder — or symlink your repository into it, which is the better arrangement — ",[18,613,614],{},"localRoot"," and ",[18,617,618],{},"remoteRoot"," are the same and the mapping is trivial. ",[18,621,622],{},"justMyCode"," set to ",[18,625,626],{},"true"," stops the debugger diving into QGIS's own Python on every exception, which is almost always what you want.",[14,629,630,631,634,635,637],{},"PyCharm Professional uses a Python Debug Server run configuration on the same port, with the same path-mapping requirement, and its own ",[18,632,633],{},"pydevd-pycharm"," package instead of ",[18,636,33],{}," — the concepts are identical.",[14,639,640],{},[38,641,644,647,650,653,660,663,671,676,681,685,688,692,696,701,706,710,713,716,718,721,723],{"viewBox":642,"role":41,"ariaLabel":643,"xmlns":43},"0 0 760 258","Comparison of a correct path mapping where the editor and QGIS see the same files against a mismatched mapping where breakpoints never bind",[45,645,646],{},"Why the breakpoint never turns solid",[49,648,649],{},"With a correct mapping, the file open in the editor and the file loaded by QGIS resolve to the same path, so the breakpoint binds and execution stops. With a mismatch, the editor has a copy in a repository folder while QGIS loaded a different copy from the plugins folder, so the breakpoint is never matched to any loaded code and remains unverified.",[53,651],{"x":55,"y":55,"width":56,"height":652,"fill":58},"258",[60,654,655],{},[63,656,658],{"id":657,"viewBox":66,"refX":67,"refY":68,"markerWidth":69,"markerHeight":69,"orient":70},"mapArrow",[72,659],{"d":74,"fill":75},[77,661,662],{"x":79,"y":80,"style":81,"fill":82,"textAnchor":83},"A hollow breakpoint means the paths do not agree",[53,664],{"x":665,"y":666,"width":667,"height":668,"rx":91,"fill":669,"stroke":670,"style":94},"20","48","348","188","#edf8e9","#15803d",[77,672,675],{"x":673,"y":674,"style":99,"fill":670,"textAnchor":83},"194","74","same file, mapped",[53,677],{"x":678,"y":679,"width":680,"height":106,"rx":107,"fill":108,"stroke":109,"style":110},"44","90","300",[77,682,684],{"x":88,"y":154,"style":683,"fill":75},"font-size:11px;font-family:sans-serif","editor: repo\u002Fparcel tools\u002Ftool.py",[53,686],{"x":678,"y":687,"width":680,"height":106,"rx":107,"fill":108,"stroke":109,"style":110},"132",[77,689,691],{"x":88,"y":690,"style":683,"fill":75},"154","QGIS: plugins\u002Fparcel tools\u002Ftool.py",[53,693],{"x":678,"y":694,"width":680,"height":678,"rx":107,"fill":108,"stroke":670,"style":695},"174","stroke-width:2",[77,697,700],{"x":673,"y":698,"style":699,"fill":670,"textAnchor":83},"201","text-anchor:middle;font-size:11px;font-weight:bold;font-family:sans-serif","mapping connects them — breakpoint binds",[53,702],{"x":703,"y":666,"width":667,"height":668,"rx":91,"fill":704,"stroke":705,"style":94},"392","#fdf2e2","#b91c1c",[77,707,709],{"x":708,"y":674,"style":99,"fill":705,"textAnchor":83},"566","two copies, no mapping",[53,711],{"x":712,"y":679,"width":680,"height":106,"rx":107,"fill":108,"stroke":109,"style":110},"416",[77,714,684],{"x":715,"y":154,"style":683,"fill":75},"432",[53,717],{"x":712,"y":687,"width":680,"height":106,"rx":107,"fill":108,"stroke":109,"style":110},[77,719,720],{"x":715,"y":690,"style":683,"fill":75},"QGIS: a copy pasted last week",[53,722],{"x":712,"y":694,"width":680,"height":678,"rx":107,"fill":58,"stroke":705,"style":695},[77,724,725],{"x":708,"y":698,"style":699,"fill":705,"textAnchor":83},"breakpoint stays hollow, and so does your afternoon",[174,727,729],{"id":728},"break-in-code-that-only-runs-on-a-click","Break in code that only runs on a click",[14,731,732],{},"Once attached, set a breakpoint in the plugin method that handles the action and trigger it in QGIS — click the toolbar button, run the tool, use the map tool. Execution stops on the line, the editor shows the call stack, and you can inspect every local variable including live QGIS objects.",[208,734,736],{"className":210,"code":735,"language":212,"meta":213,"style":213},"def run(self):\n    layer = self.iface.activeLayer()\n    request = QgsFeatureRequest().setFilterExpression(\"area_m2 > 5000\")\n    for feature in layer.getFeatures(request):        # breakpoint here\n        self.process(feature)\n",[18,737,738,750,763,778,795],{"__ignoreMap":213},[217,739,740,743,747],{"class":151,"line":219},[217,741,742],{"class":222},"def",[217,744,746],{"class":745},"svObZ"," run",[217,748,749],{"class":226},"(self):\n",[217,751,752,755,757,760],{"class":151,"line":230},[217,753,754],{"class":226},"    layer ",[217,756,325],{"class":222},[217,758,759],{"class":338}," self",[217,761,762],{"class":226},".iface.activeLayer()\n",[217,764,765,768,770,773,776],{"class":151,"line":238},[217,766,767],{"class":226},"    request ",[217,769,325],{"class":222},[217,771,772],{"class":226}," QgsFeatureRequest().setFilterExpression(",[217,774,775],{"class":251},"\"area_m2 > 5000\"",[217,777,382],{"class":226},[217,779,780,783,786,789,792],{"class":151,"line":245},[217,781,782],{"class":222},"    for",[217,784,785],{"class":226}," feature ",[217,787,788],{"class":222},"in",[217,790,791],{"class":226}," layer.getFeatures(request):        ",[217,793,794],{"class":350},"# breakpoint here\n",[217,796,797,800],{"class":151,"line":370},[217,798,799],{"class":338},"        self",[217,801,802],{"class":226},".process(feature)\n",[14,804,805,807,808,255,811,255,814,817,818,821],{},[185,806,281],{}," With execution paused, the debug console evaluates arbitrary expressions in that frame — ",[18,809,810],{},"feature[\"ref\"]",[18,812,813],{},"feature.geometry().area()",[18,815,816],{},"layer.crs().authid()"," — which is faster than any amount of printing and works on objects that have no useful string representation. Stepping into ",[18,819,820],{},"self.process()"," shows what actually happens to each feature. One caveat specific to QGIS: while execution is paused, the application is frozen, so the canvas does not repaint and the interface looks hung. That is expected; it resumes when you continue.",[14,823,824],{},[38,825,828,831,834,837,845,848,854,859,863,866,870,873,876,879,883,888,893,897,901,906,909,913],{"viewBox":826,"role":41,"ariaLabel":827,"xmlns":43},"0 0 760 240","Comparison of debugging by print statements against breakpoints, across the number of edit and rerun cycles each requires",[45,829,830],{},"Print statements against breakpoints",[49,832,833],{},"Debugging with print statements requires editing the code, reloading the plugin and re-running the action for every value you want to see, so each question costs a full cycle. With a breakpoint the execution is paused once and any expression can be evaluated in that frame, so ten questions cost one cycle.",[53,835],{"x":55,"y":55,"width":56,"height":836,"fill":58},"240",[60,838,839],{},[63,840,842],{"id":841,"viewBox":66,"refX":67,"refY":68,"markerWidth":69,"markerHeight":69,"orient":70},"dbgCmpArrow",[72,843],{"d":74,"fill":844},"#b45309",[77,846,847],{"x":79,"y":80,"style":81,"fill":82,"textAnchor":83},"Ten questions, one pause",[77,849,373],{"x":850,"y":851,"style":852,"fill":844,"textAnchor":853},"120","76","text-anchor:end;font-size:11px;font-weight:bold;font-family:sans-serif","end",[53,855],{"x":856,"y":857,"width":850,"height":858,"rx":68,"fill":704,"stroke":844,"style":110},"134","58","30",[77,860,862],{"x":673,"y":861,"style":161,"fill":75,"textAnchor":83},"78","edit",[53,864],{"x":865,"y":857,"width":850,"height":858,"rx":68,"fill":704,"stroke":844,"style":110},"270",[77,867,869],{"x":868,"y":861,"style":161,"fill":75,"textAnchor":83},"330","reload",[53,871],{"x":872,"y":857,"width":850,"height":858,"rx":68,"fill":704,"stroke":844,"style":110},"406",[77,874,875],{"x":155,"y":861,"style":161,"fill":75,"textAnchor":83},"re-run the action",[53,877],{"x":878,"y":857,"width":850,"height":858,"rx":68,"fill":704,"stroke":844,"style":110},"542",[77,880,882],{"x":881,"y":861,"style":161,"fill":75,"textAnchor":83},"602","read one value",[72,884],{"d":885,"fill":886,"stroke":844,"style":887},"M602 88 L602 108 L194 108 L194 94","none","stroke-width:2;stroke-dasharray:5 4;marker-end:url(#dbgCmpArrow)",[77,889,892],{"x":890,"y":891,"style":161,"fill":844,"textAnchor":83},"398","126","repeat for every question",[77,894,896],{"x":850,"y":895,"style":852,"fill":670,"textAnchor":853},"176","breakpoint",[53,898],{"x":856,"y":899,"width":900,"height":858,"rx":68,"fill":669,"stroke":670,"style":695},"158","180",[77,902,905],{"x":903,"y":904,"style":161,"fill":75,"textAnchor":83},"224","178","run the action once",[53,907],{"x":868,"y":899,"width":908,"height":858,"rx":68,"fill":669,"stroke":670,"style":695},"332",[77,910,912],{"x":911,"y":904,"style":161,"fill":75,"textAnchor":83},"496","paused — evaluate anything in that frame, as often as you like",[77,914,916],{"x":890,"y":915,"style":114,"fill":109,"textAnchor":83},"216","the setup costs ten minutes and repays them in the first session",[14,918,919,920,923],{},"Code running on a background task through ",[18,921,922],{},"QgsTask"," needs one extra line, because the debugger does not automatically trace threads it did not start:",[208,925,927],{"className":210,"code":926,"language":212,"meta":213,"style":213},"debugpy.debug_this_thread()\n",[18,928,929],{"__ignoreMap":213},[217,930,931],{"class":151,"line":219},[217,932,926],{"class":226},[14,934,935,937,938,941,942,339],{},[185,936,281],{}," Call it as the first line of the task's ",[18,939,940],{},"run()"," method and breakpoints inside it start working. Without it, a breakpoint in a background task is simply ignored, which looks identical to the path-mapping failure and is a good second thing to check. The threading model this fits into is described in ",[26,943,945],{"href":944},"\u002Fqgis-plugin-development\u002Fbackground-tasks-and-plugin-performance\u002Frun-background-task-with-qgstask-pyqgis\u002F","Run a Background Task with QgsTask in PyQGIS",[174,947,949],{"id":948},"keep-the-debug-hooks-out-of-the-shipped-plugin","Keep the debug hooks out of the shipped plugin",[14,951,952],{},"A convenient pattern is to gate the listener behind an environment variable so the code can stay in the repository without ever running for a user:",[208,954,956],{"className":210,"code":955,"language":212,"meta":213,"style":213},"import os\n\nif os.environ.get(\"QGIS_DEBUGPY\") == \"1\":\n    import debugpy\n    debugpy.listen((\"127.0.0.1\", 5678))\n",[18,957,958,965,969,992,999],{"__ignoreMap":213},[217,959,960,962],{"class":151,"line":219},[217,961,223],{"class":222},[217,963,964],{"class":226}," os\n",[217,966,967],{"class":151,"line":230},[217,968,242],{"emptyLinePlaceholder":241},[217,970,971,974,977,980,983,986,989],{"class":151,"line":238},[217,972,973],{"class":222},"if",[217,975,976],{"class":226}," os.environ.get(",[217,978,979],{"class":251},"\"QGIS_DEBUGPY\"",[217,981,982],{"class":226},") ",[217,984,985],{"class":222},"==",[217,987,988],{"class":251}," \"1\"",[217,990,991],{"class":226},":\n",[217,993,994,997],{"class":151,"line":245},[217,995,996],{"class":222},"    import",[217,998,310],{"class":226},[217,1000,1001,1004,1006,1008,1010],{"class":151,"line":370},[217,1002,1003],{"class":226},"    debugpy.listen((",[217,1005,359],{"class":251},[217,1007,255],{"class":226},[217,1009,364],{"class":338},[217,1011,367],{"class":226},[14,1013,1014,1016,1017,1020,1021,1023,1024,1026,1027,1031],{},[185,1015,281],{}," Setting ",[18,1018,1019],{},"QGIS_DEBUGPY=1"," before launching QGIS enables the listener; without it the import never happens, so a user who does not have ",[18,1022,33],{}," installed is unaffected. This keeps the setup reproducible for other developers — they set one variable rather than re-deriving the whole configuration — and eliminates the recurring risk of shipping a ",[18,1025,393],{}," that hangs somebody's QGIS on startup. Whatever you do, make the packaging step in ",[26,1028,1030],{"href":1029},"\u002Fqgis-plugin-development\u002Fpublishing-to-the-qgis-plugin-repository\u002Fpackage-qgis-plugin-as-zip\u002F","Package a QGIS Plugin as a Zip"," check for stray debug imports.",[174,1033,1035],{"id":1034},"qgis-version-compatibility","QGIS version compatibility",[1037,1038,1039,1055],"table",{},[1040,1041,1042],"thead",{},[1043,1044,1045,1049,1052],"tr",{},[1046,1047,1048],"th",{},"QGIS version",[1046,1050,1051],{},"Python",[1046,1053,1054],{},"Notes",[1056,1057,1058,1072,1082,1093],"tbody",{},[1043,1059,1060,1064,1067],{},[1061,1062,1063],"td",{},"3.22 LTR",[1061,1065,1066],{},"3.9",[1061,1068,1069,1071],{},[18,1070,33],{}," works; use a version compatible with Python 3.9.",[1043,1073,1074,1077,1079],{},[1061,1075,1076],{},"3.28 LTR",[1061,1078,1066],{},[1061,1080,1081],{},"Identical.",[1043,1083,1084,1087,1090],{},[1061,1085,1086],{},"3.34 LTR",[1061,1088,1089],{},"3.12",[1061,1091,1092],{},"Baseline for this page.",[1043,1094,1095,1098,1100],{},[1061,1096,1097],{},"3.40 \u002F 3.44",[1061,1099,1089],{},[1061,1101,1102],{},"Identical; nothing in the attach mechanism is QGIS-version specific.",[14,1104,1105,1106,1108],{},"What does vary is where the QGIS Python lives and whether you can write to it. On Windows use the OSGeo4W shell, on macOS the interpreter inside the application bundle, and on Linux usually the system Python — all reachable through ",[18,1107,285],{}," from the console.",[174,1110,1112],{"id":1111},"troubleshooting","Troubleshooting",[179,1114,1115,1125,1133,1142,1151,1163],{},[182,1116,1117,1120,1121,1124],{},[185,1118,1119],{},"Breakpoints stay hollow."," Path mapping. Confirm with ",[18,1122,1123],{},"import parcel_tools; print(parcel_tools.__file__)"," in the console and map that folder.",[182,1126,1127,1132],{},[185,1128,1129,339],{},[18,1130,1131],{},"Address already in use"," A listener from a previous session is still bound. Restart QGIS, or use a different port.",[182,1134,1135,1138,1139,1141],{},[185,1136,1137],{},"The editor connects and immediately disconnects."," Version mismatch between the ",[18,1140,33],{}," in QGIS and the one in the editor's extension. Update both.",[182,1143,1144,1147,1148,1150],{},[185,1145,1146],{},"QGIS freezes on startup."," A ",[18,1149,393],{}," left in the plugin. Remove it, or gate it behind an environment variable.",[182,1152,1153,1156,1157,1160,1161,339],{},[185,1154,1155],{},"Breakpoints in a background task never fire."," Add ",[18,1158,1159],{},"debugpy.debug_this_thread()"," at the start of the task's ",[18,1162,940],{},[182,1164,1165,1171,1172,1175],{},[185,1166,1167,1170],{},[18,1168,1169],{},"ModuleNotFoundError: debugpy"," in the console."," It was installed into a different Python. Install with ",[18,1173,1174],{},"sys.executable -m pip"," from inside QGIS.",[174,1177,1179],{"id":1178},"conclusion","Conclusion",[14,1181,1182,1183,1185,1186,1188,1189,1191],{},"Install ",[18,1184,33],{}," into the QGIS interpreter with ",[18,1187,1174],{},", call ",[18,1190,389],{}," from the console, and attach from your editor with a path mapping that matches where QGIS actually loaded the plugin from. Breakpoints then work in click handlers, map tools and — with one extra line — background tasks. Gate the hook behind an environment variable so the same code is safe to commit and impossible to ship by accident.",[174,1193,1195],{"id":1194},"frequently-asked-questions","Frequently Asked Questions",[14,1197,1198,1201,1202,1204],{},[185,1199,1200],{},"Does this work with PyCharm Community?","\nNo — remote debugging is a Professional feature. Use VS Code with ",[18,1203,33],{},", which is free and works identically for this purpose.",[14,1206,1207,1210,1211,1214,1215,1217],{},[185,1208,1209],{},"Can I debug a Processing algorithm?","\nYes. Attach as usual and set a breakpoint in ",[18,1212,1213],{},"processAlgorithm()",". If it runs on a background thread, add ",[18,1216,1159],{}," as the first line.",[14,1219,1220,1223,1224,1226],{},[185,1221,1222],{},"Is the debug port a security risk?","\nYes, if exposed. Bind to ",[18,1225,397],{}," only, and never leave a listener enabled on a shared or server machine.",[14,1228,1229,1232],{},[185,1230,1231],{},"Why is QGIS unresponsive while stopped at a breakpoint?","\nBecause the main thread is paused, and that thread draws the interface. It is not a crash; continue and it recovers.",[14,1234,1235,1238],{},[185,1236,1237],{},"Can two people attach at once?","\nNo. One client per listener. For pair debugging, share a screen.",[14,1240,1241,1244,1245,339],{},[185,1242,1243],{},"Does attaching slow QGIS down?","\nSlightly, while attached, and imperceptibly for most work. Detach when you are benchmarking anything — see ",[26,1246,1248],{"href":1247},"\u002Fqgis-plugin-development\u002Fbackground-tasks-and-plugin-performance\u002Fprofile-slow-pyqgis-code\u002F","Profile Slow PyQGIS Code",[174,1250,1252],{"id":1251},"related","Related",[179,1254,1255,1260,1266,1272,1278],{},[182,1256,1257,1259],{},[26,1258,29],{"href":28}," — the guide this recipe belongs to",[182,1261,1262],{},[26,1263,1265],{"href":1264},"\u002Fpyqgis-fundamentals-environment-setup\u002Fsetting-up-pycharm-for-qgis\u002Fbest-ide-for-qgis-plugin-development\u002F","Best IDE for QGIS Plugin Development",[182,1267,1268],{},[26,1269,1271],{"href":1270},"\u002Fpyqgis-fundamentals-environment-setup\u002Fdebugging-pyqgis-scripts\u002F","Debugging PyQGIS Scripts",[182,1273,1274],{},[26,1275,1277],{"href":1276},"\u002Fqgis-plugin-development\u002Fplugin-boilerplate-structure\u002Freload-qgis-plugin-without-restart\u002F","Reload a QGIS Plugin Without Restarting",[182,1279,1280],{},[26,1281,198],{"href":197},[1283,1284,1285],"style",{},"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 .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}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);}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}html pre.shiki code .sns5M, html code.shiki .sns5M{--shiki-default:#DBEDFF}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .sjoCn, html code.shiki .sjoCn{--shiki-default:#9AA79F}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}",{"title":213,"searchDepth":230,"depth":230,"links":1287},[1288,1289,1290,1291,1292,1293,1294,1295,1296,1297,1298],{"id":176,"depth":230,"text":177},{"id":202,"depth":230,"text":203},{"id":297,"depth":230,"text":298},{"id":454,"depth":230,"text":455},{"id":728,"depth":230,"text":729},{"id":948,"depth":230,"text":949},{"id":1034,"depth":230,"text":1035},{"id":1111,"depth":230,"text":1112},{"id":1178,"depth":230,"text":1179},{"id":1194,"depth":230,"text":1195},{"id":1251,"depth":230,"text":1252},"Attach a real debugger to a running QGIS — install debugpy into the QGIS Python, start a listener from the console, connect from VS Code or PyCharm, and set breakpoints in plugin code that only runs on a click.","md",{"slug":1302,"type":1303,"breadcrumb":1304,"datePublished":1305,"dateModified":1305},"debug-qgis-plugin-with-debugpy","article","Debug with debugpy","2026-08-15","\u002Fpyqgis-fundamentals-environment-setup\u002Fsetting-up-pycharm-for-qgis\u002Fdebug-qgis-plugin-with-debugpy",{"title":5,"description":1299},"pyqgis-fundamentals-environment-setup\u002Fsetting-up-pycharm-for-qgis\u002Fdebug-qgis-plugin-with-debugpy\u002Findex","ZpsZzWLZekECUZym_oSicfEL5nP-ytLYnm3aaY8qsgk",1786789584629]