[{"data":1,"prerenderedAt":762},["ShallowReactive",2],{"page-\u002Fadvanced-pytest-architecture-configuration\u002Fcoverage-measurement-and-enforcement\u002Fbranch-coverage-versus-line-coverage-in-pytest-cov\u002F":3},{"id":4,"title":5,"body":6,"description":725,"extension":726,"meta":727,"navigation":96,"path":758,"seo":759,"stem":760,"__hash__":761},"content\u002Fadvanced-pytest-architecture-configuration\u002Fcoverage-measurement-and-enforcement\u002Fbranch-coverage-versus-line-coverage-in-pytest-cov\u002Findex.md","Branch Coverage versus Line Coverage in pytest-cov",{"type":7,"value":8,"toc":714},"minimark",[9,22,25,30,52,56,116,139,147,162,309,313,325,328,332,384,388,394,414,425,428,432,435,454,457,543,547,550,553,560,563,644,648,654,666,672,676,705,710],[10,11,12,13,17,18,21],"p",{},"A line-coverage report that says 100% can still hide an untested path. ",[14,15,16],"code",{},"if discount: price -= discount"," executed with a non-zero discount marks both lines as covered, while the case where no discount applies — the implicit ",[14,19,20],{},"else"," — never ran. Branch coverage measures each possible transition out of a decision, and it reports that missing path as a partial branch. It is the same tool, one configuration line away, and it measures something much closer to what tests are meant to establish.",[10,23,24],{},"Enabling it usually drops the reported percentage by several points, which feels like a regression and is actually the measurement becoming honest. The drop is also informative in its own right: its size is a rough measure of how much of the suite's apparent thoroughness came from executing lines rather than exercising decisions, and the list of partial branches it produces is a ready-made backlog of the specific paths no test has ever taken. Handling that transition well — resetting the threshold from the new baseline, then working through the untaken branches that matter — is most of what adopting it involves.",[26,27,29],"h2",{"id":28},"prerequisites","Prerequisites",[31,32,33,44],"ul",{},[34,35,36,39,40,43],"li",{},[14,37,38],{},"pytest-cov >= 5.0"," and ",[14,41,42],{},"coverage >= 7.4",".",[34,45,46,47,43],{},"Existing coverage enforcement, as in ",[48,49,51],"a",{"href":50},"\u002Fadvanced-pytest-architecture-configuration\u002Fcoverage-measurement-and-enforcement\u002F","coverage measurement and enforcement",[26,53,55],{"id":54},"solution","Solution",[57,58,63],"pre",{"className":59,"code":60,"language":61,"meta":62,"style":62},"language-toml shiki shiki-themes github-light github-dark","# pyproject.toml\n[tool.coverage.run]\nbranch = true                # measure arcs between lines, not just lines\nsource = [\"src\u002Fmyapp\"]\n\n[tool.coverage.report]\nshow_missing = true\nfail_under = 84              # measured branch baseline, not the old line number\n","toml","",[14,64,65,73,79,85,91,98,104,110],{"__ignoreMap":62},[66,67,70],"span",{"class":68,"line":69},"line",1,[66,71,72],{},"# pyproject.toml\n",[66,74,76],{"class":68,"line":75},2,[66,77,78],{},"[tool.coverage.run]\n",[66,80,82],{"class":68,"line":81},3,[66,83,84],{},"branch = true                # measure arcs between lines, not just lines\n",[66,86,88],{"class":68,"line":87},4,[66,89,90],{},"source = [\"src\u002Fmyapp\"]\n",[66,92,94],{"class":68,"line":93},5,[66,95,97],{"emptyLinePlaceholder":96},true,"\n",[66,99,101],{"class":68,"line":100},6,[66,102,103],{},"[tool.coverage.report]\n",[66,105,107],{"class":68,"line":106},7,[66,108,109],{},"show_missing = true\n",[66,111,113],{"class":68,"line":112},8,[66,114,115],{},"fail_under = 84              # measured branch baseline, not the old line number\n",[57,117,121],{"className":118,"code":119,"language":120,"meta":62,"style":62},"language-bash shiki shiki-themes github-light github-dark","pytest --cov --cov-report=term-missing -q\n","bash",[14,122,123],{"__ignoreMap":62},[66,124,125,129,133,136],{"class":68,"line":69},[66,126,128],{"class":127},"sScJk","pytest",[66,130,132],{"class":131},"sj4cs"," --cov",[66,134,135],{"class":131}," --cov-report=term-missing",[66,137,138],{"class":131}," -q\n",[57,140,145],{"className":141,"code":143,"language":144,"meta":62},[142],"language-text","Name                     Stmts   Miss Branch BrPart  Cover   Missing\n----------------------------------------------------------------------\nsrc\u002Fmyapp\u002Fpricing.py        42      0     18      3    95%   17->19, 31->exit, 38->36\nsrc\u002Fmyapp\u002Frefunds.py        28      2     10      4    84%   22-23, 14->16, 26->exit\n----------------------------------------------------------------------\nTOTAL                      412     11    118     19    91%\n","text",[14,146,143],{"__ignoreMap":62},[10,148,149,150,153,154,157,158,161],{},"The ",[14,151,152],{},"Missing"," column now contains two kinds of entry. Plain numbers are lines never executed. Arrows are untaken branches: ",[14,155,156],{},"17->19"," means the transition from line 17 to line 19 never happened, and ",[14,159,160],{},"31->exit"," means the function never returned early from line 31.",[163,164,167,305],"figure",{"className":165},[166],"diagram",[168,169,176,177,176,181,176,185,176,209,176,217,176,226,176,234,176,240,176,243,176,247,176,250,176,254,176,259,176,263,176,271,176,277,176,284,176,289,176,293,176,297,176,301],"svg",{"viewBox":170,"role":171,"ariaLabelledBy":172,"xmlns":175},"0 0 820 262","img",[173,174],"br-t","br-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","\n  ",[178,179,180],"title",{"id":173},"The path line coverage cannot see",[182,183,184],"desc",{"id":174},"A conditional at line 17 has two outgoing paths: into the body at line 18, or skipping to line 19. The tests only ever take the first path, so every line is executed and line coverage reports 100 percent. Branch coverage records that the arc from 17 directly to 19 was never taken and reports a partial branch.",[186,187,188,189,188,203,176],"defs",{},"\n    ",[190,191,198],"marker",{"id":192,"viewBox":193,"refX":194,"refY":195,"markerWidth":196,"markerHeight":196,"orient":197},"br-a","0 0 10 10","9","5","7","auto-start-reverse",[199,200],"path",{"d":201,"fill":202},"M0 0 L10 5 L0 10 z","#81b29a",[190,204,206],{"id":205,"viewBox":193,"refX":194,"refY":195,"markerWidth":196,"markerHeight":196,"orient":197},"br-b",[199,207],{"d":201,"fill":208},"#e07a5f",[210,211],"rect",{"x":212,"y":212,"width":213,"height":214,"rx":215,"fill":216},"0","820","262","14","#fffdf8",[144,218,225],{"x":219,"y":220,"textAnchor":221,"fontSize":222,"fontWeight":223,"fill":224},"410","28","middle","16","700","#3d405b","Every line ran; one path never did",[210,227],{"x":228,"y":229,"width":230,"height":231,"rx":194,"fill":232,"stroke":202,"strokeWidth":233},"60","56","280","40","#e6f0ea","2",[144,235,239],{"x":236,"y":237,"textAnchor":221,"fontSize":238,"fill":224},"200","81","11.5","17 · if discount:",[210,241],{"x":228,"y":242,"width":230,"height":231,"rx":194,"fill":232,"stroke":202,"strokeWidth":233},"124",[144,244,246],{"x":236,"y":245,"textAnchor":221,"fontSize":238,"fill":224},"149","18 ·     price -= discount",[210,248],{"x":228,"y":249,"width":230,"height":231,"rx":194,"fill":232,"stroke":202,"strokeWidth":233},"192",[144,251,253],{"x":236,"y":252,"textAnchor":221,"fontSize":238,"fill":224},"217","19 · return price",[68,255],{"x1":236,"y1":256,"x2":236,"y2":257,"stroke":202,"strokeWidth":233,"markerEnd":258},"98","120","url(#br-a)",[68,260],{"x1":236,"y1":261,"x2":236,"y2":262,"stroke":202,"strokeWidth":233,"markerEnd":258},"166","188",[199,264],{"d":265,"fill":266,"stroke":208,"strokeWidth":233,"strokeDashArray":267,"markerEnd":270},"M340 76 C 400 76 400 212 344 212","none",[268,269],"6","4","url(#br-b)",[144,272,276],{"x":273,"y":274,"fontSize":238,"fontWeight":223,"fill":275},"408","148","#8f3d22","17 → 19 never taken",[210,278],{"x":279,"y":280,"width":281,"height":282,"rx":283,"fill":232,"stroke":202,"strokeWidth":233},"500","72","296","64","11",[144,285,288],{"x":286,"y":256,"textAnchor":221,"fontSize":287,"fontWeight":223,"fill":224},"648","12","line coverage",[144,290,292],{"x":286,"y":257,"textAnchor":221,"fontSize":283,"fill":291},"#2a5f49","3 of 3 lines · 100%",[210,294],{"x":279,"y":295,"width":281,"height":282,"rx":283,"fill":296,"stroke":208,"strokeWidth":233},"150","#fbe9e3",[144,298,300],{"x":286,"y":299,"textAnchor":221,"fontSize":287,"fontWeight":223,"fill":224},"176","branch coverage",[144,302,304],{"x":286,"y":303,"textAnchor":221,"fontSize":283,"fill":275},"198","partial branch 17→19 reported",[306,307,308],"figcaption",{},"The untested path is the no-discount case — frequently the most common one in production, and the one where an off-by-one in the return expression would go unnoticed.",[26,310,312],{"id":311},"why-this-works","Why this works",[10,314,315,316,319,320,324],{},"With ",[14,317,318],{},"branch = true",", coverage records arcs — pairs of (source line, destination line) — as the program runs, rather than only the set of lines executed. It also computes, from the compiled code, every arc that is ",[321,322,323],"em",{},"possible",". A decision point with two possible destinations where only one was observed is a partial branch, and the unobserved arc is what the report lists.",[10,326,327],{},"That is why the percentage drops. The denominator grows to include every possible branch outcome, and code that looked fully covered by lines turns out to have outcomes no test ever produced. None of the code changed; the question being asked got sharper. Line coverage asks whether each statement ran at least once; branch coverage asks whether each decision went every way it can. The second question is the one tests are actually meant to answer, which is why the stricter number is the more useful one to enforce.",[26,329,331],{"id":330},"edge-cases-and-failure-modes","Edge cases and failure modes",[31,333,334,349,358,372,378],{},[34,335,336,340,341,344,345,348],{},[337,338,339],"strong",{},"Loops that always iterate."," ",[14,342,343],{},"for item in items"," where ",[14,346,347],{},"items"," is never empty reports the loop's exit-on-first-check arc as untaken. That is a real gap — the empty case — and usually worth a test.",[34,350,351,357],{},[337,352,353,356],{},[14,354,355],{},"while True"," loops."," The arc for the loop condition being false is impossible and is excluded automatically in recent coverage versions; older versions report it spuriously.",[34,359,360,363,364,367,368,371],{},[337,361,362],{},"Defensive branches."," An ",[14,365,366],{},"else: raise AssertionError(\"unreachable\")"," is untaken by design. Mark it ",[14,369,370],{},"# pragma: no branch"," with a reason rather than lowering the threshold.",[34,373,374,377],{},[337,375,376],{},"Comprehension internals."," Conditions inside comprehensions and generator expressions are measured too, which can surface unexpected partial branches in otherwise simple code.",[34,379,380,383],{},[337,381,382],{},"Keeping the old threshold."," Carrying a 90% line threshold into branch mode fails the build immediately. Measure first, then set.",[26,385,387],{"id":386},"reading-the-arrow-notation","Reading the arrow notation",[10,389,390,391,393],{},"The branch entries in the ",[14,392,152],{}," column are compact and easy to misread, so it is worth decoding the three common forms once.",[10,395,396,398,399,402,403,405,406,409,410,413],{},[14,397,156],{}," means the arc from line 17 to line 19 was never taken. Usually line 17 is an ",[14,400,401],{},"if"," and 19 is the first line after its body, so this is the \"condition was false\" path. ",[14,404,160],{}," means that from line 31 the function could have returned — through an early ",[14,407,408],{},"return",", or by falling off the end — and never did in any test. ",[14,411,412],{},"38->36"," points backwards and almost always describes a loop: from the last line of the body, control could have gone back to the loop header at 36, and never did, which means the loop only ever ran one iteration.",[10,415,416,417,420,421,424],{},"The HTML report makes the same information visual, highlighting partially covered lines in a different colour and showing the missing destinations on hover. For working through a module's gaps it is considerably faster than the terminal output, and ",[14,418,419],{},"--cov-report=html"," alongside ",[14,422,423],{},"term-missing"," gives both without a second run.",[10,426,427],{},"A useful habit when adding a test for a partial branch is to check the specific arc disappears from the report afterwards. It confirms the new test actually takes the path intended — surprisingly often, a test written for the \"no discount\" case turns out to pass a discount of zero through a different branch entirely, leaving the original arc untaken and the gap unfilled.",[26,429,431],{"id":430},"where-untaken-branches-matter-most","Where untaken branches matter most",[10,433,434],{},"Not every partial branch deserves a test. A branch in a logging helper that only fires when a debug flag is set is low risk; a branch in refund calculation that only fires when the amount is zero is not. Working through the report by risk rather than by file order gets most of the value from a fraction of the effort.",[10,436,437,438,441,442,445,446,449,450,453],{},"The highest-value branches are usually in three places. ",[337,439,440],{},"Error handling",", where the ",[14,443,444],{},"except"," path or the validation failure path is exactly what production exercises during an incident and tests exercise least. ",[337,447,448],{},"Money, dates and units",", where the untaken branch is often a boundary — zero, negative, the last day of the month — and the defect is an off-by-one. ",[337,451,452],{},"Authorisation checks",", where the untaken branch is the one that denies access, and a bug there is a security issue rather than a wrong result.",[10,455,456],{},"Adding tests for those branches first, and only then treating the threshold as the target, means the percentage rises in the places where it corresponds to real risk reduction. Chasing the number directly tends to produce tests for the easiest untaken branches, which are usually the least important ones.",[163,458,460,540],{"className":459},[166],[168,461,176,466,176,469,176,472,176,476,176,481,176,487,176,490,176,495,176,499,176,503,176,507,176,509,176,511,176,514,176,517,176,520,176,522,176,525,176,527,176,531,176,535,176,538],{"viewBox":462,"role":171,"ariaLabelledBy":463,"xmlns":175},"0 0 800 236",[464,465],"risk-t","risk-d",[178,467,468],{"id":464},"Prioritising untaken branches by risk",[182,470,471],{"id":465},"Three high-priority categories for untaken branches: error handling paths, boundary cases in money and date arithmetic, and denial paths in authorisation checks. A lower-priority category covers logging and debug-only branches, which rarely justify a dedicated test.",[210,473],{"x":212,"y":212,"width":474,"height":475,"rx":215,"fill":216},"800","236",[144,477,480],{"x":478,"y":220,"textAnchor":221,"fontSize":479,"fontWeight":223,"fill":224},"400","15.5","Test the branches production will take during an incident",[210,482],{"x":483,"y":484,"width":485,"height":486,"rx":287,"fill":216,"stroke":208,"strokeWidth":233},"24","50","240","164",[210,488],{"x":483,"y":484,"width":485,"height":489,"rx":287,"fill":224},"30",[144,491,494],{"x":492,"y":493,"textAnchor":221,"fontSize":287,"fontWeight":223,"fill":216},"144","70","error handling",[144,496,498],{"x":231,"y":497,"fontSize":283,"fill":224},"104","except paths,",[144,500,502],{"x":231,"y":501,"fontSize":283,"fill":224},"126","validation failures",[144,504,506],{"x":231,"y":505,"fontSize":283,"fontWeight":223,"fill":275},"170","first priority",[210,508],{"x":230,"y":484,"width":485,"height":486,"rx":287,"fill":216,"stroke":208,"strokeWidth":233},[210,510],{"x":230,"y":484,"width":485,"height":489,"rx":287,"fill":224},[144,512,513],{"x":478,"y":493,"textAnchor":221,"fontSize":287,"fontWeight":223,"fill":216},"boundaries",[144,515,516],{"x":281,"y":497,"fontSize":283,"fill":224},"zero amounts, month ends,",[144,518,519],{"x":281,"y":501,"fontSize":283,"fill":224},"empty collections",[144,521,506],{"x":281,"y":505,"fontSize":283,"fontWeight":223,"fill":275},[210,523],{"x":524,"y":484,"width":485,"height":486,"rx":287,"fill":216,"stroke":208,"strokeWidth":233},"536",[210,526],{"x":524,"y":484,"width":485,"height":489,"rx":287,"fill":224},[144,528,530],{"x":529,"y":493,"textAnchor":221,"fontSize":287,"fontWeight":223,"fill":216},"656","authorisation",[144,532,534],{"x":533,"y":497,"fontSize":283,"fill":224},"552","the deny path of",[144,536,537],{"x":533,"y":501,"fontSize":283,"fill":224},"every permission check",[144,539,506],{"x":533,"y":505,"fontSize":283,"fontWeight":223,"fill":275},[306,541,542],{},"Logging and debug-only branches can wait indefinitely. These three categories are where an untested branch turns into an incident.",[26,544,546],{"id":545},"migrating-a-threshold-without-a-fight","Migrating a threshold without a fight",[10,548,549],{},"The practical obstacle to adopting branch coverage is not technical but social: the number drops, a gate that was green goes red, and somebody proposes turning it back off. A three-step migration avoids that conversation.",[10,551,552],{},"Measure first, in a branch, without touching the threshold. Record both numbers — the old line percentage and the new branch percentage — and the list of partial branches. That list is the useful artefact; the number is just its summary.",[10,554,555,556,559],{},"Then switch the configuration and set ",[14,557,558],{},"fail_under"," to the measured branch baseline, rounded down by a point. The gate stays green, nothing about enforcement has loosened, and the measurement is now stricter than it was.",[10,561,562],{},"Finally, raise the threshold as risky branches get tests, one or two points at a time. Pairing each raise with the tests that justified it keeps the number honest: it only goes up when coverage genuinely improved, rather than because somebody wrote a test for an unimportant branch to make the gate pass.",[163,564,566,641],{"className":565},[166],[168,567,176,571,176,574,176,577,176,579,176,582,176,588,176,593,176,598,176,602,176,607,176,610,176,614,176,617,176,620,176,624,176,627,176,630,176,637],{"viewBox":462,"role":171,"ariaLabelledBy":568,"xmlns":175},[569,570],"mig3-t","mig3-d",[178,572,573],{"id":569},"Moving a threshold from line to branch coverage",[182,575,576],{"id":570},"A line-coverage gate at ninety percent is replaced by a branch-coverage gate set at the measured baseline of eighty-four, so the build stays green while the measure becomes stricter. The threshold is then raised in small steps as tests are added for risky untaken branches.",[210,578],{"x":212,"y":212,"width":474,"height":475,"rx":215,"fill":216},[144,580,581],{"x":478,"y":220,"textAnchor":221,"fontSize":479,"fontWeight":223,"fill":224},"Stricter measure, same green build, rising target",[210,583],{"x":483,"y":228,"width":584,"height":585,"rx":287,"fill":586,"stroke":224,"strokeWidth":587},"220","110","#f4f1de","1.6",[144,589,592],{"x":590,"y":591,"textAnchor":221,"fontSize":287,"fontWeight":223,"fill":224},"134","86","before",[144,594,597],{"x":590,"y":595,"textAnchor":221,"fontSize":596,"fontWeight":223,"fill":224},"116","18","90%",[144,599,601],{"x":590,"y":600,"textAnchor":221,"fontSize":283,"fill":224},"142","line threshold",[210,603],{"x":604,"y":228,"width":584,"height":585,"rx":287,"fill":605,"stroke":606,"strokeWidth":233},"290","#f7f0da","#f2cc8f",[144,608,609],{"x":478,"y":591,"textAnchor":221,"fontSize":287,"fontWeight":223,"fill":224},"switch",[144,611,613],{"x":478,"y":595,"textAnchor":221,"fontSize":596,"fontWeight":223,"fill":612},"#8a5a00","84%",[144,615,616],{"x":478,"y":600,"textAnchor":221,"fontSize":283,"fill":224},"measured branch baseline",[210,618],{"x":619,"y":228,"width":584,"height":585,"rx":287,"fill":232,"stroke":202,"strokeWidth":233},"556",[144,621,623],{"x":622,"y":591,"textAnchor":221,"fontSize":287,"fontWeight":223,"fill":224},"666","over time",[144,625,626],{"x":622,"y":595,"textAnchor":221,"fontSize":596,"fontWeight":223,"fill":291},"86 → 88%",[144,628,629],{"x":622,"y":600,"textAnchor":221,"fontSize":283,"fill":224},"raised with each risky branch",[210,631],{"x":483,"y":632,"width":633,"height":634,"rx":194,"fill":216,"stroke":635,"strokeWidth":636},"186","752","32","rgba(61,64,91,0.35)","1.4",[144,638,640],{"x":478,"y":639,"textAnchor":221,"fontSize":238,"fill":224},"207","84% of branches is a stronger guarantee than 90% of lines — the number fell, the bar rose.",[306,642,643],{},"Explaining the migration in those terms — a stricter measure at a lower number — is usually enough to keep it from being reverted.",[26,645,647],{"id":646},"frequently-asked-questions","Frequently Asked Questions",[10,649,650,653],{},[337,651,652],{},"Why does my coverage percentage drop when I enable branch coverage?","\nBecause branch coverage counts things line coverage ignores: every conditional has two outcomes, and each untaken outcome is now reported. The code did not get worse; the measurement got more honest. Reset the threshold to the new baseline and raise it deliberately.",[10,655,656,659,660,662,663,665],{},[337,657,658],{},"What is a partial branch?","\nA line containing a decision where only some outcomes were exercised — an ",[14,661,401],{}," whose body always ran but whose implicit ",[14,664,20],{}," never did, or a loop that always iterated but never exited on its first check. The report lists the missing destination line numbers.",[10,667,668,671],{},[337,669,670],{},"Should the threshold be the same for branch and line coverage?","\nNo. Branch coverage is systematically lower for the same code because it is a stricter measure. A 90% line threshold corresponds roughly to a mid-80s branch threshold on typical code; set the branch threshold from a measured baseline rather than copying the line number.",[26,673,675],{"id":674},"related","Related",[31,677,678,684,691,698],{},[34,679,680,683],{},[48,681,682],{"href":50},"Coverage Measurement and Enforcement"," — thresholds, reports and CI gating.",[34,685,686,690],{},[48,687,689],{"href":688},"\u002Fadvanced-pytest-architecture-configuration\u002Fcoverage-measurement-and-enforcement\u002Fenforcing-diff-coverage-on-pull-requests\u002F","Enforcing Diff Coverage on Pull Requests"," — applying branch coverage to changed lines only.",[34,692,693,697],{},[48,694,696],{"href":695},"\u002Fadvanced-pytest-architecture-configuration\u002Fcoverage-measurement-and-enforcement\u002Fcombining-coverage-across-a-python-version-matrix\u002F","Combining Coverage Across a Python Version Matrix"," — branches that only run on one interpreter.",[34,699,700,704],{},[48,701,703],{"href":702},"\u002Fproperty-based-fuzz-testing-strategies\u002Fadvanced-property-based-testing\u002Fwriting-metamorphic-properties\u002F","Writing Metamorphic Properties"," — generating inputs that reach the untaken branches.",[10,706,707,708],{},"← Back to ",[48,709,682],{"href":50},[711,712,713],"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}",{"title":62,"searchDepth":75,"depth":75,"links":715},[716,717,718,719,720,721,722,723,724],{"id":28,"depth":75,"text":29},{"id":54,"depth":75,"text":55},{"id":311,"depth":75,"text":312},{"id":330,"depth":75,"text":331},{"id":386,"depth":75,"text":387},{"id":430,"depth":75,"text":431},{"id":545,"depth":75,"text":546},{"id":646,"depth":75,"text":647},{"id":674,"depth":75,"text":675},"Understand what branch coverage catches that line coverage misses, enable it in pytest-cov, read partial-branch reports, and set thresholds that reflect real risk.","md",{"slug":728,"type":729,"breadcrumb":730,"datePublished":731,"dateModified":731,"faq":732,"howto":739},"branch-coverage-versus-line-coverage-in-pytest-cov","article","Branch Coverage","2026-09-18",[733,735,737],{"q":652,"a":734},"Because branch coverage counts things line coverage ignores: every conditional has two outcomes, and each untaken outcome is now reported. The code did not get worse; the measurement got more honest. Reset the threshold to the new baseline and raise it deliberately.",{"q":658,"a":736},"A line containing a decision where only some outcomes were exercised — an if whose body always ran but whose implicit else never did, or a loop that always iterated but never exited on its first check. The report lists the missing destination line numbers.",{"q":670,"a":738},"No. Branch coverage is systematically lower for the same code because it is a stricter measure. A 90% line threshold corresponds roughly to a mid-80s branch threshold on typical code; set the branch threshold from a measured baseline rather than copying the line number.",{"name":740,"description":741,"steps":742},"How to adopt branch coverage with pytest-cov","Enable branch measurement, read the partial-branch report, reset the threshold from the new baseline, and target untaken branches in risky code.",[743,746,749,752,755],{"name":744,"text":745},"Enable branch measurement","Set branch = true under [tool.coverage.run] so coverage records arcs between lines.",{"name":747,"text":748},"Show missing branches","Use --cov-report=term-missing so partial branches are listed with their untaken destinations.",{"name":750,"text":751},"Reset the threshold","Measure the new baseline and set fail_under to it, rather than keeping the line-coverage number.",{"name":753,"text":754},"Prioritise by risk","Add tests for untaken branches in error handling, validation and money arithmetic first.",{"name":756,"text":757},"Exclude genuinely unreachable branches","Mark defensive branches with pragma: no branch sparingly and with a reason.","\u002Fadvanced-pytest-architecture-configuration\u002Fcoverage-measurement-and-enforcement\u002Fbranch-coverage-versus-line-coverage-in-pytest-cov",{"title":5,"description":725},"advanced-pytest-architecture-configuration\u002Fcoverage-measurement-and-enforcement\u002Fbranch-coverage-versus-line-coverage-in-pytest-cov\u002Findex","QBlaa-K7COK7S2wvz2oD9gIkvf14EJ6kV15zquQro2Q",1789718768742]