[{"data":1,"prerenderedAt":2476},["ShallowReactive",2],{"doc:\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Fprofile-a-slow-excel-report-script":3,"surround:\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Fprofile-a-slow-excel-report-script":2442,"ancestors:\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Fprofile-a-slow-excel-report-script":2451,"children:\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Fprofile-a-slow-excel-report-script":2456,"related:\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Fprofile-a-slow-excel-report-script":2457},{"id":4,"title":5,"body":6,"dateModified":2408,"datePublished":2408,"description":2409,"extension":2410,"faq":2411,"keyword":2420,"kind":2421,"meta":2422,"navigation":288,"path":2430,"seo":2431,"slug":2434,"stem":2435,"topics":2436,"type":2440,"__hash__":2441},"docs\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Fprofile-a-slow-excel-report-script\u002Findex.md","Profile a Slow Excel Report Script in Python",{"type":7,"value":8,"toc":2395},"minimark",[9,19,152,157,191,197,201,204,893,896,900,915,1176,1234,1244,1248,1255,1662,1670,1753,1757,1760,2119,2127,2257,2261,2264,2268,2303,2307,2314,2318,2330,2334,2340,2346,2352,2358,2362,2391],[10,11,12,13,18],"p",{},"A report that took ten seconds last year takes four minutes now, and the instinct is to start optimising whatever looks slow. That usually means rewriting the wrong part. A few minutes of measuring tells you where the time actually goes — and in Excel scripts the answer is often surprising: not reading the data, not the pandas transformation, but a loop that sets a font on every cell. This page, part of ",[14,15,17],"a",{"href":16},"\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002F","Testing and Packaging Excel Automation Scripts",", measures a typical slow report stage by stage, profiles the worst stage, and fixes it with before-and-after numbers.",[20,21,29,30,29,34,29,38,29,46,29,56,29,67,29,74,29,79,29,85,29,90,29,93,29,99,29,103,29,106,29,110,29,113,29,116,29,122,29,126,29,129,29,133,29,136,29,139,29,145,29,149],"svg",{"viewBox":22,"role":23,"ariaLabelledBy":24,"xmlns":27,"style":28},"4 0 752 125","img",[25,26],"prof-flow-t","prof-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  ",[31,32,33],"title",{"id":25},"Measure, then fix the biggest thing, then measure again",[35,36,37],"desc",{"id":26},"Four steps in a loop. Time each stage of the report. Profile the slowest stage with cProfile. Fix the top cost. Time the stages again to confirm the gain, and repeat while it is still too slow.",[39,40],"rect",{"x":41,"y":42,"width":43,"height":44,"fill":45},"4","0","752","125","#ffffff",[39,47],{"x":48,"y":49,"width":50,"height":51,"rx":52,"fill":53,"stroke":54,"style":55},"20.0","16.0","153.0","93.0","12","#f0f4ff","var(--brand,#5b5cf0)","stroke-width:2px",[57,58,60],"g",{"transform":59},"translate(84.5,29.0) scale(1.000)",[61,62],"path",{"d":63,"fill":64,"stroke":65,"style":66},"M12 3a9 9 0 1 0 .01 0z M12 7v5l3.5 2","none","var(--brand-strong,#4338ca)","stroke-width:1.80px;stroke-linecap:round;stroke-linejoin:round",[68,69,73],"text",{"x":70,"y":71,"style":72},"96.5","75.5","font-size:13.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Time stages",[68,75,78],{"x":70,"y":76,"style":77},"95.0","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","perf_counter",[80,81],"line",{"x1":82,"y1":83,"x2":84,"y2":83,"stroke":54,"style":55},"178.0","62.5","200.0",[86,87],"polygon",{"points":88,"fill":89},"204.0,62.5 195.0,67.5 195.0,57.5","#5b5cf0",[39,91],{"x":92,"y":49,"width":50,"height":51,"rx":52,"fill":53,"stroke":54,"style":55},"209.0",[57,94,96],{"transform":95},"translate(273.5,29.0) scale(1.000)",[61,97],{"d":98,"fill":64,"stroke":65,"style":66},"M11 4a7 7 0 1 0 .01 0z M16 16l5 5",[68,100,102],{"x":101,"y":71,"style":72},"285.5","Profile the worst",[68,104,105],{"x":101,"y":76,"style":77},"cProfile",[80,107],{"x1":108,"y1":83,"x2":109,"y2":83,"stroke":54,"style":55},"367.0","389.0",[86,111],{"points":112,"fill":89},"393.0,62.5 384.0,67.5 384.0,57.5",[39,114],{"x":115,"y":49,"width":50,"height":51,"rx":52,"fill":53,"stroke":54,"style":55},"398.0",[57,117,119],{"transform":118},"translate(462.5,29.0) scale(1.000)",[61,120],{"d":121,"fill":64,"stroke":65,"style":66},"M12 9a3 3 0 1 0 .01 0z M12 2.5v3 M12 18.5v3 M2.5 12h3 M18.5 12h3 M5.3 5.3l2.1 2.1 M16.6 16.6l2.1 2.1 M5.3 18.7l2.1-2.1 M16.6 7.4l2.1-2.1",[68,123,125],{"x":124,"y":71,"style":72},"474.5","Fix the top cost",[68,127,128],{"x":124,"y":76,"style":77},"one change",[80,130],{"x1":131,"y1":83,"x2":132,"y2":83,"stroke":54,"style":55},"556.0","578.0",[86,134],{"points":135,"fill":89},"582.0,62.5 573.0,67.5 573.0,57.5",[39,137],{"x":138,"y":49,"width":50,"height":51,"rx":52,"fill":53,"stroke":54,"style":55},"587.0",[57,140,142],{"transform":141},"translate(651.5,29.0) scale(1.000)",[61,143],{"d":144,"fill":64,"stroke":65,"style":66},"M4 20h16 M7 17v-5 M12 17V7 M17 17v-8",[68,146,148],{"x":147,"y":71,"style":72},"663.5","Measure again",[68,150,151],{"x":147,"y":76,"style":77},"confirm the gain",[153,154,156],"h2",{"id":155},"prerequisites","Prerequisites",[158,159,164],"pre",{"className":160,"code":161,"language":162,"meta":163,"style":163},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install pandas openpyxl xlsxwriter python-calamine\n","bash","",[165,166,167],"code",{"__ignoreMap":163},[168,169,171,175,179,182,185,188],"span",{"class":80,"line":170},1,[168,172,174],{"class":173},"sMTad","pip",[168,176,178],{"class":177},"srMev"," install",[168,180,181],{"class":177}," pandas",[168,183,184],{"class":177}," openpyxl",[168,186,187],{"class":177}," xlsxwriter",[168,189,190],{"class":177}," python-calamine\n",[10,192,193,196],{},[165,194,195],{},"orders.xlsx"," has 50,000 order lines. The report reads it, adds a revenue column, writes every row to a new workbook with openpyxl, sets a font and alignment on each cell, sizes the columns and saves. Timings below are from one laptop; yours will differ, but the proportions are what matter.",[153,198,200],{"id":199},"step-1-time-each-stage","Step 1 — Time each stage",[10,202,203],{},"Before any profiler, wrap each stage of the script in a timer. A context manager keeps it to one line per stage and collects the results:",[158,205,209],{"className":206,"code":207,"language":208,"meta":163,"style":163},"language-python shiki shiki-themes github-light github-dark-high-contrast","import contextlib\nimport time\nimport pandas as pd\nfrom openpyxl import Workbook\nfrom openpyxl.styles import Alignment, Font\nfrom openpyxl.utils import get_column_letter\n\ntimings = {}\n\n@contextlib.contextmanager\ndef stage(name):\n    start = time.perf_counter()\n    yield\n    timings[name] = time.perf_counter() - start\n\ndef slow_report(out=\"slow.xlsx\"):\n    with stage(\"read\"):\n        df = pd.read_excel(\"orders.xlsx\", engine=\"openpyxl\")\n    with stage(\"transform\"):\n        df[\"Revenue\"] = df[\"Qty\"] * df[\"Price\"]\n    wb = Workbook(); ws = wb.active\n    with stage(\"write cells\"):\n        ws.append(list(df.columns))\n        for row in df.itertuples(index=False):\n            ws.append(list(row))\n    with stage(\"style cells\"):\n        for row in ws.iter_rows(min_row=2):\n            for cell in row:\n                cell.font = Font(name=\"Calibri\", size=10)\n                cell.alignment = Alignment(horizontal=\"left\")\n    with stage(\"column widths\"):\n        for col in range(1, ws.max_column + 1):\n            letter = get_column_letter(col)\n            ws.column_dimensions[letter].width = max(len(str(c.value)) for c in ws[letter]) + 2\n    with stage(\"save\"):\n        wb.save(out)\n\nslow_report()\ntotal = sum(timings.values())\nfor name, t in timings.items():\n    print(f\"{name:14} {t:6.2f} s  {t \u002F total:5.0%}\")\nprint(f\"{'total':14} {total:6.2f} s\")\n","python",[165,210,211,221,229,243,257,270,283,290,302,307,314,326,337,343,360,365,384,398,427,439,472,488,500,513,538,549,561,583,597,628,649,661,691,702,742,754,760,765,771,785,798,858],{"__ignoreMap":163},[168,212,213,217],{"class":80,"line":170},[168,214,216],{"class":215},"s-kum","import",[168,218,220],{"class":219},"skGVy"," contextlib\n",[168,222,224,226],{"class":80,"line":223},2,[168,225,216],{"class":215},[168,227,228],{"class":219}," time\n",[168,230,232,234,237,240],{"class":80,"line":231},3,[168,233,216],{"class":215},[168,235,236],{"class":219}," pandas ",[168,238,239],{"class":215},"as",[168,241,242],{"class":219}," pd\n",[168,244,246,249,252,254],{"class":80,"line":245},4,[168,247,248],{"class":215},"from",[168,250,251],{"class":219}," openpyxl ",[168,253,216],{"class":215},[168,255,256],{"class":219}," Workbook\n",[168,258,260,262,265,267],{"class":80,"line":259},5,[168,261,248],{"class":215},[168,263,264],{"class":219}," openpyxl.styles ",[168,266,216],{"class":215},[168,268,269],{"class":219}," Alignment, Font\n",[168,271,273,275,278,280],{"class":80,"line":272},6,[168,274,248],{"class":215},[168,276,277],{"class":219}," openpyxl.utils ",[168,279,216],{"class":215},[168,281,282],{"class":219}," get_column_letter\n",[168,284,286],{"class":80,"line":285},7,[168,287,289],{"emptyLinePlaceholder":288},true,"\n",[168,291,293,296,299],{"class":80,"line":292},8,[168,294,295],{"class":219},"timings ",[168,297,298],{"class":215},"=",[168,300,301],{"class":219}," {}\n",[168,303,305],{"class":80,"line":304},9,[168,306,289],{"emptyLinePlaceholder":288},[168,308,310],{"class":80,"line":309},10,[168,311,313],{"class":312},"s_Opv","@contextlib.contextmanager\n",[168,315,317,320,323],{"class":80,"line":316},11,[168,318,319],{"class":215},"def",[168,321,322],{"class":312}," stage",[168,324,325],{"class":219},"(name):\n",[168,327,329,332,334],{"class":80,"line":328},12,[168,330,331],{"class":219},"    start ",[168,333,298],{"class":215},[168,335,336],{"class":219}," time.perf_counter()\n",[168,338,340],{"class":80,"line":339},13,[168,341,342],{"class":215},"    yield\n",[168,344,346,349,351,354,357],{"class":80,"line":345},14,[168,347,348],{"class":219},"    timings[name] ",[168,350,298],{"class":215},[168,352,353],{"class":219}," time.perf_counter() ",[168,355,356],{"class":215},"-",[168,358,359],{"class":219}," start\n",[168,361,363],{"class":80,"line":362},15,[168,364,289],{"emptyLinePlaceholder":288},[168,366,368,370,373,376,378,381],{"class":80,"line":367},16,[168,369,319],{"class":215},[168,371,372],{"class":312}," slow_report",[168,374,375],{"class":219},"(out",[168,377,298],{"class":215},[168,379,380],{"class":177},"\"slow.xlsx\"",[168,382,383],{"class":219},"):\n",[168,385,387,390,393,396],{"class":80,"line":386},17,[168,388,389],{"class":215},"    with",[168,391,392],{"class":219}," stage(",[168,394,395],{"class":177},"\"read\"",[168,397,383],{"class":219},[168,399,401,404,406,409,412,415,419,421,424],{"class":80,"line":400},18,[168,402,403],{"class":219},"        df ",[168,405,298],{"class":215},[168,407,408],{"class":219}," pd.read_excel(",[168,410,411],{"class":177},"\"orders.xlsx\"",[168,413,414],{"class":219},", ",[168,416,418],{"class":417},"sa561","engine",[168,420,298],{"class":215},[168,422,423],{"class":177},"\"openpyxl\"",[168,425,426],{"class":219},")\n",[168,428,430,432,434,437],{"class":80,"line":429},19,[168,431,389],{"class":215},[168,433,392],{"class":219},[168,435,436],{"class":177},"\"transform\"",[168,438,383],{"class":219},[168,440,442,445,448,451,453,456,459,461,464,466,469],{"class":80,"line":441},20,[168,443,444],{"class":219},"        df[",[168,446,447],{"class":177},"\"Revenue\"",[168,449,450],{"class":219},"] ",[168,452,298],{"class":215},[168,454,455],{"class":219}," df[",[168,457,458],{"class":177},"\"Qty\"",[168,460,450],{"class":219},[168,462,463],{"class":215},"*",[168,465,455],{"class":219},[168,467,468],{"class":177},"\"Price\"",[168,470,471],{"class":219},"]\n",[168,473,475,478,480,483,485],{"class":80,"line":474},21,[168,476,477],{"class":219},"    wb ",[168,479,298],{"class":215},[168,481,482],{"class":219}," Workbook(); ws ",[168,484,298],{"class":215},[168,486,487],{"class":219}," wb.active\n",[168,489,491,493,495,498],{"class":80,"line":490},22,[168,492,389],{"class":215},[168,494,392],{"class":219},[168,496,497],{"class":177},"\"write cells\"",[168,499,383],{"class":219},[168,501,503,506,510],{"class":80,"line":502},23,[168,504,505],{"class":219},"        ws.append(",[168,507,509],{"class":508},"sP0c6","list",[168,511,512],{"class":219},"(df.columns))\n",[168,514,516,519,522,525,528,531,533,536],{"class":80,"line":515},24,[168,517,518],{"class":215},"        for",[168,520,521],{"class":219}," row ",[168,523,524],{"class":215},"in",[168,526,527],{"class":219}," df.itertuples(",[168,529,530],{"class":417},"index",[168,532,298],{"class":215},[168,534,535],{"class":508},"False",[168,537,383],{"class":219},[168,539,541,544,546],{"class":80,"line":540},25,[168,542,543],{"class":219},"            ws.append(",[168,545,509],{"class":508},[168,547,548],{"class":219},"(row))\n",[168,550,552,554,556,559],{"class":80,"line":551},26,[168,553,389],{"class":215},[168,555,392],{"class":219},[168,557,558],{"class":177},"\"style cells\"",[168,560,383],{"class":219},[168,562,564,566,568,570,573,576,578,581],{"class":80,"line":563},27,[168,565,518],{"class":215},[168,567,521],{"class":219},[168,569,524],{"class":215},[168,571,572],{"class":219}," ws.iter_rows(",[168,574,575],{"class":417},"min_row",[168,577,298],{"class":215},[168,579,580],{"class":508},"2",[168,582,383],{"class":219},[168,584,586,589,592,594],{"class":80,"line":585},28,[168,587,588],{"class":215},"            for",[168,590,591],{"class":219}," cell ",[168,593,524],{"class":215},[168,595,596],{"class":219}," row:\n",[168,598,600,603,605,608,611,613,616,618,621,623,626],{"class":80,"line":599},29,[168,601,602],{"class":219},"                cell.font ",[168,604,298],{"class":215},[168,606,607],{"class":219}," Font(",[168,609,610],{"class":417},"name",[168,612,298],{"class":215},[168,614,615],{"class":177},"\"Calibri\"",[168,617,414],{"class":219},[168,619,620],{"class":417},"size",[168,622,298],{"class":215},[168,624,625],{"class":508},"10",[168,627,426],{"class":219},[168,629,631,634,636,639,642,644,647],{"class":80,"line":630},30,[168,632,633],{"class":219},"                cell.alignment ",[168,635,298],{"class":215},[168,637,638],{"class":219}," Alignment(",[168,640,641],{"class":417},"horizontal",[168,643,298],{"class":215},[168,645,646],{"class":177},"\"left\"",[168,648,426],{"class":219},[168,650,652,654,656,659],{"class":80,"line":651},31,[168,653,389],{"class":215},[168,655,392],{"class":219},[168,657,658],{"class":177},"\"column widths\"",[168,660,383],{"class":219},[168,662,664,666,669,671,674,677,680,683,686,689],{"class":80,"line":663},32,[168,665,518],{"class":215},[168,667,668],{"class":219}," col ",[168,670,524],{"class":215},[168,672,673],{"class":508}," range",[168,675,676],{"class":219},"(",[168,678,679],{"class":508},"1",[168,681,682],{"class":219},", ws.max_column ",[168,684,685],{"class":215},"+",[168,687,688],{"class":508}," 1",[168,690,383],{"class":219},[168,692,694,697,699],{"class":80,"line":693},33,[168,695,696],{"class":219},"            letter ",[168,698,298],{"class":215},[168,700,701],{"class":219}," get_column_letter(col)\n",[168,703,705,708,710,713,715,718,720,723,726,729,732,734,737,739],{"class":80,"line":704},34,[168,706,707],{"class":219},"            ws.column_dimensions[letter].width ",[168,709,298],{"class":215},[168,711,712],{"class":508}," max",[168,714,676],{"class":219},[168,716,717],{"class":508},"len",[168,719,676],{"class":219},[168,721,722],{"class":508},"str",[168,724,725],{"class":219},"(c.value)) ",[168,727,728],{"class":215},"for",[168,730,731],{"class":219}," c ",[168,733,524],{"class":215},[168,735,736],{"class":219}," ws[letter]) ",[168,738,685],{"class":215},[168,740,741],{"class":508}," 2\n",[168,743,745,747,749,752],{"class":80,"line":744},35,[168,746,389],{"class":215},[168,748,392],{"class":219},[168,750,751],{"class":177},"\"save\"",[168,753,383],{"class":219},[168,755,757],{"class":80,"line":756},36,[168,758,759],{"class":219},"        wb.save(out)\n",[168,761,763],{"class":80,"line":762},37,[168,764,289],{"emptyLinePlaceholder":288},[168,766,768],{"class":80,"line":767},38,[168,769,770],{"class":219},"slow_report()\n",[168,772,774,777,779,782],{"class":80,"line":773},39,[168,775,776],{"class":219},"total ",[168,778,298],{"class":215},[168,780,781],{"class":508}," sum",[168,783,784],{"class":219},"(timings.values())\n",[168,786,788,790,793,795],{"class":80,"line":787},40,[168,789,728],{"class":215},[168,791,792],{"class":219}," name, t ",[168,794,524],{"class":215},[168,796,797],{"class":219}," timings.items():\n",[168,799,801,804,806,809,812,816,818,821,824,827,830,833,835,838,840,843,846,849,852,854,856],{"class":80,"line":800},41,[168,802,803],{"class":508},"    print",[168,805,676],{"class":219},[168,807,808],{"class":215},"f",[168,810,811],{"class":177},"\"",[168,813,815],{"class":814},"sSjpA","{",[168,817,610],{"class":219},[168,819,820],{"class":215},":14",[168,822,823],{"class":814},"}",[168,825,826],{"class":814}," {",[168,828,829],{"class":219},"t",[168,831,832],{"class":215},":6.2f",[168,834,823],{"class":814},[168,836,837],{"class":177}," s  ",[168,839,815],{"class":814},[168,841,842],{"class":219},"t ",[168,844,845],{"class":215},"\u002F",[168,847,848],{"class":219}," total",[168,850,851],{"class":215},":5.0%",[168,853,823],{"class":814},[168,855,811],{"class":177},[168,857,426],{"class":219},[168,859,861,864,866,868,870,872,875,877,879,881,884,886,888,891],{"class":80,"line":860},42,[168,862,863],{"class":508},"print",[168,865,676],{"class":219},[168,867,808],{"class":215},[168,869,811],{"class":177},[168,871,815],{"class":814},[168,873,874],{"class":177},"'total'",[168,876,820],{"class":215},[168,878,823],{"class":814},[168,880,826],{"class":814},[168,882,883],{"class":219},"total",[168,885,832],{"class":215},[168,887,823],{"class":814},[168,889,890],{"class":177}," s\"",[168,892,426],{"class":219},[10,894,895],{},"One stage dominates: styling the 300,000 cells takes around two thirds of the run, far more than reading, writing and saving combined. The pandas transformation, the part people most often try to optimise, takes no measurable time at all. Stage timers are cheap enough to leave in production code; logging them on every run shows when a report starts to slow down, before anyone complains.",[153,897,899],{"id":898},"step-2-profile-the-slowest-stage","Step 2 — Profile the slowest stage",[10,901,902,903,907,908,910,911,914],{},"The timer says ",[904,905,906],"em",{},"which"," stage; ",[165,909,105],{}," says ",[904,912,913],{},"which calls inside it",". Profile only the styling loop, and sort the statistics by cumulative time — the time spent in a function including everything it calls:",[158,916,918],{"className":206,"code":917,"language":208,"meta":163,"style":163},"import cProfile\nimport io\nimport pstats\n\nwb = Workbook(); ws = wb.active\nfor row in pd.read_excel(\"orders.xlsx\", engine=\"calamine\").head(10000).itertuples(index=False):\n    ws.append(list(row))\n\nprofiler = cProfile.Profile()\nprofiler.enable()\nfor row in ws.iter_rows(min_row=1):\n    for cell in row:\n        cell.font = Font(name=\"Calibri\", size=10)\n        cell.alignment = Alignment(horizontal=\"left\")\nprofiler.disable()\n\nout = io.StringIO()\npstats.Stats(profiler, stream=out).strip_dirs().sort_stats(\"cumulative\").print_stats(6)\nprint(\"\\n\".join(line for line in out.getvalue().splitlines() if line.strip())[:1400])\n",[165,919,920,927,934,941,945,958,996,1005,1009,1019,1024,1042,1053,1078,1095,1100,1104,1114,1138],{"__ignoreMap":163},[168,921,922,924],{"class":80,"line":170},[168,923,216],{"class":215},[168,925,926],{"class":219}," cProfile\n",[168,928,929,931],{"class":80,"line":223},[168,930,216],{"class":215},[168,932,933],{"class":219}," io\n",[168,935,936,938],{"class":80,"line":231},[168,937,216],{"class":215},[168,939,940],{"class":219}," pstats\n",[168,942,943],{"class":80,"line":245},[168,944,289],{"emptyLinePlaceholder":288},[168,946,947,950,952,954,956],{"class":80,"line":259},[168,948,949],{"class":219},"wb ",[168,951,298],{"class":215},[168,953,482],{"class":219},[168,955,298],{"class":215},[168,957,487],{"class":219},[168,959,960,962,964,966,968,970,972,974,976,979,982,985,988,990,992,994],{"class":80,"line":272},[168,961,728],{"class":215},[168,963,521],{"class":219},[168,965,524],{"class":215},[168,967,408],{"class":219},[168,969,411],{"class":177},[168,971,414],{"class":219},[168,973,418],{"class":417},[168,975,298],{"class":215},[168,977,978],{"class":177},"\"calamine\"",[168,980,981],{"class":219},").head(",[168,983,984],{"class":508},"10000",[168,986,987],{"class":219},").itertuples(",[168,989,530],{"class":417},[168,991,298],{"class":215},[168,993,535],{"class":508},[168,995,383],{"class":219},[168,997,998,1001,1003],{"class":80,"line":285},[168,999,1000],{"class":219},"    ws.append(",[168,1002,509],{"class":508},[168,1004,548],{"class":219},[168,1006,1007],{"class":80,"line":292},[168,1008,289],{"emptyLinePlaceholder":288},[168,1010,1011,1014,1016],{"class":80,"line":304},[168,1012,1013],{"class":219},"profiler ",[168,1015,298],{"class":215},[168,1017,1018],{"class":219}," cProfile.Profile()\n",[168,1020,1021],{"class":80,"line":309},[168,1022,1023],{"class":219},"profiler.enable()\n",[168,1025,1026,1028,1030,1032,1034,1036,1038,1040],{"class":80,"line":316},[168,1027,728],{"class":215},[168,1029,521],{"class":219},[168,1031,524],{"class":215},[168,1033,572],{"class":219},[168,1035,575],{"class":417},[168,1037,298],{"class":215},[168,1039,679],{"class":508},[168,1041,383],{"class":219},[168,1043,1044,1047,1049,1051],{"class":80,"line":328},[168,1045,1046],{"class":215},"    for",[168,1048,591],{"class":219},[168,1050,524],{"class":215},[168,1052,596],{"class":219},[168,1054,1055,1058,1060,1062,1064,1066,1068,1070,1072,1074,1076],{"class":80,"line":339},[168,1056,1057],{"class":219},"        cell.font ",[168,1059,298],{"class":215},[168,1061,607],{"class":219},[168,1063,610],{"class":417},[168,1065,298],{"class":215},[168,1067,615],{"class":177},[168,1069,414],{"class":219},[168,1071,620],{"class":417},[168,1073,298],{"class":215},[168,1075,625],{"class":508},[168,1077,426],{"class":219},[168,1079,1080,1083,1085,1087,1089,1091,1093],{"class":80,"line":345},[168,1081,1082],{"class":219},"        cell.alignment ",[168,1084,298],{"class":215},[168,1086,638],{"class":219},[168,1088,641],{"class":417},[168,1090,298],{"class":215},[168,1092,646],{"class":177},[168,1094,426],{"class":219},[168,1096,1097],{"class":80,"line":362},[168,1098,1099],{"class":219},"profiler.disable()\n",[168,1101,1102],{"class":80,"line":367},[168,1103,289],{"emptyLinePlaceholder":288},[168,1105,1106,1109,1111],{"class":80,"line":386},[168,1107,1108],{"class":219},"out ",[168,1110,298],{"class":215},[168,1112,1113],{"class":219}," io.StringIO()\n",[168,1115,1116,1119,1122,1124,1127,1130,1133,1136],{"class":80,"line":400},[168,1117,1118],{"class":219},"pstats.Stats(profiler, ",[168,1120,1121],{"class":417},"stream",[168,1123,298],{"class":215},[168,1125,1126],{"class":219},"out).strip_dirs().sort_stats(",[168,1128,1129],{"class":177},"\"cumulative\"",[168,1131,1132],{"class":219},").print_stats(",[168,1134,1135],{"class":508},"6",[168,1137,426],{"class":219},[168,1139,1140,1142,1144,1146,1149,1151,1154,1156,1159,1161,1164,1167,1170,1173],{"class":80,"line":429},[168,1141,863],{"class":508},[168,1143,676],{"class":219},[168,1145,811],{"class":177},[168,1147,1148],{"class":814},"\\n",[168,1150,811],{"class":177},[168,1152,1153],{"class":219},".join(line ",[168,1155,728],{"class":215},[168,1157,1158],{"class":219}," line ",[168,1160,524],{"class":215},[168,1162,1163],{"class":219}," out.getvalue().splitlines() ",[168,1165,1166],{"class":215},"if",[168,1168,1169],{"class":219}," line.strip())[:",[168,1171,1172],{"class":508},"1400",[168,1174,1175],{"class":219},"])\n",[10,1177,1178,1179,1182,1183,1186,1187,1190,1191,1194,1195,1198,1199,1202,1203,1206,1207,1210,1211,1214,1215,1218,1219,1194,1222,1225,1226,1229,1230,1233],{},"Profiling 10,000 rows is enough to see the pattern and keeps the profile quick. Reading the output: ",[165,1180,1181],{},"ncalls"," is how many times each function ran, ",[165,1184,1185],{},"cumtime"," the total time inside it. The time is split between ",[904,1188,1189],{},"assigning"," styles (",[165,1192,1193],{},"__set__"," in ",[165,1196,1197],{},"styleable.py","), which looks each one up in the workbook's style table — an ",[165,1200,1201],{},"IndexedList"," that hashes the style object every time (",[165,1204,1205],{},"add"," and ",[165,1208,1209],{},"__hash__",") — and ",[904,1212,1213],{},"constructing"," a new ",[165,1216,1217],{},"Font"," for every cell (",[165,1220,1221],{},"__init__",[165,1223,1224],{},"fonts.py","), with the ",[165,1227,1228],{},"Alignment"," objects just below the cut-off. ",[165,1231,1232],{},"strip_dirs()"," shortens the long site-packages paths so the output is readable.",[10,1235,1236,1237,1239,1240,1243],{},"The profiler's own overhead is large for code like this, which makes hundreds of thousands of tiny calls: the same loop runs several times slower under ",[165,1238,105],{}," than without it. Use its output for ",[904,1241,1242],{},"proportions",", and the stage timers for real durations.",[153,1245,1247],{"id":1246},"step-3-fix-the-top-cost-and-measure-again","Step 3 — Fix the top cost and measure again",[10,1249,1250,1251,1254],{},"The profile points at two fixes, from least to most change. First, create each style object once and reuse it, so nothing is constructed per cell. Second, register a ",[165,1252,1253],{},"NamedStyle"," and assign it by name, which skips the per-assignment lookup of separate font and alignment objects:",[158,1256,1258],{"className":206,"code":1257,"language":208,"meta":163,"style":163},"def style_timing(apply):\n    wb = Workbook(); ws = wb.active\n    for row in pd.read_excel(\"orders.xlsx\", engine=\"calamine\").itertuples(index=False):\n        ws.append(list(row))\n    start = time.perf_counter()\n    apply(wb, ws)\n    return time.perf_counter() - start\n\ndef new_objects(wb, ws):\n    for row in ws.iter_rows():\n        for c in row:\n            c.font = Font(name=\"Calibri\", size=10); c.alignment = Alignment(horizontal=\"left\")\n\ndef shared_objects(wb, ws):\n    font, align = Font(name=\"Calibri\", size=10), Alignment(horizontal=\"left\")\n    for row in ws.iter_rows():\n        for c in row:\n            c.font = font; c.alignment = align\n\ndef named_style(wb, ws):\n    from openpyxl.styles import NamedStyle\n    wb.add_named_style(NamedStyle(name=\"body\", font=Font(name=\"Calibri\", size=10), alignment=Alignment(horizontal=\"left\")))\n    for row in ws.iter_rows():\n        for c in row:\n            c.style = \"body\"\n\nfor fn in (new_objects, shared_objects, named_style):\n    print(f\"{fn.__name__:15} {style_timing(fn):5.2f} s\")\n",[165,1259,1260,1270,1282,1312,1320,1328,1333,1344,1348,1358,1369,1379,1417,1421,1430,1464,1474,1484,1498,1502,1511,1523,1579,1589,1599,1609,1613,1625],{"__ignoreMap":163},[168,1261,1262,1264,1267],{"class":80,"line":170},[168,1263,319],{"class":215},[168,1265,1266],{"class":312}," style_timing",[168,1268,1269],{"class":219},"(apply):\n",[168,1271,1272,1274,1276,1278,1280],{"class":80,"line":223},[168,1273,477],{"class":219},[168,1275,298],{"class":215},[168,1277,482],{"class":219},[168,1279,298],{"class":215},[168,1281,487],{"class":219},[168,1283,1284,1286,1288,1290,1292,1294,1296,1298,1300,1302,1304,1306,1308,1310],{"class":80,"line":231},[168,1285,1046],{"class":215},[168,1287,521],{"class":219},[168,1289,524],{"class":215},[168,1291,408],{"class":219},[168,1293,411],{"class":177},[168,1295,414],{"class":219},[168,1297,418],{"class":417},[168,1299,298],{"class":215},[168,1301,978],{"class":177},[168,1303,987],{"class":219},[168,1305,530],{"class":417},[168,1307,298],{"class":215},[168,1309,535],{"class":508},[168,1311,383],{"class":219},[168,1313,1314,1316,1318],{"class":80,"line":245},[168,1315,505],{"class":219},[168,1317,509],{"class":508},[168,1319,548],{"class":219},[168,1321,1322,1324,1326],{"class":80,"line":259},[168,1323,331],{"class":219},[168,1325,298],{"class":215},[168,1327,336],{"class":219},[168,1329,1330],{"class":80,"line":272},[168,1331,1332],{"class":219},"    apply(wb, ws)\n",[168,1334,1335,1338,1340,1342],{"class":80,"line":285},[168,1336,1337],{"class":215},"    return",[168,1339,353],{"class":219},[168,1341,356],{"class":215},[168,1343,359],{"class":219},[168,1345,1346],{"class":80,"line":292},[168,1347,289],{"emptyLinePlaceholder":288},[168,1349,1350,1352,1355],{"class":80,"line":304},[168,1351,319],{"class":215},[168,1353,1354],{"class":312}," new_objects",[168,1356,1357],{"class":219},"(wb, ws):\n",[168,1359,1360,1362,1364,1366],{"class":80,"line":309},[168,1361,1046],{"class":215},[168,1363,521],{"class":219},[168,1365,524],{"class":215},[168,1367,1368],{"class":219}," ws.iter_rows():\n",[168,1370,1371,1373,1375,1377],{"class":80,"line":316},[168,1372,518],{"class":215},[168,1374,731],{"class":219},[168,1376,524],{"class":215},[168,1378,596],{"class":219},[168,1380,1381,1384,1386,1388,1390,1392,1394,1396,1398,1400,1402,1405,1407,1409,1411,1413,1415],{"class":80,"line":328},[168,1382,1383],{"class":219},"            c.font ",[168,1385,298],{"class":215},[168,1387,607],{"class":219},[168,1389,610],{"class":417},[168,1391,298],{"class":215},[168,1393,615],{"class":177},[168,1395,414],{"class":219},[168,1397,620],{"class":417},[168,1399,298],{"class":215},[168,1401,625],{"class":508},[168,1403,1404],{"class":219},"); c.alignment ",[168,1406,298],{"class":215},[168,1408,638],{"class":219},[168,1410,641],{"class":417},[168,1412,298],{"class":215},[168,1414,646],{"class":177},[168,1416,426],{"class":219},[168,1418,1419],{"class":80,"line":339},[168,1420,289],{"emptyLinePlaceholder":288},[168,1422,1423,1425,1428],{"class":80,"line":345},[168,1424,319],{"class":215},[168,1426,1427],{"class":312}," shared_objects",[168,1429,1357],{"class":219},[168,1431,1432,1435,1437,1439,1441,1443,1445,1447,1449,1451,1453,1456,1458,1460,1462],{"class":80,"line":362},[168,1433,1434],{"class":219},"    font, align ",[168,1436,298],{"class":215},[168,1438,607],{"class":219},[168,1440,610],{"class":417},[168,1442,298],{"class":215},[168,1444,615],{"class":177},[168,1446,414],{"class":219},[168,1448,620],{"class":417},[168,1450,298],{"class":215},[168,1452,625],{"class":508},[168,1454,1455],{"class":219},"), Alignment(",[168,1457,641],{"class":417},[168,1459,298],{"class":215},[168,1461,646],{"class":177},[168,1463,426],{"class":219},[168,1465,1466,1468,1470,1472],{"class":80,"line":367},[168,1467,1046],{"class":215},[168,1469,521],{"class":219},[168,1471,524],{"class":215},[168,1473,1368],{"class":219},[168,1475,1476,1478,1480,1482],{"class":80,"line":386},[168,1477,518],{"class":215},[168,1479,731],{"class":219},[168,1481,524],{"class":215},[168,1483,596],{"class":219},[168,1485,1486,1488,1490,1493,1495],{"class":80,"line":400},[168,1487,1383],{"class":219},[168,1489,298],{"class":215},[168,1491,1492],{"class":219}," font; c.alignment ",[168,1494,298],{"class":215},[168,1496,1497],{"class":219}," align\n",[168,1499,1500],{"class":80,"line":429},[168,1501,289],{"emptyLinePlaceholder":288},[168,1503,1504,1506,1509],{"class":80,"line":441},[168,1505,319],{"class":215},[168,1507,1508],{"class":312}," named_style",[168,1510,1357],{"class":219},[168,1512,1513,1516,1518,1520],{"class":80,"line":474},[168,1514,1515],{"class":215},"    from",[168,1517,264],{"class":219},[168,1519,216],{"class":215},[168,1521,1522],{"class":219}," NamedStyle\n",[168,1524,1525,1528,1530,1532,1535,1537,1540,1542,1545,1547,1549,1551,1553,1555,1557,1559,1562,1565,1567,1570,1572,1574,1576],{"class":80,"line":490},[168,1526,1527],{"class":219},"    wb.add_named_style(NamedStyle(",[168,1529,610],{"class":417},[168,1531,298],{"class":215},[168,1533,1534],{"class":177},"\"body\"",[168,1536,414],{"class":219},[168,1538,1539],{"class":417},"font",[168,1541,298],{"class":215},[168,1543,1544],{"class":219},"Font(",[168,1546,610],{"class":417},[168,1548,298],{"class":215},[168,1550,615],{"class":177},[168,1552,414],{"class":219},[168,1554,620],{"class":417},[168,1556,298],{"class":215},[168,1558,625],{"class":508},[168,1560,1561],{"class":219},"), ",[168,1563,1564],{"class":417},"alignment",[168,1566,298],{"class":215},[168,1568,1569],{"class":219},"Alignment(",[168,1571,641],{"class":417},[168,1573,298],{"class":215},[168,1575,646],{"class":177},[168,1577,1578],{"class":219},")))\n",[168,1580,1581,1583,1585,1587],{"class":80,"line":502},[168,1582,1046],{"class":215},[168,1584,521],{"class":219},[168,1586,524],{"class":215},[168,1588,1368],{"class":219},[168,1590,1591,1593,1595,1597],{"class":80,"line":515},[168,1592,518],{"class":215},[168,1594,731],{"class":219},[168,1596,524],{"class":215},[168,1598,596],{"class":219},[168,1600,1601,1604,1606],{"class":80,"line":540},[168,1602,1603],{"class":219},"            c.style ",[168,1605,298],{"class":215},[168,1607,1608],{"class":177}," \"body\"\n",[168,1610,1611],{"class":80,"line":551},[168,1612,289],{"emptyLinePlaceholder":288},[168,1614,1615,1617,1620,1622],{"class":80,"line":563},[168,1616,728],{"class":215},[168,1618,1619],{"class":219}," fn ",[168,1621,524],{"class":215},[168,1623,1624],{"class":219}," (new_objects, shared_objects, named_style):\n",[168,1626,1627,1629,1631,1633,1635,1637,1640,1643,1646,1648,1650,1653,1656,1658,1660],{"class":80,"line":585},[168,1628,803],{"class":508},[168,1630,676],{"class":219},[168,1632,808],{"class":215},[168,1634,811],{"class":177},[168,1636,815],{"class":814},[168,1638,1639],{"class":219},"fn.",[168,1641,1642],{"class":508},"__name__",[168,1644,1645],{"class":215},":15",[168,1647,823],{"class":814},[168,1649,826],{"class":814},[168,1651,1652],{"class":219},"style_timing(fn)",[168,1654,1655],{"class":215},":5.2f",[168,1657,823],{"class":814},[168,1659,890],{"class":177},[168,1661,426],{"class":219},[10,1663,1664,1665,1669],{},"Reusing the objects cuts the styling time about four-fold; the named style cuts it by more than half again. Both are small code changes that keep the script on openpyxl. ",[14,1666,1668],{"href":1667},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fstyling-excel-cells-with-openpyxl\u002F","Styling Excel Cells with openpyxl"," covers named styles alongside the other styling tools.",[20,1671,29,1677,29,1680,29,1683,29,1687,29,1693,29,1702,29,1708,29,1715,29,1720,29,1723,29,1727,29,1732,29,1737,29,1740,29,1743,29,1748],{"viewBox":1672,"role":23,"ariaLabelledBy":1673,"xmlns":27,"style":1676},"4 0 744 172",[1674,1675],"prof-style-t","prof-style-d","width:100%;max-width:744px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[31,1678,1679],{"id":1674},"Styling 250,000 cells three ways",[35,1681,1682],{"id":1675},"Bar chart of the time to set a font and alignment on 250,000 cells with openpyxl on one laptop: 3.9 seconds creating new Font and Alignment objects for every cell, 0.9 seconds reusing one shared pair of objects, and 0.3 seconds assigning a NamedStyle by name.",[39,1684],{"x":41,"y":42,"width":1685,"height":1686,"fill":45},"744","172",[68,1688,1692],{"x":1689,"y":1690,"style":1691},"380.0","31.0","font-size:14px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Seconds — lower is better",[39,1694],{"x":48,"y":1695,"width":1696,"height":1697,"rx":1698,"fill":1699,"stroke":1700,"style":1701},"48.0","156.1","28.0","7","#e7ebef","var(--line,#cdd5e6)","stroke-width:1px",[68,1703,1707],{"x":1704,"y":1705,"style":1706},"98.1","66.5","font-size:12.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","new objects per cell",[39,1709],{"x":1710,"y":1695,"width":1711,"height":1697,"rx":1698,"fill":1712,"stroke":1713,"style":1714},"186.1","517.0","#fee8f2","var(--accent,#f43f8f)","stroke-width:1.5px",[68,1716,1719],{"x":1717,"y":1705,"style":1718},"711.1","font-size:12px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:start","3.9",[39,1721],{"x":48,"y":1722,"width":1696,"height":1697,"rx":1698,"fill":1699,"stroke":1700,"style":1701},"88.0",[68,1724,1726],{"x":1704,"y":1725,"style":1706},"106.5","shared objects",[39,1728],{"x":1710,"y":1722,"width":1729,"height":1697,"rx":1698,"fill":1730,"stroke":1731,"style":1714},"119.3","#fdefd8","var(--gold,#b4740a)",[68,1733,1736],{"x":1734,"y":1725,"style":1735},"313.4","font-size:12px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:start","0.9",[39,1738],{"x":48,"y":1739,"width":1696,"height":1697,"rx":1698,"fill":1699,"stroke":1700,"style":1701},"128.0",[68,1741,1253],{"x":1704,"y":1742,"style":1706},"146.5",[39,1744],{"x":1710,"y":1739,"width":1745,"height":1697,"rx":1698,"fill":1746,"stroke":1747,"style":1714},"39.8","#d9f4f1","var(--teal,#0f9488)",[68,1749,1752],{"x":1750,"y":1742,"style":1751},"233.9","font-size:12px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:start","0.3",[153,1754,1756],{"id":1755},"step-4-rethink-the-approach-when-the-fix-is-not-enough","Step 4 — Rethink the approach when the fix is not enough",[10,1758,1759],{},"Per-cell styling is only needed when cells differ. When a whole column shares a format, the fastest option is not to style cells at all: write with xlsxwriter and attach the format to the column. Combine that with a faster reader and widths computed from the DataFrame instead of by scanning the sheet:",[158,1761,1763],{"className":206,"code":1762,"language":208,"meta":163,"style":163},"timings = {}\n\ndef fast_report(out=\"fast.xlsx\"):\n    with stage(\"read\"):\n        df = pd.read_excel(\"orders.xlsx\", engine=\"calamine\")\n    with stage(\"transform\"):\n        df[\"Revenue\"] = df[\"Qty\"] * df[\"Price\"]\n    with stage(\"write, format, save\"):\n        with pd.ExcelWriter(out, engine=\"xlsxwriter\") as writer:\n            df.to_excel(writer, index=False, sheet_name=\"Orders\")\n            ws = writer.sheets[\"Orders\"]\n            body = writer.book.add_format({\"font_name\": \"Calibri\", \"font_size\": 10, \"align\": \"left\"})\n            for i, col in enumerate(df.columns):\n                width = max(len(col), int(df[col].astype(str).str.len().max())) + 2\n                ws.set_column(i, i, width, body)\n\nfast_report()\nfor name, t in timings.items():\n    print(f\"{name:20} {t:6.2f} s\")\nprint(f\"{'total':20} {sum(timings.values()):6.2f} s\")\nprint(pd.read_excel(\"fast.xlsx\").shape, pd.read_excel(\"slow.xlsx\").shape)\n",[165,1764,1765,1773,1777,1793,1803,1823,1833,1857,1868,1891,1914,1928,1967,1982,2013,2018,2022,2027,2037,2068,2102],{"__ignoreMap":163},[168,1766,1767,1769,1771],{"class":80,"line":170},[168,1768,295],{"class":219},[168,1770,298],{"class":215},[168,1772,301],{"class":219},[168,1774,1775],{"class":80,"line":223},[168,1776,289],{"emptyLinePlaceholder":288},[168,1778,1779,1781,1784,1786,1788,1791],{"class":80,"line":231},[168,1780,319],{"class":215},[168,1782,1783],{"class":312}," fast_report",[168,1785,375],{"class":219},[168,1787,298],{"class":215},[168,1789,1790],{"class":177},"\"fast.xlsx\"",[168,1792,383],{"class":219},[168,1794,1795,1797,1799,1801],{"class":80,"line":245},[168,1796,389],{"class":215},[168,1798,392],{"class":219},[168,1800,395],{"class":177},[168,1802,383],{"class":219},[168,1804,1805,1807,1809,1811,1813,1815,1817,1819,1821],{"class":80,"line":259},[168,1806,403],{"class":219},[168,1808,298],{"class":215},[168,1810,408],{"class":219},[168,1812,411],{"class":177},[168,1814,414],{"class":219},[168,1816,418],{"class":417},[168,1818,298],{"class":215},[168,1820,978],{"class":177},[168,1822,426],{"class":219},[168,1824,1825,1827,1829,1831],{"class":80,"line":272},[168,1826,389],{"class":215},[168,1828,392],{"class":219},[168,1830,436],{"class":177},[168,1832,383],{"class":219},[168,1834,1835,1837,1839,1841,1843,1845,1847,1849,1851,1853,1855],{"class":80,"line":285},[168,1836,444],{"class":219},[168,1838,447],{"class":177},[168,1840,450],{"class":219},[168,1842,298],{"class":215},[168,1844,455],{"class":219},[168,1846,458],{"class":177},[168,1848,450],{"class":219},[168,1850,463],{"class":215},[168,1852,455],{"class":219},[168,1854,468],{"class":177},[168,1856,471],{"class":219},[168,1858,1859,1861,1863,1866],{"class":80,"line":292},[168,1860,389],{"class":215},[168,1862,392],{"class":219},[168,1864,1865],{"class":177},"\"write, format, save\"",[168,1867,383],{"class":219},[168,1869,1870,1873,1876,1878,1880,1883,1886,1888],{"class":80,"line":304},[168,1871,1872],{"class":215},"        with",[168,1874,1875],{"class":219}," pd.ExcelWriter(out, ",[168,1877,418],{"class":417},[168,1879,298],{"class":215},[168,1881,1882],{"class":177},"\"xlsxwriter\"",[168,1884,1885],{"class":219},") ",[168,1887,239],{"class":215},[168,1889,1890],{"class":219}," writer:\n",[168,1892,1893,1896,1898,1900,1902,1904,1907,1909,1912],{"class":80,"line":309},[168,1894,1895],{"class":219},"            df.to_excel(writer, ",[168,1897,530],{"class":417},[168,1899,298],{"class":215},[168,1901,535],{"class":508},[168,1903,414],{"class":219},[168,1905,1906],{"class":417},"sheet_name",[168,1908,298],{"class":215},[168,1910,1911],{"class":177},"\"Orders\"",[168,1913,426],{"class":219},[168,1915,1916,1919,1921,1924,1926],{"class":80,"line":316},[168,1917,1918],{"class":219},"            ws ",[168,1920,298],{"class":215},[168,1922,1923],{"class":219}," writer.sheets[",[168,1925,1911],{"class":177},[168,1927,471],{"class":219},[168,1929,1930,1933,1935,1938,1941,1944,1946,1948,1951,1953,1955,1957,1960,1962,1964],{"class":80,"line":328},[168,1931,1932],{"class":219},"            body ",[168,1934,298],{"class":215},[168,1936,1937],{"class":219}," writer.book.add_format({",[168,1939,1940],{"class":177},"\"font_name\"",[168,1942,1943],{"class":219},": ",[168,1945,615],{"class":177},[168,1947,414],{"class":219},[168,1949,1950],{"class":177},"\"font_size\"",[168,1952,1943],{"class":219},[168,1954,625],{"class":508},[168,1956,414],{"class":219},[168,1958,1959],{"class":177},"\"align\"",[168,1961,1943],{"class":219},[168,1963,646],{"class":177},[168,1965,1966],{"class":219},"})\n",[168,1968,1969,1971,1974,1976,1979],{"class":80,"line":339},[168,1970,588],{"class":215},[168,1972,1973],{"class":219}," i, col ",[168,1975,524],{"class":215},[168,1977,1978],{"class":508}," enumerate",[168,1980,1981],{"class":219},"(df.columns):\n",[168,1983,1984,1987,1989,1991,1993,1995,1998,2001,2004,2006,2009,2011],{"class":80,"line":345},[168,1985,1986],{"class":219},"                width ",[168,1988,298],{"class":215},[168,1990,712],{"class":508},[168,1992,676],{"class":219},[168,1994,717],{"class":508},[168,1996,1997],{"class":219},"(col), ",[168,1999,2000],{"class":508},"int",[168,2002,2003],{"class":219},"(df[col].astype(",[168,2005,722],{"class":508},[168,2007,2008],{"class":219},").str.len().max())) ",[168,2010,685],{"class":215},[168,2012,741],{"class":508},[168,2014,2015],{"class":80,"line":362},[168,2016,2017],{"class":219},"                ws.set_column(i, i, width, body)\n",[168,2019,2020],{"class":80,"line":367},[168,2021,289],{"emptyLinePlaceholder":288},[168,2023,2024],{"class":80,"line":386},[168,2025,2026],{"class":219},"fast_report()\n",[168,2028,2029,2031,2033,2035],{"class":80,"line":400},[168,2030,728],{"class":215},[168,2032,792],{"class":219},[168,2034,524],{"class":215},[168,2036,797],{"class":219},[168,2038,2039,2041,2043,2045,2047,2049,2051,2054,2056,2058,2060,2062,2064,2066],{"class":80,"line":429},[168,2040,803],{"class":508},[168,2042,676],{"class":219},[168,2044,808],{"class":215},[168,2046,811],{"class":177},[168,2048,815],{"class":814},[168,2050,610],{"class":219},[168,2052,2053],{"class":215},":20",[168,2055,823],{"class":814},[168,2057,826],{"class":814},[168,2059,829],{"class":219},[168,2061,832],{"class":215},[168,2063,823],{"class":814},[168,2065,890],{"class":177},[168,2067,426],{"class":219},[168,2069,2070,2072,2074,2076,2078,2080,2082,2084,2086,2088,2091,2094,2096,2098,2100],{"class":80,"line":441},[168,2071,863],{"class":508},[168,2073,676],{"class":219},[168,2075,808],{"class":215},[168,2077,811],{"class":177},[168,2079,815],{"class":814},[168,2081,874],{"class":177},[168,2083,2053],{"class":215},[168,2085,823],{"class":814},[168,2087,826],{"class":814},[168,2089,2090],{"class":508},"sum",[168,2092,2093],{"class":219},"(timings.values())",[168,2095,832],{"class":215},[168,2097,823],{"class":814},[168,2099,890],{"class":177},[168,2101,426],{"class":219},[168,2103,2104,2106,2109,2111,2114,2116],{"class":80,"line":474},[168,2105,863],{"class":508},[168,2107,2108],{"class":219},"(pd.read_excel(",[168,2110,1790],{"class":177},[168,2112,2113],{"class":219},").shape, pd.read_excel(",[168,2115,380],{"class":177},[168,2117,2118],{"class":219},").shape)\n",[10,2120,2121,2122,2126],{},"The whole report now runs in a fraction of the original time, and both files hold the same 50,000 rows and six columns. The calamine engine reads several times faster than openpyxl; see ",[14,2123,2125],{"href":2124},"\u002Fadvanced-data-transformation-and-cleaning\u002Freading-excel-with-polars-and-arrow\u002Fspeed-up-pandas-excel-reads-with-the-calamine-engine\u002F","Speed Up pandas Excel Reads with the calamine Engine",". A column format applies to every cell in the column that has no format of its own, so the data looks the same as with per-cell styling. The trade-off is that xlsxwriter only creates new files; when the report must edit an existing template, stay with openpyxl and use the fixes from Step 3.",[20,2128,29,2133,29,2136,29,2139,29,2142,29,2147,29,2154,29,2160,29,2165,29,2171,29,2178,29,2182,29,2187,29,2191,29,2196,29,2200,29,2205,29,2207,29,2214,29,2219,29,2223,29,2227,29,2233,29,2236,29,2241,29,2244,29,2249,29,2252],{"viewBox":2129,"role":23,"ariaLabelledBy":2130,"xmlns":27,"style":28},"4 0 752 233",[2131,2132],"prof-before-after-t","prof-before-after-d",[31,2134,2135],{"id":2131},"The same report before and after",[35,2137,2138],{"id":2132},"Two panels. Before: read with the openpyxl engine, write rows one by one, style every cell with new objects, scan every cell for widths, about 7 seconds in total. After: read with calamine, write with xlsxwriter through pandas, one format per column, widths from the DataFrame, under 2 seconds.",[39,2140],{"x":41,"y":42,"width":43,"height":2141,"fill":45},"233",[39,2143],{"x":48,"y":49,"width":2144,"height":2145,"rx":2146,"fill":1712,"stroke":1713,"style":55},"351.0","201.0","14",[57,2148,2150],{"transform":2149},"translate(182.5,29.0) scale(1.083)",[61,2151],{"d":63,"fill":64,"stroke":2152,"style":2153},"var(--accent-ink,#be185d)","stroke-width:1.66px;stroke-linecap:round;stroke-linejoin:round",[68,2155,2159],{"x":2156,"y":2157,"style":2158},"195.5","78.0","font-size:14px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","Before — about 7 s",[80,2161],{"x1":2162,"y1":2163,"x2":2164,"y2":2163,"stroke":1713,"style":1701},"36.0","91.0","355.0",[68,2166,2170],{"x":2167,"y":2168,"style":2169},"60.0","111.0","font-size:12px;font-weight:400;fill:var(--text,#172033);text-anchor:start","openpyxl engine read",[57,2172,2174],{"transform":2173},"translate(38.5,99.5) scale(0.625)",[61,2175],{"d":2176,"fill":64,"stroke":1713,"style":2177},"M6 6l12 12 M18 6L6 18","stroke-width:2.88px;stroke-linecap:round;stroke-linejoin:round",[68,2179,2181],{"x":2167,"y":2180,"style":2169},"137.0","row-by-row writes",[57,2183,2185],{"transform":2184},"translate(38.5,125.5) scale(0.625)",[61,2186],{"d":2176,"fill":64,"stroke":1713,"style":2177},[68,2188,2190],{"x":2167,"y":2189,"style":2169},"163.0","new style objects per cell",[57,2192,2194],{"transform":2193},"translate(38.5,151.5) scale(0.625)",[61,2195],{"d":2176,"fill":64,"stroke":1713,"style":2177},[68,2197,2199],{"x":2167,"y":2198,"style":2169},"189.0","widths by scanning cells",[57,2201,2203],{"transform":2202},"translate(38.5,177.5) scale(0.625)",[61,2204],{"d":2176,"fill":64,"stroke":1713,"style":2177},[39,2206],{"x":109,"y":49,"width":2144,"height":2145,"rx":2146,"fill":1746,"stroke":1747,"style":55},[57,2208,2210],{"transform":2209},"translate(551.5,29.0) scale(1.083)",[61,2211],{"d":2212,"fill":64,"stroke":2213,"style":2153},"M13 2L4 14h7l-1 8 9-12h-7z","var(--teal-ink,#0b6157)",[68,2215,2218],{"x":2216,"y":2157,"style":2217},"564.5","font-size:14px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","After — under 2 s",[80,2220],{"x1":2221,"y1":2163,"x2":2222,"y2":2163,"stroke":1747,"style":1701},"405.0","724.0",[68,2224,2226],{"x":2225,"y":2168,"style":2169},"429.0","calamine read",[57,2228,2230],{"transform":2229},"translate(407.5,99.5) scale(0.625)",[61,2231],{"d":2232,"fill":64,"stroke":1747,"style":2177},"M5 12.5l4.5 4.5L19 7",[68,2234,2235],{"x":2225,"y":2180,"style":2169},"xlsxwriter via pandas",[57,2237,2239],{"transform":2238},"translate(407.5,125.5) scale(0.625)",[61,2240],{"d":2232,"fill":64,"stroke":1747,"style":2177},[68,2242,2243],{"x":2225,"y":2189,"style":2169},"one format per column",[57,2245,2247],{"transform":2246},"translate(407.5,151.5) scale(0.625)",[61,2248],{"d":2232,"fill":64,"stroke":1747,"style":2177},[68,2250,2251],{"x":2225,"y":2198,"style":2169},"widths from the DataFrame",[57,2253,2255],{"transform":2254},"translate(407.5,177.5) scale(0.625)",[61,2256],{"d":2232,"fill":64,"stroke":1747,"style":2177},[153,2258,2260],{"id":2259},"where-excel-scripts-usually-lose-time","Where Excel scripts usually lose time",[10,2262,2263],{},"Across many report scripts the same few causes come up, roughly in this order: styling or formatting cells one at a time; reading large files with the default engine; writing cells in Python loops instead of letting pandas write whole frames; repeated work inside loops — recomputing a lookup, reopening a workbook, rescanning a column — that could be done once; and saving the same workbook several times during a run. Measuring first tells you which of them applies before you change anything.",[153,2265,2267],{"id":2266},"common-pitfalls","Common pitfalls",[2269,2270,2271,2279,2285,2291,2297],"ul",{},[2272,2273,2274,2278],"li",{},[2275,2276,2277],"strong",{},"Optimising before measuring."," Time the stages first; the slow part is rarely where you guess.",[2272,2280,2281,2284],{},[2275,2282,2283],{},"Trusting profiled durations."," cProfile adds heavy overhead to many small calls; use it for proportions.",[2272,2286,2287,2290],{},[2275,2288,2289],{},"Profiling the whole run at full size."," Profile the slowest stage on a sample to keep it fast and readable.",[2272,2292,2293,2296],{},[2275,2294,2295],{},"A new style object per cell."," Create styles once, or use a NamedStyle or column formats.",[2272,2298,2299,2302],{},[2275,2300,2301],{},"Changing several things at once."," Make one change, measure, then the next, so you know what helped.",[153,2304,2306],{"id":2305},"performance-notes","Performance notes",[10,2308,2309,2310,2313],{},"Keep the stage timer in the production script and log its results: a report whose styling time doubles after a data change points straight at the cause. For memory rather than time, ",[165,2311,2312],{},"tracemalloc"," reports which lines allocate the most, which is useful when a large workbook runs out of memory rather than time.",[153,2315,2317],{"id":2316},"conclusion","Conclusion",[10,2319,2320,2321,2323,2324,2326,2327,2329],{},"Time each stage of the report with a ",[165,2322,78],{}," context manager, profile the slowest stage on a sample with ",[165,2325,105],{}," sorted by cumulative time, and read the output for proportions rather than durations. In openpyxl, reuse style objects or assign a ",[165,2328,1253],{}," instead of creating styles per cell; when the output is a new file, read with calamine and write with xlsxwriter using column formats. Change one thing at a time and measure after each change.",[153,2331,2333],{"id":2332},"frequently-asked-questions","Frequently asked questions",[10,2335,2336,2339],{},[2275,2337,2338],{},"How do I find out why my Python Excel script is slow?","\nTime each stage — read, transform, write, style, save — with time.perf_counter, find the slowest, then run that part under cProfile and sort the statistics by cumulative time to see which calls inside it cost the most.",[10,2341,2342,2345],{},[2275,2343,2344],{},"Why is cProfile so much slower than the normal run?","\ncProfile records every function call, which adds overhead to each one. Code that makes millions of small calls, like openpyxl styling, can run several times slower under the profiler. Use it to see where time goes relatively, and stage timers without the profiler for real durations.",[10,2347,2348,2351],{},[2275,2349,2350],{},"Why is styling cells with openpyxl slow?","\nEach assignment of a Font, Fill or Alignment is looked up in the workbook's style table, and creating a new style object for every cell makes that lookup expensive. Create style objects once and reuse them, use a NamedStyle, or write with xlsxwriter and apply formats per column.",[10,2353,2354,2357],{},[2275,2355,2356],{},"What usually makes an Excel report script slow?","\nIn order of how often it happens: styling cells one at a time, reading large files with the default engine, writing rows one by one in Python loops, and recalculating things such as column widths by scanning every cell repeatedly.",[153,2359,2361],{"id":2360},"related","Related",[2269,2363,2364,2370,2377,2384],{},[2272,2365,2366,2367,2369],{},"Up one level: ",[14,2368,17],{"href":16}," — making report code dependable.",[2272,2371,2372,2376],{},[14,2373,2375],{"href":2374},"\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-large-excel-files-in-python\u002Fspeed-up-openpyxl-with-read-only-mode\u002F","Speed Up openpyxl with Read-Only Mode"," — faster reads of big workbooks.",[2272,2378,2379,2383],{},[14,2380,2382],{"href":2381},"\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-large-excel-files-in-python\u002Fwrite-large-dataframes-to-excel-with-write-only-mode\u002F","Write Large DataFrames to Excel with Write-Only Mode"," — faster writes.",[2272,2385,2386,2390],{},[14,2387,2389],{"href":2388},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Flog-python-excel-script-output-to-a-file\u002F","Log Python Excel Script Output to a File"," — keeping the stage timings.",[2392,2393,2394],"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 .s_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}html pre.shiki code .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}html pre.shiki code .sP0c6, html code.shiki .sP0c6{--shiki-default:#005CC5;--shiki-dark:#91CBFF}html pre.shiki code .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}",{"title":163,"searchDepth":223,"depth":223,"links":2396},[2397,2398,2399,2400,2401,2402,2403,2404,2405,2406,2407],{"id":155,"depth":223,"text":156},{"id":199,"depth":223,"text":200},{"id":898,"depth":223,"text":899},{"id":1246,"depth":223,"text":1247},{"id":1755,"depth":223,"text":1756},{"id":2259,"depth":223,"text":2260},{"id":2266,"depth":223,"text":2267},{"id":2305,"depth":223,"text":2306},{"id":2316,"depth":223,"text":2317},{"id":2332,"depth":223,"text":2333},{"id":2360,"depth":223,"text":2361},"2026-09-25","Find out where a slow report actually spends its time before changing anything. Time each stage with a small context manager, drill into the slowest one with cProfile and pstats, read the output correctly, and fix the usual culprits — per-cell styling in openpyxl, slow reads, row-by-row writes — measuring the gain each time.","md",[2412,2414,2416,2418],{"q":2338,"a":2413},"Time each stage — read, transform, write, style, save — with time.perf_counter, find the slowest, then run that part under cProfile and sort the statistics by cumulative time to see which calls inside it cost the most.",{"q":2344,"a":2415},"cProfile records every function call, which adds overhead to each one. Code that makes millions of small calls, like openpyxl styling, can run several times slower under the profiler. Use it to see where time goes relatively, and stage timers without the profiler for real durations.",{"q":2350,"a":2417},"Each assignment of a Font, Fill or Alignment is looked up in the workbook's style table, and creating a new style object for every cell makes that lookup expensive. Create style objects once and reuse them, use a NamedStyle, or write with xlsxwriter and apply formats per column.",{"q":2356,"a":2419},"In order of how often it happens: styling cells one at a time, reading large files with the default engine, writing rows one by one in Python loops, and recalculating things such as column widths by scanning every cell repeatedly.","profile a slow excel report script",null,{"breadcrumb":2423},[2424,2426,2429],{"name":2425,"item":845},"Home",{"name":2427,"item":2428},"Automating Reporting Workflows with Python and Excel","\u002Fautomating-reporting-workflows\u002F",{"name":17,"item":16},"\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Fprofile-a-slow-excel-report-script",{"title":2432,"description":2433},"Profile and Speed Up a Slow Python Excel Script (cProfile)","Profile a Python Excel report: stage timers with perf_counter, cProfile and pstats sorted by cumulative time, profiler overhead, openpyxl per-cell Font objects versus shared styles and NamedStyle, calamine reads and xlsxwriter column formats, with measured timings.","profile-a-slow-excel-report-script","automating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Fprofile-a-slow-excel-report-script\u002Findex",[2437,2438,2439],"large-files","openpyxl","reliable-jobs","how-to","B1wPpdGlzl9nJqz5MtYgPuNCpmLOtRuxS_8oFMSqEk4",[2443,2447],{"title":2444,"path":2445,"stem":2446,"children":-1},"Package a Python Excel Script as an EXE with PyInstaller","\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Fpackage-a-python-excel-script-as-an-exe-with-pyinstaller","automating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Fpackage-a-python-excel-script-as-an-exe-with-pyinstaller\u002Findex",{"title":2448,"path":2449,"stem":2450,"children":-1},"Set Up an Excel Automation Project with uv","\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Fset-up-an-excel-automation-project-with-uv","automating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Fset-up-an-excel-automation-project-with-uv\u002Findex",[2452,2454],{"path":2453,"title":2427},"\u002Fautomating-reporting-workflows",{"path":2455,"title":17},"\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts",[],[2458,2460,2464,2467,2470,2473],{"path":2459,"title":2125,"kind":2440},"\u002Fadvanced-data-transformation-and-cleaning\u002Freading-excel-with-polars-and-arrow\u002Fspeed-up-pandas-excel-reads-with-the-calamine-engine",{"path":2461,"title":2462,"kind":2463},"\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-large-excel-files-in-python","Working with Large Excel Files in Python","guide",{"path":2465,"title":2466,"kind":2440},"\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-large-excel-files-in-python\u002Faggregate-a-huge-excel-file-without-loading-it","Aggregate a Huge Excel File Without Loading It",{"path":2468,"title":2469,"kind":2440},"\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-large-excel-files-in-python\u002Fcount-rows-in-an-excel-file-without-loading-it","Count Rows in an Excel File Without Loading It",{"path":2471,"title":2472,"kind":2440},"\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-large-excel-files-in-python\u002Ffind-out-why-an-excel-file-is-slow-to-open","Find Out Why an Excel File Is Slow to Open",{"path":2474,"title":2475,"kind":2440},"\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-large-excel-files-in-python\u002Fprocess-multiple-excel-files-in-parallel-with-python","Process Multiple Excel Files in Parallel with Python",1790972142764]