[{"data":1,"prerenderedAt":2263},["ShallowReactive",2],{"doc:\u002Fgetting-started-with-python-excel-automation\u002Fchoosing-a-python-excel-library\u002Fbenchmark-python-excel-read-and-write-speed":3,"surround:\u002Fgetting-started-with-python-excel-automation\u002Fchoosing-a-python-excel-library\u002Fbenchmark-python-excel-read-and-write-speed":2255},{"id":4,"title":5,"body":6,"dateModified":2228,"datePublished":2228,"description":2229,"extension":2230,"faq":2231,"meta":2240,"navigation":234,"path":2248,"seo":2249,"slug":2251,"stem":2252,"type":2253,"__hash__":2254},"docs\u002Fgetting-started-with-python-excel-automation\u002Fchoosing-a-python-excel-library\u002Fbenchmark-python-excel-read-and-write-speed\u002Findex.md","Benchmark Python Excel Read and Write Speed",{"type":7,"value":8,"toc":2213},"minimark",[9,19,137,142,183,186,190,193,507,510,514,517,739,742,746,1062,1167,1170,1174,1177,1537,1548,1552,1559,1677,1683,1729,1733,1736,1887,1890,1894,1897,1900,1908,1916,1920,2011,2015,2113,2124,2131,2135,2138,2142,2149,2155,2161,2167,2171,2209],[10,11,12,13,18],"p",{},"Every recommendation about Python Excel performance comes with an unstated \"on my file\". Parse\nspeed depends on how many cells are strings, whether the workbook carries a large shared-strings\ntable, how much styling is present and whether formulas were cached — so the ratios published in\nblog posts, this one included, are hypotheses about your workbook rather than measurements of it.\nThis guide, part of ",[14,15,17],"a",{"href":16},"\u002Fgetting-started-with-python-excel-automation\u002Fchoosing-a-python-excel-library\u002F","Choosing a Python Excel Library",",\nbuilds a small harness that measures the libraries on the file you actually care about.",[20,21,29,30,29,34,29,38,29,45,29,54,29,60,29,66,29,72,29,77,29,80,29,83,29,87,29,91,29,95,29,98,29,101,29,105,29,109,29,113,29,116,29,119,29,123,29,127,29,131],"svg",{"viewBox":22,"role":23,"ariaLabelledBy":24,"xmlns":27,"style":28},"0 0 760 308","img",[25,26],"bn-harness-t","bn-harness-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:760px;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},"The four rules that make an Excel benchmark trustworthy",[35,36,37],"desc",{"id":26},"Use a file with a realistic mix of types, warm the page cache with an untimed run, repeat and keep the fastest time, and print the row count so a short read cannot look fast.",[39,40],"rect",{"x":41,"y":41,"width":42,"height":43,"fill":44},"0","760","308","#ffffff",[39,46],{"x":47,"y":48,"width":49,"height":50,"rx":51,"fill":52,"stroke":53},"22","28","716","52","10","#d9f4f1","var(--teal,#0f9488)",[55,56],"circle",{"cx":50,"cy":57,"r":58,"fill":59},"54.0","15","#5b5cf0",[61,62,65],"text",{"x":50,"y":63,"style":64},"59.0","font-size:13px;font-weight:700;fill:#ffffff;text-anchor:middle","1",[61,67,71],{"x":68,"y":69,"style":70},"82","50","font-size:13px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:start","Use a realistic file",[61,73,76],{"x":68,"y":74,"style":75},"68","font-size:11.5px;font-weight:400;fill:var(--muted,#5b6780);text-anchor:start","a sheet of floats flatters every parser equally",[39,78],{"x":47,"y":79,"width":49,"height":50,"rx":51,"fill":52,"stroke":53},"90",[55,81],{"cx":50,"cy":82,"r":58,"fill":59},"116.0",[61,84,86],{"x":50,"y":85,"style":64},"121.0","2",[61,88,90],{"x":68,"y":89,"style":70},"112","Warm the cache first",[61,92,94],{"x":68,"y":93,"style":75},"130","an untimed run, then measure",[39,96],{"x":47,"y":97,"width":49,"height":50,"rx":51,"fill":52,"stroke":53},"152",[55,99],{"cx":50,"cy":100,"r":58,"fill":59},"178.0",[61,102,104],{"x":50,"y":103,"style":64},"183.0","3",[61,106,108],{"x":68,"y":107,"style":70},"174","Repeat and keep the minimum",[61,110,112],{"x":68,"y":111,"style":75},"192","noise only ever makes a run slower",[39,114],{"x":47,"y":115,"width":49,"height":50,"rx":51,"fill":52,"stroke":53},"214",[55,117],{"cx":50,"cy":118,"r":58,"fill":59},"240.0",[61,120,122],{"x":50,"y":121,"style":64},"245.0","4",[61,124,126],{"x":68,"y":125,"style":70},"236","Print the row count",[61,128,130],{"x":68,"y":129,"style":75},"254","a candidate that read half the sheet is not fast",[61,132,136],{"x":133,"y":134,"style":135},"380.0","296","font-size:12.5px;font-weight:400;fill:var(--muted,#5b6780);text-anchor:middle","fifteen lines of harness beats any published ratio",[138,139,141],"h2",{"id":140},"prerequisites","Prerequisites",[143,144,149],"pre",{"className":145,"code":146,"language":147,"meta":148,"style":148},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install pandas polars openpyxl xlsxwriter python-calamine fastexcel\n","bash","",[150,151,152],"code",{"__ignoreMap":148},[153,154,157,161,165,168,171,174,177,180],"span",{"class":155,"line":156},"line",1,[153,158,160],{"class":159},"sMTad","pip",[153,162,164],{"class":163},"srMev"," install",[153,166,167],{"class":163}," pandas",[153,169,170],{"class":163}," polars",[153,172,173],{"class":163}," openpyxl",[153,175,176],{"class":163}," xlsxwriter",[153,178,179],{"class":163}," python-calamine",[153,181,182],{"class":163}," fastexcel\n",[10,184,185],{},"You also need a representative file. Not a synthetic sheet of random floats — a copy of the real\nexport, because its shape is the thing being measured.",[138,187,189],{"id":188},"generate-a-realistic-test-workbook","Generate a realistic test workbook",[10,191,192],{},"If you cannot use production data, build something with the same mix of types. A sheet that is all\nfloats will flatter every parser equally and tell you nothing about the one you will run.",[143,194,198],{"className":195,"code":196,"language":197,"meta":148,"style":148},"language-python shiki shiki-themes github-light github-dark-high-contrast","import numpy as np\nimport pandas as pd\n\nrows = 200_000\nrng = np.random.default_rng(7)\nframe = pd.DataFrame({\n    \"Order_ID\": np.arange(1, rows + 1),\n    \"SKU\": rng.choice([f\"SKU-{n:04d}\" for n in range(500)], rows),\n    \"Region\": rng.choice([\"North\", \"South\", \"East\", \"West\"], rows),\n    \"Ordered\": pd.date_range(\"2024-01-01\", periods=rows, freq=\"min\"),\n    \"Quantity\": rng.integers(1, 40, rows),\n    \"Unit_Price\": rng.normal(50, 12, rows).round(2),\n})\nframe.to_excel(\"bench.xlsx\", sheet_name=\"Data\", index=False)\nprint(\"wrote bench.xlsx\")\n","python",[150,199,200,216,229,236,249,266,277,300,352,382,415,434,457,463,494],{"__ignoreMap":148},[153,201,202,206,210,213],{"class":155,"line":156},[153,203,205],{"class":204},"s-kum","import",[153,207,209],{"class":208},"skGVy"," numpy ",[153,211,212],{"class":204},"as",[153,214,215],{"class":208}," np\n",[153,217,219,221,224,226],{"class":155,"line":218},2,[153,220,205],{"class":204},[153,222,223],{"class":208}," pandas ",[153,225,212],{"class":204},[153,227,228],{"class":208}," pd\n",[153,230,232],{"class":155,"line":231},3,[153,233,235],{"emptyLinePlaceholder":234},true,"\n",[153,237,239,242,245],{"class":155,"line":238},4,[153,240,241],{"class":208},"rows ",[153,243,244],{"class":204},"=",[153,246,248],{"class":247},"sP0c6"," 200_000\n",[153,250,252,255,257,260,263],{"class":155,"line":251},5,[153,253,254],{"class":208},"rng ",[153,256,244],{"class":204},[153,258,259],{"class":208}," np.random.default_rng(",[153,261,262],{"class":247},"7",[153,264,265],{"class":208},")\n",[153,267,269,272,274],{"class":155,"line":268},6,[153,270,271],{"class":208},"frame ",[153,273,244],{"class":204},[153,275,276],{"class":208}," pd.DataFrame({\n",[153,278,280,283,286,288,291,294,297],{"class":155,"line":279},7,[153,281,282],{"class":163},"    \"Order_ID\"",[153,284,285],{"class":208},": np.arange(",[153,287,65],{"class":247},[153,289,290],{"class":208},", rows ",[153,292,293],{"class":204},"+",[153,295,296],{"class":247}," 1",[153,298,299],{"class":208},"),\n",[153,301,303,306,309,312,315,319,322,325,328,331,334,337,340,343,346,349],{"class":155,"line":302},8,[153,304,305],{"class":163},"    \"SKU\"",[153,307,308],{"class":208},": rng.choice([",[153,310,311],{"class":204},"f",[153,313,314],{"class":163},"\"SKU-",[153,316,318],{"class":317},"sSjpA","{",[153,320,321],{"class":208},"n",[153,323,324],{"class":204},":04d",[153,326,327],{"class":317},"}",[153,329,330],{"class":163},"\"",[153,332,333],{"class":204}," for",[153,335,336],{"class":208}," n ",[153,338,339],{"class":204},"in",[153,341,342],{"class":247}," range",[153,344,345],{"class":208},"(",[153,347,348],{"class":247},"500",[153,350,351],{"class":208},")], rows),\n",[153,353,355,358,360,363,366,369,371,374,376,379],{"class":155,"line":354},9,[153,356,357],{"class":163},"    \"Region\"",[153,359,308],{"class":208},[153,361,362],{"class":163},"\"North\"",[153,364,365],{"class":208},", ",[153,367,368],{"class":163},"\"South\"",[153,370,365],{"class":208},[153,372,373],{"class":163},"\"East\"",[153,375,365],{"class":208},[153,377,378],{"class":163},"\"West\"",[153,380,381],{"class":208},"], rows),\n",[153,383,385,388,391,394,396,400,402,405,408,410,413],{"class":155,"line":384},10,[153,386,387],{"class":163},"    \"Ordered\"",[153,389,390],{"class":208},": pd.date_range(",[153,392,393],{"class":163},"\"2024-01-01\"",[153,395,365],{"class":208},[153,397,399],{"class":398},"sa561","periods",[153,401,244],{"class":204},[153,403,404],{"class":208},"rows, ",[153,406,407],{"class":398},"freq",[153,409,244],{"class":204},[153,411,412],{"class":163},"\"min\"",[153,414,299],{"class":208},[153,416,418,421,424,426,428,431],{"class":155,"line":417},11,[153,419,420],{"class":163},"    \"Quantity\"",[153,422,423],{"class":208},": rng.integers(",[153,425,65],{"class":247},[153,427,365],{"class":208},[153,429,430],{"class":247},"40",[153,432,433],{"class":208},", rows),\n",[153,435,437,440,443,445,447,450,453,455],{"class":155,"line":436},12,[153,438,439],{"class":163},"    \"Unit_Price\"",[153,441,442],{"class":208},": rng.normal(",[153,444,69],{"class":247},[153,446,365],{"class":208},[153,448,449],{"class":247},"12",[153,451,452],{"class":208},", rows).round(",[153,454,86],{"class":247},[153,456,299],{"class":208},[153,458,460],{"class":155,"line":459},13,[153,461,462],{"class":208},"})\n",[153,464,466,469,472,474,477,479,482,484,487,489,492],{"class":155,"line":465},14,[153,467,468],{"class":208},"frame.to_excel(",[153,470,471],{"class":163},"\"bench.xlsx\"",[153,473,365],{"class":208},[153,475,476],{"class":398},"sheet_name",[153,478,244],{"class":204},[153,480,481],{"class":163},"\"Data\"",[153,483,365],{"class":208},[153,485,486],{"class":398},"index",[153,488,244],{"class":204},[153,490,491],{"class":247},"False",[153,493,265],{"class":208},[153,495,497,500,502,505],{"class":155,"line":496},15,[153,498,499],{"class":247},"print",[153,501,345],{"class":208},[153,503,504],{"class":163},"\"wrote bench.xlsx\"",[153,506,265],{"class":208},[10,508,509],{},"Two string columns, a datetime column and three numeric ones is a reasonable approximation of a\nline-level export. Adjust the proportions towards whatever your real file looks like.",[138,511,513],{"id":512},"a-harness-that-measures-honestly","A harness that measures honestly",[10,515,516],{},"Three details separate a useful measurement from a misleading one: run each candidate more than\nonce and keep the best time rather than the mean, warm the page cache before timing, and record the\nrow count so a candidate that silently read half the sheet cannot look fast.",[143,518,520],{"className":195,"code":519,"language":197,"meta":148,"style":148},"import time\nfrom statistics import median\n\ndef bench(name, fn, repeats=3):\n    fn()                                    # warm the page cache, ignore this run\n    times = []\n    for _ in range(repeats):\n        start = time.perf_counter()\n        result = fn()\n        times.append(time.perf_counter() - start)\n    rows = getattr(result, \"height\", None) or len(result)\n    print(f\"{name:\u003C26} {min(times):6.2f}s  best   {median(times):6.2f}s  median   {rows:,} rows\")\n    return min(times)\n",[150,521,522,529,542,546,565,574,584,599,609,619,630,663,728],{"__ignoreMap":148},[153,523,524,526],{"class":155,"line":156},[153,525,205],{"class":204},[153,527,528],{"class":208}," time\n",[153,530,531,534,537,539],{"class":155,"line":218},[153,532,533],{"class":204},"from",[153,535,536],{"class":208}," statistics ",[153,538,205],{"class":204},[153,540,541],{"class":208}," median\n",[153,543,544],{"class":155,"line":231},[153,545,235],{"emptyLinePlaceholder":234},[153,547,548,551,555,558,560,562],{"class":155,"line":238},[153,549,550],{"class":204},"def",[153,552,554],{"class":553},"s_Opv"," bench",[153,556,557],{"class":208},"(name, fn, repeats",[153,559,244],{"class":204},[153,561,104],{"class":247},[153,563,564],{"class":208},"):\n",[153,566,567,570],{"class":155,"line":251},[153,568,569],{"class":208},"    fn()                                    ",[153,571,573],{"class":572},"s-wDw","# warm the page cache, ignore this run\n",[153,575,576,579,581],{"class":155,"line":268},[153,577,578],{"class":208},"    times ",[153,580,244],{"class":204},[153,582,583],{"class":208}," []\n",[153,585,586,589,592,594,596],{"class":155,"line":279},[153,587,588],{"class":204},"    for",[153,590,591],{"class":208}," _ ",[153,593,339],{"class":204},[153,595,342],{"class":247},[153,597,598],{"class":208},"(repeats):\n",[153,600,601,604,606],{"class":155,"line":302},[153,602,603],{"class":208},"        start ",[153,605,244],{"class":204},[153,607,608],{"class":208}," time.perf_counter()\n",[153,610,611,614,616],{"class":155,"line":354},[153,612,613],{"class":208},"        result ",[153,615,244],{"class":204},[153,617,618],{"class":208}," fn()\n",[153,620,621,624,627],{"class":155,"line":384},[153,622,623],{"class":208},"        times.append(time.perf_counter() ",[153,625,626],{"class":204},"-",[153,628,629],{"class":208}," start)\n",[153,631,632,635,637,640,643,646,648,651,654,657,660],{"class":155,"line":417},[153,633,634],{"class":208},"    rows ",[153,636,244],{"class":204},[153,638,639],{"class":247}," getattr",[153,641,642],{"class":208},"(result, ",[153,644,645],{"class":163},"\"height\"",[153,647,365],{"class":208},[153,649,650],{"class":247},"None",[153,652,653],{"class":208},") ",[153,655,656],{"class":204},"or",[153,658,659],{"class":247}," len",[153,661,662],{"class":208},"(result)\n",[153,664,665,668,670,672,674,676,679,682,684,687,690,693,696,698,701,703,706,708,710,713,715,718,721,723,726],{"class":155,"line":436},[153,666,667],{"class":247},"    print",[153,669,345],{"class":208},[153,671,311],{"class":204},[153,673,330],{"class":163},[153,675,318],{"class":317},[153,677,678],{"class":208},"name",[153,680,681],{"class":204},":\u003C26",[153,683,327],{"class":317},[153,685,686],{"class":317}," {",[153,688,689],{"class":247},"min",[153,691,692],{"class":208},"(times)",[153,694,695],{"class":204},":6.2f",[153,697,327],{"class":317},[153,699,700],{"class":163},"s  best   ",[153,702,318],{"class":317},[153,704,705],{"class":208},"median(times)",[153,707,695],{"class":204},[153,709,327],{"class":317},[153,711,712],{"class":163},"s  median   ",[153,714,318],{"class":317},[153,716,717],{"class":208},"rows",[153,719,720],{"class":204},":,",[153,722,327],{"class":317},[153,724,725],{"class":163}," rows\"",[153,727,265],{"class":208},[153,729,730,733,736],{"class":155,"line":459},[153,731,732],{"class":204},"    return",[153,734,735],{"class":247}," min",[153,737,738],{"class":208},"(times)\n",[10,740,741],{},"Keeping the minimum is deliberate. Anything that makes a run slower — another process, a garbage\ncollection pause, the scheduler — is noise added on top of the true cost, so the fastest observed\nrun is the closest estimate of it.",[138,743,745],{"id":744},"measure-the-readers","Measure the readers",[143,747,749],{"className":195,"code":748,"language":197,"meta":148,"style":148},"import pandas as pd\nimport polars as pl\nfrom openpyxl import load_workbook\n\ndef read_openpyxl():\n    return pd.read_excel(\"bench.xlsx\", sheet_name=\"Data\", engine=\"openpyxl\")\n\ndef read_calamine():\n    return pd.read_excel(\"bench.xlsx\", sheet_name=\"Data\", engine=\"calamine\")\n\ndef read_polars():\n    return pl.read_excel(\"bench.xlsx\", sheet_name=\"Data\")\n\ndef read_streaming():\n    book = load_workbook(\"bench.xlsx\", read_only=True, data_only=True)\n    rows = list(book[\"Data\"].iter_rows(min_row=2, values_only=True))\n    book.close()\n    return rows\n\nfor label, fn in [(\"pandas + openpyxl\", read_openpyxl),\n                  (\"pandas + calamine\", read_calamine),\n                  (\"polars.read_excel\", read_polars),\n                  (\"openpyxl read_only\", read_streaming)]:\n    bench(label, fn)\n",[150,750,751,761,773,785,789,799,828,832,841,868,872,881,900,904,913,946,983,989,997,1002,1022,1034,1045,1056],{"__ignoreMap":148},[153,752,753,755,757,759],{"class":155,"line":156},[153,754,205],{"class":204},[153,756,223],{"class":208},[153,758,212],{"class":204},[153,760,228],{"class":208},[153,762,763,765,768,770],{"class":155,"line":218},[153,764,205],{"class":204},[153,766,767],{"class":208}," polars ",[153,769,212],{"class":204},[153,771,772],{"class":208}," pl\n",[153,774,775,777,780,782],{"class":155,"line":231},[153,776,533],{"class":204},[153,778,779],{"class":208}," openpyxl ",[153,781,205],{"class":204},[153,783,784],{"class":208}," load_workbook\n",[153,786,787],{"class":155,"line":238},[153,788,235],{"emptyLinePlaceholder":234},[153,790,791,793,796],{"class":155,"line":251},[153,792,550],{"class":204},[153,794,795],{"class":553}," read_openpyxl",[153,797,798],{"class":208},"():\n",[153,800,801,803,806,808,810,812,814,816,818,821,823,826],{"class":155,"line":268},[153,802,732],{"class":204},[153,804,805],{"class":208}," pd.read_excel(",[153,807,471],{"class":163},[153,809,365],{"class":208},[153,811,476],{"class":398},[153,813,244],{"class":204},[153,815,481],{"class":163},[153,817,365],{"class":208},[153,819,820],{"class":398},"engine",[153,822,244],{"class":204},[153,824,825],{"class":163},"\"openpyxl\"",[153,827,265],{"class":208},[153,829,830],{"class":155,"line":279},[153,831,235],{"emptyLinePlaceholder":234},[153,833,834,836,839],{"class":155,"line":302},[153,835,550],{"class":204},[153,837,838],{"class":553}," read_calamine",[153,840,798],{"class":208},[153,842,843,845,847,849,851,853,855,857,859,861,863,866],{"class":155,"line":354},[153,844,732],{"class":204},[153,846,805],{"class":208},[153,848,471],{"class":163},[153,850,365],{"class":208},[153,852,476],{"class":398},[153,854,244],{"class":204},[153,856,481],{"class":163},[153,858,365],{"class":208},[153,860,820],{"class":398},[153,862,244],{"class":204},[153,864,865],{"class":163},"\"calamine\"",[153,867,265],{"class":208},[153,869,870],{"class":155,"line":384},[153,871,235],{"emptyLinePlaceholder":234},[153,873,874,876,879],{"class":155,"line":417},[153,875,550],{"class":204},[153,877,878],{"class":553}," read_polars",[153,880,798],{"class":208},[153,882,883,885,888,890,892,894,896,898],{"class":155,"line":436},[153,884,732],{"class":204},[153,886,887],{"class":208}," pl.read_excel(",[153,889,471],{"class":163},[153,891,365],{"class":208},[153,893,476],{"class":398},[153,895,244],{"class":204},[153,897,481],{"class":163},[153,899,265],{"class":208},[153,901,902],{"class":155,"line":459},[153,903,235],{"emptyLinePlaceholder":234},[153,905,906,908,911],{"class":155,"line":465},[153,907,550],{"class":204},[153,909,910],{"class":553}," read_streaming",[153,912,798],{"class":208},[153,914,915,918,920,923,925,927,930,932,935,937,940,942,944],{"class":155,"line":496},[153,916,917],{"class":208},"    book ",[153,919,244],{"class":204},[153,921,922],{"class":208}," load_workbook(",[153,924,471],{"class":163},[153,926,365],{"class":208},[153,928,929],{"class":398},"read_only",[153,931,244],{"class":204},[153,933,934],{"class":247},"True",[153,936,365],{"class":208},[153,938,939],{"class":398},"data_only",[153,941,244],{"class":204},[153,943,934],{"class":247},[153,945,265],{"class":208},[153,947,949,951,953,956,959,961,964,967,969,971,973,976,978,980],{"class":155,"line":948},16,[153,950,634],{"class":208},[153,952,244],{"class":204},[153,954,955],{"class":247}," list",[153,957,958],{"class":208},"(book[",[153,960,481],{"class":163},[153,962,963],{"class":208},"].iter_rows(",[153,965,966],{"class":398},"min_row",[153,968,244],{"class":204},[153,970,86],{"class":247},[153,972,365],{"class":208},[153,974,975],{"class":398},"values_only",[153,977,244],{"class":204},[153,979,934],{"class":247},[153,981,982],{"class":208},"))\n",[153,984,986],{"class":155,"line":985},17,[153,987,988],{"class":208},"    book.close()\n",[153,990,992,994],{"class":155,"line":991},18,[153,993,732],{"class":204},[153,995,996],{"class":208}," rows\n",[153,998,1000],{"class":155,"line":999},19,[153,1001,235],{"emptyLinePlaceholder":234},[153,1003,1005,1008,1011,1013,1016,1019],{"class":155,"line":1004},20,[153,1006,1007],{"class":204},"for",[153,1009,1010],{"class":208}," label, fn ",[153,1012,339],{"class":204},[153,1014,1015],{"class":208}," [(",[153,1017,1018],{"class":163},"\"pandas + openpyxl\"",[153,1020,1021],{"class":208},", read_openpyxl),\n",[153,1023,1025,1028,1031],{"class":155,"line":1024},21,[153,1026,1027],{"class":208},"                  (",[153,1029,1030],{"class":163},"\"pandas + calamine\"",[153,1032,1033],{"class":208},", read_calamine),\n",[153,1035,1037,1039,1042],{"class":155,"line":1036},22,[153,1038,1027],{"class":208},[153,1040,1041],{"class":163},"\"polars.read_excel\"",[153,1043,1044],{"class":208},", read_polars),\n",[153,1046,1048,1050,1053],{"class":155,"line":1047},23,[153,1049,1027],{"class":208},[153,1051,1052],{"class":163},"\"openpyxl read_only\"",[153,1054,1055],{"class":208},", read_streaming)]:\n",[153,1057,1059],{"class":155,"line":1058},24,[153,1060,1061],{"class":208},"    bench(label, fn)\n",[20,1063,29,1069,29,1072,29,1075,29,1079,29,1085,29,1092,29,1101,29,1107,29,1111,29,1114,29,1118,29,1123,29,1127,29,1130,29,1136,29,1140,29,1144,29,1147,29,1153,29,1158,29,1162],{"viewBox":1064,"role":23,"ariaLabelledBy":1065,"xmlns":27,"style":1068},"0 0 720 240",[1066,1067],"bn-read-t","bn-read-d","width:100%;max-width:720px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[31,1070,1071],{"id":1066},"A representative read ranking on a 200,000-row mixed-type sheet",[35,1073,1074],{"id":1067},"pandas with openpyxl is the slowest path, pandas and Polars with calamine are several times faster, and an openpyxl streaming scan that never builds a frame is faster still.",[39,1076],{"x":41,"y":41,"width":1077,"height":1078,"fill":44},"720","240",[61,1080,1084],{"x":1081,"y":1082,"style":1083},"20","56","font-size:12px;font-weight:600;fill:var(--text,#172033);text-anchor:start","pandas + openpyxl",[39,1086],{"x":1087,"y":430,"width":133,"height":1088,"rx":1089,"fill":1090,"stroke":1091},"200","26","6","#e7ebef","var(--line,#cdd5e6)",[39,1093],{"x":1094,"y":1095,"width":1096,"height":1097,"rx":1098,"fill":1099,"stroke":1100},"201","41","378.0","24","5","#fee8f2","var(--accent,#d81b73)",[61,1102,1106],{"x":1103,"y":1104,"style":1105},"592.0","58","font-size:12px;font-weight:700;fill:var(--accent,#d81b73);text-anchor:start","baseline",[61,1108,1110],{"x":1081,"y":1109,"style":1083},"100","pandas + calamine",[39,1112],{"x":1087,"y":1113,"width":133,"height":1088,"rx":1089,"fill":1090,"stroke":1091},"84",[39,1115],{"x":1094,"y":1116,"width":1117,"height":1097,"rx":1098,"fill":52,"stroke":53},"85","99.2",[61,1119,1122],{"x":1103,"y":1120,"style":1121},"102","font-size:12px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:start","Rust parser",[61,1124,1126],{"x":1081,"y":1125,"style":1083},"144","polars.read_excel",[39,1128],{"x":1087,"y":1129,"width":133,"height":1088,"rx":1089,"fill":1090,"stroke":1091},"128",[39,1131],{"x":1094,"y":1132,"width":1133,"height":1097,"rx":1098,"fill":1134,"stroke":1135},"129","83.9","#f0f4ff","var(--brand,#5b5cf0)",[61,1137,1122],{"x":1103,"y":1138,"style":1139},"146","font-size:12px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:start",[61,1141,1143],{"x":1081,"y":1142,"style":1083},"188","openpyxl read_only",[39,1145],{"x":1087,"y":1146,"width":133,"height":1088,"rx":1089,"fill":1090,"stroke":1091},"172",[39,1148],{"x":1094,"y":1149,"width":1150,"height":1097,"rx":1098,"fill":1151,"stroke":1152},"173","229.0","#fdefd8","var(--gold,#b4740a)",[61,1154,1157],{"x":1103,"y":1155,"style":1156},"190","font-size:12px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:start","no frame built",[61,1159,1161],{"x":1081,"y":1081,"style":1160},"font-size:11.5px;font-weight:600;fill:var(--muted,#5b6780);text-anchor:start","relative cost",[61,1163,1166],{"x":1164,"y":1165,"style":135},"360.0","230","your file decides the gap; only the ordering travels",[10,1168,1169],{},"The ordering is usually stable — the Rust parsers ahead of the Python one, streaming ahead of\nanything that builds a frame — but the size of the gap is not, and that is the number you are\nafter. On a workbook with a very large shared-strings table the gap widens; on a small sheet of\nfloats it can nearly vanish.",[138,1171,1173],{"id":1172},"measure-the-writers","Measure the writers",[10,1175,1176],{},"Writing is the half people forget to measure, and the one where the differences are largest.",[143,1178,1180],{"className":195,"code":1179,"language":197,"meta":148,"style":148},"import pandas as pd\nimport xlsxwriter\n\nframe = pd.read_excel(\"bench.xlsx\", sheet_name=\"Data\", engine=\"calamine\")\n\ndef write_openpyxl():\n    frame.to_excel(\"out-openpyxl.xlsx\", index=False, engine=\"openpyxl\")\n    return frame\n\ndef write_xlsxwriter():\n    frame.to_excel(\"out-xlsxwriter.xlsx\", index=False, engine=\"xlsxwriter\")\n    return frame\n\ndef write_constant_memory():\n    book = xlsxwriter.Workbook(\"out-constant.xlsx\", {\"constant_memory\": True})\n    sheet = book.add_worksheet(\"Data\")\n    sheet.write_row(0, 0, list(frame.columns))\n    for index, row in enumerate(frame.itertuples(index=False), start=1):\n        sheet.write_row(index, 0, [str(v) if hasattr(v, \"year\") else v for v in row])\n    book.close()\n    return frame\n\nfor label, fn in [(\"to_excel + openpyxl\", write_openpyxl),\n                  (\"to_excel + xlsxwriter\", write_xlsxwriter),\n                  (\"xlsxwriter constant\", write_constant_memory)]:\n    bench(label, fn, repeats=2)\n",[150,1181,1182,1192,1199,1203,1231,1235,1244,1270,1277,1281,1290,1316,1322,1326,1335,1360,1374,1393,1426,1471,1475,1481,1485,1501,1511,1522],{"__ignoreMap":148},[153,1183,1184,1186,1188,1190],{"class":155,"line":156},[153,1185,205],{"class":204},[153,1187,223],{"class":208},[153,1189,212],{"class":204},[153,1191,228],{"class":208},[153,1193,1194,1196],{"class":155,"line":218},[153,1195,205],{"class":204},[153,1197,1198],{"class":208}," xlsxwriter\n",[153,1200,1201],{"class":155,"line":231},[153,1202,235],{"emptyLinePlaceholder":234},[153,1204,1205,1207,1209,1211,1213,1215,1217,1219,1221,1223,1225,1227,1229],{"class":155,"line":238},[153,1206,271],{"class":208},[153,1208,244],{"class":204},[153,1210,805],{"class":208},[153,1212,471],{"class":163},[153,1214,365],{"class":208},[153,1216,476],{"class":398},[153,1218,244],{"class":204},[153,1220,481],{"class":163},[153,1222,365],{"class":208},[153,1224,820],{"class":398},[153,1226,244],{"class":204},[153,1228,865],{"class":163},[153,1230,265],{"class":208},[153,1232,1233],{"class":155,"line":251},[153,1234,235],{"emptyLinePlaceholder":234},[153,1236,1237,1239,1242],{"class":155,"line":268},[153,1238,550],{"class":204},[153,1240,1241],{"class":553}," write_openpyxl",[153,1243,798],{"class":208},[153,1245,1246,1249,1252,1254,1256,1258,1260,1262,1264,1266,1268],{"class":155,"line":279},[153,1247,1248],{"class":208},"    frame.to_excel(",[153,1250,1251],{"class":163},"\"out-openpyxl.xlsx\"",[153,1253,365],{"class":208},[153,1255,486],{"class":398},[153,1257,244],{"class":204},[153,1259,491],{"class":247},[153,1261,365],{"class":208},[153,1263,820],{"class":398},[153,1265,244],{"class":204},[153,1267,825],{"class":163},[153,1269,265],{"class":208},[153,1271,1272,1274],{"class":155,"line":302},[153,1273,732],{"class":204},[153,1275,1276],{"class":208}," frame\n",[153,1278,1279],{"class":155,"line":354},[153,1280,235],{"emptyLinePlaceholder":234},[153,1282,1283,1285,1288],{"class":155,"line":384},[153,1284,550],{"class":204},[153,1286,1287],{"class":553}," write_xlsxwriter",[153,1289,798],{"class":208},[153,1291,1292,1294,1297,1299,1301,1303,1305,1307,1309,1311,1314],{"class":155,"line":417},[153,1293,1248],{"class":208},[153,1295,1296],{"class":163},"\"out-xlsxwriter.xlsx\"",[153,1298,365],{"class":208},[153,1300,486],{"class":398},[153,1302,244],{"class":204},[153,1304,491],{"class":247},[153,1306,365],{"class":208},[153,1308,820],{"class":398},[153,1310,244],{"class":204},[153,1312,1313],{"class":163},"\"xlsxwriter\"",[153,1315,265],{"class":208},[153,1317,1318,1320],{"class":155,"line":436},[153,1319,732],{"class":204},[153,1321,1276],{"class":208},[153,1323,1324],{"class":155,"line":459},[153,1325,235],{"emptyLinePlaceholder":234},[153,1327,1328,1330,1333],{"class":155,"line":465},[153,1329,550],{"class":204},[153,1331,1332],{"class":553}," write_constant_memory",[153,1334,798],{"class":208},[153,1336,1337,1339,1341,1344,1347,1350,1353,1356,1358],{"class":155,"line":496},[153,1338,917],{"class":208},[153,1340,244],{"class":204},[153,1342,1343],{"class":208}," xlsxwriter.Workbook(",[153,1345,1346],{"class":163},"\"out-constant.xlsx\"",[153,1348,1349],{"class":208},", {",[153,1351,1352],{"class":163},"\"constant_memory\"",[153,1354,1355],{"class":208},": ",[153,1357,934],{"class":247},[153,1359,462],{"class":208},[153,1361,1362,1365,1367,1370,1372],{"class":155,"line":948},[153,1363,1364],{"class":208},"    sheet ",[153,1366,244],{"class":204},[153,1368,1369],{"class":208}," book.add_worksheet(",[153,1371,481],{"class":163},[153,1373,265],{"class":208},[153,1375,1376,1379,1381,1383,1385,1387,1390],{"class":155,"line":985},[153,1377,1378],{"class":208},"    sheet.write_row(",[153,1380,41],{"class":247},[153,1382,365],{"class":208},[153,1384,41],{"class":247},[153,1386,365],{"class":208},[153,1388,1389],{"class":247},"list",[153,1391,1392],{"class":208},"(frame.columns))\n",[153,1394,1395,1397,1400,1402,1405,1408,1410,1412,1414,1417,1420,1422,1424],{"class":155,"line":991},[153,1396,588],{"class":204},[153,1398,1399],{"class":208}," index, row ",[153,1401,339],{"class":204},[153,1403,1404],{"class":247}," enumerate",[153,1406,1407],{"class":208},"(frame.itertuples(",[153,1409,486],{"class":398},[153,1411,244],{"class":204},[153,1413,491],{"class":247},[153,1415,1416],{"class":208},"), ",[153,1418,1419],{"class":398},"start",[153,1421,244],{"class":204},[153,1423,65],{"class":247},[153,1425,564],{"class":208},[153,1427,1428,1431,1433,1436,1439,1442,1445,1448,1451,1454,1456,1459,1462,1464,1466,1468],{"class":155,"line":999},[153,1429,1430],{"class":208},"        sheet.write_row(index, ",[153,1432,41],{"class":247},[153,1434,1435],{"class":208},", [",[153,1437,1438],{"class":247},"str",[153,1440,1441],{"class":208},"(v) ",[153,1443,1444],{"class":204},"if",[153,1446,1447],{"class":247}," hasattr",[153,1449,1450],{"class":208},"(v, ",[153,1452,1453],{"class":163},"\"year\"",[153,1455,653],{"class":208},[153,1457,1458],{"class":204},"else",[153,1460,1461],{"class":208}," v ",[153,1463,1007],{"class":204},[153,1465,1461],{"class":208},[153,1467,339],{"class":204},[153,1469,1470],{"class":208}," row])\n",[153,1472,1473],{"class":155,"line":1004},[153,1474,988],{"class":208},[153,1476,1477,1479],{"class":155,"line":1024},[153,1478,732],{"class":204},[153,1480,1276],{"class":208},[153,1482,1483],{"class":155,"line":1036},[153,1484,235],{"emptyLinePlaceholder":234},[153,1486,1487,1489,1491,1493,1495,1498],{"class":155,"line":1047},[153,1488,1007],{"class":204},[153,1490,1010],{"class":208},[153,1492,339],{"class":204},[153,1494,1015],{"class":208},[153,1496,1497],{"class":163},"\"to_excel + openpyxl\"",[153,1499,1500],{"class":208},", write_openpyxl),\n",[153,1502,1503,1505,1508],{"class":155,"line":1058},[153,1504,1027],{"class":208},[153,1506,1507],{"class":163},"\"to_excel + xlsxwriter\"",[153,1509,1510],{"class":208},", write_xlsxwriter),\n",[153,1512,1514,1516,1519],{"class":155,"line":1513},25,[153,1515,1027],{"class":208},[153,1517,1518],{"class":163},"\"xlsxwriter constant\"",[153,1520,1521],{"class":208},", write_constant_memory)]:\n",[153,1523,1525,1528,1531,1533,1535],{"class":155,"line":1524},26,[153,1526,1527],{"class":208},"    bench(label, fn, ",[153,1529,1530],{"class":398},"repeats",[153,1532,244],{"class":204},[153,1534,86],{"class":247},[153,1536,265],{"class":208},[10,1538,1539,1542,1543,1547],{},[150,1540,1541],{},"constant_memory"," mode changes the memory profile far more than the clock: it flushes each row as\nit is written, so the process footprint stops tracking the row count. The details are in\n",[14,1544,1546],{"href":1545},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fbuilding-excel-reports-with-xlsxwriter\u002Fwrite-a-million-rows-to-excel-with-xlsxwriter-constant-memory\u002F","Write a Million Rows to Excel with XlsxWriter in Constant Memory",".",[138,1549,1551],{"id":1550},"measure-memory-not-just-time","Measure memory, not just time",[10,1553,1554,1555,1558],{},"A job that finishes in four seconds and peaks at 3 GB will still be killed by a container limit.\n",[150,1556,1557],{},"tracemalloc"," covers everything allocated through Python, which is exactly what openpyxl's cell\nobjects are.",[143,1560,1562],{"className":195,"code":1561,"language":197,"meta":148,"style":148},"import tracemalloc\n\ndef peak_mb(fn):\n    tracemalloc.start()\n    fn()\n    _, peak = tracemalloc.get_traced_memory()\n    tracemalloc.stop()\n    return peak \u002F 1_048_576\n\nprint(f\"openpyxl frame : {peak_mb(read_openpyxl):7.1f} MB\")\nprint(f\"read_only scan : {peak_mb(read_streaming):7.1f} MB\")\n",[150,1563,1564,1571,1575,1585,1590,1595,1605,1610,1623,1627,1653],{"__ignoreMap":148},[153,1565,1566,1568],{"class":155,"line":156},[153,1567,205],{"class":204},[153,1569,1570],{"class":208}," tracemalloc\n",[153,1572,1573],{"class":155,"line":218},[153,1574,235],{"emptyLinePlaceholder":234},[153,1576,1577,1579,1582],{"class":155,"line":231},[153,1578,550],{"class":204},[153,1580,1581],{"class":553}," peak_mb",[153,1583,1584],{"class":208},"(fn):\n",[153,1586,1587],{"class":155,"line":238},[153,1588,1589],{"class":208},"    tracemalloc.start()\n",[153,1591,1592],{"class":155,"line":251},[153,1593,1594],{"class":208},"    fn()\n",[153,1596,1597,1600,1602],{"class":155,"line":268},[153,1598,1599],{"class":208},"    _, peak ",[153,1601,244],{"class":204},[153,1603,1604],{"class":208}," tracemalloc.get_traced_memory()\n",[153,1606,1607],{"class":155,"line":279},[153,1608,1609],{"class":208},"    tracemalloc.stop()\n",[153,1611,1612,1614,1617,1620],{"class":155,"line":302},[153,1613,732],{"class":204},[153,1615,1616],{"class":208}," peak ",[153,1618,1619],{"class":204},"\u002F",[153,1621,1622],{"class":247}," 1_048_576\n",[153,1624,1625],{"class":155,"line":354},[153,1626,235],{"emptyLinePlaceholder":234},[153,1628,1629,1631,1633,1635,1638,1640,1643,1646,1648,1651],{"class":155,"line":384},[153,1630,499],{"class":247},[153,1632,345],{"class":208},[153,1634,311],{"class":204},[153,1636,1637],{"class":163},"\"openpyxl frame : ",[153,1639,318],{"class":317},[153,1641,1642],{"class":208},"peak_mb(read_openpyxl)",[153,1644,1645],{"class":204},":7.1f",[153,1647,327],{"class":317},[153,1649,1650],{"class":163}," MB\"",[153,1652,265],{"class":208},[153,1654,1655,1657,1659,1661,1664,1666,1669,1671,1673,1675],{"class":155,"line":417},[153,1656,499],{"class":247},[153,1658,345],{"class":208},[153,1660,311],{"class":204},[153,1662,1663],{"class":163},"\"read_only scan : ",[153,1665,318],{"class":317},[153,1667,1668],{"class":208},"peak_mb(read_streaming)",[153,1670,1645],{"class":204},[153,1672,327],{"class":317},[153,1674,1650],{"class":163},[153,1676,265],{"class":208},[10,1678,1679,1680,1682],{},"For calamine and Polars, which allocate in Rust rather than through the Python allocator,\n",[150,1681,1557],{}," under-reports badly. Use the resident set size instead:",[143,1684,1686],{"className":195,"code":1685,"language":197,"meta":148,"style":148},"import resource\n\ndef peak_rss_mb():\n    return resource.getrusage(resource.RUSAGE_SELF).ru_maxrss \u002F 1024   # KB on Linux\n",[150,1687,1688,1695,1699,1708],{"__ignoreMap":148},[153,1689,1690,1692],{"class":155,"line":156},[153,1691,205],{"class":204},[153,1693,1694],{"class":208}," resource\n",[153,1696,1697],{"class":155,"line":218},[153,1698,235],{"emptyLinePlaceholder":234},[153,1700,1701,1703,1706],{"class":155,"line":231},[153,1702,550],{"class":204},[153,1704,1705],{"class":553}," peak_rss_mb",[153,1707,798],{"class":208},[153,1709,1710,1712,1715,1718,1721,1723,1726],{"class":155,"line":238},[153,1711,732],{"class":204},[153,1713,1714],{"class":208}," resource.getrusage(resource.",[153,1716,1717],{"class":247},"RUSAGE_SELF",[153,1719,1720],{"class":208},").ru_maxrss ",[153,1722,1619],{"class":204},[153,1724,1725],{"class":247}," 1024",[153,1727,1728],{"class":572},"   # KB on Linux\n",[138,1730,1732],{"id":1731},"what-to-record-alongside-the-numbers","What to record alongside the numbers",[10,1734,1735],{},"A benchmark that is not reproducible next quarter is a screenshot, not a measurement. Four things\nbelong in the same file as the timings: the library versions, the machine, the file's shape, and\nwhether the run was warm or cold. Without them a result cannot be compared against the one you take\nafter an upgrade, which is the comparison that eventually matters.",[143,1737,1739],{"className":195,"code":1738,"language":197,"meta":148,"style":148},"import platform\nimport pandas as pd\nimport polars as pl\nimport openpyxl\nimport xlsxwriter\n\nprint({\n    \"python\": platform.python_version(),\n    \"machine\": platform.machine(),\n    \"pandas\": pd.__version__,\n    \"polars\": pl.__version__,\n    \"openpyxl\": openpyxl.__version__,\n    \"xlsxwriter\": xlsxwriter.__version__,\n    \"rows\": len(frame),\n    \"columns\": list(frame.columns),\n})\n",[150,1740,1741,1748,1758,1768,1775,1781,1785,1792,1800,1808,1822,1834,1846,1858,1871,1883],{"__ignoreMap":148},[153,1742,1743,1745],{"class":155,"line":156},[153,1744,205],{"class":204},[153,1746,1747],{"class":208}," platform\n",[153,1749,1750,1752,1754,1756],{"class":155,"line":218},[153,1751,205],{"class":204},[153,1753,223],{"class":208},[153,1755,212],{"class":204},[153,1757,228],{"class":208},[153,1759,1760,1762,1764,1766],{"class":155,"line":231},[153,1761,205],{"class":204},[153,1763,767],{"class":208},[153,1765,212],{"class":204},[153,1767,772],{"class":208},[153,1769,1770,1772],{"class":155,"line":238},[153,1771,205],{"class":204},[153,1773,1774],{"class":208}," openpyxl\n",[153,1776,1777,1779],{"class":155,"line":251},[153,1778,205],{"class":204},[153,1780,1198],{"class":208},[153,1782,1783],{"class":155,"line":268},[153,1784,235],{"emptyLinePlaceholder":234},[153,1786,1787,1789],{"class":155,"line":279},[153,1788,499],{"class":247},[153,1790,1791],{"class":208},"({\n",[153,1793,1794,1797],{"class":155,"line":302},[153,1795,1796],{"class":163},"    \"python\"",[153,1798,1799],{"class":208},": platform.python_version(),\n",[153,1801,1802,1805],{"class":155,"line":354},[153,1803,1804],{"class":163},"    \"machine\"",[153,1806,1807],{"class":208},": platform.machine(),\n",[153,1809,1810,1813,1816,1819],{"class":155,"line":384},[153,1811,1812],{"class":163},"    \"pandas\"",[153,1814,1815],{"class":208},": pd.",[153,1817,1818],{"class":247},"__version__",[153,1820,1821],{"class":208},",\n",[153,1823,1824,1827,1830,1832],{"class":155,"line":417},[153,1825,1826],{"class":163},"    \"polars\"",[153,1828,1829],{"class":208},": pl.",[153,1831,1818],{"class":247},[153,1833,1821],{"class":208},[153,1835,1836,1839,1842,1844],{"class":155,"line":436},[153,1837,1838],{"class":163},"    \"openpyxl\"",[153,1840,1841],{"class":208},": openpyxl.",[153,1843,1818],{"class":247},[153,1845,1821],{"class":208},[153,1847,1848,1851,1854,1856],{"class":155,"line":459},[153,1849,1850],{"class":163},"    \"xlsxwriter\"",[153,1852,1853],{"class":208},": xlsxwriter.",[153,1855,1818],{"class":247},[153,1857,1821],{"class":208},[153,1859,1860,1863,1865,1868],{"class":155,"line":465},[153,1861,1862],{"class":163},"    \"rows\"",[153,1864,1355],{"class":208},[153,1866,1867],{"class":247},"len",[153,1869,1870],{"class":208},"(frame),\n",[153,1872,1873,1876,1878,1880],{"class":155,"line":496},[153,1874,1875],{"class":163},"    \"columns\"",[153,1877,1355],{"class":208},[153,1879,1389],{"class":247},[153,1881,1882],{"class":208},"(frame.columns),\n",[153,1884,1885],{"class":155,"line":948},[153,1886,462],{"class":208},[10,1888,1889],{},"Version drift in this ecosystem is real and mostly in your favour — calamine support arrived in\npandas 2.2, and Polars' Excel reader has changed engines more than once — so a benchmark that\nrecords its versions turns an upgrade into a decision you can measure rather than a leap.",[138,1891,1893],{"id":1892},"reading-the-result-not-just-the-ranking","Reading the result, not just the ranking",[10,1895,1896],{},"The ranking is the least interesting part of the output. Three patterns in the numbers tell you\nwhat to do next.",[10,1898,1899],{},"If the streaming scan is close to the frame-building readers, the bottleneck is parsing rather than\nmemory allocation, and the fix is a faster parser. If it is far ahead, the cost is in building the\nobjects, and the fix is to avoid building them — stream, or prune columns.",[10,1901,1902,1903,1907],{},"If the writers are slower than the readers, the output is doing more work than the input, which\nusually means styling applied cell by cell. Setting a format on a column once, as\n",[14,1904,1906],{"href":1905},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fstyling-excel-cells-with-openpyxl\u002Fapply-a-reusable-style-theme-across-an-excel-report\u002F","Apply a Reusable Style Theme Across an Excel Report","\ndescribes, collapses that cost.",[10,1909,1910,1911,1915],{},"And if every candidate is within a few percent of the others, the file is small enough that library\nchoice is not your problem. Spend the effort on correctness instead — the checks in\n",[14,1912,1914],{"href":1913},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fvalidate-an-excel-report-before-sending-it\u002F","Validate an Excel Report Before Sending It","\nwill save more time than any parser will.",[138,1917,1919],{"id":1918},"common-pitfalls","Common pitfalls",[1921,1922,1923,1939],"table",{},[1924,1925,1926],"thead",{},[1927,1928,1929,1933,1936],"tr",{},[1930,1931,1932],"th",{},"Symptom",[1930,1934,1935],{},"Cause",[1930,1937,1938],{},"Fix",[1940,1941,1942,1954,1965,1976,1987,1998],"tbody",{},[1927,1943,1944,1948,1951],{},[1945,1946,1947],"td",{},"Every library looks equally fast",[1945,1949,1950],{},"The test file is too small for parse time to dominate",[1945,1952,1953],{},"Benchmark at production scale, or generate ten times the rows",[1927,1955,1956,1959,1962],{},[1945,1957,1958],{},"The first candidate is always slowest",[1945,1960,1961],{},"Cold page cache on the first read",[1945,1963,1964],{},"Warm with an untimed call, as the harness above does",[1927,1966,1967,1970,1973],{},[1945,1968,1969],{},"A candidate is impossibly fast",[1945,1971,1972],{},"It read fewer rows — wrong sheet, or a header offset",[1945,1974,1975],{},"Print the row count with every result and compare them",[1927,1977,1978,1981,1984],{},[1945,1979,1980],{},"Times swing by 50% between runs",[1945,1982,1983],{},"Another process, or CPU frequency scaling",[1945,1985,1986],{},"Take the minimum of several runs, and close other work",[1927,1988,1989,1992,1995],{},[1945,1990,1991],{},"calamine is not faster",[1945,1993,1994],{},"The file is small, or dominated by dates needing conversion",[1945,1996,1997],{},"Check the column mix; the advantage is largest on wide string-heavy sheets",[1927,1999,2000,2003,2006],{},[1945,2001,2002],{},"Memory numbers look wrong for Polars",[1945,2004,2005],{},"Arrow buffers are allocated outside Python",[1945,2007,2008,2009],{},"Measure RSS rather than ",[150,2010,1557],{},[138,2012,2014],{"id":2013},"performance-and-scale","Performance and scale",[20,2016,29,2021,29,2024,29,2027,29,2029,29,2035,29,2041,29,2047,29,2052,29,2056,29,2060,29,2063,29,2068,29,2072,29,2075,29,2078,29,2081,29,2088,29,2093,29,2098,29,2102,29,2106,29,2109],{"viewBox":2017,"role":23,"ariaLabelledBy":2018,"xmlns":27,"style":28},"0 0 760 201",[2019,2020],"bn-cells-t","bn-cells-d",[31,2022,2023],{"id":2019},"Parse cost tracks cells, not rows",[35,2025,2026],{"id":2020},"Halving the number of columns read reduces the work as reliably as halving the rows, and converting the sheet once to a columnar file makes every later read almost free.",[39,2028],{"x":41,"y":41,"width":42,"height":1094,"fill":44},[39,2030],{"x":1081,"y":48,"width":2031,"height":2032,"rx":2033,"fill":1099,"stroke":1100,"style":2034},"270.0","139","14","stroke-width:2px",[61,2036,2040],{"x":2037,"y":2038,"style":2039},"155.0","54","font-size:13px;font-weight:700;fill:var(--accent,#d81b73);text-anchor:middle","read the whole sheet",[155,2042],{"x1":2043,"y1":2044,"x2":2045,"y2":2044,"stroke":1100,"style":2046},"36","64","274.0","stroke-width:1px",[61,2048,2051],{"x":2037,"y":2049,"style":2050},"86","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","all 40 columns",[61,2053,2055],{"x":2037,"y":2054,"style":2050},"109","every cell parsed",[61,2057,2059],{"x":2037,"y":2058,"style":2050},"132","same cost every run",[39,2061],{"x":2062,"y":48,"width":2031,"height":2032,"rx":2033,"fill":52,"stroke":53,"style":2034},"470.0",[61,2064,2067],{"x":2065,"y":2038,"style":2066},"605.0","font-size:13px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","prune, then convert",[155,2069],{"x1":2070,"y1":2044,"x2":2071,"y2":2044,"stroke":53,"style":2046},"486.0","724.0",[61,2073,2074],{"x":2065,"y":2049,"style":2050},"6 columns via usecols",[61,2076,2077],{"x":2065,"y":2054,"style":2050},"converted once",[61,2079,2080],{"x":2065,"y":2058,"style":2050},"later reads near-free",[39,2082],{"x":2083,"y":2084,"width":1129,"height":2085,"rx":2086,"fill":2087,"stroke":1135},"316.0","78.5","38","19","#ebebfd",[61,2089,2092],{"x":133,"y":2090,"style":2091},"102.5","font-size:12.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","usecols",[155,2094],{"x1":2095,"y1":2096,"x2":2097,"y2":2096,"stroke":1135,"style":2034},"295.0","97.5","309.0",[2099,2100],"polygon",{"points":2101,"fill":59},"309.0,97.5 300.0,92.5 300.0,102.5",[155,2103],{"x1":2104,"y1":2096,"x2":2105,"y2":2096,"stroke":1135,"style":2034},"449.0","463.0",[2099,2107],{"points":2108,"fill":59},"463.0,97.5 454.0,92.5 454.0,102.5",[61,2110,2112],{"x":133,"y":2111,"style":135},"187","the cheapest optimisation is not reading the cell at all",[10,2114,2115,2116,2118,2119,2123],{},"Two effects dominate anything else you will measure. Parsing cost scales with cells, not rows, so\nhalving the columns with ",[150,2117,2092],{}," halves the work as reliably as halving the rows — the technique\nin ",[14,2120,2122],{"href":2121},"\u002Fgetting-started-with-python-excel-automation\u002Freading-excel-files-with-pandas\u002Fread-specific-columns-from-excel-with-pandas\u002F","Read Specific Columns from Excel with Pandas",".\nAnd repeated reads of an unchanged file are pure waste: converting once to Parquet turns a\nmulti-second parse into a fraction of a second for every run afterwards.",[10,2125,2126,2127,2130],{},"That reframes the benchmark's real purpose. You are not looking for the fastest library so much as\nfor the point where the format itself is the bottleneck — and past that point the answer is to stop\nreading ",[150,2128,2129],{},".xlsx"," on every run, not to shave 20% off the parser.",[138,2132,2134],{"id":2133},"conclusion","Conclusion",[10,2136,2137],{},"Benchmark the file you have, not the one in someone else's article. A harness of fifteen lines —\nwarm-up, repeats, minimum time, a printed row count — is enough to rank the readers and writers on\nyour own data, and pairing it with a peak-memory number turns \"which is faster\" into the question\nthat actually decides deployments: what fits in the container. Expect the Rust parsers ahead of\nopenpyxl, streaming ahead of frame-building, and a converted columnar copy ahead of all of them.",[138,2139,2141],{"id":2140},"frequently-asked-questions","Frequently asked questions",[10,2143,2144,2148],{},[2145,2146,2147],"strong",{},"Why are my numbers different from every benchmark I read online?","\nBecause the file decides. A sheet of floats parses several times faster than the same number of cells holding formatted dates and strings, and a workbook with heavy styling carries a shared-strings table and a style index that both have to be parsed. Benchmark your own file; published ratios are only a starting hypothesis.",[10,2150,2151,2154],{},[2145,2152,2153],{},"Should I use timeit instead of time.perf_counter?","\nFor an operation measured in seconds, perf_counter is fine and far easier to read. timeit earns its place for microsecond-scale calls where loop overhead would dominate — not for reading a workbook.",[10,2156,2157,2160],{},[2145,2158,2159],{},"Does the first run being slower mean the benchmark is wrong?","\nIt means the operating system's page cache was cold. Run once to warm it, then measure — or measure both deliberately, because a scheduled job that reads a file straight off a network share never gets the warm case.",[10,2162,2163,2166],{},[2145,2164,2165],{},"How do I measure memory as well as time?","\ntracemalloc measures Python-allocated memory, which covers openpyxl's cell objects well. For libraries that allocate outside Python — calamine and Polars both do — read the process RSS from resource.getrusage or psutil instead.",[138,2168,2170],{"id":2169},"related","Related",[2172,2173,2174,2181,2188,2195,2202],"ul",{},[2175,2176,2177,2178,2180],"li",{},"Up one level: ",[14,2179,17],{"href":16}," — the capability comparison these numbers support.",[2175,2182,2183,2187],{},[14,2184,2186],{"href":2185},"\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"," — the single biggest read-side change, in one keyword argument.",[2175,2189,2190,2194],{},[14,2191,2193],{"href":2192},"\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"," — what streaming gives up in exchange for a flat memory curve.",[2175,2196,2197,2201],{},[14,2198,2200],{"href":2199},"\u002Fadvanced-data-transformation-and-cleaning\u002Freading-excel-with-polars-and-arrow\u002Fconvert-excel-files-to-parquet-with-python\u002F","Convert Excel Files to Parquet with Python"," — the change that beats every parser choice on repeated reads.",[2175,2203,2204,2208],{},[14,2205,2207],{"href":2206},"\u002Fgetting-started-with-python-excel-automation\u002Fchoosing-a-python-excel-library\u002Fpandas-vs-polars-for-excel-workflows\u002F","pandas vs Polars for Excel Workflows"," — the transform half of the same question.",[2210,2211,2212],"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 .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}html pre.shiki code .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}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}",{"title":148,"searchDepth":218,"depth":218,"links":2214},[2215,2216,2217,2218,2219,2220,2221,2222,2223,2224,2225,2226,2227],{"id":140,"depth":218,"text":141},{"id":188,"depth":218,"text":189},{"id":512,"depth":218,"text":513},{"id":744,"depth":218,"text":745},{"id":1172,"depth":218,"text":1173},{"id":1550,"depth":218,"text":1551},{"id":1731,"depth":218,"text":1732},{"id":1892,"depth":218,"text":1893},{"id":1918,"depth":218,"text":1919},{"id":2013,"depth":218,"text":2014},{"id":2133,"depth":218,"text":2134},{"id":2140,"depth":218,"text":2141},{"id":2169,"depth":218,"text":2170},"2026-09-04","Published speed ratios are hypotheses about your file. Build a small harness that times openpyxl, calamine, Polars and xlsxwriter on the workbook you actually run.","md",[2232,2234,2236,2238],{"q":2147,"a":2233},"Because the file decides. A sheet of floats parses several times faster than the same number of cells holding formatted dates and strings, and a workbook with heavy styling carries a shared-strings table and a style index that both have to be parsed. Benchmark your own file; published ratios are only a starting hypothesis.",{"q":2153,"a":2235},"For an operation measured in seconds, perf_counter is fine and far easier to read. timeit earns its place for microsecond-scale calls where loop overhead would dominate — not for reading a workbook.",{"q":2159,"a":2237},"It means the operating system's page cache was cold. Run once to warm it, then measure — or measure both deliberately, because a scheduled job that reads a file straight off a network share never gets the warm case.",{"q":2165,"a":2239},"tracemalloc measures Python-allocated memory, which covers openpyxl's cell objects well. For libraries that allocate outside Python — calamine and Polars both do — read the process RSS from resource.getrusage or psutil instead.",{"breadcrumb":2241},[2242,2244,2247],{"name":2243,"item":1619},"Home",{"name":2245,"item":2246},"Getting Started with Python Excel Automation","\u002Fgetting-started-with-python-excel-automation\u002F",{"name":17,"item":16},"\u002Fgetting-started-with-python-excel-automation\u002Fchoosing-a-python-excel-library\u002Fbenchmark-python-excel-read-and-write-speed",{"title":5,"description":2250},"A repeatable harness for timing pandas, Polars, openpyxl, calamine and xlsxwriter on your own workbook — with warm-up, repeats, row-count checks and peak memory.","benchmark-python-excel-read-and-write-speed","getting-started-with-python-excel-automation\u002Fchoosing-a-python-excel-library\u002Fbenchmark-python-excel-read-and-write-speed\u002Findex","how-to","eEXRSHaFSQtAQoWOE1BLarzpsP4szz2D3SmHWmgmGUY",[2256,2259],{"title":17,"path":2257,"stem":2258,"children":-1},"\u002Fgetting-started-with-python-excel-automation\u002Fchoosing-a-python-excel-library","getting-started-with-python-excel-automation\u002Fchoosing-a-python-excel-library\u002Findex",{"title":2260,"path":2261,"stem":2262,"children":-1},"Excel vs CSV vs Parquet for Python Data Pipelines","\u002Fgetting-started-with-python-excel-automation\u002Fchoosing-a-python-excel-library\u002Fexcel-vs-csv-vs-parquet-for-python-data-pipelines","getting-started-with-python-excel-automation\u002Fchoosing-a-python-excel-library\u002Fexcel-vs-csv-vs-parquet-for-python-data-pipelines\u002Findex",1788710154531]