[{"data":1,"prerenderedAt":2218},["ShallowReactive",2],{"doc:\u002Fgetting-started-with-python-excel-automation\u002Fchoosing-a-python-excel-library":3,"surround:\u002Fgetting-started-with-python-excel-automation\u002Fchoosing-a-python-excel-library":2210},{"id":4,"title":5,"body":6,"dateModified":2178,"datePublished":2178,"description":2179,"extension":2180,"faq":2181,"meta":2194,"navigation":342,"path":2202,"seo":2203,"slug":2206,"stem":2207,"type":2208,"__hash__":2209},"docs\u002Fgetting-started-with-python-excel-automation\u002Fchoosing-a-python-excel-library\u002Findex.md","Choosing a Python Excel Library",{"type":7,"value":8,"toc":2163},"minimark",[9,24,150,153,158,161,297,310,441,445,448,544,552,693,701,705,712,802,813,1000,1007,1011,1022,1109,1117,1121,1124,1141,1145,1148,1218,1478,1481,1485,1492,1590,1604,1781,1789,1793,1796,1803,1809,1812,1906,1910,1913,1921,1925,1928,1970,1977,1992,2009,2016,2024,2028,2052,2056,2063,2069,2075,2081,2087,2093,2097,2159],[10,11,12,13,18,19,23],"p",{},"Every Python Excel job starts with the same question: which library. The ecosystem has five real\nanswers — openpyxl, xlsxwriter, pandas, Polars and xlwings — plus a handful of engines that sit\nunderneath them, and the honest summary is that they are not competitors so much as tools for\ndifferent halves of the job. This guide, part of\n",[14,15,17],"a",{"href":16},"\u002Fgetting-started-with-python-excel-automation\u002F","Getting Started with Python Excel Automation",",\nmaps the landscape by the decision that actually drives it: what you need to ",[20,21,22],"em",{},"do"," to the file.",[25,26,34,35,34,39,34,43,34,50,34,60,34,67,34,75,34,81,34,86,34,91,34,96,34,102,34,107,34,112,34,115,34,117,34,120,34,124,34,128,34,133,34,136,34,138,34,141,34,145],"svg",{"viewBox":27,"role":28,"ariaLabelledBy":29,"xmlns":32,"style":33},"0 0 760 264","img",[30,31],"cpl-pick-t","cpl-pick-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  ",[36,37,38],"title",{"id":30},"Choosing a Python Excel library by the job at hand",[40,41,42],"desc",{"id":31},"A decision node asks what the job needs to do to the file. Shaping data leads to pandas or Polars, producing a new formatted file leads to xlsxwriter, and editing an existing workbook or driving Excel leads to openpyxl or xlwings.",[44,45],"rect",{"x":46,"y":46,"width":47,"height":48,"fill":49},"0","760","264","#ffffff",[44,51],{"x":52,"y":53,"width":54,"height":55,"rx":56,"fill":57,"stroke":58,"style":59},"230.0","26","300","56","12","#ebebfd","var(--brand,#5b5cf0)","stroke-width:2px",[61,62,66],"text",{"x":63,"y":64,"style":65},"380.0","60","font-size:13.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","What does the job do to the file?",[44,68],{"x":69,"y":70,"width":71,"height":72,"rx":56,"fill":73,"stroke":74,"style":59},"18.0","148","226.7","84","#d9f4f1","var(--teal,#0f9488)",[61,76,80],{"x":77,"y":78,"style":79},"131.35","182","font-size:13px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","pandas \u002F Polars",[61,82,85],{"x":77,"y":83,"style":84},"204","font-size:11.5px;font-weight:400;fill:var(--muted,#5b6780);text-anchor:middle","group, join, reshape",[87,88],"line",{"x1":77,"y1":89,"x2":77,"y2":90,"stroke":58,"style":59},"87","141",[92,93],"polygon",{"points":94,"fill":95},"131.35,141 126.35,132 136.35,132","#5b5cf0",[61,97,101],{"x":98,"y":99,"style":100},"137.35","116","font-size:11px;font-weight:600;fill:var(--muted,#5b6780);text-anchor:start","shape data",[44,103],{"x":104,"y":70,"width":71,"height":72,"rx":56,"fill":105,"stroke":106,"style":59},"266.7","#fdefd8","var(--gold,#b4740a)",[61,108,111],{"x":109,"y":78,"style":110},"380.04999999999995","font-size:13px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","xlsxwriter",[61,113,114],{"x":109,"y":83,"style":84},"charts and formats",[87,116],{"x1":109,"y1":89,"x2":109,"y2":90,"stroke":58,"style":59},[92,118],{"points":119,"fill":95},"380.04999999999995,141 375.04999999999995,132 385.04999999999995,132",[61,121,123],{"x":122,"y":99,"style":100},"386.04999999999995","build new",[44,125],{"x":126,"y":70,"width":71,"height":72,"rx":56,"fill":127,"stroke":58,"style":59},"515.4","#f0f4ff",[61,129,132],{"x":130,"y":78,"style":131},"628.75","font-size:13px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","openpyxl \u002F xlwings",[61,134,135],{"x":130,"y":83,"style":84},"change what exists",[87,137],{"x1":130,"y1":89,"x2":130,"y2":90,"stroke":58,"style":59},[92,139],{"points":140,"fill":95},"628.75,141 623.75,132 633.75,132",[61,142,144],{"x":143,"y":99,"style":100},"634.75","edit \u002F drive",[61,146,149],{"x":63,"y":147,"style":148},"252","font-size:12.5px;font-weight:400;fill:var(--muted,#5b6780);text-anchor:middle","the library follows the verb, not the file extension",[10,151,152],{},"The short version: pandas and Polars are for the data, openpyxl and xlsxwriter are for the file,\nand xlwings is for the application. Almost every production script uses two of them together — a\nframe library to shape the numbers and a file library to lay them out — and the mistakes people\nmake are nearly always the result of asking one of them to do the other's job.",[154,155,157],"h2",{"id":156},"what-each-library-is-actually-for","What each library is actually for",[10,159,160],{},"Read the table below as a set of jobs, not a ranking. openpyxl is the only one that can open an\nexisting workbook, change three cells and save it with everything else intact — that single\ncapability is why it stays installed even on projects that write with something faster.\nxlsxwriter is write-only by design, and that constraint is what lets it stream a million rows in\nconstant memory and expose the richest formatting API of the group. pandas and Polars do not touch\nExcel at all: they delegate to one of the others and spend their effort on the table in between.",[25,162,34,167,34,170,34,173,34,176,34,185,34,191,34,195,34,199,34,203,34,207,34,210,34,215,34,219,34,221,34,223,34,226,34,229,34,233,34,235,34,237,34,239,34,241,34,244,34,249,34,252,34,254,34,256,34,258,34,261,34,265,34,268,34,271,34,273,34,275,34,280,34,285,34,287,34,289,34,291,34,293],{"viewBox":163,"role":28,"ariaLabelledBy":164,"xmlns":32,"style":33},"0 0 760 296",[165,166],"cpl-caps-t","cpl-caps-d",[36,168,169],{"id":165},"What each Python Excel library can and cannot do",[40,171,172],{"id":166},"A capability grid: openpyxl reads, writes and edits in place; xlsxwriter writes only but has the richest formatting; pandas and Polars delegate to an engine; xlwings drives a running copy of Excel.",[44,174],{"x":46,"y":46,"width":47,"height":175,"fill":49},"296",[44,177],{"x":178,"y":179,"width":180,"height":181,"rx":182,"fill":183,"stroke":184},"18","30","724","34","8","#e7ebef","var(--line,#cdd5e6)",[61,186,190],{"x":187,"y":188,"style":189},"126.6","52","font-size:12px;font-weight:700;fill:var(--muted,#5b6780);text-anchor:middle","Library",[61,192,194],{"x":193,"y":188,"style":189},"298.55","Reads",[61,196,198],{"x":197,"y":188,"style":189},"425.25","Writes",[61,200,202],{"x":201,"y":188,"style":189},"551.95","Edits in place",[61,204,206],{"x":205,"y":188,"style":189},"678.65","Needs Excel",[44,208],{"x":178,"y":209,"width":180,"height":181,"rx":182,"fill":127,"stroke":58},"67",[61,211,214],{"x":187,"y":212,"style":213},"88.0","font-size:12px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","openpyxl",[61,216,218],{"x":193,"y":212,"style":217},"font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","yes",[61,220,218],{"x":197,"y":212,"style":217},[61,222,218],{"x":201,"y":212,"style":217},[61,224,225],{"x":205,"y":212,"style":217},"no",[44,227],{"x":178,"y":228,"width":180,"height":181,"rx":182,"fill":105,"stroke":106},"107",[61,230,111],{"x":187,"y":231,"style":232},"128.0","font-size:12px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle",[61,234,225],{"x":193,"y":231,"style":217},[61,236,218],{"x":197,"y":231,"style":217},[61,238,225],{"x":201,"y":231,"style":217},[61,240,225],{"x":205,"y":231,"style":217},[44,242],{"x":178,"y":243,"width":180,"height":181,"rx":182,"fill":73,"stroke":74},"147",[61,245,248],{"x":187,"y":246,"style":247},"168.0","font-size:12px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","pandas",[61,250,251],{"x":193,"y":246,"style":217},"via engine",[61,253,251],{"x":197,"y":246,"style":217},[61,255,225],{"x":201,"y":246,"style":217},[61,257,225],{"x":205,"y":246,"style":217},[44,259],{"x":178,"y":260,"width":180,"height":181,"rx":182,"fill":73,"stroke":74},"187",[61,262,264],{"x":187,"y":263,"style":247},"208.0","Polars",[61,266,267],{"x":193,"y":263,"style":217},"via calamine",[61,269,270],{"x":197,"y":263,"style":217},"via xlsxwriter",[61,272,225],{"x":201,"y":263,"style":217},[61,274,225],{"x":205,"y":263,"style":217},[44,276],{"x":178,"y":277,"width":180,"height":181,"rx":182,"fill":278,"stroke":279},"227","#fee8f2","var(--accent,#d81b73)",[61,281,284],{"x":187,"y":282,"style":283},"248.0","font-size:12px;font-weight:700;fill:var(--accent,#d81b73);text-anchor:middle","xlwings",[61,286,218],{"x":193,"y":282,"style":217},[61,288,218],{"x":197,"y":282,"style":217},[61,290,218],{"x":201,"y":282,"style":217},[61,292,218],{"x":205,"y":282,"style":217},[61,294,296],{"x":63,"y":295,"style":148},"284","capability, not quality — each row is a different job",[10,298,299,300,304,305,309],{},"The engines matter as much as the libraries. ",[301,302,303],"code",{},"pandas.read_excel()"," is a thin front end over\nwhatever parser can handle the extension you gave it, so \"pandas is slow at Excel\" is usually a\nstatement about openpyxl. Swapping the engine — covered in\n",[14,306,308],{"href":307},"\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"," —\nchanges the number without changing a line of your own logic.",[311,312,317],"pre",{"className":313,"code":314,"language":315,"meta":316,"style":316},"language-python shiki shiki-themes github-light github-dark-high-contrast","import pandas as pd\n\n# Same call, three different parsers underneath.\ndefault = pd.read_excel(\"sales.xlsx\")                          # openpyxl\nfast = pd.read_excel(\"sales.xlsx\", engine=\"calamine\")          # python-calamine (Rust)\nlegacy = pd.read_excel(\"archive.xls\", engine=\"xlrd\")           # xlrd 1.2.0 only\nprint(default.shape, fast.shape, legacy.shape)\n","python","",[301,318,319,337,344,351,373,403,431],{"__ignoreMap":316},[320,321,323,327,331,334],"span",{"class":87,"line":322},1,[320,324,326],{"class":325},"s-kum","import",[320,328,330],{"class":329},"skGVy"," pandas ",[320,332,333],{"class":325},"as",[320,335,336],{"class":329}," pd\n",[320,338,340],{"class":87,"line":339},2,[320,341,343],{"emptyLinePlaceholder":342},true,"\n",[320,345,347],{"class":87,"line":346},3,[320,348,350],{"class":349},"s-wDw","# Same call, three different parsers underneath.\n",[320,352,354,357,360,363,367,370],{"class":87,"line":353},4,[320,355,356],{"class":329},"default ",[320,358,359],{"class":325},"=",[320,361,362],{"class":329}," pd.read_excel(",[320,364,366],{"class":365},"srMev","\"sales.xlsx\"",[320,368,369],{"class":329},")                          ",[320,371,372],{"class":349},"# openpyxl\n",[320,374,376,379,381,383,385,388,392,394,397,400],{"class":87,"line":375},5,[320,377,378],{"class":329},"fast ",[320,380,359],{"class":325},[320,382,362],{"class":329},[320,384,366],{"class":365},[320,386,387],{"class":329},", ",[320,389,391],{"class":390},"sa561","engine",[320,393,359],{"class":325},[320,395,396],{"class":365},"\"calamine\"",[320,398,399],{"class":329},")          ",[320,401,402],{"class":349},"# python-calamine (Rust)\n",[320,404,406,409,411,413,416,418,420,422,425,428],{"class":87,"line":405},6,[320,407,408],{"class":329},"legacy ",[320,410,359],{"class":325},[320,412,362],{"class":329},[320,414,415],{"class":365},"\"archive.xls\"",[320,417,387],{"class":329},[320,419,391],{"class":390},[320,421,359],{"class":325},[320,423,424],{"class":365},"\"xlrd\"",[320,426,427],{"class":329},")           ",[320,429,430],{"class":349},"# xlrd 1.2.0 only\n",[320,432,434,438],{"class":87,"line":433},7,[320,435,437],{"class":436},"sP0c6","print",[320,439,440],{"class":329},"(default.shape, fast.shape, legacy.shape)\n",[154,442,444],{"id":443},"reading-get-the-data-in-then-forget-the-format","Reading: get the data in, then forget the format",[10,446,447],{},"For reading, the ranking is stable and mostly about the parser. openpyxl walks the sheet XML in\nPython; calamine does the same work in Rust and hands back a ready table. On a wide export the\ndifference is the largest single speed lever available in this ecosystem, and it costs one keyword\nargument.",[25,449,34,455,34,458,34,461,34,465,34,470,34,476,34,483,34,489,34,493,34,495,34,499,34,504,34,508,34,511,34,515,34,519,34,523,34,526,34,530,34,535,34,539],{"viewBox":450,"role":28,"ariaLabelledBy":451,"xmlns":32,"style":454},"0 0 720 240",[452,453],"cpl-read-t","cpl-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",[36,456,457],{"id":452},"Relative time to read the same 200,000-row sheet",[40,459,460],{"id":453},"openpyxl through pandas is the slowest path, python-calamine is several times faster on the same file, and reading a Parquet copy of the same data is faster again by a wide margin.",[44,462],{"x":46,"y":46,"width":463,"height":464,"fill":49},"720","240",[61,466,469],{"x":467,"y":55,"style":468},"20","font-size:12px;font-weight:600;fill:var(--text,#172033);text-anchor:start","pandas + openpyxl",[44,471],{"x":472,"y":473,"width":474,"height":53,"rx":475,"fill":183,"stroke":184},"200","40","371.6","6",[44,477],{"x":478,"y":479,"width":480,"height":481,"rx":482,"fill":127,"stroke":58},"201","41","369.6","24","5",[61,484,488],{"x":485,"y":486,"style":487},"583.6","58","font-size:12px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:start","slowest",[61,490,492],{"x":467,"y":491,"style":468},"100","pandas + calamine",[44,494],{"x":472,"y":72,"width":474,"height":53,"rx":475,"fill":183,"stroke":184},[44,496],{"x":478,"y":497,"width":498,"height":481,"rx":482,"fill":73,"stroke":74},"85","93.2",[61,500,503],{"x":485,"y":501,"style":502},"102","font-size:12px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:start","4x faster",[61,505,507],{"x":467,"y":506,"style":468},"144","Polars + calamine",[44,509],{"x":472,"y":510,"width":474,"height":53,"rx":475,"fill":183,"stroke":184},"128",[44,512],{"x":478,"y":513,"width":514,"height":481,"rx":482,"fill":73,"stroke":74},"129","74.5",[61,516,518],{"x":485,"y":517,"style":502},"146","5x faster",[61,520,522],{"x":467,"y":521,"style":468},"188","Parquet copy",[44,524],{"x":472,"y":525,"width":474,"height":53,"rx":475,"fill":183,"stroke":184},"172",[44,527],{"x":478,"y":528,"width":529,"height":481,"rx":482,"fill":105,"stroke":106},"173","28",[61,531,534],{"x":485,"y":532,"style":533},"190","font-size:12px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:start","read once, reuse",[61,536,538],{"x":467,"y":467,"style":537},"font-size:11.5px;font-weight:600;fill:var(--muted,#5b6780);text-anchor:start","relative cost",[61,540,543],{"x":541,"y":542,"style":148},"360.0","230","shape holds across files; the exact ratios depend on column types",[10,545,546,547,551],{},"The numbers move with the file, but the shape holds: a Rust parser is several times faster than\nthe pure-Python one, and converting the sheet to a columnar format once makes every later read\nalmost free. That last point is the one worth internalising — if the same workbook is read more\nthan twice, converting it is cheaper than parsing it again, as\n",[14,548,550],{"href":549},"\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"," shows.",[311,553,555],{"className":313,"code":554,"language":315,"meta":316,"style":316},"import time\nimport pandas as pd\n\nfor engine in (\"openpyxl\", \"calamine\"):\n    start = time.perf_counter()\n    frame = pd.read_excel(\"big.xlsx\", engine=engine)\n    print(f\"{engine:>9}: {time.perf_counter() - start:5.2f}s  {frame.shape}\")\n",[301,556,557,564,574,578,602,612,633],{"__ignoreMap":316},[320,558,559,561],{"class":87,"line":322},[320,560,326],{"class":325},[320,562,563],{"class":329}," time\n",[320,565,566,568,570,572],{"class":87,"line":339},[320,567,326],{"class":325},[320,569,330],{"class":329},[320,571,333],{"class":325},[320,573,336],{"class":329},[320,575,576],{"class":87,"line":346},[320,577,343],{"emptyLinePlaceholder":342},[320,579,580,583,586,589,592,595,597,599],{"class":87,"line":353},[320,581,582],{"class":325},"for",[320,584,585],{"class":329}," engine ",[320,587,588],{"class":325},"in",[320,590,591],{"class":329}," (",[320,593,594],{"class":365},"\"openpyxl\"",[320,596,387],{"class":329},[320,598,396],{"class":365},[320,600,601],{"class":329},"):\n",[320,603,604,607,609],{"class":87,"line":375},[320,605,606],{"class":329},"    start ",[320,608,359],{"class":325},[320,610,611],{"class":329}," time.perf_counter()\n",[320,613,614,617,619,621,624,626,628,630],{"class":87,"line":405},[320,615,616],{"class":329},"    frame ",[320,618,359],{"class":325},[320,620,362],{"class":329},[320,622,623],{"class":365},"\"big.xlsx\"",[320,625,387],{"class":329},[320,627,391],{"class":390},[320,629,359],{"class":325},[320,631,632],{"class":329},"engine)\n",[320,634,635,638,641,644,647,651,653,656,659,662,664,667,670,673,676,678,681,683,686,688,690],{"class":87,"line":433},[320,636,637],{"class":436},"    print",[320,639,640],{"class":329},"(",[320,642,643],{"class":325},"f",[320,645,646],{"class":365},"\"",[320,648,650],{"class":649},"sSjpA","{",[320,652,391],{"class":329},[320,654,655],{"class":325},":>9",[320,657,658],{"class":649},"}",[320,660,661],{"class":365},": ",[320,663,650],{"class":649},[320,665,666],{"class":329},"time.perf_counter() ",[320,668,669],{"class":325},"-",[320,671,672],{"class":329}," start",[320,674,675],{"class":325},":5.2f",[320,677,658],{"class":649},[320,679,680],{"class":365},"s  ",[320,682,650],{"class":649},[320,684,685],{"class":329},"frame.shape",[320,687,658],{"class":649},[320,689,646],{"class":365},[320,691,692],{"class":329},")\n",[10,694,695,696,700],{},"Reading is also where the defensive habits pay off. Whichever library you pick, name the sheet\nrather than trusting position, prune the columns you do not need, and pin the dtypes of anything\nthat looks like an identifier — the reasoning is set out in\n",[14,697,699],{"href":698},"\u002Fgetting-started-with-python-excel-automation\u002Freading-excel-files-with-pandas\u002F","Reading Excel Files with Pandas",".",[154,702,704],{"id":703},"writing-formatting-versus-volume","Writing: formatting versus volume",[10,706,707,708,711],{},"Writing splits cleanly. If the output is a table someone will open, sort and read, xlsxwriter gives\nyou the most control for the least code: number formats, conditional formats, charts, autofilters\nand frozen panes are all first-class, and its ",[301,709,710],{},"constant_memory"," mode keeps a million-row export\ninside a fixed footprint. If the output has to merge into a workbook that already exists — a\ntemplate with a logo, a summary tab, three years of history — openpyxl is the only option, because\nxlsxwriter cannot open a file at all.",[25,713,34,718,34,721,34,724,34,727,34,732,34,735,34,741,34,744,34,747,34,751,34,755,34,757,34,759,34,763,34,766,34,769,34,772,34,775,34,778,34,782,34,786,34,789,34,792,34,795,34,798],{"viewBox":714,"role":28,"ariaLabelledBy":715,"xmlns":32,"style":33},"0 0 760 232",[716,717],"cpl-write-t","cpl-write-d",[36,719,720],{"id":716},"Choosing a writer by what the output has to be",[40,722,723],{"id":717},"Three writing paths: xlsxwriter for new formatted files, openpyxl for editing a workbook that already exists, and pandas.ExcelWriter as the shortest route from a DataFrame to a sheet.",[44,725],{"x":46,"y":46,"width":47,"height":726,"fill":49},"232",[44,728],{"x":729,"y":53,"width":71,"height":730,"rx":731,"fill":105,"stroke":106,"style":59},"20.0","164","14",[61,733,111],{"x":734,"y":188,"style":110},"133.35",[87,736],{"x1":737,"y1":738,"x2":739,"y2":738,"stroke":106,"style":740},"36.0","62","230.7","stroke-width:1px",[61,742,743],{"x":734,"y":72,"style":217},"new files only",[61,745,746],{"x":734,"y":228,"style":217},"charts, formats, sparklines",[61,748,750],{"x":734,"y":749,"style":217},"130","constant-memory mode",[61,752,754],{"x":734,"y":753,"style":217},"153","fastest bulk writes",[44,756],{"x":104,"y":53,"width":71,"height":730,"rx":731,"fill":127,"stroke":58,"style":59},[61,758,214],{"x":109,"y":188,"style":131},[87,760],{"x1":761,"y1":738,"x2":762,"y2":738,"stroke":58,"style":740},"282.7","477.4",[61,764,765],{"x":109,"y":72,"style":217},"opens what exists",[61,767,768],{"x":109,"y":228,"style":217},"patch cells, keep the rest",[61,770,771],{"x":109,"y":749,"style":217},"styles as objects",[61,773,774],{"x":109,"y":753,"style":217},"lossy for pivot caches",[44,776],{"x":777,"y":53,"width":71,"height":730,"rx":731,"fill":73,"stroke":74,"style":59},"513.4",[61,779,781],{"x":780,"y":188,"style":79},"626.75","pandas.ExcelWriter",[87,783],{"x1":784,"y1":738,"x2":785,"y2":738,"stroke":74,"style":740},"529.4","724.0999999999999",[61,787,788],{"x":780,"y":72,"style":217},"frame in, sheet out",[61,790,791],{"x":780,"y":228,"style":217},"wraps either engine",[61,793,794],{"x":780,"y":749,"style":217},"least code by far",[61,796,797],{"x":780,"y":753,"style":217},"reach through for detail",[61,799,801],{"x":63,"y":800,"style":148},"218","the question is whether the file already exists",[10,803,804,805,808,809,812],{},"pandas sits on top of either. ",[301,806,807],{},"DataFrame.to_excel()"," with an ",[301,810,811],{},"ExcelWriter"," is the shortest path\nfrom a frame to a formatted sheet, and the engine choice is yours:",[311,814,816],{"className":313,"code":815,"language":315,"meta":316,"style":316},"import pandas as pd\n\nframe = pd.DataFrame({\"Region\": [\"North\", \"South\"], \"Revenue\": [128400.0, 96150.5]})\n\nwith pd.ExcelWriter(\"report.xlsx\", engine=\"xlsxwriter\") as writer:\n    frame.to_excel(writer, sheet_name=\"Summary\", index=False)\n    book, sheet = writer.book, writer.sheets[\"Summary\"]\n    money = book.add_format({\"num_format\": \"#,##0.00\"})\n    sheet.set_column(\"B:B\", 14, money)\n    sheet.freeze_panes(1, 0)\n",[301,817,818,828,832,875,879,907,932,947,969,985],{"__ignoreMap":316},[320,819,820,822,824,826],{"class":87,"line":322},[320,821,326],{"class":325},[320,823,330],{"class":329},[320,825,333],{"class":325},[320,827,336],{"class":329},[320,829,830],{"class":87,"line":339},[320,831,343],{"emptyLinePlaceholder":342},[320,833,834,837,839,842,845,848,851,853,856,859,862,864,867,869,872],{"class":87,"line":346},[320,835,836],{"class":329},"frame ",[320,838,359],{"class":325},[320,840,841],{"class":329}," pd.DataFrame({",[320,843,844],{"class":365},"\"Region\"",[320,846,847],{"class":329},": [",[320,849,850],{"class":365},"\"North\"",[320,852,387],{"class":329},[320,854,855],{"class":365},"\"South\"",[320,857,858],{"class":329},"], ",[320,860,861],{"class":365},"\"Revenue\"",[320,863,847],{"class":329},[320,865,866],{"class":436},"128400.0",[320,868,387],{"class":329},[320,870,871],{"class":436},"96150.5",[320,873,874],{"class":329},"]})\n",[320,876,877],{"class":87,"line":353},[320,878,343],{"emptyLinePlaceholder":342},[320,880,881,884,887,890,892,894,896,899,902,904],{"class":87,"line":375},[320,882,883],{"class":325},"with",[320,885,886],{"class":329}," pd.ExcelWriter(",[320,888,889],{"class":365},"\"report.xlsx\"",[320,891,387],{"class":329},[320,893,391],{"class":390},[320,895,359],{"class":325},[320,897,898],{"class":365},"\"xlsxwriter\"",[320,900,901],{"class":329},") ",[320,903,333],{"class":325},[320,905,906],{"class":329}," writer:\n",[320,908,909,912,915,917,920,922,925,927,930],{"class":87,"line":405},[320,910,911],{"class":329},"    frame.to_excel(writer, ",[320,913,914],{"class":390},"sheet_name",[320,916,359],{"class":325},[320,918,919],{"class":365},"\"Summary\"",[320,921,387],{"class":329},[320,923,924],{"class":390},"index",[320,926,359],{"class":325},[320,928,929],{"class":436},"False",[320,931,692],{"class":329},[320,933,934,937,939,942,944],{"class":87,"line":433},[320,935,936],{"class":329},"    book, sheet ",[320,938,359],{"class":325},[320,940,941],{"class":329}," writer.book, writer.sheets[",[320,943,919],{"class":365},[320,945,946],{"class":329},"]\n",[320,948,950,953,955,958,961,963,966],{"class":87,"line":949},8,[320,951,952],{"class":329},"    money ",[320,954,359],{"class":325},[320,956,957],{"class":329}," book.add_format({",[320,959,960],{"class":365},"\"num_format\"",[320,962,661],{"class":329},[320,964,965],{"class":365},"\"#,##0.00\"",[320,967,968],{"class":329},"})\n",[320,970,972,975,978,980,982],{"class":87,"line":971},9,[320,973,974],{"class":329},"    sheet.set_column(",[320,976,977],{"class":365},"\"B:B\"",[320,979,387],{"class":329},[320,981,731],{"class":436},[320,983,984],{"class":329},", money)\n",[320,986,988,991,994,996,998],{"class":87,"line":987},10,[320,989,990],{"class":329},"    sheet.freeze_panes(",[320,992,993],{"class":436},"1",[320,995,387],{"class":329},[320,997,46],{"class":436},[320,999,692],{"class":329},[10,1001,1002,1003,1006],{},"The same script with ",[301,1004,1005],{},"engine=\"openpyxl\""," writes the same values but reaches formatting through\nopenpyxl's style objects instead. Neither is more correct; the deciding question is whether the\nfile already exists.",[154,1008,1010],{"id":1009},"editing-a-workbook-without-breaking-it","Editing a workbook without breaking it",[10,1012,1013,1014,1017,1018,1021],{},"This is the capability that surprises people. Most of the ecosystem writes files; only openpyxl and\nthe application drivers ",[20,1015,1016],{},"edit"," them. And openpyxl's edit is not lossless — it rebuilds the parts of\nthe file it understands and drops what it does not, which historically has meant pivot table caches,\nsome chart types, VBA in files opened without ",[301,1019,1020],{},"keep_vba=True",", and slicers.",[311,1023,1025],{"className":313,"code":1024,"language":315,"meta":316,"style":316},"from openpyxl import load_workbook\n\nbook = load_workbook(\"quarterly.xlsx\")           # add keep_vba=True for .xlsm\nsheet = book[\"Summary\"]\nsheet[\"B2\"] = 128400.0                            # patch one number\nbook.save(\"quarterly-revised.xlsx\")               # never save over the original\n",[301,1026,1027,1040,1044,1062,1076,1095],{"__ignoreMap":316},[320,1028,1029,1032,1035,1037],{"class":87,"line":322},[320,1030,1031],{"class":325},"from",[320,1033,1034],{"class":329}," openpyxl ",[320,1036,326],{"class":325},[320,1038,1039],{"class":329}," load_workbook\n",[320,1041,1042],{"class":87,"line":339},[320,1043,343],{"emptyLinePlaceholder":342},[320,1045,1046,1049,1051,1054,1057,1059],{"class":87,"line":346},[320,1047,1048],{"class":329},"book ",[320,1050,359],{"class":325},[320,1052,1053],{"class":329}," load_workbook(",[320,1055,1056],{"class":365},"\"quarterly.xlsx\"",[320,1058,427],{"class":329},[320,1060,1061],{"class":349},"# add keep_vba=True for .xlsm\n",[320,1063,1064,1067,1069,1072,1074],{"class":87,"line":353},[320,1065,1066],{"class":329},"sheet ",[320,1068,359],{"class":325},[320,1070,1071],{"class":329}," book[",[320,1073,919],{"class":365},[320,1075,946],{"class":329},[320,1077,1078,1081,1084,1087,1089,1092],{"class":87,"line":375},[320,1079,1080],{"class":329},"sheet[",[320,1082,1083],{"class":365},"\"B2\"",[320,1085,1086],{"class":329},"] ",[320,1088,359],{"class":325},[320,1090,1091],{"class":436}," 128400.0",[320,1093,1094],{"class":349},"                            # patch one number\n",[320,1096,1097,1100,1103,1106],{"class":87,"line":405},[320,1098,1099],{"class":329},"book.save(",[320,1101,1102],{"class":365},"\"quarterly-revised.xlsx\"",[320,1104,1105],{"class":329},")               ",[320,1107,1108],{"class":349},"# never save over the original\n",[10,1110,1111,1112,1116],{},"Saving under a new name is not fussiness — it is the difference between a bad run costing you a\nminute and costing you the source file. When the workbook carries objects openpyxl cannot round-trip,\nthe alternatives are to drive Excel itself (see below) or to rebuild the file from scratch, and\n",[14,1113,1115],{"href":1114},"\u002Fautomating-reporting-workflows\u002Fgenerating-excel-reports-from-templates\u002Fpopulate-excel-template-without-losing-formatting\u002F","Populate an Excel Template Without Losing Formatting","\nwalks through choosing between them.",[154,1118,1120],{"id":1119},"driving-excel-itself-and-when-it-is-worth-it","Driving Excel itself, and when it is worth it",[10,1122,1123],{},"xlwings and pywin32 are a different category: they do not parse the file, they remote-control a\nrunning copy of Excel. That buys the things the format-level libraries cannot do — recalculating\nformulas with Excel's own engine, refreshing a Power Query connection, running an existing macro,\nexporting a range to PDF exactly as Excel would print it — at the cost of needing Excel installed,\nbeing effectively Windows- or macOS-only, and being an order of magnitude slower per operation.",[10,1125,1126,1127,1130,1131,1135,1136,1140],{},"The rule that holds up in practice: reach for an application driver only when the ",[20,1128,1129],{},"value you need\ncomes from Excel",", not from the data. Recalculated values, a native PDF export and a macro run all\nqualify. Writing 40,000 rows does not — that belongs to xlsxwriter, and pushing it through COM will\ntake minutes instead of seconds. ",[14,1132,1134],{"href":1133},"\u002Fgetting-started-with-python-excel-automation\u002Fautomating-excel-with-xlwings-basics\u002F","Automating Excel with xlwings Basics","\ncovers the friendly API, and ",[14,1137,1139],{"href":1138},"\u002Fgetting-started-with-python-excel-automation\u002Fautomating-excel-with-com-and-pywin32\u002F","Automating Excel with COM and pywin32","\ncovers the raw interface underneath it.",[154,1142,1144],{"id":1143},"combining-them-in-one-pipeline","Combining them in one pipeline",[10,1146,1147],{},"The realistic answer to \"which library\" is usually \"two\". A reporting job that reads a large export,\nreshapes it and ships a formatted workbook uses a fast reader, a frame library and a formatting\nwriter, and each hands off cleanly to the next.",[25,1149,34,1153,34,1156,34,1159,34,1161,34,1166,34,1171,34,1176,34,1180,34,1185,34,1188,34,1191,34,1194,34,1197,34,1201,34,1204,34,1207,34,1211,34,1214],{"viewBox":714,"role":28,"ariaLabelledBy":1150,"xmlns":32,"style":33},[1151,1152],"cpl-pipe-t","cpl-pipe-d",[36,1154,1155],{"id":1151},"A typical pipeline uses three libraries in sequence",[40,1157,1158],{"id":1152},"A fast parser reads the export, a frame library groups and sorts it, and a formatting writer emits the delivered workbook.",[44,1160],{"x":46,"y":46,"width":47,"height":726,"fill":49},[61,1162,1165],{"x":63,"y":1163,"style":1164},"32","font-size:13px;font-weight:600;fill:var(--muted,#5b6780);text-anchor:middle","one job, three libraries",[44,1167],{"x":1168,"y":1169,"width":263,"height":1170,"rx":56,"fill":73,"stroke":74,"style":59},"24.0","74","96",[61,1172,1175],{"x":231,"y":1173,"style":1174},"114","font-size:14px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","calamine",[61,1177,1179],{"x":231,"y":1178,"style":84},"136","parse the export",[87,1181],{"x1":1182,"y1":1183,"x2":1184,"y2":1183,"stroke":58,"style":59},"237.0","122.0","269.0",[92,1186],{"points":1187,"fill":95},"269.0,122.0 260.0,117.0 260.0,127.0",[44,1189],{"x":1190,"y":1169,"width":263,"height":1170,"rx":56,"fill":127,"stroke":58,"style":59},"276.0",[61,1192,248],{"x":63,"y":1173,"style":1193},"font-size:14px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle",[61,1195,1196],{"x":63,"y":1178,"style":84},"clean and group",[87,1198],{"x1":1199,"y1":1183,"x2":1200,"y2":1183,"stroke":58,"style":59},"489.0","521.0",[92,1202],{"points":1203,"fill":95},"521.0,122.0 512.0,117.0 512.0,127.0",[44,1205],{"x":1206,"y":1169,"width":263,"height":1170,"rx":56,"fill":105,"stroke":106,"style":59},"528.0",[61,1208,111],{"x":1209,"y":1173,"style":1210},"632.0","font-size:14px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle",[61,1212,1213],{"x":1209,"y":1178,"style":84},"format and ship",[61,1215,1217],{"x":63,"y":1216,"style":148},"210","pick the reader for speed, the writer for what the file must look like",[311,1219,1221],{"className":313,"code":1220,"language":315,"meta":316,"style":316},"import pandas as pd\n\nraw = pd.read_excel(\"export.xlsx\", engine=\"calamine\", dtype={\"Account\": \"string\"})\nsummary = (\n    raw.dropna(subset=[\"Revenue\"])\n       .groupby(\"Region\", as_index=False)[\"Revenue\"].sum()\n       .sort_values(\"Revenue\", ascending=False)\n)\n\nwith pd.ExcelWriter(\"regional-summary.xlsx\", engine=\"xlsxwriter\") as writer:\n    summary.to_excel(writer, sheet_name=\"By region\", index=False)\n    sheet = writer.sheets[\"By region\"]\n    sheet.set_column(\"A:A\", 18)\n    sheet.set_column(\"B:B\", 16, writer.book.add_format({\"num_format\": \"#,##0\"}))\n    sheet.autofilter(0, 0, len(summary), 1)\n",[301,1222,1223,1233,1237,1276,1286,1304,1328,1346,1350,1354,1377,1400,1415,1429,1454],{"__ignoreMap":316},[320,1224,1225,1227,1229,1231],{"class":87,"line":322},[320,1226,326],{"class":325},[320,1228,330],{"class":329},[320,1230,333],{"class":325},[320,1232,336],{"class":329},[320,1234,1235],{"class":87,"line":339},[320,1236,343],{"emptyLinePlaceholder":342},[320,1238,1239,1242,1244,1246,1249,1251,1253,1255,1257,1259,1262,1264,1266,1269,1271,1274],{"class":87,"line":346},[320,1240,1241],{"class":329},"raw ",[320,1243,359],{"class":325},[320,1245,362],{"class":329},[320,1247,1248],{"class":365},"\"export.xlsx\"",[320,1250,387],{"class":329},[320,1252,391],{"class":390},[320,1254,359],{"class":325},[320,1256,396],{"class":365},[320,1258,387],{"class":329},[320,1260,1261],{"class":390},"dtype",[320,1263,359],{"class":325},[320,1265,650],{"class":329},[320,1267,1268],{"class":365},"\"Account\"",[320,1270,661],{"class":329},[320,1272,1273],{"class":365},"\"string\"",[320,1275,968],{"class":329},[320,1277,1278,1281,1283],{"class":87,"line":353},[320,1279,1280],{"class":329},"summary ",[320,1282,359],{"class":325},[320,1284,1285],{"class":329}," (\n",[320,1287,1288,1291,1294,1296,1299,1301],{"class":87,"line":375},[320,1289,1290],{"class":329},"    raw.dropna(",[320,1292,1293],{"class":390},"subset",[320,1295,359],{"class":325},[320,1297,1298],{"class":329},"[",[320,1300,861],{"class":365},[320,1302,1303],{"class":329},"])\n",[320,1305,1306,1309,1311,1313,1316,1318,1320,1323,1325],{"class":87,"line":405},[320,1307,1308],{"class":329},"       .groupby(",[320,1310,844],{"class":365},[320,1312,387],{"class":329},[320,1314,1315],{"class":390},"as_index",[320,1317,359],{"class":325},[320,1319,929],{"class":436},[320,1321,1322],{"class":329},")[",[320,1324,861],{"class":365},[320,1326,1327],{"class":329},"].sum()\n",[320,1329,1330,1333,1335,1337,1340,1342,1344],{"class":87,"line":433},[320,1331,1332],{"class":329},"       .sort_values(",[320,1334,861],{"class":365},[320,1336,387],{"class":329},[320,1338,1339],{"class":390},"ascending",[320,1341,359],{"class":325},[320,1343,929],{"class":436},[320,1345,692],{"class":329},[320,1347,1348],{"class":87,"line":949},[320,1349,692],{"class":329},[320,1351,1352],{"class":87,"line":971},[320,1353,343],{"emptyLinePlaceholder":342},[320,1355,1356,1358,1360,1363,1365,1367,1369,1371,1373,1375],{"class":87,"line":987},[320,1357,883],{"class":325},[320,1359,886],{"class":329},[320,1361,1362],{"class":365},"\"regional-summary.xlsx\"",[320,1364,387],{"class":329},[320,1366,391],{"class":390},[320,1368,359],{"class":325},[320,1370,898],{"class":365},[320,1372,901],{"class":329},[320,1374,333],{"class":325},[320,1376,906],{"class":329},[320,1378,1380,1383,1385,1387,1390,1392,1394,1396,1398],{"class":87,"line":1379},11,[320,1381,1382],{"class":329},"    summary.to_excel(writer, ",[320,1384,914],{"class":390},[320,1386,359],{"class":325},[320,1388,1389],{"class":365},"\"By region\"",[320,1391,387],{"class":329},[320,1393,924],{"class":390},[320,1395,359],{"class":325},[320,1397,929],{"class":436},[320,1399,692],{"class":329},[320,1401,1403,1406,1408,1411,1413],{"class":87,"line":1402},12,[320,1404,1405],{"class":329},"    sheet ",[320,1407,359],{"class":325},[320,1409,1410],{"class":329}," writer.sheets[",[320,1412,1389],{"class":365},[320,1414,946],{"class":329},[320,1416,1418,1420,1423,1425,1427],{"class":87,"line":1417},13,[320,1419,974],{"class":329},[320,1421,1422],{"class":365},"\"A:A\"",[320,1424,387],{"class":329},[320,1426,178],{"class":436},[320,1428,692],{"class":329},[320,1430,1432,1434,1436,1438,1441,1444,1446,1448,1451],{"class":87,"line":1431},14,[320,1433,974],{"class":329},[320,1435,977],{"class":365},[320,1437,387],{"class":329},[320,1439,1440],{"class":436},"16",[320,1442,1443],{"class":329},", writer.book.add_format({",[320,1445,960],{"class":365},[320,1447,661],{"class":329},[320,1449,1450],{"class":365},"\"#,##0\"",[320,1452,1453],{"class":329},"}))\n",[320,1455,1457,1460,1462,1464,1466,1468,1471,1474,1476],{"class":87,"line":1456},15,[320,1458,1459],{"class":329},"    sheet.autofilter(",[320,1461,46],{"class":436},[320,1463,387],{"class":329},[320,1465,46],{"class":436},[320,1467,387],{"class":329},[320,1469,1470],{"class":436},"len",[320,1472,1473],{"class":329},"(summary), ",[320,1475,993],{"class":436},[320,1477,692],{"class":329},[10,1479,1480],{},"Three libraries, twelve lines, and each one is doing the job it is best at. That is the pattern the\nrest of this section elaborates: pick the reader for speed, the frame library for the transform, and\nthe writer for what the file has to look like when it lands.",[154,1482,1484],{"id":1483},"memory-the-number-that-decides-for-you","Memory: the number that decides for you",[10,1486,1487,1488,1491],{},"Speed is negotiable; memory is not. A ",[301,1489,1490],{},".xlsx"," file is a zip of XML, and every library that reads\none has to expand it. openpyxl's normal mode builds a Python object per cell, which is roughly\none to two kilobytes each once you count the object header, the style reference and the dictionary\nthat holds it — so a 500,000-cell sheet can cost well over a gigabyte before your own code sees a\nsingle value. That is the number that turns \"it works on my laptop\" into a killed container at\n06:00.",[25,1493,34,1498,34,1501,34,1504,34,1507,34,1511,34,1517,34,1522,34,1526,34,1530,34,1534,34,1538,34,1541,34,1545,34,1549,34,1552,34,1555,34,1558,34,1561,34,1567,34,1572,34,1577,34,1580,34,1584,34,1587],{"viewBox":1494,"role":28,"ariaLabelledBy":1495,"xmlns":32,"style":33},"0 0 760 224",[1496,1497],"cpl-mem-t","cpl-mem-d",[36,1499,1500],{"id":1496},"Normal mode versus streaming mode on a large sheet",[40,1502,1503],{"id":1497},"In normal mode openpyxl builds a Python object for every cell, so memory grows with the sheet. In read-only or constant-memory mode a single row is held at a time and the footprint stays flat.",[44,1505],{"x":46,"y":46,"width":47,"height":1506,"fill":49},"224",[44,1508],{"x":467,"y":529,"width":1509,"height":1510,"rx":731,"fill":278,"stroke":279,"style":59},"270.0","162",[61,1512,1516],{"x":1513,"y":1514,"style":1515},"155.0","54","font-size:13px;font-weight:700;fill:var(--accent,#d81b73);text-anchor:middle","normal mode",[87,1518],{"x1":1519,"y1":1520,"x2":1521,"y2":1520,"stroke":279,"style":740},"36","64","274.0",[61,1523,1525],{"x":1513,"y":1524,"style":217},"86","one object per cell",[61,1527,1529],{"x":1513,"y":1528,"style":217},"109","grid held in RAM",[61,1531,1533],{"x":1513,"y":1532,"style":217},"132","random access",[61,1535,1537],{"x":1513,"y":1536,"style":217},"155","grows with the file",[44,1539],{"x":1540,"y":529,"width":1509,"height":1510,"rx":731,"fill":73,"stroke":74,"style":59},"470.0",[61,1542,1544],{"x":1543,"y":1514,"style":79},"605.0","streaming mode",[87,1546],{"x1":1547,"y1":1520,"x2":1548,"y2":1520,"stroke":74,"style":740},"486.0","724.0",[61,1550,1551],{"x":1543,"y":1524,"style":217},"one row at a time",[61,1553,1554],{"x":1543,"y":1528,"style":217},"flat footprint",[61,1556,1557],{"x":1543,"y":1532,"style":217},"forward-only",[61,1559,1560],{"x":1543,"y":1536,"style":217},"survives huge sheets",[44,1562],{"x":1563,"y":1564,"width":510,"height":1565,"rx":1566,"fill":57,"stroke":58},"316.0","90.0","38","19",[61,1568,1571],{"x":63,"y":1569,"style":1570},"114.0","font-size:12.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","read_only",[87,1573],{"x1":1574,"y1":1575,"x2":1576,"y2":1575,"stroke":58,"style":59},"295.0","109.0","309.0",[92,1578],{"points":1579,"fill":95},"309.0,109.0 300.0,104.0 300.0,114.0",[87,1581],{"x1":1582,"y1":1575,"x2":1583,"y2":1575,"stroke":58,"style":59},"449.0","463.0",[92,1585],{"points":1586,"fill":95},"463.0,109.0 454.0,104.0 454.0,114.0",[61,1588,1589],{"x":63,"y":1216,"style":148},"the same file, two very different memory curves",[10,1591,1592,1593,1596,1597,1600,1601,1603],{},"Each library has one lever that changes the shape of that curve. openpyxl has ",[301,1594,1595],{},"read_only=True",",\nwhich streams rows instead of materialising the grid, and ",[301,1598,1599],{},"write_only=True",", which does the same\nin reverse. xlsxwriter has ",[301,1602,710],{},", which flushes each row to disk as soon as it is\nwritten and never holds more than one row. calamine parses into a compact Rust structure and hands\nback one table. Polars keeps columnar Arrow buffers rather than Python objects.",[311,1605,1607],{"className":313,"code":1606,"language":315,"meta":316,"style":316},"from openpyxl import load_workbook\n\n# Streaming read: rows arrive as tuples, the grid is never built.\nbook = load_workbook(\"huge.xlsx\", read_only=True, data_only=True)\nsheet = book[\"Data\"]\ntotal = 0.0\nfor row in sheet.iter_rows(min_row=2, values_only=True):\n    if row[3] is not None:\n        total += row[3]\nbook.close()\nprint(f\"{total:,.2f}\")\n",[301,1608,1609,1619,1623,1628,1659,1672,1682,1713,1738,1752,1757],{"__ignoreMap":316},[320,1610,1611,1613,1615,1617],{"class":87,"line":322},[320,1612,1031],{"class":325},[320,1614,1034],{"class":329},[320,1616,326],{"class":325},[320,1618,1039],{"class":329},[320,1620,1621],{"class":87,"line":339},[320,1622,343],{"emptyLinePlaceholder":342},[320,1624,1625],{"class":87,"line":346},[320,1626,1627],{"class":349},"# Streaming read: rows arrive as tuples, the grid is never built.\n",[320,1629,1630,1632,1634,1636,1639,1641,1643,1645,1648,1650,1653,1655,1657],{"class":87,"line":353},[320,1631,1048],{"class":329},[320,1633,359],{"class":325},[320,1635,1053],{"class":329},[320,1637,1638],{"class":365},"\"huge.xlsx\"",[320,1640,387],{"class":329},[320,1642,1571],{"class":390},[320,1644,359],{"class":325},[320,1646,1647],{"class":436},"True",[320,1649,387],{"class":329},[320,1651,1652],{"class":390},"data_only",[320,1654,359],{"class":325},[320,1656,1647],{"class":436},[320,1658,692],{"class":329},[320,1660,1661,1663,1665,1667,1670],{"class":87,"line":375},[320,1662,1066],{"class":329},[320,1664,359],{"class":325},[320,1666,1071],{"class":329},[320,1668,1669],{"class":365},"\"Data\"",[320,1671,946],{"class":329},[320,1673,1674,1677,1679],{"class":87,"line":405},[320,1675,1676],{"class":329},"total ",[320,1678,359],{"class":325},[320,1680,1681],{"class":436}," 0.0\n",[320,1683,1684,1686,1689,1691,1694,1697,1699,1702,1704,1707,1709,1711],{"class":87,"line":433},[320,1685,582],{"class":325},[320,1687,1688],{"class":329}," row ",[320,1690,588],{"class":325},[320,1692,1693],{"class":329}," sheet.iter_rows(",[320,1695,1696],{"class":390},"min_row",[320,1698,359],{"class":325},[320,1700,1701],{"class":436},"2",[320,1703,387],{"class":329},[320,1705,1706],{"class":390},"values_only",[320,1708,359],{"class":325},[320,1710,1647],{"class":436},[320,1712,601],{"class":329},[320,1714,1715,1718,1721,1724,1726,1729,1732,1735],{"class":87,"line":949},[320,1716,1717],{"class":325},"    if",[320,1719,1720],{"class":329}," row[",[320,1722,1723],{"class":436},"3",[320,1725,1086],{"class":329},[320,1727,1728],{"class":325},"is",[320,1730,1731],{"class":325}," not",[320,1733,1734],{"class":436}," None",[320,1736,1737],{"class":329},":\n",[320,1739,1740,1743,1746,1748,1750],{"class":87,"line":971},[320,1741,1742],{"class":329},"        total ",[320,1744,1745],{"class":325},"+=",[320,1747,1720],{"class":329},[320,1749,1723],{"class":436},[320,1751,946],{"class":329},[320,1753,1754],{"class":87,"line":987},[320,1755,1756],{"class":329},"book.close()\n",[320,1758,1759,1761,1763,1765,1767,1769,1772,1775,1777,1779],{"class":87,"line":1379},[320,1760,437],{"class":436},[320,1762,640],{"class":329},[320,1764,643],{"class":325},[320,1766,646],{"class":365},[320,1768,650],{"class":649},[320,1770,1771],{"class":329},"total",[320,1773,1774],{"class":325},":,.2f",[320,1776,658],{"class":649},[320,1778,646],{"class":365},[320,1780,692],{"class":329},[10,1782,1783,1784,1788],{},"The catch with every streaming mode is that it gives up random access: you get the rows in order,\nonce, and you cannot go back. That trade is nearly always worth taking on a file large enough to\ncare about, and ",[14,1785,1787],{"href":1786},"\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","\ncovers what changes in the API when you do.",[154,1790,1792],{"id":1791},"what-the-wrong-choice-costs","What the wrong choice costs",[10,1794,1795],{},"Three failure modes account for most of the wasted afternoons in this ecosystem, and each is a\nlibrary choice made on the wrong axis.",[10,1797,1798,1799,1802],{},"Reaching for pandas when the job is cell-level. ",[301,1800,1801],{},"to_excel"," writes values; it does not write borders,\nmerged title rows, conditional formats or column widths. Scripts that try to force those through\npandas alone end up reopening the file with openpyxl anyway, and the honest version of that script\nis shorter than the one that resisted it.",[10,1804,1805,1806,1808],{},"Reaching for openpyxl when the job is bulk. Writing 300,000 rows through the normal workbook API\nmeans 300,000 Python objects and a save step that serialises all of them at once. The same export\nthrough xlsxwriter in ",[301,1807,710],{}," mode finishes in a fraction of the time and a fixed amount\nof RAM, and the code is not meaningfully longer.",[10,1810,1811],{},"Reaching for xlwings when the job is data. Every read and write through the automation interface is\na cross-process call; a loop that writes cells one at a time will take minutes where a file-level\nlibrary takes a second. When xlwings is genuinely needed, the fix is to move whole ranges in one\ncall rather than to abandon it.",[311,1813,1815],{"className":313,"code":1814,"language":315,"meta":316,"style":316},"# Slow: one cross-process round trip per cell.\nfor row_index, value in enumerate(values, start=2):\n    sheet.range(f\"B{row_index}\").value = value\n\n# Fast: one round trip for the whole block.\nsheet.range(\"B2\").value = [[value] for value in values]\n",[301,1816,1817,1822,1846,1873,1877,1882],{"__ignoreMap":316},[320,1818,1819],{"class":87,"line":322},[320,1820,1821],{"class":349},"# Slow: one cross-process round trip per cell.\n",[320,1823,1824,1826,1829,1831,1834,1837,1840,1842,1844],{"class":87,"line":339},[320,1825,582],{"class":325},[320,1827,1828],{"class":329}," row_index, value ",[320,1830,588],{"class":325},[320,1832,1833],{"class":436}," enumerate",[320,1835,1836],{"class":329},"(values, ",[320,1838,1839],{"class":390},"start",[320,1841,359],{"class":325},[320,1843,1701],{"class":436},[320,1845,601],{"class":329},[320,1847,1848,1851,1853,1856,1858,1861,1863,1865,1868,1870],{"class":87,"line":346},[320,1849,1850],{"class":329},"    sheet.range(",[320,1852,643],{"class":325},[320,1854,1855],{"class":365},"\"B",[320,1857,650],{"class":649},[320,1859,1860],{"class":329},"row_index",[320,1862,658],{"class":649},[320,1864,646],{"class":365},[320,1866,1867],{"class":329},").value ",[320,1869,359],{"class":325},[320,1871,1872],{"class":329}," value\n",[320,1874,1875],{"class":87,"line":353},[320,1876,343],{"emptyLinePlaceholder":342},[320,1878,1879],{"class":87,"line":375},[320,1880,1881],{"class":349},"# Fast: one round trip for the whole block.\n",[320,1883,1884,1887,1889,1891,1893,1896,1898,1901,1903],{"class":87,"line":405},[320,1885,1886],{"class":329},"sheet.range(",[320,1888,1083],{"class":365},[320,1890,1867],{"class":329},[320,1892,359],{"class":325},[320,1894,1895],{"class":329}," [[value] ",[320,1897,582],{"class":325},[320,1899,1900],{"class":329}," value ",[320,1902,588],{"class":325},[320,1904,1905],{"class":329}," values]\n",[154,1907,1909],{"id":1908},"a-thirty-second-checklist","A thirty-second checklist",[10,1911,1912],{},"Answer these in order and the choice usually makes itself. Does the output need to land inside a\nworkbook that already exists? If yes, openpyxl, and no other file-level library will do. Does the\nresult need native charts, conditional formats or more than about 100,000 rows? If yes, xlsxwriter.\nIs the hard part the transform — joins, groupings, reshaping? Then pandas or Polars, with the file\nlibraries reduced to the first and last line of the script. Does the answer depend on Excel\nrecalculating something, refreshing a connection, or running an existing macro? Only then does an\napplication driver enter the picture.",[10,1914,1915,1916,1920],{},"The one question worth asking before any of these: does the deliverable have to be a spreadsheet at\nall? A CSV or Parquet handoff between two automated systems removes the entire question, and\n",[14,1917,1919],{"href":1918},"\u002Fgetting-started-with-python-excel-automation\u002Fchoosing-a-python-excel-library\u002Fexcel-vs-csv-vs-parquet-for-python-data-pipelines\u002F","Excel vs CSV vs Parquet for Python Data Pipelines","\nmakes that case in full. Excel earns its place when a person opens the file — not when a machine\nreads it.",[154,1922,1924],{"id":1923},"version-traps-worth-knowing-before-you-pin","Version traps worth knowing before you pin",[10,1926,1927],{},"Most of the confusing errors in this ecosystem are version problems wearing a library's name, and\nfour of them recur often enough to be worth memorising.",[10,1929,1930,1933,1934,1937,1938,1940,1941,1943,1944,1947,1948,1951,1952,1955,1956,387,1958,1960,1961,1964,1965,1969],{},[301,1931,1932],{},"xlrd"," 2.0 dropped support for ",[301,1935,1936],{},".xls"," entirely. A requirements file that says ",[301,1939,1932],{}," and a workbook\nthat ends in ",[301,1942,1936],{}," will produce ",[301,1945,1946],{},"XLRDError: Excel xlsx file; not supported"," or a flat refusal,\ndepending on which way round the mismatch falls. Pin ",[301,1949,1950],{},"xlrd==1.2.0"," if you genuinely need the legacy\nformat, or install ",[301,1953,1954],{},"python-calamine"," and stop thinking about it — it reads ",[301,1957,1936],{},[301,1959,1490],{}," and\n",[301,1962,1963],{},".xlsb"," through one parser. ",[14,1966,1968],{"href":1967},"\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-openpyxl-does-not-support-the-old-xls-format\u002F","Fix \"openpyxl does not support the old .xls format\"","\nwalks through that specific message.",[10,1971,1972,1973,1976],{},"pandas needs an engine and does not install one. ",[301,1974,1975],{},"ModuleNotFoundError: No module named 'openpyxl'","\nfrom a script that only imported pandas is not a bug — pandas deliberately leaves the parser as an\noptional dependency. Install the engines you actually use and pin them alongside pandas itself.",[10,1978,1979,1980,1983,1984,1987,1988,700],{},"openpyxl's ",[301,1981,1982],{},"data_only=True"," returns ",[301,1985,1986],{},"None",", not a value, when the workbook was last written by a\ntool that never calculated the formulas — which includes every file openpyxl itself produced. The\ncached value only exists if Excel put it there. That trap is set out in\n",[14,1989,1991],{"href":1990},"\u002Fgetting-started-with-python-excel-automation\u002Fworking-with-excel-formulas-in-python\u002Fread-formula-results-with-openpyxl-data-only\u002F","Read Formula Results with openpyxl data_only",[10,1993,1994,1995,1998,1999,2002,2003,2005,2006,2008],{},"Engine names changed. ",[301,1996,1997],{},"engine=\"calamine\""," in ",[301,2000,2001],{},"read_excel"," requires a reasonably recent pandas and\nthe ",[301,2004,1954],{}," package — the pip name and the engine string are deliberately different, and\ninstalling ",[301,2007,1175],{}," instead gets you an unrelated project.",[311,2010,2014],{"className":2011,"code":2013,"language":61,"meta":316},[2012],"language-text","pandas>=2.2          # engine=\"calamine\" support\nopenpyxl>=3.1        # .xlsx read\u002Fwrite and in-place edits\nXlsxWriter>=3.2      # formatted writes, constant_memory\npython-calamine>=0.2 # fast reads, .xls\u002F.xlsb\u002F.xlsx in one parser\n",[301,2015,2013],{"__ignoreMap":316},[10,2017,2018,2019,2023],{},"A pinned requirements block like that one is worth more than any benchmark: it makes the container\nthat runs at 06:00 behave the way the laptop did, which is the failure this ecosystem produces most\noften. ",[14,2020,2022],{"href":2021},"\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Fkeep-excel-report-settings-in-a-config-file\u002F","Keep Excel Report Settings in a Config File","\ncovers the rest of making a job reproducible once the libraries are settled.",[154,2025,2027],{"id":2026},"key-takeaways","Key takeaways",[2029,2030,2031,2035,2038,2041,2046,2049],"ul",{},[2032,2033,2034],"li",{},"The libraries divide by job, not by quality: pandas and Polars shape data, openpyxl and xlsxwriter\nproduce files, xlwings and pywin32 drive the application.",[2032,2036,2037],{},"openpyxl is the only one that can edit an existing workbook, and that edit is lossy for pivot\ncaches, some charts and macros unless you take precautions.",[2032,2039,2040],{},"xlsxwriter cannot open a file, which is exactly why it can stream a million rows in constant memory\nand expose the richest formatting API.",[2032,2042,2043,2045],{},[301,2044,303],{}," is a front end over an engine; swapping in calamine is usually the single\nbiggest read speedup available, and it needs one keyword argument.",[2032,2047,2048],{},"Application drivers earn their overhead only when the value comes from Excel itself — recalculated\nformulas, native PDF output, an existing macro — never for bulk writing.",[2032,2050,2051],{},"Most production scripts use two libraries together, and the combination is the answer, not a\ncompromise.",[154,2053,2055],{"id":2054},"frequently-asked-questions","Frequently asked questions",[10,2057,2058,2062],{},[2059,2060,2061],"strong",{},"Which library should I install first?","\npandas plus openpyxl. That pair reads and writes .xlsx, covers the read-transform-write shape of most reporting jobs, and needs no Excel installation. Add xlsxwriter when the output needs charts or heavy formatting, and python-calamine when reads become the bottleneck.",[10,2064,2065,2068],{},[2059,2066,2067],{},"Can openpyxl and xlsxwriter be used in the same script?","\nNot on the same file handle, but easily in the same job. openpyxl edits an existing workbook in place; xlsxwriter builds a new one from scratch and cannot open a file. A common split is to write the data-heavy sheets with xlsxwriter and reopen the result with openpyxl only if something must be patched afterwards.",[10,2070,2071,2074],{},[2059,2072,2073],{},"Do any of these libraries need Excel installed?","\nOnly xlwings and pywin32 do — they drive a real copy of Excel through its automation interface, so they need Windows or macOS with Excel present. openpyxl, xlsxwriter, pandas, Polars and calamine parse and write the file format directly and run happily on a headless Linux container.",[10,2076,2077,2080],{},[2059,2078,2079],{},"Is Polars a replacement for pandas in Excel work?","\nFor the transform step, often yes — it is faster and its expression API is stricter. For the Excel edges it is thinner: read_excel delegates to calamine or openpyxl anyway, and write_excel wraps xlsxwriter. Choose Polars for the middle of the pipeline, not because of its spreadsheet support.",[10,2082,2083,2086],{},[2059,2084,2085],{},"What reads .xlsb and legacy .xls files?","\nBinary .xlsb needs pyxlsb or python-calamine; legacy .xls needs xlrd pinned to 1.2.0, or calamine, which handles both. openpyxl deliberately refuses anything that is not an OOXML file and raises InvalidFileException.",[10,2088,2089,2092],{},[2059,2090,2091],{},"How much does the choice actually matter?","\nFor a 500-row monthly report, almost not at all — any of them finishes in under a second. The choice starts to matter at three points: when files grow past roughly 100,000 rows, when the output has to carry native charts or conditional formats, and when an existing workbook must be edited without losing what is already in it.",[154,2094,2096],{"id":2095},"related","Related",[2029,2098,2099,2105,2112,2119,2126,2133,2138,2145,2152],{},[2032,2100,2101,2102,2104],{},"Up one level: ",[14,2103,17],{"href":16}," — the read, transform and write pipeline these libraries slot into.",[2032,2106,2107,2111],{},[14,2108,2110],{"href":2109},"\u002Fgetting-started-with-python-excel-automation\u002Fchoosing-a-python-excel-library\u002Fopenpyxl-vs-pandas-for-excel-automation\u002F","openpyxl vs pandas for Excel Automation"," — the two most-installed options, compared on the work each does well.",[2032,2113,2114,2118],{},[14,2115,2117],{"href":2116},"\u002Fgetting-started-with-python-excel-automation\u002Fchoosing-a-python-excel-library\u002Fpandas-vs-polars-for-excel-workflows\u002F","pandas vs Polars for Excel Workflows"," — where Polars wins in the middle of a pipeline and where it is still thin at the edges.",[2032,2120,2121,2125],{},[14,2122,2124],{"href":2123},"\u002Fgetting-started-with-python-excel-automation\u002Fchoosing-a-python-excel-library\u002Fbenchmark-python-excel-read-and-write-speed\u002F","Benchmark Python Excel Read and Write Speed"," — a repeatable harness that measures the libraries on your own files.",[2032,2127,2128,2132],{},[14,2129,2131],{"href":2130},"\u002Fgetting-started-with-python-excel-automation\u002Fchoosing-a-python-excel-library\u002Fpick-an-excel-engine-for-xlsx-xlsm-xls-xlsb-and-ods\u002F","Pick an Excel Engine for .xlsx, .xlsm, .xls, .xlsb and .ods"," — the engine matrix by file extension, with the install lines.",[2032,2134,2135,2137],{},[14,2136,1919],{"href":1918}," — when to stop using Excel as the interchange format.",[2032,2139,2140,2144],{},[14,2141,2143],{"href":2142},"\u002Fgetting-started-with-python-excel-automation\u002Fchoosing-a-python-excel-library\u002Fwhen-to-use-xlwings-instead-of-openpyxl\u002F","When to Use xlwings Instead of openpyxl"," — the four jobs that genuinely need a running copy of Excel.",[2032,2146,2147,2151],{},[14,2148,2150],{"href":2149},"\u002Fgetting-started-with-python-excel-automation\u002Fwriting-dataframes-to-excel-with-pandas\u002Fopenpyxl-vs-xlsxwriter-vs-pandas-excelwriter\u002F","openpyxl vs xlsxwriter vs pandas.ExcelWriter"," — the writing-side comparison in depth.",[2032,2153,2154,2158],{},[14,2155,2157],{"href":2156},"\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-large-excel-files-in-python\u002F","Working with Large Excel Files in Python"," — what changes once the file no longer fits comfortably in memory.",[2160,2161,2162],"style",{},"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-wDw, html code.shiki .s-wDw{--shiki-default:#6A737D;--shiki-dark:#BDC4CC}html pre.shiki code .srMev, html code.shiki .srMev{--shiki-default:#032F62;--shiki-dark:#ADDCFF}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 .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 .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}",{"title":316,"searchDepth":339,"depth":339,"links":2164},[2165,2166,2167,2168,2169,2170,2171,2172,2173,2174,2175,2176,2177],{"id":156,"depth":339,"text":157},{"id":443,"depth":339,"text":444},{"id":703,"depth":339,"text":704},{"id":1009,"depth":339,"text":1010},{"id":1119,"depth":339,"text":1120},{"id":1143,"depth":339,"text":1144},{"id":1483,"depth":339,"text":1484},{"id":1791,"depth":339,"text":1792},{"id":1908,"depth":339,"text":1909},{"id":1923,"depth":339,"text":1924},{"id":2026,"depth":339,"text":2027},{"id":2054,"depth":339,"text":2055},{"id":2095,"depth":339,"text":2096},"2026-09-04","openpyxl, xlsxwriter, pandas, Polars or xlwings? Compare what each library can actually do — read, write, edit in place, drive Excel — and pick by the job.","md",[2182,2184,2186,2188,2190,2192],{"q":2061,"a":2183},"pandas plus openpyxl. That pair reads and writes .xlsx, covers the read-transform-write shape of most reporting jobs, and needs no Excel installation. Add xlsxwriter when the output needs charts or heavy formatting, and python-calamine when reads become the bottleneck.",{"q":2067,"a":2185},"Not on the same file handle, but easily in the same job. openpyxl edits an existing workbook in place; xlsxwriter builds a new one from scratch and cannot open a file. A common split is to write the data-heavy sheets with xlsxwriter and reopen the result with openpyxl only if something must be patched afterwards.",{"q":2073,"a":2187},"Only xlwings and pywin32 do — they drive a real copy of Excel through its automation interface, so they need Windows or macOS with Excel present. openpyxl, xlsxwriter, pandas, Polars and calamine parse and write the file format directly and run happily on a headless Linux container.",{"q":2079,"a":2189},"For the transform step, often yes — it is faster and its expression API is stricter. For the Excel edges it is thinner: read_excel delegates to calamine or openpyxl anyway, and write_excel wraps xlsxwriter. Choose Polars for the middle of the pipeline, not because of its spreadsheet support.",{"q":2085,"a":2191},"Binary .xlsb needs pyxlsb or python-calamine; legacy .xls needs xlrd pinned to 1.2.0, or calamine, which handles both. openpyxl deliberately refuses anything that is not an OOXML file and raises InvalidFileException.",{"q":2091,"a":2193},"For a 500-row monthly report, almost not at all — any of them finishes in under a second. The choice starts to matter at three points: when files grow past roughly 100,000 rows, when the output has to carry native charts or conditional formats, and when an existing workbook must be edited without losing what is already in it.",{"breadcrumb":2195},[2196,2199,2200],{"name":2197,"item":2198},"Home","\u002F",{"name":17,"item":16},{"name":5,"item":2201},"\u002Fgetting-started-with-python-excel-automation\u002Fchoosing-a-python-excel-library\u002F","\u002Fgetting-started-with-python-excel-automation\u002Fchoosing-a-python-excel-library",{"title":2204,"description":2205},"Choosing a Python Excel Library: openpyxl vs xlsxwriter vs pandas","A practical comparison of openpyxl, xlsxwriter, pandas, Polars and xlwings: what each can read, write and edit, how fast they are, and which combination to use.","choosing-a-python-excel-library","getting-started-with-python-excel-automation\u002Fchoosing-a-python-excel-library\u002Findex","guide","o9lkByl8nKUZUwzRicAP9RbC1twKflf8rJ7_FA4cH1k",[2211,2215],{"title":2212,"path":2213,"stem":2214,"children":-1},"xlwings: Run a VBA Macro From Python (Example)","\u002Fgetting-started-with-python-excel-automation\u002Fautomating-excel-with-xlwings-basics\u002Fxlwings-run-macro-from-python-example","getting-started-with-python-excel-automation\u002Fautomating-excel-with-xlwings-basics\u002Fxlwings-run-macro-from-python-example\u002Findex",{"title":2124,"path":2216,"stem":2217,"children":-1},"\u002Fgetting-started-with-python-excel-automation\u002Fchoosing-a-python-excel-library\u002Fbenchmark-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",1788710151644]