[{"data":1,"prerenderedAt":1677},["ShallowReactive",2],{"page-\u002Fsystematic-debugging-performance-profiling\u002Freading-tracebacks-and-exception-chains\u002F":3},{"id":4,"title":5,"body":6,"description":1636,"extension":1637,"meta":1638,"navigation":221,"path":1673,"seo":1674,"stem":1675,"__hash__":1676},"content\u002Fsystematic-debugging-performance-profiling\u002Freading-tracebacks-and-exception-chains\u002Findex.md","Reading Tracebacks & Exception Chains",{"type":7,"value":8,"toc":1615},"minimark",[9,13,18,49,53,56,165,190,194,199,287,291,352,366,370,385,392,418,422,488,494,498,592,611,615,618,675,690,694,807,811,817,886,914,917,991,995,998,1001,1054,1060,1067,1074,1078,1081,1165,1178,1193,1197,1200,1213,1222,1234,1247,1292,1296,1299,1313,1324,1336,1340,1343,1487,1494,1504,1508,1521,1537,1543,1556,1565,1569,1604,1611],[10,11,12],"p",{},"A traceback contains everything needed to diagnose most failures, and most of it goes unread. Engineers scan for a familiar exception name, look at one line, and start guessing. The habits that replace guessing are mechanical — read bottom-up, follow the chain markers, narrow with the caret — and they turn a ten-minute hunt into a twenty-second read.",[14,15,17],"h2",{"id":16},"prerequisites","Prerequisites",[19,20,21,30,36],"ul",{},[22,23,24,25,29],"li",{},"Python 3.11+ for fine-grained error locations and ",[26,27,28],"code",{},"ExceptionGroup","; the rest applies to 3.9 onward.",[22,31,32,35],{},[26,33,34],{},"pytest >= 8.0"," for the traceback styles discussed here.",[22,37,38,39,42,43,48],{},"Familiarity with ",[26,40,41],{},"pdb"," for the cases a traceback alone cannot resolve — see ",[44,45,47],"a",{"href":46},"\u002Fsystematic-debugging-performance-profiling\u002Finteractive-debugging-with-pdb-and-ipdb\u002F","interactive debugging with pdb and ipdb",".",[14,50,52],{"id":51},"core-concept-the-shape-of-a-traceback","Core concept: the shape of a traceback",[10,54,55],{},"A traceback is a stack, printed outermost first. The top frame is where execution entered, the bottom frame is where the exception was raised, and the last line is the exception itself. Chained exceptions print as several such stacks separated by a marker that says how they relate.",[57,58,61,161],"figure",{"className":59},[60],"diagram",[62,63,70,71,70,75,70,79,70,87,70,97,70,107,70,113,70,118,70,122,70,128,70,135,70,139,70,145,70,148,70,152,70,156],"svg",{"viewBox":64,"role":65,"ariaLabelledBy":66,"xmlns":69},"0 0 840 300","img",[67,68],"tb-t","tb-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","\n  ",[72,73,74],"title",{"id":67},"Anatomy of a chained traceback",[76,77,78],"desc",{"id":68},"A traceback printed in two blocks. The upper block is the original exception with its own frames. A separator line states either that it was the direct cause, from an explicit raise-from, or that it occurred during handling, from an implicit context. The lower block is the exception that actually escaped, with the exception type and message on the final line.",[80,81],"rect",{"x":82,"y":82,"width":83,"height":84,"rx":85,"fill":86},"0","840","300","14","#fffdf8",[88,89,96],"text",{"x":90,"y":91,"textAnchor":92,"fontSize":93,"fontWeight":94,"fill":95},"420","28","middle","16","700","#3d405b","Read the last line, then the bottom block, then upward",[80,98],{"x":99,"y":100,"width":101,"height":102,"rx":103,"fill":104,"stroke":105,"strokeWidth":106},"34","50","772","80","11","#f4f1de","rgba(61,64,91,0.35)","1.6",[88,108,112],{"x":109,"y":110,"fontSize":111,"fontWeight":94,"fill":95},"52","72","11.5","Traceback (most recent call last):",[88,114,117],{"x":115,"y":116,"fontSize":103,"fill":95},"70","92","File \"app\u002Fhttp.py\", line 44, in fetch_json",[88,119,121],{"x":115,"y":120,"fontSize":103,"fill":95},"110","File \"json\u002Fdecoder.py\", line 355, in raw_decode",[88,123,127],{"x":124,"y":125,"fontSize":103,"fontWeight":94,"fill":126},"640","101","#8f3d22","JSONDecodeError",[80,129],{"x":99,"y":130,"width":101,"height":99,"rx":131,"fill":132,"stroke":133,"strokeWidth":134},"140","9","#f7f0da","#f2cc8f","1.8",[88,136,138],{"x":90,"y":137,"textAnchor":92,"fontSize":111,"fontWeight":94,"fill":95},"162","The above exception was the direct cause of the following exception",[80,140],{"x":99,"y":141,"width":101,"height":102,"rx":103,"fill":142,"stroke":143,"strokeWidth":144},"184","#fbe9e3","#e07a5f","2",[88,146,112],{"x":109,"y":147,"fontSize":111,"fontWeight":94,"fill":95},"206",[88,149,151],{"x":115,"y":150,"fontSize":103,"fill":95},"226","File \"app\u002Fapi.py\", line 18, in get_invoice   ← your code",[88,153,155],{"x":115,"y":154,"fontSize":103,"fill":95},"244","File \"app\u002Fhttp.py\", line 47, in fetch_json",[88,157,160],{"x":90,"y":158,"textAnchor":92,"fontSize":159,"fontWeight":94,"fill":126},"284","12","UpstreamError: billing returned malformed JSON",[162,163,164],"figcaption",{},"Two blocks, one separator. The separator's wording is the single most informative token in the whole output, because it says whether the conversion was deliberate.",[10,166,167,168,172,173,176,177,180,181,184,185,189],{},"Two separators exist and they mean different things. ",[169,170,171],"strong",{},"\"The above exception was the direct cause\""," comes from ",[26,174,175],{},"raise X from Y"," and marks a deliberate conversion — somebody decided the caller should see a domain error. ",[169,178,179],{},"\"During handling of the above exception, another exception occurred\""," is implicit: a second exception escaped from inside an ",[26,182,183],{},"except"," block. The second usually means the ",[186,187,188],"em",{},"handler"," is broken, which is a different and often more embarrassing bug than the original.",[14,191,193],{"id":192},"step-by-step-implementation","Step-by-step implementation",[195,196,198],"h3",{"id":197},"_1-chain-deliberately-when-converting","1. Chain deliberately when converting",[200,201,206],"pre",{"className":202,"code":203,"language":204,"meta":205,"style":205},"language-python shiki shiki-themes github-light github-dark","import json\n\nfrom myapp.errors import UpstreamError\n\n\ndef fetch_json(url: str) -> dict:\n    body = _get(url)\n    try:\n        return json.loads(body)\n    except json.JSONDecodeError as exc:\n        # `from exc` sets __cause__: the original traceback is preserved and\n        # the printed separator says the conversion was intentional.\n        raise UpstreamError(f\"{url} returned malformed JSON\") from exc\n","python","",[26,207,208,216,223,229,234,239,245,251,257,263,269,275,281],{"__ignoreMap":205},[209,210,213],"span",{"class":211,"line":212},"line",1,[209,214,215],{},"import json\n",[209,217,219],{"class":211,"line":218},2,[209,220,222],{"emptyLinePlaceholder":221},true,"\n",[209,224,226],{"class":211,"line":225},3,[209,227,228],{},"from myapp.errors import UpstreamError\n",[209,230,232],{"class":211,"line":231},4,[209,233,222],{"emptyLinePlaceholder":221},[209,235,237],{"class":211,"line":236},5,[209,238,222],{"emptyLinePlaceholder":221},[209,240,242],{"class":211,"line":241},6,[209,243,244],{},"def fetch_json(url: str) -> dict:\n",[209,246,248],{"class":211,"line":247},7,[209,249,250],{},"    body = _get(url)\n",[209,252,254],{"class":211,"line":253},8,[209,255,256],{},"    try:\n",[209,258,260],{"class":211,"line":259},9,[209,261,262],{},"        return json.loads(body)\n",[209,264,266],{"class":211,"line":265},10,[209,267,268],{},"    except json.JSONDecodeError as exc:\n",[209,270,272],{"class":211,"line":271},11,[209,273,274],{},"        # `from exc` sets __cause__: the original traceback is preserved and\n",[209,276,278],{"class":211,"line":277},12,[209,279,280],{},"        # the printed separator says the conversion was intentional.\n",[209,282,284],{"class":211,"line":283},13,[209,285,286],{},"        raise UpstreamError(f\"{url} returned malformed JSON\") from exc\n",[195,288,290],{"id":289},"_2-suppress-a-chain-only-when-the-original-is-genuinely-noise","2. Suppress a chain only when the original is genuinely noise",[200,292,294],{"className":202,"code":293,"language":204,"meta":205,"style":205},"import os\n\nfrom myapp.errors import ConfigError\n\n\ndef load_port() -> int:\n    try:\n        return int(os.environ[\"PORT\"])\n    except (KeyError, ValueError):\n        # `from None` clears __context__: the caller sees one clean error\n        # rather than a KeyError traceback that adds nothing.\n        raise ConfigError(\"PORT must be set to an integer\") from None\n",[26,295,296,301,305,310,314,318,323,327,332,337,342,347],{"__ignoreMap":205},[209,297,298],{"class":211,"line":212},[209,299,300],{},"import os\n",[209,302,303],{"class":211,"line":218},[209,304,222],{"emptyLinePlaceholder":221},[209,306,307],{"class":211,"line":225},[209,308,309],{},"from myapp.errors import ConfigError\n",[209,311,312],{"class":211,"line":231},[209,313,222],{"emptyLinePlaceholder":221},[209,315,316],{"class":211,"line":236},[209,317,222],{"emptyLinePlaceholder":221},[209,319,320],{"class":211,"line":241},[209,321,322],{},"def load_port() -> int:\n",[209,324,325],{"class":211,"line":247},[209,326,256],{},[209,328,329],{"class":211,"line":253},[209,330,331],{},"        return int(os.environ[\"PORT\"])\n",[209,333,334],{"class":211,"line":259},[209,335,336],{},"    except (KeyError, ValueError):\n",[209,338,339],{"class":211,"line":265},[209,340,341],{},"        # `from None` clears __context__: the caller sees one clean error\n",[209,343,344],{"class":211,"line":271},[209,345,346],{},"        # rather than a KeyError traceback that adds nothing.\n",[209,348,349],{"class":211,"line":277},[209,350,351],{},"        raise ConfigError(\"PORT must be set to an integer\") from None\n",[10,353,354,357,358,361,362,365],{},[26,355,356],{},"from None"," is the right call when the internal exception carries no information the caller can act on. It is the wrong call when it does — suppressing a ",[26,359,360],{},"ConnectionResetError"," behind a generic ",[26,363,364],{},"ServiceUnavailable"," removes the one detail an operator needed.",[195,367,369],{"id":368},"_3-read-the-caret-markers","3. Read the caret markers",[200,371,373],{"className":202,"code":372,"language":204,"meta":205,"style":205},"def total(order):\n    return order.customer.address.postcode        # something here is None\n",[26,374,375,380],{"__ignoreMap":205},[209,376,377],{"class":211,"line":212},[209,378,379],{},"def total(order):\n",[209,381,382],{"class":211,"line":218},[209,383,384],{},"    return order.customer.address.postcode        # something here is None\n",[200,386,390],{"className":387,"code":389,"language":88,"meta":205},[388],"language-text","Traceback (most recent call last):\n  File \"app\u002Fbilling.py\", line 12, in total\n    return order.customer.address.postcode\n           ^^^^^^^^^^^^^^^^^^^^^^\nAttributeError: 'NoneType' object has no attribute 'postcode'\n",[26,391,389],{"__ignoreMap":205},[10,393,394,395,398,399,402,403,406,407,410,411,414,415,417],{},"Before 3.11 this said only that ",[186,396,397],{},"something"," on the line was ",[26,400,401],{},"None",". The carets say it was ",[26,404,405],{},"order.customer.address"," — three attribute accesses in, so ",[26,408,409],{},"customer"," exists and ",[26,412,413],{},"address"," is ",[26,416,401],{},". On a line with several calls, that distinction saves the entire investigation.",[195,419,421],{"id":420},"_4-choose-a-traceback-style-per-situation","4. Choose a traceback style per situation",[200,423,427],{"className":424,"code":425,"language":426,"meta":205,"style":205},"language-bash shiki shiki-themes github-light github-dark","pytest --tb=short -q          # CI: one frame per file, readable in volume\npytest --tb=long              # diagnosing: full frames with source\npytest --tb=line              # a sweep: one line per failure\npytest --showlocals --tb=long # when the values matter more than the path\npytest --tb=native            # exactly what Python would print\n","bash",[26,428,429,446,456,466,478],{"__ignoreMap":205},[209,430,431,435,439,442],{"class":211,"line":212},[209,432,434],{"class":433},"sScJk","pytest",[209,436,438],{"class":437},"sj4cs"," --tb=short",[209,440,441],{"class":437}," -q",[209,443,445],{"class":444},"sJ8bj","          # CI: one frame per file, readable in volume\n",[209,447,448,450,453],{"class":211,"line":218},[209,449,434],{"class":433},[209,451,452],{"class":437}," --tb=long",[209,454,455],{"class":444},"              # diagnosing: full frames with source\n",[209,457,458,460,463],{"class":211,"line":225},[209,459,434],{"class":433},[209,461,462],{"class":437}," --tb=line",[209,464,465],{"class":444},"              # a sweep: one line per failure\n",[209,467,468,470,473,475],{"class":211,"line":231},[209,469,434],{"class":433},[209,471,472],{"class":437}," --showlocals",[209,474,452],{"class":437},[209,476,477],{"class":444}," # when the values matter more than the path\n",[209,479,480,482,485],{"class":211,"line":236},[209,481,434],{"class":433},[209,483,484],{"class":437}," --tb=native",[209,486,487],{"class":444},"            # exactly what Python would print\n",[10,489,490,493],{},[26,491,492],{},"--showlocals"," is underused. A failure whose traceback is obvious but whose cause is not — \"why was that list empty?\" — is usually answered immediately by the locals in the frame, with no re-run needed.",[195,495,497],{"id":496},"_5-capture-tracebacks-from-threads-and-tasks","5. Capture tracebacks from threads and tasks",[200,499,501],{"className":202,"code":500,"language":204,"meta":205,"style":205},"import threading\n\n\ndef test_worker_exceptions_reach_the_test():\n    errors: list[BaseException] = []\n\n    def hook(args: threading.ExceptHookArgs) -> None:\n        errors.append(args.exc_value)         # a raw Thread otherwise swallows it\n\n    original, threading.excepthook = threading.excepthook, hook\n    try:\n        thread = threading.Thread(target=broken_worker)\n        thread.start()\n        thread.join(timeout=5)\n    finally:\n        threading.excepthook = original\n\n    assert not errors, errors[0]\n",[26,502,503,508,512,516,521,526,530,535,540,544,549,553,558,563,569,575,581,586],{"__ignoreMap":205},[209,504,505],{"class":211,"line":212},[209,506,507],{},"import threading\n",[209,509,510],{"class":211,"line":218},[209,511,222],{"emptyLinePlaceholder":221},[209,513,514],{"class":211,"line":225},[209,515,222],{"emptyLinePlaceholder":221},[209,517,518],{"class":211,"line":231},[209,519,520],{},"def test_worker_exceptions_reach_the_test():\n",[209,522,523],{"class":211,"line":236},[209,524,525],{},"    errors: list[BaseException] = []\n",[209,527,528],{"class":211,"line":241},[209,529,222],{"emptyLinePlaceholder":221},[209,531,532],{"class":211,"line":247},[209,533,534],{},"    def hook(args: threading.ExceptHookArgs) -> None:\n",[209,536,537],{"class":211,"line":253},[209,538,539],{},"        errors.append(args.exc_value)         # a raw Thread otherwise swallows it\n",[209,541,542],{"class":211,"line":259},[209,543,222],{"emptyLinePlaceholder":221},[209,545,546],{"class":211,"line":265},[209,547,548],{},"    original, threading.excepthook = threading.excepthook, hook\n",[209,550,551],{"class":211,"line":271},[209,552,256],{},[209,554,555],{"class":211,"line":277},[209,556,557],{},"        thread = threading.Thread(target=broken_worker)\n",[209,559,560],{"class":211,"line":283},[209,561,562],{},"        thread.start()\n",[209,564,566],{"class":211,"line":565},14,[209,567,568],{},"        thread.join(timeout=5)\n",[209,570,572],{"class":211,"line":571},15,[209,573,574],{},"    finally:\n",[209,576,578],{"class":211,"line":577},16,[209,579,580],{},"        threading.excepthook = original\n",[209,582,584],{"class":211,"line":583},17,[209,585,222],{"emptyLinePlaceholder":221},[209,587,589],{"class":211,"line":588},18,[209,590,591],{},"    assert not errors, errors[0]\n",[10,593,594,595,598,599,602,603,606,607,610],{},"An exception in a ",[26,596,597],{},"threading.Thread"," prints to stderr and is then discarded; the test passes. ",[26,600,601],{},"threading.excepthook"," (3.8+) is the standard-library answer, and ",[26,604,605],{},"ThreadPoolExecutor"," with ",[26,608,609],{},"future.result()"," is the simpler one where it applies.",[14,612,614],{"id":613},"verification","Verification",[10,616,617],{},"Check that chains survive your own error handling, which is where they are most often lost:",[200,619,621],{"className":202,"code":620,"language":204,"meta":205,"style":205},"import pytest\n\n\ndef test_upstream_error_preserves_the_cause():\n    with pytest.raises(UpstreamError) as excinfo:\n        fetch_json(\"https:\u002F\u002Fbilling.test\u002Fbad\")\n\n    # The chain is part of the contract: a caller logging exc.__cause__\n    # must get the decoding error, not None.\n    assert isinstance(excinfo.value.__cause__, json.JSONDecodeError)\n    assert excinfo.value.__suppress_context__ is False\n",[26,622,623,628,632,636,641,646,651,655,660,665,670],{"__ignoreMap":205},[209,624,625],{"class":211,"line":212},[209,626,627],{},"import pytest\n",[209,629,630],{"class":211,"line":218},[209,631,222],{"emptyLinePlaceholder":221},[209,633,634],{"class":211,"line":225},[209,635,222],{"emptyLinePlaceholder":221},[209,637,638],{"class":211,"line":231},[209,639,640],{},"def test_upstream_error_preserves_the_cause():\n",[209,642,643],{"class":211,"line":236},[209,644,645],{},"    with pytest.raises(UpstreamError) as excinfo:\n",[209,647,648],{"class":211,"line":241},[209,649,650],{},"        fetch_json(\"https:\u002F\u002Fbilling.test\u002Fbad\")\n",[209,652,653],{"class":211,"line":247},[209,654,222],{"emptyLinePlaceholder":221},[209,656,657],{"class":211,"line":253},[209,658,659],{},"    # The chain is part of the contract: a caller logging exc.__cause__\n",[209,661,662],{"class":211,"line":259},[209,663,664],{},"    # must get the decoding error, not None.\n",[209,666,667],{"class":211,"line":265},[209,668,669],{},"    assert isinstance(excinfo.value.__cause__, json.JSONDecodeError)\n",[209,671,672],{"class":211,"line":271},[209,673,674],{},"    assert excinfo.value.__suppress_context__ is False\n",[10,676,677,678,681,682,685,686,689],{},"Asserting on ",[26,679,680],{},"__cause__"," looks fussy until the first time somebody \"tidies\" a ",[26,683,684],{},"raise ... from exc"," into a bare ",[26,687,688],{},"raise UpstreamError(...)"," and every production traceback loses its origin.",[14,691,693],{"id":692},"troubleshooting","Troubleshooting",[695,696,697,713],"table",{},[698,699,700],"thead",{},[701,702,703,707,710],"tr",{},[704,705,706],"th",{},"Symptom",[704,708,709],{},"Root cause",[704,711,712],{},"Fix",[714,715,716,731,754,768,783,796],"tbody",{},[701,717,718,722,728],{},[719,720,721],"td",{},"\"During handling of the above exception\"",[719,723,724,725,727],{},"A second exception escaped an ",[26,726,183],{}," block",[719,729,730],{},"Fix the handler; it is usually the real bug",[701,732,733,736,748],{},[719,734,735],{},"Chain missing entirely",[719,737,738,741,742,745,746],{},[26,739,740],{},"raise X"," without ",[26,743,744],{},"from exc"," inside ",[26,747,183],{},[719,749,750,751,753],{},"Add ",[26,752,744],{},", or let the original propagate",[701,755,756,759,762],{},[719,757,758],{},"Traceback points at library code only",[719,760,761],{},"Your frames were above the visible window",[719,763,764,765],{},"Read upward; use ",[26,766,767],{},"--tb=long",[701,769,770,773,778],{},[719,771,772],{},"No traceback at all from a worker",[719,774,775,777],{},[26,776,597],{}," swallowed it",[719,779,780,782],{},[26,781,601],{},", or use futures",[701,784,785,790,793],{},[719,786,787,789],{},[26,788,28],{}," with confusing frames",[719,791,792],{},"Several concurrent failures",[719,794,795],{},"Read the leaf tracebacks, not the group's",[701,797,798,801,804],{},[719,799,800],{},"Caret markers absent",[719,802,803],{},"Running on Python 3.10 or earlier",[719,805,806],{},"Upgrade, or split the expression across lines",[14,808,810],{"id":809},"exception-groups-and-except","Exception groups and except*",[10,812,813,814,816],{},"Task groups change the shape of what arrives. When several children fail concurrently, the block raises an ",[26,815,28],{}," containing all of them, and the printed traceback nests one sub-traceback per contained exception.",[200,818,820],{"className":202,"code":819,"language":204,"meta":205,"style":205},"import asyncio\n\ntry:\n    async with asyncio.TaskGroup() as tg:\n        tg.create_task(fetch(\"\u002Fa\"))\n        tg.create_task(fetch(\"\u002Fb\"))\nexcept* TimeoutError as group:\n    # `group` is itself an ExceptionGroup containing only the TimeoutErrors.\n    for exc in group.exceptions:\n        logger.warning(\"timed out: %s\", exc)\nexcept* ValueError as group:\n    # Both handlers can run for the same original group.\n    raise UpstreamError(\"bad payload\") from group\n",[26,821,822,827,831,836,841,846,851,856,861,866,871,876,881],{"__ignoreMap":205},[209,823,824],{"class":211,"line":212},[209,825,826],{},"import asyncio\n",[209,828,829],{"class":211,"line":218},[209,830,222],{"emptyLinePlaceholder":221},[209,832,833],{"class":211,"line":225},[209,834,835],{},"try:\n",[209,837,838],{"class":211,"line":231},[209,839,840],{},"    async with asyncio.TaskGroup() as tg:\n",[209,842,843],{"class":211,"line":236},[209,844,845],{},"        tg.create_task(fetch(\"\u002Fa\"))\n",[209,847,848],{"class":211,"line":241},[209,849,850],{},"        tg.create_task(fetch(\"\u002Fb\"))\n",[209,852,853],{"class":211,"line":247},[209,854,855],{},"except* TimeoutError as group:\n",[209,857,858],{"class":211,"line":253},[209,859,860],{},"    # `group` is itself an ExceptionGroup containing only the TimeoutErrors.\n",[209,862,863],{"class":211,"line":259},[209,864,865],{},"    for exc in group.exceptions:\n",[209,867,868],{"class":211,"line":265},[209,869,870],{},"        logger.warning(\"timed out: %s\", exc)\n",[209,872,873],{"class":211,"line":271},[209,874,875],{},"except* ValueError as group:\n",[209,877,878],{"class":211,"line":277},[209,879,880],{},"    # Both handlers can run for the same original group.\n",[209,882,883],{"class":211,"line":283},[209,884,885],{},"    raise UpstreamError(\"bad payload\") from group\n",[10,887,888,889,892,893,895,896,899,900,903,904,907,908,910,911,913],{},"Two things about ",[26,890,891],{},"except*"," catch people out. It always binds an ",[26,894,28],{},", even when exactly one exception matched, so ",[26,897,898],{},"group.exceptions"," is the thing to iterate rather than ",[26,901,902],{},"group"," itself. And ",[186,905,906],{},"several"," ",[26,909,891],{}," clauses can run for a single raised group, unlike ordinary ",[26,912,183],{}," where the first match wins — the group is split by type and each matching clause receives its share.",[10,915,916],{},"Reading the printed form follows the same rule as any chain: the outermost frames describe where the group was assembled, which is almost never interesting, and the leaves describe the actual failures. A group with three leaves and one distinct message is usually one bug hit by three concurrent requests; a group with three different messages is usually three bugs, or one bug plus two cancellations.",[57,918,920,988],{"className":919},[60],[62,921,70,926,70,929,70,932,70,936,70,940,70,945,70,950,70,954,70,959,70,964,70,967,70,971,70,974,70,978,70,984],{"viewBox":922,"role":65,"ariaLabelledBy":923,"xmlns":69},"0 0 820 268",[924,925],"eg-t","eg-d",[72,927,928],{"id":924},"Structure of an ExceptionGroup traceback",[76,930,931],{"id":925},"An outer exception group frame from the task group block contains three nested sub-tracebacks. Two are timeout errors from the same call site and one is a value error from a different one. A note indicates that the leaves carry the diagnosis while the outer frames only show where the group was assembled.",[80,933],{"x":82,"y":82,"width":934,"height":935,"rx":85,"fill":86},"820","268",[88,937,939],{"x":938,"y":91,"textAnchor":92,"fontSize":93,"fontWeight":94,"fill":95},"410","The diagnosis is in the leaves",[80,941],{"x":942,"y":100,"width":943,"height":944,"rx":159,"fill":104,"stroke":95,"strokeWidth":134},"26","768","180",[88,946,949],{"x":947,"y":948,"fontSize":159,"fontWeight":94,"fill":95},"46","74","ExceptionGroup: unhandled errors in a TaskGroup (3 sub-exceptions)",[88,951,953],{"x":947,"y":952,"fontSize":103,"fill":95},"94","File \"app\u002Fsync.py\", line 61, in refresh_all — where the group was assembled",[80,955],{"x":109,"y":956,"width":957,"height":958,"rx":131,"fill":142,"stroke":143,"strokeWidth":134},"106","716","36",[88,960,963],{"x":961,"y":962,"fontSize":103,"fill":95},"68","129","+---------------- 1 ---------------- TimeoutError: \u002Fa after 5s",[80,965],{"x":109,"y":966,"width":957,"height":958,"rx":131,"fill":142,"stroke":143,"strokeWidth":134},"148",[88,968,970],{"x":961,"y":969,"fontSize":103,"fill":95},"171","+---------------- 2 ---------------- TimeoutError: \u002Fb after 5s",[80,972],{"x":109,"y":973,"width":957,"height":99,"rx":131,"fill":132,"stroke":133,"strokeWidth":134},"190",[88,975,977],{"x":961,"y":976,"fontSize":103,"fill":95},"212","+---------------- 3 ---------------- ValueError: malformed payload",[80,979],{"x":942,"y":980,"width":943,"height":981,"rx":982,"fill":86,"stroke":105,"strokeWidth":983},"238","24","8","1.4",[88,985,987],{"x":938,"y":986,"textAnchor":92,"fontSize":103,"fill":95},"255","Two identical leaves and one different: one timeout bug hit twice, plus a separate parsing failure.",[162,989,990],{},"Counting distinct leaf messages is the fastest triage available on a group — it separates \"one bug, many requests\" from \"several unrelated failures\".",[14,992,994],{"id":993},"frames-and-how-far-up-to-read","Frames, and how far up to read",[10,996,997],{},"The mechanical skill worth practising is deciding which frame to look at. A traceback through a web framework, an ORM and a driver can be forty frames deep, of which three are yours.",[10,999,1000],{},"The reliable procedure is to scan upward from the bottom for the first frame whose file path is inside your project. That frame is where your code handed control to somebody else's, and the bad value almost always originated there or above it. Library frames below it are usually correct code correctly rejecting bad input.",[200,1002,1004],{"className":202,"code":1003,"language":204,"meta":205,"style":205},"import traceback\n\n\ndef project_frames(exc: BaseException, root: str = \"\u002Fapp\u002F\") -> list[str]:\n    \"\"\"The frames that are yours, in the order they appear.\"\"\"\n    return [\n        f\"{frame.filename}:{frame.lineno} in {frame.name}\"\n        for frame in traceback.extract_tb(exc.__traceback__)\n        if frame.filename.startswith(root)\n    ]\n",[26,1005,1006,1011,1015,1019,1024,1029,1034,1039,1044,1049],{"__ignoreMap":205},[209,1007,1008],{"class":211,"line":212},[209,1009,1010],{},"import traceback\n",[209,1012,1013],{"class":211,"line":218},[209,1014,222],{"emptyLinePlaceholder":221},[209,1016,1017],{"class":211,"line":225},[209,1018,222],{"emptyLinePlaceholder":221},[209,1020,1021],{"class":211,"line":231},[209,1022,1023],{},"def project_frames(exc: BaseException, root: str = \"\u002Fapp\u002F\") -> list[str]:\n",[209,1025,1026],{"class":211,"line":236},[209,1027,1028],{},"    \"\"\"The frames that are yours, in the order they appear.\"\"\"\n",[209,1030,1031],{"class":211,"line":241},[209,1032,1033],{},"    return [\n",[209,1035,1036],{"class":211,"line":247},[209,1037,1038],{},"        f\"{frame.filename}:{frame.lineno} in {frame.name}\"\n",[209,1040,1041],{"class":211,"line":253},[209,1042,1043],{},"        for frame in traceback.extract_tb(exc.__traceback__)\n",[209,1045,1046],{"class":211,"line":259},[209,1047,1048],{},"        if frame.filename.startswith(root)\n",[209,1050,1051],{"class":211,"line":265},[209,1052,1053],{},"    ]\n",[200,1055,1058],{"className":1056,"code":1057,"language":88,"meta":205},[388],"['\u002Fapp\u002Fapi.py:18 in get_invoice', '\u002Fapp\u002Fhttp.py:47 in fetch_json']\n",[26,1059,1057],{"__ignoreMap":205},[10,1061,1062,1063,1066],{},"Two frames instead of forty, and the second is where to start. Logging this alongside the full traceback makes production errors triageable at a glance, and the same filter drives the ",[26,1064,1065],{},"--tb=short"," style pytest offers.",[10,1068,1069,1070,1073],{},"Two exceptions to the \"read your own frames\" rule are worth knowing. A ",[26,1071,1072],{},"TypeError"," deep inside a library often means you passed the wrong type several frames up, and the library frame names the parameter — useful information that the filter discards. And an exception raised inside a C extension may have no Python frame for the actual failure at all, in which case the deepest Python frame is the call that entered the extension and the investigation moves to its arguments.",[14,1075,1077],{"id":1076},"tracebacks-that-cross-a-process-boundary","Tracebacks that cross a process boundary",[10,1079,1080],{},"A traceback is a live object graph referencing code objects and frames, and none of that survives being sent to another process. What arrives is a formatted string, or nothing.",[200,1082,1084],{"className":202,"code":1083,"language":204,"meta":205,"style":205},"import concurrent.futures\nimport traceback\n\n\ndef worker(payload):\n    try:\n        return process(payload)\n    except Exception as exc:\n        # Format here, where the frames still exist; the parent gets text.\n        raise RuntimeError(\n            f\"worker failed on {payload['id']}:\\n{traceback.format_exc()}\"\n        ) from None\n\n\nwith concurrent.futures.ProcessPoolExecutor() as pool:\n    future = pool.submit(worker, {\"id\": \"a\"})\n    result = future.result()        # re-raises, with the child's text attached\n",[26,1085,1086,1091,1095,1099,1103,1108,1112,1117,1122,1127,1132,1137,1142,1146,1150,1155,1160],{"__ignoreMap":205},[209,1087,1088],{"class":211,"line":212},[209,1089,1090],{},"import concurrent.futures\n",[209,1092,1093],{"class":211,"line":218},[209,1094,1010],{},[209,1096,1097],{"class":211,"line":225},[209,1098,222],{"emptyLinePlaceholder":221},[209,1100,1101],{"class":211,"line":231},[209,1102,222],{"emptyLinePlaceholder":221},[209,1104,1105],{"class":211,"line":236},[209,1106,1107],{},"def worker(payload):\n",[209,1109,1110],{"class":211,"line":241},[209,1111,256],{},[209,1113,1114],{"class":211,"line":247},[209,1115,1116],{},"        return process(payload)\n",[209,1118,1119],{"class":211,"line":253},[209,1120,1121],{},"    except Exception as exc:\n",[209,1123,1124],{"class":211,"line":259},[209,1125,1126],{},"        # Format here, where the frames still exist; the parent gets text.\n",[209,1128,1129],{"class":211,"line":265},[209,1130,1131],{},"        raise RuntimeError(\n",[209,1133,1134],{"class":211,"line":271},[209,1135,1136],{},"            f\"worker failed on {payload['id']}:\\n{traceback.format_exc()}\"\n",[209,1138,1139],{"class":211,"line":277},[209,1140,1141],{},"        ) from None\n",[209,1143,1144],{"class":211,"line":283},[209,1145,222],{"emptyLinePlaceholder":221},[209,1147,1148],{"class":211,"line":565},[209,1149,222],{"emptyLinePlaceholder":221},[209,1151,1152],{"class":211,"line":571},[209,1153,1154],{},"with concurrent.futures.ProcessPoolExecutor() as pool:\n",[209,1156,1157],{"class":211,"line":577},[209,1158,1159],{},"    future = pool.submit(worker, {\"id\": \"a\"})\n",[209,1161,1162],{"class":211,"line":583},[209,1163,1164],{},"    result = future.result()        # re-raises, with the child's text attached\n",[10,1166,1167,1170,1171,1173,1174,1177],{},[26,1168,1169],{},"concurrent.futures"," pickles the exception and re-raises it in the parent, which preserves the type and message but not the child's frames — so the parent's traceback shows the ",[26,1172,609],{}," call and nothing about where the work actually failed. Formatting in the child and carrying the text is the standard workaround; ",[26,1175,1176],{},"tblib"," automates it if the volume justifies a dependency.",[10,1179,1180,1181,1184,1185,1188,1189,48],{},"The same applies to ",[26,1182,1183],{},"pytest-xdist",", which does this for you: a failure in a worker is serialized and reported by the controller with the worker's formatted traceback. When a traceback from an xdist run looks truncated, it is usually because the failure was in worker ",[186,1186,1187],{},"setup"," rather than a test, and the mechanics of isolating those are in ",[44,1190,1192],{"href":1191},"\u002Fsystematic-debugging-performance-profiling\u002Fdebugging-tests-in-ci-and-containers\u002Fdebugging-a-test-that-only-fails-under-xdist\u002F","debugging a test that only fails under xdist",[14,1194,1196],{"id":1195},"making-your-own-exceptions-readable","Making your own exceptions readable",[10,1198,1199],{},"Half of traceback quality is decided when the exception is defined rather than when it is read.",[10,1201,1202,907,1205,1208,1209,1212],{},[169,1203,1204],{},"Put the values in the message.",[26,1206,1207],{},"InvalidInvoice(\"total 1234 does not match lines summing to 1200\")"," answers the next question; ",[26,1210,1211],{},"InvalidInvoice(\"invalid invoice\")"," guarantees a debugging session. Include the identifiers needed to find the record, and never include credentials or personal data, which end up in logs.",[10,1214,1215,907,1218,1221],{},[169,1216,1217],{},"Give the exception attributes, not just a string.",[26,1219,1220],{},"exc.invoice_id"," lets a handler act; a formatted message forces it to parse prose.",[10,1223,1224,907,1227,1230,1231,1233],{},[169,1225,1226],{},"Do not catch and re-raise for no reason.",[26,1228,1229],{},"except Exception as e: raise MyError(str(e))"," destroys the traceback, drops the type, and adds nothing. If the conversion is worth making, use ",[26,1232,744],{},"; if it is not, let the original propagate.",[10,1235,1236,1243,1244,1246],{},[169,1237,1238,1239,1242],{},"Keep ",[26,1240,1241],{},"__str__"," cheap and total."," An exception whose ",[26,1245,1241],{}," queries a database or raises on a missing attribute turns a simple failure into an unprintable one, and the resulting \"exception while printing exception\" is among the least pleasant outputs Python produces.",[200,1248,1250],{"className":202,"code":1249,"language":204,"meta":205,"style":205},"class InvalidInvoice(Exception):\n    def __init__(self, invoice_id: str, expected: int, actual: int) -> None:\n        self.invoice_id = invoice_id\n        self.expected = expected\n        self.actual = actual\n        super().__init__(\n            f\"invoice {invoice_id}: total {actual} != sum of lines {expected}\"\n        )\n",[26,1251,1252,1257,1262,1267,1272,1277,1282,1287],{"__ignoreMap":205},[209,1253,1254],{"class":211,"line":212},[209,1255,1256],{},"class InvalidInvoice(Exception):\n",[209,1258,1259],{"class":211,"line":218},[209,1260,1261],{},"    def __init__(self, invoice_id: str, expected: int, actual: int) -> None:\n",[209,1263,1264],{"class":211,"line":225},[209,1265,1266],{},"        self.invoice_id = invoice_id\n",[209,1268,1269],{"class":211,"line":231},[209,1270,1271],{},"        self.expected = expected\n",[209,1273,1274],{"class":211,"line":236},[209,1275,1276],{},"        self.actual = actual\n",[209,1278,1279],{"class":211,"line":241},[209,1280,1281],{},"        super().__init__(\n",[209,1283,1284],{"class":211,"line":247},[209,1285,1286],{},"            f\"invoice {invoice_id}: total {actual} != sum of lines {expected}\"\n",[209,1288,1289],{"class":211,"line":253},[209,1290,1291],{},"        )\n",[14,1293,1295],{"id":1294},"tracebacks-in-a-test-suite","Tracebacks in a test suite",[10,1297,1298],{},"pytest rewrites the presentation but not the substance, and a few habits make its output do more work.",[10,1300,1301,1302,1305,1306,1308,1309,1312],{},"The ",[26,1303,1304],{},"--tb"," style should differ by context, as above. ",[26,1307,492],{}," belongs in CI for a suite where failures are rare and expensive to reproduce; it is noisy in a local loop. And ",[26,1310,1311],{},"-r a"," at the end of a run prints a short reason for every non-passing outcome, which is the fastest way to see that twelve \"failures\" are actually one error and eleven cascading skips.",[10,1314,1315,1316,1319,1320,1323],{},"For failures inside fixtures, pytest distinguishes an ",[186,1317,1318],{},"error"," from a ",[186,1321,1322],{},"failure",", and the distinction is worth reading rather than skimming: an error means setup or teardown raised, so the test body never ran and the traceback is about the fixture. Chasing a test body for a bug that is in a fixture is a common waste of ten minutes, and the report says which it was on the first line.",[10,1325,1326,1327,1330,1331,1335],{},"Where the traceback genuinely is not enough, the next step is a post-mortem debugger session in the failing frame — ",[26,1328,1329],{},"--pdb"," drops straight into it with all locals intact, as described in ",[44,1332,1334],{"href":1333},"\u002Fsystematic-debugging-performance-profiling\u002Finteractive-debugging-with-pdb-and-ipdb\u002Fpost-mortem-debugging-with-pdb-pm\u002F","post-mortem debugging with pdb.pm()",". That is the right escalation once reading has been exhausted, and it is much faster than adding prints and re-running.",[14,1337,1339],{"id":1338},"a-reading-order-that-works","A reading order that works",[10,1341,1342],{},"Put the pieces together and the procedure is four steps, in this order, every time.",[57,1344,1346,1484],{"className":1345},[60],[62,1347,70,1352,70,1355,70,1358,70,1374,70,1377,70,1380,70,1385,70,1390,70,1395,70,1399,70,1404,70,1409,70,1412,70,1416,70,1420,70,1423,70,1427,70,1431,70,1434,70,1438,70,1442,70,1445,70,1448,70,1452,70,1456,70,1460,70,1464,70,1467,70,1470,70,1476,70,1480],{"viewBox":1348,"role":65,"ariaLabelledBy":1349,"xmlns":69},"0 0 820 262",[1350,1351],"ord-t","ord-d",[72,1353,1354],{"id":1350},"Four-step reading order for any traceback",[76,1356,1357],{"id":1351},"A sequence. First read the final line for the exception type and message. Second, identify the chain separators and pick the block that escaped. Third, scan upward from the bottom frame to the first frame inside the project. Fourth, use the caret markers or showlocals to narrow to the sub-expression or value, escalating to a post-mortem debugger only if the value is still unexplained.",[1359,1360,1361,1362,70],"defs",{},"\n    ",[1363,1364,1370],"marker",{"id":1365,"viewBox":1366,"refX":131,"refY":1367,"markerWidth":1368,"markerHeight":1368,"orient":1369},"ord-a","0 0 10 10","5","7","auto-start-reverse",[1371,1372],"path",{"d":1373,"fill":95},"M0 0 L10 5 L0 10 z",[80,1375],{"x":82,"y":82,"width":934,"height":1376,"rx":85,"fill":86},"262",[88,1378,1379],{"x":938,"y":91,"textAnchor":92,"fontSize":93,"fontWeight":94,"fill":95},"What to look at, in order",[80,1381],{"x":942,"y":109,"width":944,"height":1382,"rx":103,"fill":1383,"stroke":1384,"strokeWidth":144},"108","#e6f0ea","#81b29a",[88,1386,1389],{"x":1387,"y":1388,"textAnchor":92,"fontSize":159,"fontWeight":94,"fill":95},"116","78","1 · last line",[88,1391,1394],{"x":1392,"y":1393,"fontSize":103,"fill":95},"42","104","exception type",[88,1396,1398],{"x":1392,"y":1397,"fontSize":103,"fill":95},"124","and message",[88,1400,1403],{"x":1392,"y":1401,"fontSize":103,"fill":1402},"146","#2a5f49","what went wrong",[211,1405],{"x1":1406,"y1":956,"x2":1407,"y2":956,"stroke":95,"strokeWidth":106,"markerEnd":1408},"210","234","url(#ord-a)",[80,1410],{"x":1411,"y":109,"width":944,"height":1382,"rx":103,"fill":132,"stroke":133,"strokeWidth":144},"240",[88,1413,1415],{"x":1414,"y":1388,"textAnchor":92,"fontSize":159,"fontWeight":94,"fill":95},"330","2 · separators",[88,1417,1419],{"x":1418,"y":1393,"fontSize":103,"fill":95},"256","direct cause, or",[88,1421,1422],{"x":1418,"y":1397,"fontSize":103,"fill":95},"during handling?",[88,1424,1426],{"x":1418,"y":1401,"fontSize":103,"fill":1425},"#8a5a00","which block escaped",[211,1428],{"x1":1429,"y1":956,"x2":1430,"y2":956,"stroke":95,"strokeWidth":106,"markerEnd":1408},"424","448",[80,1432],{"x":1433,"y":109,"width":944,"height":1382,"rx":103,"fill":132,"stroke":133,"strokeWidth":144},"454",[88,1435,1437],{"x":1436,"y":1388,"textAnchor":92,"fontSize":159,"fontWeight":94,"fill":95},"544","3 · your frame",[88,1439,1441],{"x":1440,"y":1393,"fontSize":103,"fill":95},"470","scan upward to the",[88,1443,1444],{"x":1440,"y":1397,"fontSize":103,"fill":95},"first project file",[88,1446,1447],{"x":1440,"y":1401,"fontSize":103,"fill":1425},"where to start",[211,1449],{"x1":1450,"y1":956,"x2":1451,"y2":956,"stroke":95,"strokeWidth":106,"markerEnd":1408},"638","662",[80,1453],{"x":1454,"y":109,"width":1455,"height":1382,"rx":103,"fill":1383,"stroke":1384,"strokeWidth":144},"668","126",[88,1457,1459],{"x":1458,"y":1388,"textAnchor":92,"fontSize":159,"fontWeight":94,"fill":95},"731","4 · narrow",[88,1461,1463],{"x":1462,"y":1393,"fontSize":103,"fill":95},"684","carets and",[88,1465,1466],{"x":1462,"y":1397,"fontSize":103,"fill":95},"showlocals",[88,1468,1469],{"x":1462,"y":1401,"fontSize":103,"fill":1402},"which value",[80,1471],{"x":942,"y":1472,"width":943,"height":1473,"rx":1474,"fill":86,"stroke":105,"strokeWidth":1475},"176","66","10","1.5",[88,1477,1479],{"x":938,"y":1478,"textAnchor":92,"fontSize":159,"fill":95},"200","Still unexplained after step four? That is the point to escalate to a post-mortem debugger,",[88,1481,1483],{"x":938,"y":1482,"textAnchor":92,"fontSize":159,"fill":95},"222","not before — the four steps resolve the large majority of failures in under a minute.",[162,1485,1486],{},"The order matters: reading frames before reading the message is how people end up debugging the wrong exception in a chained pair.",[10,1488,1489,1490,1493],{},"The step most often skipped is the second. In a chained traceback the eye is drawn to the first block because it is printed first, but the exception that actually escaped is the last one, and the first block may be entirely irrelevant — a handled ",[26,1491,1492],{},"KeyError"," inside a cache lookup, for example, that is only visible because a later, unrelated exception dragged its context along.",[10,1495,1496,1497,1499,1500,1503],{},"A useful habit for production code follows from that: wherever an ",[26,1498,183],{}," block does real work — retries, cleanup, fallback logic — wrap the risky part of the handler in its own ",[26,1501,1502],{},"try"," so a failure there is reported as its own error rather than as an implicit chain on top of whatever it was handling. The resulting log is two clear entries instead of one confusing composite, and the on-call engineer reading it at three in the morning does not have to work out which half matters.",[14,1505,1507],{"id":1506},"frequently-asked-questions","Frequently Asked Questions",[10,1509,1510,1513,1514,1516,1517,1520],{},[169,1511,1512],{},"What does 'During handling of the above exception, another exception occurred' mean?","\nA second exception was raised while an ",[26,1515,183],{}," block was handling the first, and Python printed both. The lower traceback is the one that escaped; the upper one is the original cause. It is implicit chaining via ",[26,1518,1519],{},"__context__",", and it usually means the error handler itself is broken — the second exception is often a bug in logging or cleanup rather than the real failure.",[10,1522,1523,1526,1527,1530,1531,1533,1534,1536],{},[169,1524,1525],{},"When should I use raise ... from?","\nWhenever you convert a low-level exception into a domain one. ",[26,1528,1529],{},"raise InvalidInvoice(...) from exc"," sets ",[26,1532,680],{},", which prints \"The above exception was the direct cause\" and preserves the original traceback. Use ",[26,1535,356],{}," to suppress a noisy internal exception deliberately, and never leave the conversion unqualified when the original matters.",[10,1538,1539,1542],{},[169,1540,1541],{},"Why does the traceback point at a line that looks fine?","\nUsually because the frame shown is the one that raised, not the one that caused the bad value. Read upward: the topmost frame is the entry point and the bottom is where the exception was raised, so the defect is often in a caller that passed something wrong. From Python 3.11 the caret markers narrow it to a sub-expression, which resolves most of these immediately.",[10,1544,1545,1548,1549,1551,1552,1555],{},[169,1546,1547],{},"How do I see the traceback from another thread or task?","\nThreads need ",[26,1550,601],{}," or a wrapper that captures and re-raises in the caller; a raw ",[26,1553,1554],{},"Thread"," prints to stderr and does not fail the test. For asyncio, an un-retrieved task exception is reported by the loop's exception handler at garbage-collection time, so awaiting or gathering the task is what surfaces it in the right place.",[10,1557,1558,1561,1562,1564],{},[169,1559,1560],{},"What is an ExceptionGroup and how do I read one?","\nIt is a container raised by task groups and by code using ",[26,1563,891],{}," when several exceptions occur concurrently. The traceback is nested: the outer frames are the group's, and each contained exception has its own traceback indented beneath. Read the leaves; the outer frames only tell you where the group was assembled.",[14,1566,1568],{"id":1567},"related-guides","Related guides",[19,1570,1571,1578,1585,1590,1597],{},[22,1572,1573,1574,48],{},"Decode the implicit-chain message in ",[44,1575,1577],{"href":1576},"\u002Fsystematic-debugging-performance-profiling\u002Freading-tracebacks-and-exception-chains\u002Fdecoding-during-handling-of-the-above-exception\u002F","decoding \"During handling of the above exception\"",[22,1579,1580,1581,48],{},"Recover diagnostics from workers with ",[44,1582,1584],{"href":1583},"\u002Fsystematic-debugging-performance-profiling\u002Freading-tracebacks-and-exception-chains\u002Fgetting-useful-tracebacks-from-threads-and-tasks\u002F","getting useful tracebacks from threads and tasks",[22,1586,1587,1588,48],{},"Escalate into the failing frame using ",[44,1589,1334],{"href":1333},[22,1591,1592,1593,48],{},"Keep the surrounding context in the failure report via ",[44,1594,1596],{"href":1595},"\u002Fsystematic-debugging-performance-profiling\u002Flogging-and-observability-for-debugging\u002Fstructured-logging-that-survives-pytest-capture\u002F","structured logging that survives pytest capture",[22,1598,1599,1600,48],{},"For groups raised by concurrent code, see ",[44,1601,1603],{"href":1602},"\u002Ftesting-async-and-concurrent-python\u002Ftesting-with-anyio-and-trio\u002Ftesting-code-that-uses-task-groups\u002F","testing code that uses task groups",[10,1605,1606,1607],{},"← Back to ",[44,1608,1610],{"href":1609},"\u002Fsystematic-debugging-performance-profiling\u002F","Systematic Debugging & Performance Profiling",[1612,1613,1614],"style",{},"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);}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}",{"title":205,"searchDepth":218,"depth":218,"links":1616},[1617,1618,1619,1626,1627,1628,1629,1630,1631,1632,1633,1634,1635],{"id":16,"depth":218,"text":17},{"id":51,"depth":218,"text":52},{"id":192,"depth":218,"text":193,"children":1620},[1621,1622,1623,1624,1625],{"id":197,"depth":225,"text":198},{"id":289,"depth":225,"text":290},{"id":368,"depth":225,"text":369},{"id":420,"depth":225,"text":421},{"id":496,"depth":225,"text":497},{"id":613,"depth":218,"text":614},{"id":692,"depth":218,"text":693},{"id":809,"depth":218,"text":810},{"id":993,"depth":218,"text":994},{"id":1076,"depth":218,"text":1077},{"id":1195,"depth":218,"text":1196},{"id":1294,"depth":218,"text":1295},{"id":1338,"depth":218,"text":1339},{"id":1506,"depth":218,"text":1507},{"id":1567,"depth":218,"text":1568},"Read a Python traceback precisely: frame order, exception chaining with raise from, context suppression, 3.11 fine-grained locations, exception groups, and pytest's tb styles.","md",{"slug":1639,"type":1640,"breadcrumb":1641,"datePublished":1642,"dateModified":1642,"faq":1643,"howto":1654},"reading-tracebacks-and-exception-chains","topic","Tracebacks","2026-09-18",[1644,1646,1648,1650,1652],{"q":1512,"a":1645},"A second exception was raised while an except block was handling the first, and Python printed both. The lower traceback is the one that escaped; the upper one is the original cause. It is implicit chaining via __context__, and it usually means the error handler itself is broken — the second exception is often a bug in logging or cleanup rather than the real failure.",{"q":1525,"a":1647},"Whenever you convert a low-level exception into a domain one. raise InvalidInvoice(...) from exc sets __cause__, which prints 'The above exception was the direct cause' and preserves the original traceback. Use 'from None' to suppress a noisy internal exception deliberately, and never leave the conversion unqualified when the original matters.",{"q":1541,"a":1649},"Usually because the frame shown is the one that raised, not the one that caused the bad value. Read upward: the topmost frame is the entry point and the bottom is where the exception was raised, so the defect is often in a caller that passed something wrong. From Python 3.11 the caret markers narrow it to a sub-expression, which resolves most of these immediately.",{"q":1547,"a":1651},"Threads need threading.excepthook or a wrapper that captures and re-raises in the caller; a raw Thread prints to stderr and does not fail the test. For asyncio, an un-retrieved task exception is reported by the loop's exception handler at garbage-collection time, so awaiting or gathering the task is what surfaces it in the right place.",{"q":1560,"a":1653},"It is a container raised by task groups and by code using except* when several exceptions occur concurrently. The traceback is nested: the outer frames are the group's, and each contained exception has its own traceback indented beneath. Read the leaves; the outer frames only tell you where the group was assembled.",{"name":1655,"description":1656,"steps":1657},"How to read a Python traceback efficiently","Work from the bottom, follow the chain, and use the tooling that narrows the location.",[1658,1661,1664,1667,1670],{"name":1659,"text":1660},"Read the last line first","The exception type and message on the final line say what went wrong; everything above says where.",{"name":1662,"text":1663},"Read frames from the bottom up","The bottom frame raised the exception; move upward until a frame belongs to your code, which is usually where the bad value originated.",{"name":1665,"text":1666},"Follow the chain markers","Distinguish 'direct cause' from 'during handling' to tell a deliberate conversion from a failure inside an error handler.",{"name":1668,"text":1669},"Use the caret markers on 3.11+","Fine-grained locations narrow the failure to a sub-expression, which resolves attribute errors on chained calls immediately.",{"name":1671,"text":1672},"Choose a pytest traceback style","Use --tb=short for CI volume, --tb=long while diagnosing, and --showlocals when the values matter more than the frames.","\u002Fsystematic-debugging-performance-profiling\u002Freading-tracebacks-and-exception-chains",{"title":5,"description":1636},"systematic-debugging-performance-profiling\u002Freading-tracebacks-and-exception-chains\u002Findex","JCAZA0VGAhB1bM_mvuoXdNwQeVDnYm0f45yyq97upTY",1789718765721]