[{"data":1,"prerenderedAt":1050},["ShallowReactive",2],{"page-\u002Fsystematic-debugging-performance-profiling\u002Finteractive-debugging-with-pdb-and-ipdb\u002Fusing-breakpoint-and-pythonbreakpoint-hooks\u002F":3},{"id":4,"title":5,"body":6,"description":1016,"extension":1017,"meta":1018,"navigation":135,"path":1046,"seo":1047,"stem":1048,"__hash__":1049},"content\u002Fsystematic-debugging-performance-profiling\u002Finteractive-debugging-with-pdb-and-ipdb\u002Fusing-breakpoint-and-pythonbreakpoint-hooks\u002Findex.md","Using breakpoint() and the PYTHONBREAKPOINT Hook",{"type":7,"value":8,"toc":1004},"minimark",[9,29,35,40,73,77,188,286,308,466,470,499,510,514,533,558,626,630,637,655,658,725,739,802,806,826,829,833,843,849,855,859,921,925,941,953,962,966,995,1000],[10,11,12,16,17,20,21,24,25,28],"p",{},[13,14,15],"code",{},"breakpoint()"," looks like a shortcut for ",[13,18,19],{},"import pdb; pdb.set_trace()",", and in its default configuration that is all it does. The difference is that it is a hook. Since Python 3.7, the built-in calls ",[13,22,23],{},"sys.breakpointhook()",", and the default hook consults the ",[13,26,27],{},"PYTHONBREAKPOINT"," environment variable to decide what to run. That one level of indirection means the same line of code can open pdb on your laptop, ipdb for a colleague who prefers it, a remote debugger inside a container, or nothing at all in CI — without editing the source.",[10,30,31,32,34],{},"Most teams use a fraction of this. Knowing the rest pays off in three situations: when you want a better debugger than pdb without changing every call site, when a forgotten ",[13,33,15],{}," hangs a pipeline waiting for input that never comes, and when you debug code inside a container or service where there is no terminal attached.",[36,37,39],"h2",{"id":38},"prerequisites","Prerequisites",[41,42,43,51,65],"ul",{},[44,45,46,47,50],"li",{},"Python 3.11 or later, ",[13,48,49],{},"pytest >= 8.0",".",[44,52,53,54,57,58,61,62,50],{},"Optionally ",[13,55,56],{},"ipdb",", ",[13,59,60],{},"pudb",", or ",[13,63,64],{},"debugpy",[44,66,67,68,50],{},"Basics from ",[69,70,72],"a",{"href":71},"\u002Fsystematic-debugging-performance-profiling\u002Finteractive-debugging-with-pdb-and-ipdb\u002F","Interactive debugging with pdb and ipdb",[36,74,76],{"id":75},"solution","Solution",[78,79,84],"pre",{"className":80,"code":81,"language":82,"meta":83,"style":83},"language-bash shiki shiki-themes github-light github-dark","# Pick the debugger per shell, per run, or per project (.envrc, IDE run config).\nexport PYTHONBREAKPOINT=ipdb.set_trace\npytest tests\u002Ftest_checkout.py -k discount\n\n# pudb's full-screen UI for one run:\nPYTHONBREAKPOINT=pudb.set_trace python -m app.cli reprice\n\n# Turn every breakpoint() into a no-op — CI, containers, production images.\nexport PYTHONBREAKPOINT=0\n","bash","",[13,85,86,95,112,130,137,143,165,170,176],{"__ignoreMap":83},[87,88,91],"span",{"class":89,"line":90},"line",1,[87,92,94],{"class":93},"sJ8bj","# Pick the debugger per shell, per run, or per project (.envrc, IDE run config).\n",[87,96,98,102,106,109],{"class":89,"line":97},2,[87,99,101],{"class":100},"szBVR","export",[87,103,105],{"class":104},"sVt8B"," PYTHONBREAKPOINT",[87,107,108],{"class":100},"=",[87,110,111],{"class":104},"ipdb.set_trace\n",[87,113,115,119,123,127],{"class":89,"line":114},3,[87,116,118],{"class":117},"sScJk","pytest",[87,120,122],{"class":121},"sZZnC"," tests\u002Ftest_checkout.py",[87,124,126],{"class":125},"sj4cs"," -k",[87,128,129],{"class":121}," discount\n",[87,131,133],{"class":89,"line":132},4,[87,134,136],{"emptyLinePlaceholder":135},true,"\n",[87,138,140],{"class":89,"line":139},5,[87,141,142],{"class":93},"# pudb's full-screen UI for one run:\n",[87,144,146,148,150,153,156,159,162],{"class":89,"line":145},6,[87,147,27],{"class":104},[87,149,108],{"class":100},[87,151,152],{"class":121},"pudb.set_trace",[87,154,155],{"class":117}," python",[87,157,158],{"class":125}," -m",[87,160,161],{"class":121}," app.cli",[87,163,164],{"class":121}," reprice\n",[87,166,168],{"class":89,"line":167},7,[87,169,136],{"emptyLinePlaceholder":135},[87,171,173],{"class":89,"line":172},8,[87,174,175],{"class":93},"# Turn every breakpoint() into a no-op — CI, containers, production images.\n",[87,177,179,181,183,185],{"class":89,"line":178},9,[87,180,101],{"class":100},[87,182,105],{"class":104},[87,184,108],{"class":100},[87,186,187],{"class":125},"0\n",[78,189,193],{"className":190,"code":191,"language":192,"meta":83,"style":83},"language-python shiki shiki-themes github-light github-dark","# A custom hook for behaviour an environment variable cannot express:\n# attach a remote debugger when running in a container, fall back to pdb locally.\nimport os\nimport sys\n\ndef _hook(*args, **kwargs):\n    if os.environ.get(\"IN_CONTAINER\"):\n        import debugpy\n        debugpy.listen((\"0.0.0.0\", 5678))\n        print(\"waiting for debugger on :5678\", file=sys.stderr)\n        debugpy.wait_for_client()\n        debugpy.breakpoint()\n    else:\n        import pdb\n        pdb.Pdb().set_trace(sys._getframe(1))\n\nsys.breakpointhook = _hook\n","python",[13,194,195,200,205,210,215,219,224,229,234,239,245,251,257,263,269,275,280],{"__ignoreMap":83},[87,196,197],{"class":89,"line":90},[87,198,199],{},"# A custom hook for behaviour an environment variable cannot express:\n",[87,201,202],{"class":89,"line":97},[87,203,204],{},"# attach a remote debugger when running in a container, fall back to pdb locally.\n",[87,206,207],{"class":89,"line":114},[87,208,209],{},"import os\n",[87,211,212],{"class":89,"line":132},[87,213,214],{},"import sys\n",[87,216,217],{"class":89,"line":139},[87,218,136],{"emptyLinePlaceholder":135},[87,220,221],{"class":89,"line":145},[87,222,223],{},"def _hook(*args, **kwargs):\n",[87,225,226],{"class":89,"line":167},[87,227,228],{},"    if os.environ.get(\"IN_CONTAINER\"):\n",[87,230,231],{"class":89,"line":172},[87,232,233],{},"        import debugpy\n",[87,235,236],{"class":89,"line":178},[87,237,238],{},"        debugpy.listen((\"0.0.0.0\", 5678))\n",[87,240,242],{"class":89,"line":241},10,[87,243,244],{},"        print(\"waiting for debugger on :5678\", file=sys.stderr)\n",[87,246,248],{"class":89,"line":247},11,[87,249,250],{},"        debugpy.wait_for_client()\n",[87,252,254],{"class":89,"line":253},12,[87,255,256],{},"        debugpy.breakpoint()\n",[87,258,260],{"class":89,"line":259},13,[87,261,262],{},"    else:\n",[87,264,266],{"class":89,"line":265},14,[87,267,268],{},"        import pdb\n",[87,270,272],{"class":89,"line":271},15,[87,273,274],{},"        pdb.Pdb().set_trace(sys._getframe(1))\n",[87,276,278],{"class":89,"line":277},16,[87,279,136],{"emptyLinePlaceholder":135},[87,281,283],{"class":89,"line":282},17,[87,284,285],{},"sys.breakpointhook = _hook\n",[78,287,291],{"className":288,"code":289,"language":290,"meta":83,"style":83},"language-toml shiki shiki-themes github-light github-dark","# pyproject.toml — catch leftovers before they merge.\n[tool.ruff.lint]\nextend-select = [\"T10\"]      # T100: breakpoint(), pdb.set_trace and friends\n","toml",[13,292,293,298,303],{"__ignoreMap":83},[87,294,295],{"class":89,"line":90},[87,296,297],{},"# pyproject.toml — catch leftovers before they merge.\n",[87,299,300],{"class":89,"line":97},[87,301,302],{},"[tool.ruff.lint]\n",[87,304,305],{"class":89,"line":114},[87,306,307],{},"extend-select = [\"T10\"]      # T100: breakpoint(), pdb.set_trace and friends\n",[309,310,313,462],"figure",{"className":311},[312],"diagram",[314,315,322,323,322,327,322,331,322,349,322,357,322,367,322,374,322,379,322,386,322,392,322,397,322,404,322,410,322,416,322,421,322,425,322,428,322,432,322,439,322,442,322,446,322,451,322,455,322,458],"svg",{"viewBox":316,"role":317,"ariaLabelledBy":318,"xmlns":321},"0 0 800 256","img",[319,320],"bp-t","bp-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","\n  ",[324,325,326],"title",{"id":319},"How breakpoint() dispatches",[328,329,330],"desc",{"id":320},"A breakpoint call invokes sys.breakpointhook. The default hook reads PYTHONBREAKPOINT: if unset it runs pdb.set_trace; if set to zero it returns immediately; if set to a dotted name such as ipdb.set_trace it imports and calls that. A custom hook assigned to sys.breakpointhook replaces the whole decision.",[332,333,334,335,322],"defs",{},"\n    ",[336,337,344],"marker",{"id":338,"viewBox":339,"refX":340,"refY":341,"markerWidth":342,"markerHeight":342,"orient":343},"bp-a","0 0 10 10","9","5","7","auto-start-reverse",[345,346],"path",{"d":347,"fill":348},"M0 0 L10 5 L0 10 z","#81b29a",[350,351],"rect",{"x":352,"y":352,"width":353,"height":354,"rx":355,"fill":356},"0","800","256","14","#fffdf8",[358,359,366],"text",{"x":360,"y":361,"textAnchor":362,"fontSize":363,"fontWeight":364,"fill":365},"400","28","middle","15.5","700","#3d405b","One call, a configurable destination",[350,368],{"x":369,"y":370,"width":371,"height":372,"rx":373,"fill":365},"26","102","130","50","10",[358,375,15],{"x":376,"y":377,"textAnchor":362,"fontSize":378,"fontWeight":364,"fill":356},"91","132","12",[350,380],{"x":381,"y":370,"width":382,"height":372,"rx":373,"fill":383,"stroke":384,"strokeWidth":385},"196","170","#f7f0da","#f2cc8f","2",[358,387,391],{"x":388,"y":389,"textAnchor":362,"fontSize":390,"fontWeight":364,"fill":365},"281","124","11.5","sys.breakpointhook",[358,393,396],{"x":388,"y":394,"textAnchor":362,"fontSize":373,"fill":395},"142","#8a5a00","replaceable",[89,398],{"x1":399,"y1":400,"x2":401,"y2":400,"stroke":348,"strokeWidth":402,"markerEnd":403},"158","127","192","1.8","url(#bp-a)",[350,405],{"x":406,"y":372,"width":407,"height":408,"rx":340,"fill":409,"stroke":348,"strokeWidth":402},"440","160","40","#e6f0ea",[358,411,415],{"x":412,"y":413,"textAnchor":362,"fontSize":414,"fill":365},"520","75","11","unset → pdb",[350,417],{"x":406,"y":418,"width":407,"height":408,"rx":340,"fill":419,"stroke":420,"strokeWidth":402},"108","#fbe9e3","#e07a5f",[358,422,424],{"x":412,"y":423,"textAnchor":362,"fontSize":414,"fill":365},"133","\"0\" → no-op",[350,426],{"x":406,"y":427,"width":407,"height":408,"rx":340,"fill":409,"stroke":348,"strokeWidth":402},"166",[358,429,431],{"x":412,"y":430,"textAnchor":362,"fontSize":414,"fill":365},"191","\"ipdb.set_trace\"",[89,433],{"x1":434,"y1":435,"x2":436,"y2":437,"stroke":348,"strokeWidth":438,"markerEnd":403},"368","118","436","72","1.6",[89,440],{"x1":434,"y1":400,"x2":436,"y2":441,"stroke":348,"strokeWidth":438,"markerEnd":403},"128",[89,443],{"x1":434,"y1":444,"x2":436,"y2":445,"stroke":348,"strokeWidth":438,"markerEnd":403},"136","184",[358,447,450],{"x":448,"y":413,"fontSize":449,"fill":365},"620","10.5","default",[358,452,454],{"x":448,"y":423,"fontSize":449,"fill":453},"#8f3d22","CI, images",[358,456,457],{"x":448,"y":430,"fontSize":449,"fill":365},"any importable callable",[358,459,461],{"x":360,"y":460,"textAnchor":362,"fontSize":414,"fill":365},"236","PYTHONBREAKPOINT is read on every call, so changing it mid-process takes effect.",[463,464,465],"figcaption",{},"The source line never changes; the environment decides what a breakpoint means.",[36,467,469],{"id":468},"why-this-works","Why this works",[10,471,472,473,476,477,480,481,484,485,487,488,491,492,494,495,498],{},"The built-in ",[13,474,475],{},"breakpoint(*args, **kwargs)"," does nothing except call ",[13,478,479],{},"sys.breakpointhook(*args, **kwargs)",". The default hook, ",[13,482,483],{},"sys.__breakpointhook__",", reads ",[13,486,27],{}," each time it runs. An empty or missing value means ",[13,489,490],{},"pdb.set_trace",". The value ",[13,493,352],{}," means return immediately. Any other value is treated as a dotted import path; the hook imports the module, looks up the attribute and calls it with the same arguments. If the import fails, it issues a ",[13,496,497],{},"RuntimeWarning"," and continues without stopping, rather than crashing the program.",[10,500,501,502,505,506,509],{},"Because the variable is read on every call rather than at startup, you can change it with ",[13,503,504],{},"os.environ"," inside a running process, and the next breakpoint follows. Because the hook is an ordinary attribute of ",[13,507,508],{},"sys",", a project can replace it entirely — as in the container example — for behaviour that depends on more than a single dotted name.",[36,511,513],{"id":512},"breakpoints-inside-pytest","Breakpoints inside pytest",[10,515,516,517,519,520,522,523,526,527,529,530,50],{},"pytest captures stdout and stderr, which would normally make an interactive prompt unusable. It handles ",[13,518,15],{}," specially: its debugging plugin wraps ",[13,521,391],{},", and when the hook fires it suspends capture, starts the debugger, and resumes capture when you type ",[13,524,525],{},"continue",". That is why ",[13,528,15],{}," inside a test or the code it calls just works, even without ",[13,531,532],{},"-s",[10,534,535,536,539,540,543,544,546,547,549,550,553,554,557],{},"Two options interact with this. ",[13,537,538],{},"--pdbcls=IPython.terminal.debugger:TerminalPdb"," sets the debugger class pytest uses for both ",[13,541,542],{},"--pdb"," post-mortems and ",[13,545,15],{}," calls, so ipdb-style behaviour is available without setting ",[13,548,27],{},". And under ",[13,551,552],{},"pytest-xdist",", workers have no terminal, so a breakpoint in a worker cannot be answered; run the failing test without ",[13,555,556],{},"-n"," to debug it interactively.",[309,559,561,620],{"className":560},[312],[314,562,322,567,322,570,322,573,322,576,322,579,322,586,322,591,322,595,322,599,322,602,322,605,322,609,322,612,322,614,322,616],{"viewBox":563,"role":317,"ariaLabelledBy":564,"xmlns":321},"0 0 800 226",[565,566],"bpp-t","bpp-d",[324,568,569],{"id":565},"pytest suspending capture around a breakpoint",[328,571,572],{"id":566},"A timeline of a captured test. Output is captured until breakpoint is called. pytest's wrapped hook suspends capture, the debugger prompt runs with the real terminal, and when the user continues, capture resumes for the rest of the test.",[350,574],{"x":352,"y":352,"width":353,"height":575,"rx":355,"fill":356},"226",[358,577,578],{"x":360,"y":361,"textAnchor":362,"fontSize":363,"fontWeight":364,"fill":365},"Capture pauses exactly as long as the prompt",[350,580],{"x":408,"y":581,"width":582,"height":408,"rx":583,"fill":584,"stroke":585},"90","230","8","#f4f1de","rgba(61,64,91,0.4)",[358,587,590],{"x":588,"y":589,"textAnchor":362,"fontSize":414,"fill":365},"155","115","output captured",[350,592],{"x":593,"y":581,"width":594,"height":408,"rx":583,"fill":409,"stroke":348,"strokeWidth":385},"280","240",[358,596,598],{"x":360,"y":589,"textAnchor":362,"fontSize":414,"fontWeight":364,"fill":597},"#2a5f49","(Pdb) prompt — real terminal",[350,600],{"x":601,"y":581,"width":582,"height":408,"rx":583,"fill":584,"stroke":585},"530",[358,603,590],{"x":604,"y":589,"textAnchor":362,"fontSize":414,"fill":365},"645",[89,606],{"x1":593,"y1":607,"x2":593,"y2":608,"stroke":420,"strokeWidth":385},"70","150",[358,610,15],{"x":593,"y":611,"textAnchor":362,"fontSize":449,"fill":453},"62",[89,613],{"x1":412,"y1":607,"x2":412,"y2":608,"stroke":420,"strokeWidth":385},[358,615,525],{"x":412,"y":611,"textAnchor":362,"fontSize":449,"fill":453},[358,617,619],{"x":360,"y":618,"textAnchor":362,"fontSize":414,"fill":365},"186","Under xdist there is no terminal to hand over — debug that test without -n.",[463,621,622,623,625],{},"pytest's wrapper around the hook is why no ",[13,624,532],{}," flag is needed to use breakpoint() in a test.",[36,627,629],{"id":628},"stopping-only-when-it-matters","Stopping only when it matters",[10,631,632,633,636],{},"A breakpoint inside a loop that runs ten thousand times is useless if the interesting iteration is the 9,876th. The simplest fix is ordinary Python around the call: ",[13,634,635],{},"if order.total \u003C 0: breakpoint()",". That is often clearer than any debugger feature, and it costs nothing when the condition is false.",[10,638,639,640,642,643,646,647,650,651,654],{},"Because ",[13,641,15],{}," forwards its arguments to the hook, a custom hook can accept its own parameters. Python 3.13's ",[13,644,645],{},"pdb.set_trace(commands=[...])"," lets a breakpoint run debugger commands on arrival, and a thin hook can pass them through, so ",[13,648,649],{},"breakpoint(commands=[\"p order\", \"where\"])"," prints the object and the stack before handing over the prompt. For older versions, the same effect comes from a ",[13,652,653],{},".pdbrc"," file in the project directory, whose commands run at every stop.",[10,656,657],{},"A counting hook helps with the \"nth time\" problem when there is no natural condition:",[78,659,661],{"className":190,"code":660,"language":192,"meta":83,"style":83},"import collections, pdb, sys\n\n_hits = collections.Counter()\n\ndef nth_hook(*, nth=1, key=None, **kw):\n    caller = sys._getframe(1)\n    k = key or (caller.f_code.co_filename, caller.f_lineno)\n    _hits[k] += 1\n    if _hits[k] == nth:\n        pdb.Pdb().set_trace(caller)\n\nsys.breakpointhook = nth_hook\n# breakpoint(nth=250)  → stops on the 250th pass through this line\n",[13,662,663,668,672,677,681,686,691,696,701,706,711,715,720],{"__ignoreMap":83},[87,664,665],{"class":89,"line":90},[87,666,667],{},"import collections, pdb, sys\n",[87,669,670],{"class":89,"line":97},[87,671,136],{"emptyLinePlaceholder":135},[87,673,674],{"class":89,"line":114},[87,675,676],{},"_hits = collections.Counter()\n",[87,678,679],{"class":89,"line":132},[87,680,136],{"emptyLinePlaceholder":135},[87,682,683],{"class":89,"line":139},[87,684,685],{},"def nth_hook(*, nth=1, key=None, **kw):\n",[87,687,688],{"class":89,"line":145},[87,689,690],{},"    caller = sys._getframe(1)\n",[87,692,693],{"class":89,"line":167},[87,694,695],{},"    k = key or (caller.f_code.co_filename, caller.f_lineno)\n",[87,697,698],{"class":89,"line":172},[87,699,700],{},"    _hits[k] += 1\n",[87,702,703],{"class":89,"line":178},[87,704,705],{},"    if _hits[k] == nth:\n",[87,707,708],{"class":89,"line":241},[87,709,710],{},"        pdb.Pdb().set_trace(caller)\n",[87,712,713],{"class":89,"line":247},[87,714,136],{"emptyLinePlaceholder":135},[87,716,717],{"class":89,"line":253},[87,718,719],{},"sys.breakpointhook = nth_hook\n",[87,721,722],{"class":89,"line":259},[87,723,724],{},"# breakpoint(nth=250)  → stops on the 250th pass through this line\n",[10,726,727,728,731,732,735,736,738],{},"Keep such hooks in a development-only module that a ",[13,729,730],{},"conftest.py"," or a ",[13,733,734],{},"sitecustomize.py"," in the virtualenv imports, never in application code. The point of the hook mechanism is that production code contains only plain ",[13,737,15],{}," calls — ideally none at all by the time it merges — and everything clever lives in the developer's environment.",[309,740,742,799],{"className":741},[312],[314,743,322,747,322,750,322,753,322,755,322,758,322,761,322,767,322,771,322,774,322,777,322,780,322,783,322,786,322,789,322,793,322,796],{"viewBox":563,"role":317,"ariaLabelledBy":744,"xmlns":321},[745,746],"bpc-t","bpc-d",[324,748,749],{"id":745},"Three ways to stop on the right iteration",[328,751,752],{"id":746},"Three options for conditional stops: an if statement around breakpoint for conditions expressible in code, pdb commands passed through the hook to print context on arrival, and a counting hook that stops on the nth pass through a line.",[350,754],{"x":352,"y":352,"width":353,"height":575,"rx":355,"fill":356},[358,756,757],{"x":360,"y":361,"textAnchor":362,"fontSize":363,"fontWeight":364,"fill":365},"Skip the boring iterations",[350,759],{"x":369,"y":372,"width":460,"height":760,"rx":378,"fill":409,"stroke":348,"strokeWidth":385},"152",[358,762,766],{"x":763,"y":764,"textAnchor":362,"fontSize":765,"fontWeight":364,"fill":365},"144","78","12.5","if … : breakpoint()",[358,768,770],{"x":763,"y":769,"textAnchor":362,"fontSize":449,"fill":365},"110","condition in plain Python",[358,772,773],{"x":763,"y":382,"textAnchor":362,"fontSize":449,"fontWeight":364,"fill":597},"simplest, clearest",[350,775],{"x":776,"y":372,"width":460,"height":760,"rx":378,"fill":383,"stroke":384,"strokeWidth":385},"282",[358,778,779],{"x":360,"y":764,"textAnchor":362,"fontSize":765,"fontWeight":364,"fill":365},"commands on arrival",[358,781,782],{"x":360,"y":769,"textAnchor":362,"fontSize":449,"fill":365},"p order · where",[358,784,785],{"x":360,"y":382,"textAnchor":362,"fontSize":449,"fontWeight":364,"fill":395},"context before the prompt",[350,787],{"x":788,"y":372,"width":460,"height":760,"rx":378,"fill":419,"stroke":420,"strokeWidth":385},"538",[358,790,792],{"x":791,"y":764,"textAnchor":362,"fontSize":765,"fontWeight":364,"fill":365},"656","counting hook",[358,794,795],{"x":791,"y":769,"textAnchor":362,"fontSize":449,"fill":365},"breakpoint(nth=250)",[358,797,798],{"x":791,"y":382,"textAnchor":362,"fontSize":449,"fontWeight":364,"fill":453},"no natural condition",[463,800,801],{},"Reach for the leftmost option first; the hook-based ones are for when the condition cannot be written as code.",[36,803,805],{"id":804},"a-team-convention-that-keeps-breakpoints-safe","A team convention that keeps breakpoints safe",[10,807,808,809,811,812,814,815,818,819,822,823,825],{},"The hook mechanism is most valuable when a whole team shares a small convention. Everyone writes plain ",[13,810,15],{},", never ",[13,813,19],{}," or ",[13,816,817],{},"import ipdb; ipdb.set_trace()",", so the choice of debugger is personal and lives in each developer's environment. CI and every container image set ",[13,820,821],{},"PYTHONBREAKPOINT=0",", so a forgotten call is inert rather than a hung job. The linter flags any ",[13,824,15],{}," in committed code, so forgotten calls are caught in review rather than in production logs. And the pre-commit configuration runs that lint rule locally, so most are never pushed at all.",[10,827,828],{},"With those four pieces in place, breakpoints stop being a source of incidents and become what they should be: a zero-cost, zero-setup way to stop and look.",[36,830,832],{"id":831},"debugging-a-running-service-with-the-hook","Debugging a running service with the hook",[10,834,835,836,839,840,842],{},"The container hook in the solution deserves a closer look, because it solves a problem that otherwise needs code changes: stopping inside a service that has no terminal. A web worker running under gunicorn in a container has stdin closed, so plain pdb cannot take input. With the custom hook installed at startup and ",[13,837,838],{},"IN_CONTAINER=1"," set, the same ",[13,841,15],{}," instead opens a debug adapter on port 5678 and waits.",[10,844,845,846,848],{},"The workflow is then: publish the port from the container, add a ",[13,847,15],{}," in the handler under investigation, send the request that reaches it, and attach from VS Code or PyCharm with a \"remote attach\" configuration pointing at the published port. The request pauses at the breakpoint with full variable inspection, stepping and an interactive console, exactly as if it were running locally.",[10,850,851,852,854],{},"Two precautions matter. Run a single worker process while debugging, or several workers will race to listen on the same port. And never ship an image with the hook enabled by default: a debug adapter listening on all interfaces is remote code execution for anyone who can reach the port. Gate it behind an environment variable that only development compose files set, and let ",[13,853,821],{}," in production images act as a second line of defence.",[36,856,858],{"id":857},"edge-cases-and-failure-modes","Edge cases and failure modes",[41,860,861,875,887,907,915],{},[44,862,863,867,868,871,872,874],{},[864,865,866],"strong",{},"Breakpoint hangs CI."," A stray call waits forever for input on a closed stdin — or fails with ",[13,869,870],{},"BdbQuit"," in some runners. Set ",[13,873,821],{}," in CI and lint for leftovers.",[44,876,877,880,881,884,885,50],{},[864,878,879],{},"Misspelled debugger."," ",[13,882,883],{},"PYTHONBREAKPOINT=ipbd.set_trace"," fails to import, warns, and skips the breakpoint entirely. If a breakpoint seems ignored, look for the ",[13,886,497],{},[44,888,889,899,900,902,903,906],{},[864,890,891,894,895,898],{},[13,892,893],{},"-E"," and ",[13,896,897],{},"-I"," flags."," Python started with ",[13,901,893],{}," or in isolated mode ignores ",[13,904,905],{},"PYTHON*"," environment variables, including this one. Set the hook in code if you must use those flags.",[44,908,909,880,912,914],{},[864,910,911],{},"Debugger not installed in the environment.",[13,913,56],{}," in your global Python does not help a virtualenv. Install it as a dev dependency.",[44,916,917,920],{},[864,918,919],{},"Remote debuggers blocking startup."," A hook that waits for a client blocks the process until one attaches. Use it only behind an explicit flag.",[36,922,924],{"id":923},"frequently-asked-questions","Frequently Asked Questions",[10,926,927,930,931,934,935,937,938,940],{},[864,928,929],{},"How do I make breakpoint() open ipdb instead of pdb?","\nSet the environment variable ",[13,932,933],{},"PYTHONBREAKPOINT=ipdb.set_trace",". ",[13,936,15],{}," imports the named callable when it is called and runs it, so any importable function works, including ",[13,939,152],{}," or a remote debugger's entry point.",[10,942,943,946,947,949,950,952],{},[864,944,945],{},"How do I stop a forgotten breakpoint() from hanging CI?","\nSet ",[13,948,821],{}," in the CI environment, which turns every ",[13,951,15],{}," call into a no-op. Also add a lint rule such as Ruff's T100 so breakpoints are caught before they are merged.",[10,954,955,958,959,961],{},[864,956,957],{},"Why does breakpoint() work inside pytest even though output is captured?","\npytest wraps ",[13,960,391],{}," and suspends output capture before the debugger starts, then resumes it when you continue. That is why you can type at the pdb prompt inside a captured test.",[36,963,965],{"id":964},"related","Related",[41,967,968,974,981,988],{},[44,969,970,973],{},[69,971,972],{"href":71},"Interactive Debugging with pdb and ipdb"," — commands and workflow.",[44,975,976,980],{},[69,977,979],{"href":978},"\u002Fsystematic-debugging-performance-profiling\u002Finteractive-debugging-with-pdb-and-ipdb\u002Fdropping-into-pdb-on-test-failure\u002F","Dropping into pdb on Test Failure"," — --pdb and --trace.",[44,982,983,987],{},[69,984,986],{"href":985},"\u002Fsystematic-debugging-performance-profiling\u002Finteractive-debugging-with-pdb-and-ipdb\u002Fsetting-conditional-breakpoints-in-pdb\u002F","Setting Conditional Breakpoints in pdb"," — stopping only when it matters.",[44,989,990,994],{},[69,991,993],{"href":992},"\u002Fsystematic-debugging-performance-profiling\u002Fdebugging-tests-in-ci-and-containers\u002Fattaching-debugpy-to-a-container\u002F","Attaching debugpy to a Container"," — the remote hook in practice.",[10,996,997,998],{},"← Back to ",[69,999,972],{"href":71},[1001,1002,1003],"style",{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#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);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":83,"searchDepth":97,"depth":97,"links":1005},[1006,1007,1008,1009,1010,1011,1012,1013,1014,1015],{"id":38,"depth":97,"text":39},{"id":75,"depth":97,"text":76},{"id":468,"depth":97,"text":469},{"id":512,"depth":97,"text":513},{"id":628,"depth":97,"text":629},{"id":804,"depth":97,"text":805},{"id":831,"depth":97,"text":832},{"id":857,"depth":97,"text":858},{"id":923,"depth":97,"text":924},{"id":964,"depth":97,"text":965},"Control Python's built-in breakpoint(): choosing ipdb, pudb or a remote debugger with PYTHONBREAKPOINT, disabling stray breakpoints in CI, sys.breakpointhook, and how pytest handles capture.","md",{"slug":1019,"type":1020,"breadcrumb":1021,"datePublished":1022,"dateModified":1022,"faq":1023,"howto":1030},"using-breakpoint-and-pythonbreakpoint-hooks","article","breakpoint() & hooks","2026-09-18",[1024,1026,1028],{"q":929,"a":1025},"Set the environment variable PYTHONBREAKPOINT=ipdb.set_trace. breakpoint() imports the named callable when it is called and runs it, so any importable function works, including pudb.set_trace or a remote debugger's entry point.",{"q":945,"a":1027},"Set PYTHONBREAKPOINT=0 in the CI environment, which turns every breakpoint() call into a no-op. Also add a lint rule such as Ruff's T100 so breakpoints are caught before they are merged.",{"q":957,"a":1029},"pytest wraps sys.breakpointhook and suspends output capture before the debugger starts, then resumes it when you continue. That is why you can type at the pdb prompt inside a captured test.",{"name":1031,"description":1032,"steps":1033},"How to configure breakpoint() for your workflow","Select the debugger with PYTHONBREAKPOINT, disable breakpoints where no one is watching, and customise the hook when needed.",[1034,1037,1040,1043],{"name":1035,"text":1036},"Choose a debugger","Set PYTHONBREAKPOINT to ipdb.set_trace, pudb.set_trace or another importable callable.",{"name":1038,"text":1039},"Disable in CI","Set PYTHONBREAKPOINT=0 in pipelines and containers so stray calls do nothing.",{"name":1041,"text":1042},"Lint for leftovers","Enable a rule that flags breakpoint() and pdb.set_trace in committed code.",{"name":1044,"text":1045},"Customise the hook","Assign sys.breakpointhook for behaviour an environment variable cannot express, such as conditional remote attach.","\u002Fsystematic-debugging-performance-profiling\u002Finteractive-debugging-with-pdb-and-ipdb\u002Fusing-breakpoint-and-pythonbreakpoint-hooks",{"title":5,"description":1016},"systematic-debugging-performance-profiling\u002Finteractive-debugging-with-pdb-and-ipdb\u002Fusing-breakpoint-and-pythonbreakpoint-hooks\u002Findex","kIdCmXGlqJkP_suAdcds7yoyZwHcO1jEzoLqzyFzBPo",1789718767812]