[{"data":1,"prerenderedAt":2660},["ShallowReactive",2],{"doc:\u002Fgetting-started-with-python-excel-automation\u002Fworking-with-excel-formulas-in-python\u002Fmap-formula-dependencies-in-a-workbook-with-python":3,"surround:\u002Fgetting-started-with-python-excel-automation\u002Fworking-with-excel-formulas-in-python\u002Fmap-formula-dependencies-in-a-workbook-with-python":2625,"ancestors:\u002Fgetting-started-with-python-excel-automation\u002Fworking-with-excel-formulas-in-python\u002Fmap-formula-dependencies-in-a-workbook-with-python":2634,"children:\u002Fgetting-started-with-python-excel-automation\u002Fworking-with-excel-formulas-in-python\u002Fmap-formula-dependencies-in-a-workbook-with-python":2639,"related:\u002Fgetting-started-with-python-excel-automation\u002Fworking-with-excel-formulas-in-python\u002Fmap-formula-dependencies-in-a-workbook-with-python":2640},{"id":4,"title":5,"body":6,"dateModified":2592,"datePublished":2592,"description":2593,"extension":2594,"faq":2595,"keyword":2604,"kind":2605,"meta":2606,"navigation":330,"path":2615,"seo":2616,"slug":2619,"stem":2620,"topics":2621,"type":2623,"__hash__":2624},"docs\u002Fgetting-started-with-python-excel-automation\u002Fworking-with-excel-formulas-in-python\u002Fmap-formula-dependencies-in-a-workbook-with-python\u002Findex.md","Map Formula Dependencies in a Workbook with Python",{"type":7,"value":8,"toc":2579},"minimark",[9,19,24,50,75,266,270,289,541,555,559,588,1133,1159,1163,1166,1493,1511,1615,1619,1622,1836,1853,1857,1860,2174,2420,2424,2483,2487,2494,2498,2511,2515,2521,2527,2533,2539,2543,2575],[10,11,12,13,18],"p",{},"Excel's Trace Precedents arrows answer \"what feeds this cell?\" one cell at a time, one sheet at a time, by clicking. That is fine for checking a single number and hopeless for the questions that come up when a model is handed over or audited: which inputs does the headline figure actually depend on, which inputs are never used, what breaks if this assumption changes, is there a circular reference hiding on some sheet? Every formula in a workbook is stored as text, and openpyxl ships a tokenizer that splits that text into references, functions and operators. From there a dependency graph is a dictionary, and Python's standard library can sort it and find cycles. This page is part of ",[14,15,17],"a",{"href":16},"\u002Fgetting-started-with-python-excel-automation\u002Fworking-with-excel-formulas-in-python\u002F","Working with Excel Formulas in Python",".",[20,21,23],"h2",{"id":22},"prerequisites","Prerequisites",[25,26,31],"pre",{"className":27,"code":28,"language":29,"meta":30,"style":30},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install openpyxl\n","bash","",[32,33,34],"code",{"__ignoreMap":30},[35,36,39,43,47],"span",{"class":37,"line":38},"line",1,[35,40,42],{"class":41},"sMTad","pip",[35,44,46],{"class":45},"srMev"," install",[35,48,49],{"class":45}," openpyxl\n",[10,51,52,55,56,59,60,63,64,67,68,71,72,74],{},[32,53,54],{},"model.xlsx"," is a small three-sheet model: assumptions on ",[32,57,58],{},"Inputs"," (one of them, the tax rate, reached through the name ",[32,61,62],{},"TaxRate","), calculations on ",[32,65,66],{},"Calc",", and results on ",[32,69,70],{},"Summary",". Two cells on ",[32,73,66],{}," refer to each other, and one input is never used — the kind of thing an audit should find.",[76,77,85,86,85,90,85,94,85,102,85,112,85,123,85,130,85,134,85,139,85,145,85,150,85,154,85,161,85,166,85,170,85,173,85,177,85,180,85,183,85,189,85,193,85,196,85,199,85,203,85,206,85,211,85,218,85,223,85,226,85,229,85,233,85,236,85,241,85,248,85,254,85,258,85,262],"svg",{"viewBox":78,"role":79,"ariaLabelledBy":80,"xmlns":83,"style":84},"4 0 752 160","img",[81,82],"dep-flow-t","dep-flow-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:752px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[87,88,89],"title",{"id":81},"From formula text to a dependency map",[91,92,93],"desc",{"id":82},"Every formula is read as text, split into tokens, the reference tokens are resolved to sheet-qualified cells, the pairs form a graph, and the graph answers questions about precedents, dependents, order and cycles.",[95,96],"rect",{"x":97,"y":98,"width":99,"height":100,"fill":101},"4","0","752","160","#ffffff",[95,103],{"x":104,"y":105,"width":106,"height":107,"rx":108,"fill":109,"stroke":110,"style":111},"20.0","16.0","115.2","128.0","12","#e7ebef","var(--line,#cdd5e6)","stroke-width:2px",[113,114,116],"g",{"transform":115},"translate(65.6,37.8) scale(1.000)",[117,118],"path",{"d":119,"fill":120,"stroke":121,"style":122},"M6 3h9l4 4v14H6z M15 3v4h4 M9.5 11.5l4.5 6 M14 11.5l-4.5 6","none","var(--text,#172033)","stroke-width:1.80px;stroke-linecap:round;stroke-linejoin:round",[124,125,129],"text",{"x":126,"y":127,"style":128},"77.6","84.2","font-size:13.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","Read",[124,131,133],{"x":126,"y":132,"style":128},"101.8","formulas",[124,135,138],{"x":126,"y":136,"style":137},"121.2","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","every sheet",[37,140],{"x1":141,"y1":142,"x2":143,"y2":142,"stroke":144,"style":111},"140.2","80.0","162.2","var(--brand,#5b5cf0)",[146,147],"polygon",{"points":148,"fill":149},"166.2,80.0 157.2,85.0 157.2,75.0","#5b5cf0",[95,151],{"x":152,"y":105,"width":106,"height":107,"rx":108,"fill":153,"stroke":144,"style":111},"171.2","#f0f4ff",[113,155,157],{"transform":156},"translate(216.8,37.8) scale(1.000)",[117,158],{"d":159,"fill":120,"stroke":160,"style":122},"M9 8l-4 4 4 4 M15 8l4 4-4 4","var(--brand-strong,#4338ca)",[124,162,165],{"x":163,"y":127,"style":164},"228.8","font-size:13.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Tokenize",[124,167,169],{"x":163,"y":168,"style":137},"103.8","openpyxl",[124,171,172],{"x":163,"y":136,"style":137},"Tokenizer",[37,174],{"x1":175,"y1":142,"x2":176,"y2":142,"stroke":144,"style":111},"291.4","313.4",[146,178],{"points":179,"fill":149},"317.4,80.0 308.4,85.0 308.4,75.0",[95,181],{"x":182,"y":105,"width":106,"height":107,"rx":108,"fill":153,"stroke":144,"style":111},"322.4",[113,184,186],{"transform":185},"translate(368.0,37.8) scale(1.000)",[117,187],{"d":188,"fill":120,"stroke":160,"style":122},"M10 14a4 4 0 0 0 5.7 0l3-3a4 4 0 0 0-5.7-5.7l-1 1 M14 10a4 4 0 0 0-5.7 0l-3 3a4 4 0 0 0 5.7 5.7l1-1",[124,190,192],{"x":191,"y":127,"style":164},"380.0","Resolve refs",[124,194,195],{"x":191,"y":168,"style":137},"sheets, ranges,",[124,197,198],{"x":191,"y":136,"style":137},"names",[37,200],{"x1":201,"y1":142,"x2":202,"y2":142,"stroke":144,"style":111},"442.6","464.6",[146,204],{"points":205,"fill":149},"468.6,80.0 459.6,85.0 459.6,75.0",[95,207],{"x":208,"y":105,"width":106,"height":107,"rx":108,"fill":209,"stroke":210,"style":111},"473.6","#d9f4f1","var(--teal,#0f9488)",[113,212,214],{"transform":213},"translate(519.2,37.8) scale(1.000)",[117,215],{"d":216,"fill":120,"stroke":217,"style":122},"M12 3l9 5-9 5-9-5z M3 13l9 5 9-5 M3 17l9 5 9-5","var(--teal-ink,#0b6157)",[124,219,222],{"x":220,"y":127,"style":221},"531.2","font-size:13.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","Build graph",[124,224,225],{"x":220,"y":168,"style":137},"cell → cells it",[124,227,228],{"x":220,"y":136,"style":137},"reads",[37,230],{"x1":231,"y1":142,"x2":232,"y2":142,"stroke":144,"style":111},"593.8","615.8",[146,234],{"points":235,"fill":149},"619.8,80.0 610.8,85.0 610.8,75.0",[95,237],{"x":238,"y":105,"width":106,"height":107,"rx":108,"fill":239,"stroke":240,"style":111},"624.8","#fdefd8","var(--gold,#b4740a)",[113,242,244],{"transform":243},"translate(670.4,29.0) scale(1.000)",[117,245],{"d":246,"fill":120,"stroke":247,"style":122},"M11 4a7 7 0 1 0 .01 0z M16 16l5 5","var(--gold-ink,#7a4e06)",[124,249,253],{"x":250,"y":251,"style":252},"682.4","75.5","font-size:13.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","Ask",[124,255,257],{"x":250,"y":256,"style":252},"93.0","questions",[124,259,261],{"x":250,"y":260,"style":137},"112.5","precedents,",[124,263,265],{"x":250,"y":264,"style":137},"130.0","cycles",[20,267,269],{"id":268},"step-1-pull-the-references-out-of-each-formula","Step 1 — Pull the references out of each formula",[10,271,272,273,276,277,280,281,284,285,288],{},"Load the workbook normally (not ",[32,274,275],{},"data_only",", which would give the cached values instead of the formulas). For each formula, ",[32,278,279],{},"Tokenizer(formula).items"," returns tokens; references are the ",[32,282,283],{},"OPERAND"," tokens with subtype ",[32,286,287],{},"RANGE",":",[25,290,294],{"className":291,"code":292,"language":293,"meta":30,"style":30},"language-python shiki shiki-themes github-light github-dark-high-contrast","from openpyxl import load_workbook\nfrom openpyxl.formula import Tokenizer\n\nwb = load_workbook(\"model.xlsx\")\nformulas = {}\nfor ws in wb.worksheets:\n    for row in ws.iter_rows():\n        for cell in row:\n            if cell.data_type == \"f\":\n                formulas[(ws.title, cell.coordinate)] = cell.value\n\nfor key, f in list(formulas.items())[:4]:\n    refs = [t.value for t in Tokenizer(f).items if t.type == \"OPERAND\" and t.subtype == \"RANGE\"]\n    print(key, f, \"->\", refs)\nprint(len(formulas), \"formulas\")\n","python",[32,295,296,312,325,332,350,361,376,390,404,422,433,438,460,506,521],{"__ignoreMap":30},[35,297,298,302,306,309],{"class":37,"line":38},[35,299,301],{"class":300},"s-kum","from",[35,303,305],{"class":304},"skGVy"," openpyxl ",[35,307,308],{"class":300},"import",[35,310,311],{"class":304}," load_workbook\n",[35,313,315,317,320,322],{"class":37,"line":314},2,[35,316,301],{"class":300},[35,318,319],{"class":304}," openpyxl.formula ",[35,321,308],{"class":300},[35,323,324],{"class":304}," Tokenizer\n",[35,326,328],{"class":37,"line":327},3,[35,329,331],{"emptyLinePlaceholder":330},true,"\n",[35,333,335,338,341,344,347],{"class":37,"line":334},4,[35,336,337],{"class":304},"wb ",[35,339,340],{"class":300},"=",[35,342,343],{"class":304}," load_workbook(",[35,345,346],{"class":45},"\"model.xlsx\"",[35,348,349],{"class":304},")\n",[35,351,353,356,358],{"class":37,"line":352},5,[35,354,355],{"class":304},"formulas ",[35,357,340],{"class":300},[35,359,360],{"class":304}," {}\n",[35,362,364,367,370,373],{"class":37,"line":363},6,[35,365,366],{"class":300},"for",[35,368,369],{"class":304}," ws ",[35,371,372],{"class":300},"in",[35,374,375],{"class":304}," wb.worksheets:\n",[35,377,379,382,385,387],{"class":37,"line":378},7,[35,380,381],{"class":300},"    for",[35,383,384],{"class":304}," row ",[35,386,372],{"class":300},[35,388,389],{"class":304}," ws.iter_rows():\n",[35,391,393,396,399,401],{"class":37,"line":392},8,[35,394,395],{"class":300},"        for",[35,397,398],{"class":304}," cell ",[35,400,372],{"class":300},[35,402,403],{"class":304}," row:\n",[35,405,407,410,413,416,419],{"class":37,"line":406},9,[35,408,409],{"class":300},"            if",[35,411,412],{"class":304}," cell.data_type ",[35,414,415],{"class":300},"==",[35,417,418],{"class":45}," \"f\"",[35,420,421],{"class":304},":\n",[35,423,425,428,430],{"class":37,"line":424},10,[35,426,427],{"class":304},"                formulas[(ws.title, cell.coordinate)] ",[35,429,340],{"class":300},[35,431,432],{"class":304}," cell.value\n",[35,434,436],{"class":37,"line":435},11,[35,437,331],{"emptyLinePlaceholder":330},[35,439,441,443,446,448,452,455,457],{"class":37,"line":440},12,[35,442,366],{"class":300},[35,444,445],{"class":304}," key, f ",[35,447,372],{"class":300},[35,449,451],{"class":450},"sP0c6"," list",[35,453,454],{"class":304},"(formulas.items())[:",[35,456,97],{"class":450},[35,458,459],{"class":304},"]:\n",[35,461,463,466,468,471,473,476,478,481,484,487,489,492,495,498,500,503],{"class":37,"line":462},13,[35,464,465],{"class":304},"    refs ",[35,467,340],{"class":300},[35,469,470],{"class":304}," [t.value ",[35,472,366],{"class":300},[35,474,475],{"class":304}," t ",[35,477,372],{"class":300},[35,479,480],{"class":304}," Tokenizer(f).items ",[35,482,483],{"class":300},"if",[35,485,486],{"class":304}," t.type ",[35,488,415],{"class":300},[35,490,491],{"class":45}," \"OPERAND\"",[35,493,494],{"class":300}," and",[35,496,497],{"class":304}," t.subtype ",[35,499,415],{"class":300},[35,501,502],{"class":45}," \"RANGE\"",[35,504,505],{"class":304},"]\n",[35,507,509,512,515,518],{"class":37,"line":508},14,[35,510,511],{"class":450},"    print",[35,513,514],{"class":304},"(key, f, ",[35,516,517],{"class":45},"\"->\"",[35,519,520],{"class":304},", refs)\n",[35,522,524,527,530,533,536,539],{"class":37,"line":523},15,[35,525,526],{"class":450},"print",[35,528,529],{"class":304},"(",[35,531,532],{"class":450},"len",[35,534,535],{"class":304},"(formulas), ",[35,537,538],{"class":45},"\"formulas\"",[35,540,349],{"class":304},[10,542,543,544,547,548,550,551,554],{},"The tokenizer handles the parts that trip up a regular expression: text in quotes that looks like a reference (",[32,545,546],{},"\"see A1\"","), sheet names with spaces in single quotes, and function names next to parentheses. But \"RANGE\" to the tokenizer means anything that is not a literal — a cell, a range, a defined name such as ",[32,549,62],{},", or a table reference like ",[32,552,553],{},"Sales[Amount]"," — so the next step sorts them out.",[20,556,558],{"id":557},"step-2-resolve-each-token-to-sheet-qualified-cells","Step 2 — Resolve each token to sheet-qualified cells",[10,560,561,562,565,566,568,569,572,573,576,577,579,580,583,584,587],{},"A bare ",[32,563,564],{},"B2"," in a formula on ",[32,567,66],{}," means ",[32,570,571],{},"Calc!B2","; ",[32,574,575],{},"Inputs!B3"," names its sheet; ",[32,578,62],{}," is a defined name pointing somewhere else; ",[32,581,582],{},"B2:B4"," is three cells. Normalise everything to ",[32,585,586],{},"(sheet, cell)"," pairs:",[25,589,591],{"className":291,"code":590,"language":293,"meta":30,"style":30},"import re\nfrom openpyxl.utils import range_boundaries, get_column_letter\n\nREF = re.compile(r\"^(?:(?P\u003Csheet>'[^']+'|[^'!]+)!)?(?P\u003Crng>\\$?[A-Z]{1,3}\\$?\\d+(?::\\$?[A-Z]{1,3}\\$?\\d+)?)$\")\n\ndef cells_in(sheet, rng, limit=10_000):\n    min_col, min_row, max_col, max_row = range_boundaries(rng.replace(\"$\", \"\"))\n    if (max_col - min_col + 1) * (max_row - min_row + 1) > limit:\n        return [(sheet, rng)]                      # keep huge ranges as one node\n    return [(sheet, f\"{get_column_letter(c)}{r}\")\n            for r in range(min_row, max_row + 1) for c in range(min_col, max_col + 1)]\n\ndef resolve(token, home):\n    m = REF.match(token)\n    if m:\n        sheet = (m.group(\"sheet\") or home).strip(\"'\")\n        return cells_in(sheet, m.group(\"rng\"))\n    if token in wb.defined_names:\n        out = []\n        for sheet, ref in wb.defined_names[token].destinations:\n            out += cells_in(sheet, ref)\n        return out\n    return [(\"?\", token)]                          # table refs, external links, errors\n\nprint(resolve(\"B2\", \"Calc\"), resolve(\"Inputs!B3\", \"Calc\"), resolve(\"TaxRate\", \"Calc\"))\nprint(resolve(\"Calc!B2:B4\", \"Summary\"))\n",[32,592,593,600,612,616,751,755,775,797,842,854,885,926,930,940,953,960,987,1000,1013,1024,1037,1049,1057,1074,1079,1116],{"__ignoreMap":30},[35,594,595,597],{"class":37,"line":38},[35,596,308],{"class":300},[35,598,599],{"class":304}," re\n",[35,601,602,604,607,609],{"class":37,"line":314},[35,603,301],{"class":300},[35,605,606],{"class":304}," openpyxl.utils ",[35,608,308],{"class":300},[35,610,611],{"class":304}," range_boundaries, get_column_letter\n",[35,613,614],{"class":37,"line":327},[35,615,331],{"emptyLinePlaceholder":330},[35,617,618,621,624,627,630,633,636,640,643,646,649,652,655,658,660,663,665,667,670,672,674,677,680,682,685,687,690,694,696,698,701,703,706,708,710,713,715,718,720,722,724,726,728,730,732,734,736,738,740,742,744,747,749],{"class":37,"line":334},[35,619,620],{"class":450},"REF",[35,622,623],{"class":300}," =",[35,625,626],{"class":304}," re.compile(",[35,628,629],{"class":300},"r",[35,631,632],{"class":45},"\"",[35,634,635],{"class":450},"^(?:(",[35,637,639],{"class":638},"sx1sP","?P\u003Csheet>",[35,641,642],{"class":45},"'",[35,644,645],{"class":450},"[",[35,647,648],{"class":300},"^",[35,650,642],{"class":651},"sSjpA",[35,653,654],{"class":450},"]",[35,656,657],{"class":300},"+",[35,659,642],{"class":45},[35,661,662],{"class":300},"|",[35,664,645],{"class":450},[35,666,648],{"class":300},[35,668,669],{"class":651},"'!",[35,671,654],{"class":450},[35,673,657],{"class":300},[35,675,676],{"class":450},")",[35,678,679],{"class":45},"!",[35,681,676],{"class":450},[35,683,684],{"class":300},"?",[35,686,529],{"class":450},[35,688,689],{"class":638},"?P\u003Crng>",[35,691,693],{"class":692},"s_b0D","\\$",[35,695,684],{"class":300},[35,697,645],{"class":450},[35,699,700],{"class":651},"A-Z",[35,702,654],{"class":450},[35,704,705],{"class":300},"{1,3}",[35,707,693],{"class":692},[35,709,684],{"class":300},[35,711,712],{"class":450},"\\d",[35,714,657],{"class":300},[35,716,717],{"class":450},"(?:",[35,719,288],{"class":45},[35,721,693],{"class":692},[35,723,684],{"class":300},[35,725,645],{"class":450},[35,727,700],{"class":651},[35,729,654],{"class":450},[35,731,705],{"class":300},[35,733,693],{"class":692},[35,735,684],{"class":300},[35,737,712],{"class":450},[35,739,657],{"class":300},[35,741,676],{"class":450},[35,743,684],{"class":300},[35,745,746],{"class":450},")$",[35,748,632],{"class":45},[35,750,349],{"class":304},[35,752,753],{"class":37,"line":352},[35,754,331],{"emptyLinePlaceholder":330},[35,756,757,760,764,767,769,772],{"class":37,"line":363},[35,758,759],{"class":300},"def",[35,761,763],{"class":762},"s_Opv"," cells_in",[35,765,766],{"class":304},"(sheet, rng, limit",[35,768,340],{"class":300},[35,770,771],{"class":450},"10_000",[35,773,774],{"class":304},"):\n",[35,776,777,780,782,785,788,791,794],{"class":37,"line":378},[35,778,779],{"class":304},"    min_col, min_row, max_col, max_row ",[35,781,340],{"class":300},[35,783,784],{"class":304}," range_boundaries(rng.replace(",[35,786,787],{"class":45},"\"$\"",[35,789,790],{"class":304},", ",[35,792,793],{"class":45},"\"\"",[35,795,796],{"class":304},"))\n",[35,798,799,802,805,808,811,813,816,819,822,825,827,830,832,834,836,839],{"class":37,"line":392},[35,800,801],{"class":300},"    if",[35,803,804],{"class":304}," (max_col ",[35,806,807],{"class":300},"-",[35,809,810],{"class":304}," min_col ",[35,812,657],{"class":300},[35,814,815],{"class":450}," 1",[35,817,818],{"class":304},") ",[35,820,821],{"class":300},"*",[35,823,824],{"class":304}," (max_row ",[35,826,807],{"class":300},[35,828,829],{"class":304}," min_row ",[35,831,657],{"class":300},[35,833,815],{"class":450},[35,835,818],{"class":304},[35,837,838],{"class":300},">",[35,840,841],{"class":304}," limit:\n",[35,843,844,847,850],{"class":37,"line":406},[35,845,846],{"class":300},"        return",[35,848,849],{"class":304}," [(sheet, rng)]                      ",[35,851,853],{"class":852},"s-wDw","# keep huge ranges as one node\n",[35,855,856,859,862,865,867,870,873,876,878,881,883],{"class":37,"line":424},[35,857,858],{"class":300},"    return",[35,860,861],{"class":304}," [(sheet, ",[35,863,864],{"class":300},"f",[35,866,632],{"class":45},[35,868,869],{"class":651},"{",[35,871,872],{"class":304},"get_column_letter(c)",[35,874,875],{"class":651},"}{",[35,877,629],{"class":304},[35,879,880],{"class":651},"}",[35,882,632],{"class":45},[35,884,349],{"class":304},[35,886,887,890,893,895,898,901,903,905,907,909,912,914,916,919,921,923],{"class":37,"line":435},[35,888,889],{"class":300},"            for",[35,891,892],{"class":304}," r ",[35,894,372],{"class":300},[35,896,897],{"class":450}," range",[35,899,900],{"class":304},"(min_row, max_row ",[35,902,657],{"class":300},[35,904,815],{"class":450},[35,906,818],{"class":304},[35,908,366],{"class":300},[35,910,911],{"class":304}," c ",[35,913,372],{"class":300},[35,915,897],{"class":450},[35,917,918],{"class":304},"(min_col, max_col ",[35,920,657],{"class":300},[35,922,815],{"class":450},[35,924,925],{"class":304},")]\n",[35,927,928],{"class":37,"line":440},[35,929,331],{"emptyLinePlaceholder":330},[35,931,932,934,937],{"class":37,"line":462},[35,933,759],{"class":300},[35,935,936],{"class":762}," resolve",[35,938,939],{"class":304},"(token, home):\n",[35,941,942,945,947,950],{"class":37,"line":508},[35,943,944],{"class":304},"    m ",[35,946,340],{"class":300},[35,948,949],{"class":450}," REF",[35,951,952],{"class":304},".match(token)\n",[35,954,955,957],{"class":37,"line":523},[35,956,801],{"class":300},[35,958,959],{"class":304}," m:\n",[35,961,963,966,968,971,974,976,979,982,985],{"class":37,"line":962},16,[35,964,965],{"class":304},"        sheet ",[35,967,340],{"class":300},[35,969,970],{"class":304}," (m.group(",[35,972,973],{"class":45},"\"sheet\"",[35,975,818],{"class":304},[35,977,978],{"class":300},"or",[35,980,981],{"class":304}," home).strip(",[35,983,984],{"class":45},"\"'\"",[35,986,349],{"class":304},[35,988,990,992,995,998],{"class":37,"line":989},17,[35,991,846],{"class":300},[35,993,994],{"class":304}," cells_in(sheet, m.group(",[35,996,997],{"class":45},"\"rng\"",[35,999,796],{"class":304},[35,1001,1003,1005,1008,1010],{"class":37,"line":1002},18,[35,1004,801],{"class":300},[35,1006,1007],{"class":304}," token ",[35,1009,372],{"class":300},[35,1011,1012],{"class":304}," wb.defined_names:\n",[35,1014,1016,1019,1021],{"class":37,"line":1015},19,[35,1017,1018],{"class":304},"        out ",[35,1020,340],{"class":300},[35,1022,1023],{"class":304}," []\n",[35,1025,1027,1029,1032,1034],{"class":37,"line":1026},20,[35,1028,395],{"class":300},[35,1030,1031],{"class":304}," sheet, ref ",[35,1033,372],{"class":300},[35,1035,1036],{"class":304}," wb.defined_names[token].destinations:\n",[35,1038,1040,1043,1046],{"class":37,"line":1039},21,[35,1041,1042],{"class":304},"            out ",[35,1044,1045],{"class":300},"+=",[35,1047,1048],{"class":304}," cells_in(sheet, ref)\n",[35,1050,1052,1054],{"class":37,"line":1051},22,[35,1053,846],{"class":300},[35,1055,1056],{"class":304}," out\n",[35,1058,1060,1062,1065,1068,1071],{"class":37,"line":1059},23,[35,1061,858],{"class":300},[35,1063,1064],{"class":304}," [(",[35,1066,1067],{"class":45},"\"?\"",[35,1069,1070],{"class":304},", token)]                          ",[35,1072,1073],{"class":852},"# table refs, external links, errors\n",[35,1075,1077],{"class":37,"line":1076},24,[35,1078,331],{"emptyLinePlaceholder":330},[35,1080,1082,1084,1087,1090,1092,1095,1098,1101,1103,1105,1107,1110,1112,1114],{"class":37,"line":1081},25,[35,1083,526],{"class":450},[35,1085,1086],{"class":304},"(resolve(",[35,1088,1089],{"class":45},"\"B2\"",[35,1091,790],{"class":304},[35,1093,1094],{"class":45},"\"Calc\"",[35,1096,1097],{"class":304},"), resolve(",[35,1099,1100],{"class":45},"\"Inputs!B3\"",[35,1102,790],{"class":304},[35,1104,1094],{"class":45},[35,1106,1097],{"class":304},[35,1108,1109],{"class":45},"\"TaxRate\"",[35,1111,790],{"class":304},[35,1113,1094],{"class":45},[35,1115,796],{"class":304},[35,1117,1119,1121,1123,1126,1128,1131],{"class":37,"line":1118},26,[35,1120,526],{"class":450},[35,1122,1086],{"class":304},[35,1124,1125],{"class":45},"\"Calc!B2:B4\"",[35,1127,790],{"class":304},[35,1129,1130],{"class":45},"\"Summary\"",[35,1132,796],{"class":304},[10,1134,1135,1136,1139,1140,1143,1144,1146,1147,1150,1151,1154,1155,1158],{},"Ranges are expanded to their cells so that ",[32,1137,1138],{},"SUM(Calc!B2:B4)"," counts as depending on each of the three, with a limit so that a reference like ",[32,1141,1142],{},"A1:Z100000"," stays one node instead of 2.6 million. Anything the resolver does not understand is kept with a ",[32,1145,684],{}," sheet rather than dropped: table references, references into other workbooks (",[32,1148,1149],{},"[1]Sheet1!A1",") and ",[32,1152,1153],{},"#REF!"," all deserve a look during an audit. Whole-column references such as ",[32,1156,1157],{},"A:A"," also land there; add a rule for them if your models use them.",[20,1160,1162],{"id":1161},"step-3-build-the-graph-and-trace-in-both-directions","Step 3 — Build the graph and trace in both directions",[10,1164,1165],{},"Map each formula cell to the set of cells it reads — its precedents — and invert that for dependents:",[25,1167,1169],{"className":291,"code":1168,"language":293,"meta":30,"style":30},"from collections import defaultdict\n\nprecedents = {}\nfor (sheet, coord), f in formulas.items():\n    refs = [t.value for t in Tokenizer(f).items if t.type == \"OPERAND\" and t.subtype == \"RANGE\"]\n    precedents[(sheet, coord)] = {c for r in refs for c in resolve(r, sheet)}\n\ndependents = defaultdict(set)\nfor cell, pre in precedents.items():\n    for p in pre:\n        dependents[p].add(cell)\n\ndef walk(start, edges):\n    seen, stack = set(), [start]\n    while stack:\n        for nxt in edges.get(stack.pop(), ()):\n            if nxt not in seen:\n                seen.add(nxt)\n                stack.append(nxt)\n    return seen\n\nnet = (\"Summary\", \"B2\")\nfeeds = walk(net, precedents)\nprint(\"inputs behind net profit:\", sorted(c for c in feeds if c not in formulas))\nprint(\"affected by the growth rate:\", sorted(walk((\"Inputs\", \"B4\"), dependents)))\n",[32,1170,1171,1183,1187,1196,1208,1242,1270,1274,1289,1301,1313,1318,1322,1332,1345,1353,1365,1380,1385,1390,1397,1401,1419,1429,1466],{"__ignoreMap":30},[35,1172,1173,1175,1178,1180],{"class":37,"line":38},[35,1174,301],{"class":300},[35,1176,1177],{"class":304}," collections ",[35,1179,308],{"class":300},[35,1181,1182],{"class":304}," defaultdict\n",[35,1184,1185],{"class":37,"line":314},[35,1186,331],{"emptyLinePlaceholder":330},[35,1188,1189,1192,1194],{"class":37,"line":327},[35,1190,1191],{"class":304},"precedents ",[35,1193,340],{"class":300},[35,1195,360],{"class":304},[35,1197,1198,1200,1203,1205],{"class":37,"line":334},[35,1199,366],{"class":300},[35,1201,1202],{"class":304}," (sheet, coord), f ",[35,1204,372],{"class":300},[35,1206,1207],{"class":304}," formulas.items():\n",[35,1209,1210,1212,1214,1216,1218,1220,1222,1224,1226,1228,1230,1232,1234,1236,1238,1240],{"class":37,"line":352},[35,1211,465],{"class":304},[35,1213,340],{"class":300},[35,1215,470],{"class":304},[35,1217,366],{"class":300},[35,1219,475],{"class":304},[35,1221,372],{"class":300},[35,1223,480],{"class":304},[35,1225,483],{"class":300},[35,1227,486],{"class":304},[35,1229,415],{"class":300},[35,1231,491],{"class":45},[35,1233,494],{"class":300},[35,1235,497],{"class":304},[35,1237,415],{"class":300},[35,1239,502],{"class":45},[35,1241,505],{"class":304},[35,1243,1244,1247,1249,1252,1254,1256,1258,1261,1263,1265,1267],{"class":37,"line":363},[35,1245,1246],{"class":304},"    precedents[(sheet, coord)] ",[35,1248,340],{"class":300},[35,1250,1251],{"class":304}," {c ",[35,1253,366],{"class":300},[35,1255,892],{"class":304},[35,1257,372],{"class":300},[35,1259,1260],{"class":304}," refs ",[35,1262,366],{"class":300},[35,1264,911],{"class":304},[35,1266,372],{"class":300},[35,1268,1269],{"class":304}," resolve(r, sheet)}\n",[35,1271,1272],{"class":37,"line":378},[35,1273,331],{"emptyLinePlaceholder":330},[35,1275,1276,1279,1281,1284,1287],{"class":37,"line":392},[35,1277,1278],{"class":304},"dependents ",[35,1280,340],{"class":300},[35,1282,1283],{"class":304}," defaultdict(",[35,1285,1286],{"class":450},"set",[35,1288,349],{"class":304},[35,1290,1291,1293,1296,1298],{"class":37,"line":406},[35,1292,366],{"class":300},[35,1294,1295],{"class":304}," cell, pre ",[35,1297,372],{"class":300},[35,1299,1300],{"class":304}," precedents.items():\n",[35,1302,1303,1305,1308,1310],{"class":37,"line":424},[35,1304,381],{"class":300},[35,1306,1307],{"class":304}," p ",[35,1309,372],{"class":300},[35,1311,1312],{"class":304}," pre:\n",[35,1314,1315],{"class":37,"line":435},[35,1316,1317],{"class":304},"        dependents[p].add(cell)\n",[35,1319,1320],{"class":37,"line":440},[35,1321,331],{"emptyLinePlaceholder":330},[35,1323,1324,1326,1329],{"class":37,"line":462},[35,1325,759],{"class":300},[35,1327,1328],{"class":762}," walk",[35,1330,1331],{"class":304},"(start, edges):\n",[35,1333,1334,1337,1339,1342],{"class":37,"line":508},[35,1335,1336],{"class":304},"    seen, stack ",[35,1338,340],{"class":300},[35,1340,1341],{"class":450}," set",[35,1343,1344],{"class":304},"(), [start]\n",[35,1346,1347,1350],{"class":37,"line":523},[35,1348,1349],{"class":300},"    while",[35,1351,1352],{"class":304}," stack:\n",[35,1354,1355,1357,1360,1362],{"class":37,"line":962},[35,1356,395],{"class":300},[35,1358,1359],{"class":304}," nxt ",[35,1361,372],{"class":300},[35,1363,1364],{"class":304}," edges.get(stack.pop(), ()):\n",[35,1366,1367,1369,1371,1374,1377],{"class":37,"line":989},[35,1368,409],{"class":300},[35,1370,1359],{"class":304},[35,1372,1373],{"class":300},"not",[35,1375,1376],{"class":300}," in",[35,1378,1379],{"class":304}," seen:\n",[35,1381,1382],{"class":37,"line":1002},[35,1383,1384],{"class":304},"                seen.add(nxt)\n",[35,1386,1387],{"class":37,"line":1015},[35,1388,1389],{"class":304},"                stack.append(nxt)\n",[35,1391,1392,1394],{"class":37,"line":1026},[35,1393,858],{"class":300},[35,1395,1396],{"class":304}," seen\n",[35,1398,1399],{"class":37,"line":1039},[35,1400,331],{"emptyLinePlaceholder":330},[35,1402,1403,1406,1408,1411,1413,1415,1417],{"class":37,"line":1051},[35,1404,1405],{"class":304},"net ",[35,1407,340],{"class":300},[35,1409,1410],{"class":304}," (",[35,1412,1130],{"class":45},[35,1414,790],{"class":304},[35,1416,1089],{"class":45},[35,1418,349],{"class":304},[35,1420,1421,1424,1426],{"class":37,"line":1059},[35,1422,1423],{"class":304},"feeds ",[35,1425,340],{"class":300},[35,1427,1428],{"class":304}," walk(net, precedents)\n",[35,1430,1431,1433,1435,1438,1440,1443,1446,1448,1450,1452,1455,1457,1459,1461,1463],{"class":37,"line":1076},[35,1432,526],{"class":450},[35,1434,529],{"class":304},[35,1436,1437],{"class":45},"\"inputs behind net profit:\"",[35,1439,790],{"class":304},[35,1441,1442],{"class":450},"sorted",[35,1444,1445],{"class":304},"(c ",[35,1447,366],{"class":300},[35,1449,911],{"class":304},[35,1451,372],{"class":300},[35,1453,1454],{"class":304}," feeds ",[35,1456,483],{"class":300},[35,1458,911],{"class":304},[35,1460,1373],{"class":300},[35,1462,1376],{"class":300},[35,1464,1465],{"class":304}," formulas))\n",[35,1467,1468,1470,1472,1475,1477,1479,1482,1485,1487,1490],{"class":37,"line":1081},[35,1469,526],{"class":450},[35,1471,529],{"class":304},[35,1473,1474],{"class":45},"\"affected by the growth rate:\"",[35,1476,790],{"class":304},[35,1478,1442],{"class":450},[35,1480,1481],{"class":304},"(walk((",[35,1483,1484],{"class":45},"\"Inputs\"",[35,1486,790],{"class":304},[35,1488,1489],{"class":45},"\"B4\"",[35,1491,1492],{"class":304},"), dependents)))\n",[10,1494,1495,1498,1499,1502,1503,1506,1507,1510],{},[32,1496,1497],{},"walk"," follows edges until nothing new turns up, so it finds indirect links too: net profit depends on ",[32,1500,1501],{},"Calc!B4"," and ",[32,1504,1505],{},"Calc!B6",", which depend on revenue and the tax rate, which depend on units and price. Filtering the result to cells that are ",[1508,1509,1373],"em",{}," formulas gives the true inputs of a result — the list to review when someone asks \"what assumptions is this number based on?\" Going the other way, a changed growth rate affects only the next-year figures, which tells you which parts of a report need re-checking after the change.",[76,1512,85,1517,85,1520,85,1523,85,1526,85,1531,85,1537,85,1542,85,1546,85,1550,85,1556,85,1562,85,1567,85,1570,85,1573,85,1576,85,1581,85,1584,85,1587,85,1590,85,1592,85,1595,85,1601,85,1606,85,1610,85,1613],{"viewBox":1513,"role":79,"ariaLabelledBy":1514,"xmlns":83,"style":84},"4 0 752 332",[1515,1516],"dep-tree-t","dep-tree-d",[87,1518,1519],{"id":1515},"What net profit is built from",[91,1521,1522],{"id":1516},"The net profit cell on the Summary sheet fans in from its direct precedents on the Calc sheet, gross and tax, which in turn come from units, price and the tax rate on the Inputs sheet.",[95,1524],{"x":97,"y":98,"width":99,"height":1525,"fill":101},"332",[95,1527],{"x":1528,"y":1529,"width":1530,"height":256,"rx":108,"fill":153,"stroke":144,"style":111},"520.0","119.5","220.0",[113,1532,1534],{"transform":1533},"translate(618.0,132.5) scale(1.000)",[117,1535],{"d":1536,"fill":120,"stroke":160,"style":122},"M4 20h16 M7 17v-5 M12 17V7 M17 17v-8",[124,1538,1541],{"x":1539,"y":1540,"style":164},"630.0","179.0","Summary!B2 — Net profit",[124,1543,1545],{"x":1539,"y":1544,"style":137},"198.5","=Calc!B4-Calc!B6",[95,1547],{"x":104,"y":105,"width":1548,"height":1549,"rx":108,"fill":209,"stroke":210,"style":111},"250.0","92.0",[113,1551,1553],{"transform":1552},"translate(133.0,29.0) scale(1.000)",[117,1554],{"d":1555,"fill":120,"stroke":217,"style":122},"M4 4h16v16H4z M4 9h16 M4 14h16 M10 4v16",[124,1557,1561],{"x":1558,"y":1559,"style":1560},"145.0","75.0","font-size:13px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","Inputs!B2 — Units",[124,1563,1566],{"x":1558,"y":1564,"style":1565},"94.0","font-size:11px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","via revenue and gross",[117,1568],{"d":1569,"fill":120,"stroke":144,"style":111},"M274.0,62.0 C394.0,62.0 394.0,166.0 510.0,166.0",[146,1571],{"points":1572,"fill":149},"514.0,166.0 505.0,171.0 505.0,161.0",[95,1574],{"x":104,"y":1575,"width":1548,"height":1549,"rx":108,"fill":209,"stroke":210,"style":111},"120.0",[113,1577,1579],{"transform":1578},"translate(133.0,133.0) scale(1.000)",[117,1580],{"d":1555,"fill":120,"stroke":217,"style":122},[124,1582,1583],{"x":1558,"y":1540,"style":1560},"Inputs!B3 — Price",[124,1585,1566],{"x":1558,"y":1586,"style":1565},"198.0",[117,1588],{"d":1589,"fill":120,"stroke":144,"style":111},"M274.0,166.0 C394.0,166.0 394.0,166.0 510.0,166.0",[146,1591],{"points":1572,"fill":149},[95,1593],{"x":104,"y":1594,"width":1548,"height":1549,"rx":108,"fill":239,"stroke":240,"style":111},"224.0",[113,1596,1598],{"transform":1597},"translate(133.0,237.0) scale(1.000)",[117,1599],{"d":1600,"fill":120,"stroke":247,"style":122},"M8 11a4 4 0 1 0 .01 0z M11 13l9-9 M17 7l2 2 M15 9l2 2",[124,1602,1605],{"x":1558,"y":1603,"style":1604},"283.0","font-size:13px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","Inputs!B5 — Tax rate",[124,1607,1609],{"x":1558,"y":1608,"style":1565},"302.0","via the name TaxRate",[117,1611],{"d":1612,"fill":120,"stroke":144,"style":111},"M274.0,270.0 C394.0,270.0 394.0,166.0 510.0,166.0",[146,1614],{"points":1572,"fill":149},[20,1616,1618],{"id":1617},"step-4-unused-inputs-calculation-order-and-circular-references","Step 4 — Unused inputs, calculation order and circular references",[10,1620,1621],{},"Three audit checks fall out of the graph. An input that nothing reads is either dead or a sign that a formula was meant to use it. A topological sort of the graph gives an order in which every cell comes after the cells it reads — and fails if there is a cycle, naming it:",[25,1623,1625],{"className":291,"code":1624,"language":293,"meta":30,"style":30},"from graphlib import TopologicalSorter, CycleError\n\nreferenced = set().union(*precedents.values())\ninputs = [(ws.title, c.coordinate) for ws in wb.worksheets for row in ws.iter_rows(min_col=2)\n          for c in row if c.value is not None and c.data_type == \"n\"]\nprint(\"unused inputs:\", [i for i in inputs if i not in referenced])\n\ntry:\n    order = list(TopologicalSorter(precedents).static_order())\n    print(\"calculation order:\", order)\nexcept CycleError as err:\n    print(\"circular reference:\", err.args[1])\n",[32,1626,1627,1639,1643,1660,1699,1736,1769,1773,1780,1792,1804,1818],{"__ignoreMap":30},[35,1628,1629,1631,1634,1636],{"class":37,"line":38},[35,1630,301],{"class":300},[35,1632,1633],{"class":304}," graphlib ",[35,1635,308],{"class":300},[35,1637,1638],{"class":304}," TopologicalSorter, CycleError\n",[35,1640,1641],{"class":37,"line":314},[35,1642,331],{"emptyLinePlaceholder":330},[35,1644,1645,1648,1650,1652,1655,1657],{"class":37,"line":327},[35,1646,1647],{"class":304},"referenced ",[35,1649,340],{"class":300},[35,1651,1341],{"class":450},[35,1653,1654],{"class":304},"().union(",[35,1656,821],{"class":300},[35,1658,1659],{"class":304},"precedents.values())\n",[35,1661,1662,1665,1667,1670,1672,1674,1676,1679,1681,1683,1685,1688,1692,1694,1697],{"class":37,"line":334},[35,1663,1664],{"class":304},"inputs ",[35,1666,340],{"class":300},[35,1668,1669],{"class":304}," [(ws.title, c.coordinate) ",[35,1671,366],{"class":300},[35,1673,369],{"class":304},[35,1675,372],{"class":300},[35,1677,1678],{"class":304}," wb.worksheets ",[35,1680,366],{"class":300},[35,1682,384],{"class":304},[35,1684,372],{"class":300},[35,1686,1687],{"class":304}," ws.iter_rows(",[35,1689,1691],{"class":1690},"sa561","min_col",[35,1693,340],{"class":300},[35,1695,1696],{"class":450},"2",[35,1698,349],{"class":304},[35,1700,1701,1704,1706,1708,1710,1712,1715,1718,1721,1724,1726,1729,1731,1734],{"class":37,"line":352},[35,1702,1703],{"class":300},"          for",[35,1705,911],{"class":304},[35,1707,372],{"class":300},[35,1709,384],{"class":304},[35,1711,483],{"class":300},[35,1713,1714],{"class":304}," c.value ",[35,1716,1717],{"class":300},"is",[35,1719,1720],{"class":300}," not",[35,1722,1723],{"class":450}," None",[35,1725,494],{"class":300},[35,1727,1728],{"class":304}," c.data_type ",[35,1730,415],{"class":300},[35,1732,1733],{"class":45}," \"n\"",[35,1735,505],{"class":304},[35,1737,1738,1740,1742,1745,1748,1750,1753,1755,1758,1760,1762,1764,1766],{"class":37,"line":363},[35,1739,526],{"class":450},[35,1741,529],{"class":304},[35,1743,1744],{"class":45},"\"unused inputs:\"",[35,1746,1747],{"class":304},", [i ",[35,1749,366],{"class":300},[35,1751,1752],{"class":304}," i ",[35,1754,372],{"class":300},[35,1756,1757],{"class":304}," inputs ",[35,1759,483],{"class":300},[35,1761,1752],{"class":304},[35,1763,1373],{"class":300},[35,1765,1376],{"class":300},[35,1767,1768],{"class":304}," referenced])\n",[35,1770,1771],{"class":37,"line":378},[35,1772,331],{"emptyLinePlaceholder":330},[35,1774,1775,1778],{"class":37,"line":392},[35,1776,1777],{"class":300},"try",[35,1779,421],{"class":304},[35,1781,1782,1785,1787,1789],{"class":37,"line":406},[35,1783,1784],{"class":304},"    order ",[35,1786,340],{"class":300},[35,1788,451],{"class":450},[35,1790,1791],{"class":304},"(TopologicalSorter(precedents).static_order())\n",[35,1793,1794,1796,1798,1801],{"class":37,"line":424},[35,1795,511],{"class":450},[35,1797,529],{"class":304},[35,1799,1800],{"class":45},"\"calculation order:\"",[35,1802,1803],{"class":304},", order)\n",[35,1805,1806,1809,1812,1815],{"class":37,"line":435},[35,1807,1808],{"class":300},"except",[35,1810,1811],{"class":304}," CycleError ",[35,1813,1814],{"class":300},"as",[35,1816,1817],{"class":304}," err:\n",[35,1819,1820,1822,1824,1827,1830,1833],{"class":37,"line":440},[35,1821,511],{"class":450},[35,1823,529],{"class":304},[35,1825,1826],{"class":45},"\"circular reference:\"",[35,1828,1829],{"class":304},", err.args[",[35,1831,1832],{"class":450},"1",[35,1834,1835],{"class":304},"])\n",[10,1837,1838,1841,1842,1845,1846,1502,1849,1852],{},[32,1839,1840],{},"graphlib"," has been in the standard library since Python 3.9. ",[32,1843,1844],{},"CycleError"," carries the cycle as its second argument — here ",[32,1847,1848],{},"Calc!B7",[32,1850,1851],{},"Calc!B8",", which refer to each other. Excel would only say \"There are one or more circular references\" and point at one of them; the graph names every cell in the loop. The unused input is the FX rate: somebody added it for a currency conversion that never got built. To get the order anyway, remove the cells of the cycle from the graph and sort the rest.",[20,1854,1856],{"id":1855},"step-5-write-the-map-into-the-workbook","Step 5 — Write the map into the workbook",[10,1858,1859],{},"An audit is easier to share as a sheet than as console output. Write one row per formula with its direct precedents, its dependents and whether it sits in a cycle:",[25,1861,1863],{"className":291,"code":1862,"language":293,"meta":30,"style":30},"def label(c):\n    sheet, coord = c\n    return f\"{sheet}!{coord}\"\n\ncyc = set()\ntry:\n    TopologicalSorter(precedents).prepare()\nexcept CycleError as err:\n    cyc = set(err.args[1])\n\nreport = wb.create_sheet(\"Dependencies\")\nreport.append([\"Cell\", \"Formula\", \"Reads\", \"Read by\", \"In cycle\"])\nfor cell in sorted(formulas):\n    report.append([label(cell), formulas[cell], \", \".join(map(label, sorted(precedents[cell]))),\n                   \", \".join(map(label, sorted(dependents.get(cell, ())))), \"yes\" if cell in cyc else \"\"])\nreport.column_dimensions[\"B\"].width = 26\nreport.column_dimensions[\"C\"].width = 40\nwb.save(\"model_audited.xlsx\")\nfor row in report.iter_rows(min_row=1, max_row=4, values_only=True):\n    print(row)\n",[32,1864,1865,1875,1885,1913,1917,1929,1935,1940,1950,1966,1970,1985,2015,2029,2051,2088,2104,2118,2128,2167],{"__ignoreMap":30},[35,1866,1867,1869,1872],{"class":37,"line":38},[35,1868,759],{"class":300},[35,1870,1871],{"class":762}," label",[35,1873,1874],{"class":304},"(c):\n",[35,1876,1877,1880,1882],{"class":37,"line":314},[35,1878,1879],{"class":304},"    sheet, coord ",[35,1881,340],{"class":300},[35,1883,1884],{"class":304}," c\n",[35,1886,1887,1889,1892,1894,1896,1899,1901,1903,1905,1908,1910],{"class":37,"line":327},[35,1888,858],{"class":300},[35,1890,1891],{"class":300}," f",[35,1893,632],{"class":45},[35,1895,869],{"class":651},[35,1897,1898],{"class":304},"sheet",[35,1900,880],{"class":651},[35,1902,679],{"class":45},[35,1904,869],{"class":651},[35,1906,1907],{"class":304},"coord",[35,1909,880],{"class":651},[35,1911,1912],{"class":45},"\"\n",[35,1914,1915],{"class":37,"line":334},[35,1916,331],{"emptyLinePlaceholder":330},[35,1918,1919,1922,1924,1926],{"class":37,"line":352},[35,1920,1921],{"class":304},"cyc ",[35,1923,340],{"class":300},[35,1925,1341],{"class":450},[35,1927,1928],{"class":304},"()\n",[35,1930,1931,1933],{"class":37,"line":363},[35,1932,1777],{"class":300},[35,1934,421],{"class":304},[35,1936,1937],{"class":37,"line":378},[35,1938,1939],{"class":304},"    TopologicalSorter(precedents).prepare()\n",[35,1941,1942,1944,1946,1948],{"class":37,"line":392},[35,1943,1808],{"class":300},[35,1945,1811],{"class":304},[35,1947,1814],{"class":300},[35,1949,1817],{"class":304},[35,1951,1952,1955,1957,1959,1962,1964],{"class":37,"line":406},[35,1953,1954],{"class":304},"    cyc ",[35,1956,340],{"class":300},[35,1958,1341],{"class":450},[35,1960,1961],{"class":304},"(err.args[",[35,1963,1832],{"class":450},[35,1965,1835],{"class":304},[35,1967,1968],{"class":37,"line":424},[35,1969,331],{"emptyLinePlaceholder":330},[35,1971,1972,1975,1977,1980,1983],{"class":37,"line":435},[35,1973,1974],{"class":304},"report ",[35,1976,340],{"class":300},[35,1978,1979],{"class":304}," wb.create_sheet(",[35,1981,1982],{"class":45},"\"Dependencies\"",[35,1984,349],{"class":304},[35,1986,1987,1990,1993,1995,1998,2000,2003,2005,2008,2010,2013],{"class":37,"line":440},[35,1988,1989],{"class":304},"report.append([",[35,1991,1992],{"class":45},"\"Cell\"",[35,1994,790],{"class":304},[35,1996,1997],{"class":45},"\"Formula\"",[35,1999,790],{"class":304},[35,2001,2002],{"class":45},"\"Reads\"",[35,2004,790],{"class":304},[35,2006,2007],{"class":45},"\"Read by\"",[35,2009,790],{"class":304},[35,2011,2012],{"class":45},"\"In cycle\"",[35,2014,1835],{"class":304},[35,2016,2017,2019,2021,2023,2026],{"class":37,"line":462},[35,2018,366],{"class":300},[35,2020,398],{"class":304},[35,2022,372],{"class":300},[35,2024,2025],{"class":450}," sorted",[35,2027,2028],{"class":304},"(formulas):\n",[35,2030,2031,2034,2037,2040,2043,2046,2048],{"class":37,"line":508},[35,2032,2033],{"class":304},"    report.append([label(cell), formulas[cell], ",[35,2035,2036],{"class":45},"\", \"",[35,2038,2039],{"class":304},".join(",[35,2041,2042],{"class":450},"map",[35,2044,2045],{"class":304},"(label, ",[35,2047,1442],{"class":450},[35,2049,2050],{"class":304},"(precedents[cell]))),\n",[35,2052,2053,2056,2058,2060,2062,2064,2067,2070,2073,2075,2077,2080,2083,2086],{"class":37,"line":523},[35,2054,2055],{"class":45},"                   \", \"",[35,2057,2039],{"class":304},[35,2059,2042],{"class":450},[35,2061,2045],{"class":304},[35,2063,1442],{"class":450},[35,2065,2066],{"class":304},"(dependents.get(cell, ())))), ",[35,2068,2069],{"class":45},"\"yes\"",[35,2071,2072],{"class":300}," if",[35,2074,398],{"class":304},[35,2076,372],{"class":300},[35,2078,2079],{"class":304}," cyc ",[35,2081,2082],{"class":300},"else",[35,2084,2085],{"class":45}," \"\"",[35,2087,1835],{"class":304},[35,2089,2090,2093,2096,2099,2101],{"class":37,"line":962},[35,2091,2092],{"class":304},"report.column_dimensions[",[35,2094,2095],{"class":45},"\"B\"",[35,2097,2098],{"class":304},"].width ",[35,2100,340],{"class":300},[35,2102,2103],{"class":450}," 26\n",[35,2105,2106,2108,2111,2113,2115],{"class":37,"line":989},[35,2107,2092],{"class":304},[35,2109,2110],{"class":45},"\"C\"",[35,2112,2098],{"class":304},[35,2114,340],{"class":300},[35,2116,2117],{"class":450}," 40\n",[35,2119,2120,2123,2126],{"class":37,"line":1002},[35,2121,2122],{"class":304},"wb.save(",[35,2124,2125],{"class":45},"\"model_audited.xlsx\"",[35,2127,349],{"class":304},[35,2129,2130,2132,2134,2136,2139,2142,2144,2146,2148,2151,2153,2155,2157,2160,2162,2165],{"class":37,"line":1015},[35,2131,366],{"class":300},[35,2133,384],{"class":304},[35,2135,372],{"class":300},[35,2137,2138],{"class":304}," report.iter_rows(",[35,2140,2141],{"class":1690},"min_row",[35,2143,340],{"class":300},[35,2145,1832],{"class":450},[35,2147,790],{"class":304},[35,2149,2150],{"class":1690},"max_row",[35,2152,340],{"class":300},[35,2154,97],{"class":450},[35,2156,790],{"class":304},[35,2158,2159],{"class":1690},"values_only",[35,2161,340],{"class":300},[35,2163,2164],{"class":450},"True",[35,2166,774],{"class":304},[35,2168,2169,2171],{"class":37,"line":1026},[35,2170,511],{"class":450},[35,2172,2173],{"class":304},"(row)\n",[76,2175,85,2181,85,2184,85,2187,85,2192,85,2199,85,2204,85,2210,85,2216,85,2222,85,2225,85,2231,85,2235,85,2239,85,2243,85,2247,85,2251,85,2255,85,2259,85,2263,85,2267,85,2271,85,2273,85,2277,85,2279,85,2282,85,2284,85,2287,85,2289,85,2292,85,2294,85,2297,85,2300,85,2303,85,2305,85,2307,85,2309,85,2312,85,2314,85,2317,85,2319,85,2322,85,2324,85,2327,85,2330,85,2332,85,2334,85,2336,85,2339,85,2341,85,2343,85,2345,85,2347,85,2351,85,2355,85,2358,85,2361,85,2363,85,2365,85,2367,85,2370,85,2372,85,2374,85,2376,85,2378,85,2380,85,2382,85,2387,85,2391,85,2395,85,2398,85,2402,85,2405,85,2409,85,2415],{"viewBox":2176,"role":79,"ariaLabelledBy":2177,"xmlns":83,"style":2180},"67 0 625 222",[2178,2179],"dep-report-t","dep-report-d","width:100%;max-width:625px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[87,2182,2183],{"id":2178},"The Dependencies sheet",[91,2185,2186],{"id":2179},"A worksheet listing each formula cell with its formula, the cells it reads, the cells that read it, and a flag for cells in a circular reference.",[95,2188],{"x":2189,"y":98,"width":2190,"height":2191,"fill":101},"67","625","222",[95,2193],{"x":2194,"y":105,"width":2195,"height":2196,"rx":2197,"fill":109,"stroke":110,"style":2198},"83.8","592.5","30.0","8","stroke-width:1.5px",[95,2200],{"x":2194,"y":2201,"width":2195,"height":2202,"rx":98,"fill":109,"stroke":110,"style":2203},"38.0","8.0","stroke-width:0px",[113,2205,2207],{"transform":2206},"translate(93.8,23.0) scale(0.667)",[117,2208],{"d":119,"fill":120,"stroke":217,"style":2209},"stroke-width:2.70px;stroke-linecap:round;stroke-linejoin:round",[124,2211,2215],{"x":2212,"y":2213,"style":2214},"115.8","36.0","font-size:12px;font-weight:700;fill:var(--text,#172033);text-anchor:start","model_audited.xlsx",[95,2217],{"x":2194,"y":2218,"width":2219,"height":2220,"rx":98,"fill":109,"stroke":110,"style":2221},"46.0","32.0","24.0","stroke-width:1px",[95,2223],{"x":2212,"y":2218,"width":2224,"height":2220,"rx":98,"fill":109,"stroke":110,"style":2221},"68.2",[124,2226,2230],{"x":2227,"y":2228,"style":2229},"149.9","63.0","font-size:11px;font-weight:600;fill:var(--muted,#5b6780);text-anchor:middle","A",[95,2232],{"x":2233,"y":2218,"width":2234,"height":2220,"rx":98,"fill":109,"stroke":110,"style":2221},"184.0","147.4",[124,2236,2238],{"x":2237,"y":2228,"style":2229},"257.7","B",[95,2240],{"x":2241,"y":2218,"width":2242,"height":2220,"rx":98,"fill":109,"stroke":110,"style":2221},"331.4","139.7",[124,2244,2246],{"x":2245,"y":2228,"style":2229},"401.2","C",[95,2248],{"x":2249,"y":2218,"width":2250,"height":2220,"rx":98,"fill":109,"stroke":110,"style":2221},"471.0","137.5",[124,2252,2254],{"x":2253,"y":2228,"style":2229},"539.8","D",[95,2256],{"x":2257,"y":2218,"width":2258,"height":2220,"rx":98,"fill":109,"stroke":110,"style":2221},"608.5","67.7",[124,2260,2262],{"x":2261,"y":2228,"style":2229},"642.4","E",[95,2264],{"x":2194,"y":2265,"width":2219,"height":2266,"rx":98,"fill":109,"stroke":110,"style":2221},"70.0","28.0",[124,2268,1832],{"x":2269,"y":2270,"style":2229},"99.8","89.0",[95,2272],{"x":2212,"y":2265,"width":2224,"height":2266,"rx":98,"fill":153,"stroke":144,"style":2198},[124,2274,2276],{"x":2227,"y":2270,"style":2275},"font-size:11px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Cell",[95,2278],{"x":2233,"y":2265,"width":2234,"height":2266,"rx":98,"fill":153,"stroke":144,"style":2198},[124,2280,2281],{"x":2237,"y":2270,"style":2275},"Formula",[95,2283],{"x":2241,"y":2265,"width":2242,"height":2266,"rx":98,"fill":153,"stroke":144,"style":2198},[124,2285,2286],{"x":2245,"y":2270,"style":2275},"Reads",[95,2288],{"x":2249,"y":2265,"width":2250,"height":2266,"rx":98,"fill":153,"stroke":144,"style":2198},[124,2290,2291],{"x":2253,"y":2270,"style":2275},"Read by",[95,2293],{"x":2257,"y":2265,"width":2258,"height":2266,"rx":98,"fill":153,"stroke":144,"style":2198},[124,2295,2296],{"x":2261,"y":2270,"style":2275},"In cycle",[95,2298],{"x":2194,"y":2299,"width":2219,"height":2266,"rx":98,"fill":109,"stroke":110,"style":2221},"98.0",[124,2301,1696],{"x":2269,"y":2302,"style":2229},"117.0",[95,2304],{"x":2212,"y":2299,"width":2224,"height":2266,"rx":98,"fill":101,"stroke":110,"style":2221},[124,2306,571],{"x":2227,"y":2302,"style":1565},[95,2308],{"x":2233,"y":2299,"width":2234,"height":2266,"rx":98,"fill":101,"stroke":110,"style":2221},[124,2310,2311],{"x":2237,"y":2302,"style":1565},"=Inputs!B2*Inputs!B3",[95,2313],{"x":2241,"y":2299,"width":2242,"height":2266,"rx":98,"fill":101,"stroke":110,"style":2221},[124,2315,2316],{"x":2245,"y":2302,"style":1565},"Inputs!B2, Inputs!B3",[95,2318],{"x":2249,"y":2299,"width":2250,"height":2266,"rx":98,"fill":101,"stroke":110,"style":2221},[124,2320,2321],{"x":2253,"y":2302,"style":1565},"Calc!B3, Calc!B4, …",[95,2323],{"x":2257,"y":2299,"width":2258,"height":2266,"rx":98,"fill":101,"stroke":110,"style":2221},[95,2325],{"x":2194,"y":2326,"width":2219,"height":2266,"rx":98,"fill":109,"stroke":110,"style":2221},"126.0",[124,2328,2329],{"x":2269,"y":1558,"style":2229},"3",[95,2331],{"x":2212,"y":2326,"width":2224,"height":2266,"rx":98,"fill":101,"stroke":110,"style":2221},[124,2333,1848],{"x":2227,"y":1558,"style":1565},[95,2335],{"x":2233,"y":2326,"width":2234,"height":2266,"rx":98,"fill":101,"stroke":110,"style":2221},[124,2337,2338],{"x":2237,"y":1558,"style":1565},"=B8",[95,2340],{"x":2241,"y":2326,"width":2242,"height":2266,"rx":98,"fill":101,"stroke":110,"style":2221},[124,2342,1851],{"x":2245,"y":1558,"style":1565},[95,2344],{"x":2249,"y":2326,"width":2250,"height":2266,"rx":98,"fill":101,"stroke":110,"style":2221},[124,2346,1851],{"x":2253,"y":1558,"style":1565},[95,2348],{"x":2257,"y":2326,"width":2258,"height":2266,"rx":98,"fill":2349,"stroke":2350,"style":2198},"#fce9e9","var(--danger,#dc2626)",[124,2352,2354],{"x":2261,"y":1558,"style":2353},"font-size:11px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","yes",[95,2356],{"x":2194,"y":2357,"width":2219,"height":2266,"rx":98,"fill":109,"stroke":110,"style":2221},"154.0",[124,2359,97],{"x":2269,"y":2360,"style":2229},"173.0",[95,2362],{"x":2212,"y":2357,"width":2224,"height":2266,"rx":98,"fill":101,"stroke":110,"style":2221},[124,2364,1851],{"x":2227,"y":2360,"style":1565},[95,2366],{"x":2233,"y":2357,"width":2234,"height":2266,"rx":98,"fill":101,"stroke":110,"style":2221},[124,2368,2369],{"x":2237,"y":2360,"style":1565},"=B7+10",[95,2371],{"x":2241,"y":2357,"width":2242,"height":2266,"rx":98,"fill":101,"stroke":110,"style":2221},[124,2373,1848],{"x":2245,"y":2360,"style":1565},[95,2375],{"x":2249,"y":2357,"width":2250,"height":2266,"rx":98,"fill":101,"stroke":110,"style":2221},[124,2377,1848],{"x":2253,"y":2360,"style":1565},[95,2379],{"x":2257,"y":2357,"width":2258,"height":2266,"rx":98,"fill":2349,"stroke":2350,"style":2198},[124,2381,2354],{"x":2261,"y":2360,"style":2353},[95,2383],{"x":2384,"y":2385,"width":2386,"height":2220,"rx":98,"fill":109,"stroke":110,"style":2221},"91.8","182.0","59.2",[124,2388,58],{"x":2389,"y":1544,"style":2390},"121.4","font-size:11.5px;font-weight:400;fill:var(--muted,#5b6780);text-anchor:middle",[95,2392],{"x":2393,"y":2385,"width":2394,"height":2220,"rx":98,"fill":109,"stroke":110,"style":2221},"153.0","49.4",[124,2396,66],{"x":2397,"y":1544,"style":2390},"177.7",[95,2399],{"x":2400,"y":2385,"width":2401,"height":2220,"rx":98,"fill":109,"stroke":110,"style":2221},"204.4","78.2",[124,2403,70],{"x":2404,"y":1544,"style":2390},"243.5",[95,2406],{"x":2407,"y":2385,"width":2408,"height":2220,"rx":98,"fill":101,"stroke":110,"style":2221},"284.6","108.9",[37,2410],{"x1":2411,"y1":2412,"x2":2413,"y2":2412,"stroke":210,"style":2414},"286.6","205.0","391.5","stroke-width:3px",[124,2416,2419],{"x":2417,"y":1544,"style":2418},"339.0","font-size:11.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","Dependencies",[20,2421,2423],{"id":2422},"common-pitfalls","Common pitfalls",[2425,2426,2427,2438,2447,2460,2470],"ul",{},[2428,2429,2430,2437],"li",{},[2431,2432,2433,2434,18],"strong",{},"Loading with ",[32,2435,2436],{},"data_only=True"," You get cached values, not formulas; load normally for this job.",[2428,2439,2440,2443,2444,2446],{},[2431,2441,2442],{},"Matching references with a regular expression on the whole formula."," Text inside quotes and sheet names with ",[32,2445,679],{}," in them produce false references. Tokenize first.",[2428,2448,2449,2452,2453,2456,2457,18],{},[2431,2450,2451],{},"Forgetting names and tables."," Defined names resolve through ",[32,2454,2455],{},"wb.defined_names","; table references need the table's range from ",[32,2458,2459],{},"ws.tables",[2428,2461,2462,2465,2466,2469],{},[2431,2463,2464],{},"Huge ranges."," Expanding ",[32,2467,2468],{},"A1:XFD1048576"," is not an option; cap the expansion and keep large ranges as single nodes.",[2428,2471,2472,2475,2476,1502,2479,2482],{},[2431,2473,2474],{},"Volatile and indirect references."," ",[32,2477,2478],{},"INDIRECT",[32,2480,2481],{},"OFFSET"," build references at calculation time. The graph cannot see through them; list every formula that uses them as a separate audit finding.",[20,2484,2486],{"id":2485},"performance-notes","Performance notes",[10,2488,2489,2490,2493],{},"Tokenizing is fast — tens of thousands of formulas per second — and the graph is a dictionary of sets. The cost is in expanded ranges: a model full of ",[32,2491,2492],{},"SUMIFS"," over 50,000-row columns multiplies the number of edges. Keep large ranges as single range nodes and treat \"reads range X\" as the dependency; for audit purposes that is usually the question being asked anyway.",[20,2495,2497],{"id":2496},"conclusion","Conclusion",[10,2499,2500,2501,2503,2504,2506,2507,2510],{},"Tokenize every formula with openpyxl, keep the ",[32,2502,287],{}," operands, resolve them to sheet-qualified cells — bare references to the formula's own sheet, names through ",[32,2505,2455],{},", ranges expanded up to a limit — and store them as a precedents dictionary. Walk it forwards and backwards to trace inputs and impact, use ",[32,2508,2509],{},"graphlib.TopologicalSorter"," for the calculation order and to name circular references, compare constants with referenced cells to find unused inputs, and write the result to a sheet reviewers can filter.",[20,2512,2514],{"id":2513},"frequently-asked-questions","Frequently asked questions",[10,2516,2517,2520],{},[2431,2518,2519],{},"How do I find which cells a formula depends on in Python?","\nLoad the workbook with openpyxl, split the formula with openpyxl.formula.Tokenizer and keep OPERAND tokens with subtype RANGE. Resolve each to a sheet and cell — bare references belong to the formula's own sheet, names are looked up in wb.defined_names — and repeat recursively for indirect precedents.",[10,2522,2523,2526],{},[2431,2524,2525],{},"Can Python find circular references in an Excel workbook?","\nYes. Build a dictionary from each formula cell to the cells it reads and pass it to graphlib.TopologicalSorter. If there is a loop, it raises CycleError, and the error's second argument lists the cells in the cycle.",[10,2528,2529,2532],{},[2431,2530,2531],{},"How do I find inputs that no formula uses?","\nCollect every cell referenced by any formula, then list the constant cells on your input sheets that are not in that set. They are either unused assumptions or a sign that a formula points at the wrong cell.",[10,2534,2535,2538],{},[2431,2536,2537],{},"Does this work with INDIRECT and OFFSET?","\nNot fully. Those functions build references while Excel calculates, so the formula text does not say which cells they read. Flag formulas that use them and review them by hand.",[20,2540,2542],{"id":2541},"related","Related",[2425,2544,2545,2551,2558,2568],{},[2428,2546,2547,2548,2550],{},"Up one level: ",[14,2549,17],{"href":16}," — writing, reading and checking formulas.",[2428,2552,2553,2557],{},[14,2554,2556],{"href":2555},"\u002Fgetting-started-with-python-excel-automation\u002Fworking-with-excel-formulas-in-python\u002Faudit-every-formula-in-a-workbook-with-python\u002F","Audit Every Formula in a Workbook with Python"," — an inventory of formulas, functions and hard-coded numbers.",[2428,2559,2560,2564,2565,2567],{},[14,2561,2563],{"href":2562},"\u002Fgetting-started-with-python-excel-automation\u002Fworking-with-excel-formulas-in-python\u002Ffind-external-links-and-broken-references-in-a-workbook\u002F","Find External Links and Broken References in a Workbook"," — the ",[32,2566,684],{}," nodes in detail.",[2428,2569,2570,2574],{},[14,2571,2573],{"href":2572},"\u002Fgetting-started-with-python-excel-automation\u002Fworking-with-excel-formulas-in-python\u002Frun-what-if-scenarios-on-an-excel-model-with-python\u002F","Run What-If Scenarios on an Excel Model with Python"," — changing the inputs this map identifies.",[2576,2577,2578],"style",{},"html pre.shiki code .sMTad, html code.shiki .sMTad{--shiki-default:#6F42C1;--shiki-dark:#FFB757}html pre.shiki code .srMev, html code.shiki .srMev{--shiki-default:#032F62;--shiki-dark:#ADDCFF}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 .s-kum, html code.shiki .s-kum{--shiki-default:#D73A49;--shiki-dark:#FF9492}html pre.shiki code .skGVy, html code.shiki .skGVy{--shiki-default:#24292E;--shiki-dark:#F0F3F6}html pre.shiki code .sP0c6, html code.shiki .sP0c6{--shiki-default:#005CC5;--shiki-dark:#91CBFF}html pre.shiki code .sx1sP, html code.shiki .sx1sP{--shiki-default:#22863A;--shiki-dark:#72F088}html pre.shiki code .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}html pre.shiki code .s_b0D, html code.shiki .s_b0D{--shiki-default:#22863A;--shiki-default-font-weight:bold;--shiki-dark:#72F088;--shiki-dark-font-weight:bold}html pre.shiki code .s_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}html pre.shiki code .s-wDw, html code.shiki .s-wDw{--shiki-default:#6A737D;--shiki-dark:#BDC4CC}html pre.shiki code .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}",{"title":30,"searchDepth":314,"depth":314,"links":2580},[2581,2582,2583,2584,2585,2586,2587,2588,2589,2590,2591],{"id":22,"depth":314,"text":23},{"id":268,"depth":314,"text":269},{"id":557,"depth":314,"text":558},{"id":1161,"depth":314,"text":1162},{"id":1617,"depth":314,"text":1618},{"id":1855,"depth":314,"text":1856},{"id":2422,"depth":314,"text":2423},{"id":2485,"depth":314,"text":2486},{"id":2496,"depth":314,"text":2497},{"id":2513,"depth":314,"text":2514},{"id":2541,"depth":314,"text":2542},"2026-09-29","Trace precedents and dependents across every sheet of a workbook without Excel: parse formulas with openpyxl's Tokenizer, resolve cross-sheet references and named ranges, build a dependency graph, list what feeds a result and what a changed input affects, find unused inputs and circular references, and write the map to a sheet.","md",[2596,2598,2600,2602],{"q":2519,"a":2597},"Load the workbook with openpyxl, split the formula with openpyxl.formula.Tokenizer and keep OPERAND tokens with subtype RANGE. Resolve each to a sheet and cell — bare references belong to the formula's own sheet, names are looked up in wb.defined_names — and repeat recursively for indirect precedents.",{"q":2525,"a":2599},"Yes. Build a dictionary from each formula cell to the cells it reads and pass it to graphlib.TopologicalSorter. If there is a loop, it raises CycleError, and the error's second argument lists the cells in the cycle.",{"q":2531,"a":2601},"Collect every cell referenced by any formula, then list the constant cells on your input sheets that are not in that set. They are either unused assumptions or a sign that a formula points at the wrong cell.",{"q":2537,"a":2603},"Not fully. Those functions build references while Excel calculates, so the formula text does not say which cells they read. Flag formulas that use them and review them by hand.","map formula dependencies in a workbook with python",null,{"breadcrumb":2607},[2608,2611,2614],{"name":2609,"item":2610},"Home","\u002F",{"name":2612,"item":2613},"Getting Started with Python Excel Automation","\u002Fgetting-started-with-python-excel-automation\u002F",{"name":17,"item":16},"\u002Fgetting-started-with-python-excel-automation\u002Fworking-with-excel-formulas-in-python\u002Fmap-formula-dependencies-in-a-workbook-with-python",{"title":2617,"description":2618},"Trace Excel Formula Precedents and Dependents in Python","Build an Excel formula dependency graph with openpyxl Tokenizer and graphlib: cross-sheet refs, named ranges, ranges expanded to cells, precedents and dependents, calculation order, circular reference detection and unused inputs.","map-formula-dependencies-in-a-workbook-with-python","getting-started-with-python-excel-automation\u002Fworking-with-excel-formulas-in-python\u002Fmap-formula-dependencies-in-a-workbook-with-python\u002Findex",[2622,169],"excel-formulas","how-to","WND6v9ASxM_MGgIGWyA4iHKJpnuilp0noVhswTUZMLk",[2626,2630],{"title":2627,"path":2628,"stem":2629,"children":-1},"Handle Circular References and Iterative Calculation in Python","\u002Fgetting-started-with-python-excel-automation\u002Fworking-with-excel-formulas-in-python\u002Fhandle-circular-references-and-iterative-calculation-in-python","getting-started-with-python-excel-automation\u002Fworking-with-excel-formulas-in-python\u002Fhandle-circular-references-and-iterative-calculation-in-python\u002Findex",{"title":2631,"path":2632,"stem":2633,"children":-1},"Read Formula Results with openpyxl data_only","\u002Fgetting-started-with-python-excel-automation\u002Fworking-with-excel-formulas-in-python\u002Fread-formula-results-with-openpyxl-data-only","getting-started-with-python-excel-automation\u002Fworking-with-excel-formulas-in-python\u002Fread-formula-results-with-openpyxl-data-only\u002Findex",[2635,2637],{"path":2636,"title":2612},"\u002Fgetting-started-with-python-excel-automation",{"path":2638,"title":17},"\u002Fgetting-started-with-python-excel-automation\u002Fworking-with-excel-formulas-in-python",[],[2641,2644,2648,2651,2654,2657],{"path":2642,"title":2643,"kind":2623},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fbuilding-excel-reports-with-xlsxwriter\u002Fstore-formula-results-with-xlsxwriter","Store Formula Results with xlsxwriter So Every Viewer Shows Values",{"path":2645,"title":2646,"kind":2647},"\u002Fadvanced-data-transformation-and-cleaning\u002Fapplying-conditional-formatting-with-openpyxl\u002Fhighlight-cells-with-formula-errors-using-conditional-formatting","Highlight Cells with Formula Errors Using Conditional Formatting","fix",{"path":2649,"title":2650,"kind":2623},"\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Fgoal-seek-in-python-instead-of-excel","Goal Seek in Python Instead of Excel",{"path":2652,"title":2653,"kind":2623},"\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fcheck-that-totals-in-a-report-add-up","Check That Totals in a Report Add Up",{"path":2655,"title":2656,"kind":2623},"\u002Fautomating-reporting-workflows\u002Fautomating-excel-online-with-microsoft-graph\u002Fcall-excel-worksheet-functions-through-microsoft-graph","Call Excel Worksheet Functions Through Microsoft Graph",{"path":2658,"title":2659,"kind":2623},"\u002Fautomating-reporting-workflows\u002Fautomating-excel-online-with-microsoft-graph\u002Frecalculate-excel-online-workbooks-with-graph-sessions","Recalculate Excel Online Workbooks with Graph Sessions",1790972075721]