[{"data":1,"prerenderedAt":4237},["ShallowReactive",2],{"doc:\u002Fgetting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions":3,"surround:\u002Fgetting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions":4229},{"id":4,"title":5,"body":6,"dateModified":4199,"datePublished":4199,"description":4200,"extension":4201,"faq":4202,"meta":4220,"navigation":760,"path":4221,"seo":4222,"slug":4225,"stem":4226,"type":4227,"__hash__":4228},"docs\u002Fgetting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions\u002Findex.md","Handling Excel File Formats and Conversions",{"type":7,"value":8,"toc":4185},"minimark",[9,39,225,230,233,255,272,292,314,325,464,467,553,573,577,583,726,733,887,894,1110,1125,1129,1135,1181,1187,1287,1307,1311,1317,1422,1425,1534,1546,1550,1556,2018,2038,2046,2050,2063,2146,2482,2490,2725,2729,2735,2832,2835,2842,2845,2853,2865,2884,2887,3155,3165,3169,3172,3293,3436,3447,3450,3611,3625,3629,3632,3724,3731,3737,3908,3913,3920,3924,4001,4005,4034,4058,4073,4093,4105,4129,4133,4181],[10,11,12,13,17,18,21,22,25,26,28,29,32,33,38],"p",{},"The word \"Excel file\" hides at least six different file formats, and Python treats each one differently. A ",[14,15,16],"code",{},".xlsx"," is a zip archive of XML; a ",[14,19,20],{},".xls"," is a decades-old binary format from a different era; a ",[14,23,24],{},".xlsm"," is an ",[14,27,16],{}," carrying a VBA project that most libraries will quietly throw away; a ",[14,30,31],{},".xlsb"," is a binary variant that only a couple of Python packages can read at all. Get the format wrong and you meet a confusing error — or worse, a silent data loss. This page maps the landscape, shows which engine handles each format, and gives you the conversion and detection recipes that keep a reporting job from breaking when somebody emails you the wrong kind of spreadsheet. It sits alongside the other foundations in ",[34,35,37],"a",{"href":36},"\u002Fgetting-started-with-python-excel-automation\u002F","Getting Started with Python Excel Automation",".",[40,41,50,51,50,55,50,59,50,66,50,73,50,77,50,87,50,92,50,97,50,102,50,106,50,110,50,115,50,119,50,123,50,127,50,132,50,137,50,142,50,147,50,152,50,155,50,157,50,161,50,164,50,166,50,170,50,173,50,175,50,179,50,181,50,185,50,206],"svg",{"viewBox":42,"role":43,"ariaLabel":44,"ariaLabelledBy":45,"xmlns":48,"style":49},"0 0 820 300","img","Map of spreadsheet formats and the Python engines that read and write each: xlsx and xlsm through openpyxl, xls through xlrd or calamine, xlsb read-only through pyxlsb, ods through odfpy, and csv through the standard library.",[46,47],"fmt-map-t","fmt-map-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:820px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[52,53,54],"title",{"id":46},"Spreadsheet formats and the Python engine behind each",[56,57,58],"desc",{"id":47},"Five format families on the left connect to the Python packages that handle them on the right. The OOXML zip family, xlsx and xlsm, is served by openpyxl and xlsxwriter for both reading and writing. Legacy binary xls is read by xlrd or python-calamine and cannot be written. The binary xlsb format is read-only through pyxlsb or calamine. OpenDocument ods uses odfpy. CSV needs nothing beyond the standard library or pandas.",[60,61],"rect",{"x":62,"y":62,"width":63,"height":64,"fill":65},"0","820","300","#ffffff",[67,68,72],"text",{"x":69,"y":70,"style":71},"120","26","font-size:12px;font-weight:700;fill:var(--muted,#5b6780);text-anchor:middle","file format",[67,74,76],{"x":75,"y":70,"style":71},"600","Python engine",[60,78],{"x":79,"y":80,"width":81,"height":82,"rx":83,"fill":84,"stroke":85,"style":86},"16","40","208","46","10","#ebebfd","var(--brand,#5b5cf0)","stroke-width:2px",[67,88,91],{"x":69,"y":89,"style":90},"62","font-size:13px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle",".xlsx · .xlsm",[67,93,96],{"x":69,"y":94,"style":95},"78","font-size:10.5px;fill:var(--muted,#5b6780);text-anchor:middle","OOXML — a zip of XML",[60,98],{"x":79,"y":99,"width":81,"height":82,"rx":83,"fill":100,"stroke":101,"style":86},"96","#fdefd8","var(--gold,#b4740a)",[67,103,20],{"x":69,"y":104,"style":105},"118","font-size:13px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle",[67,107,109],{"x":69,"y":108,"style":95},"134","legacy binary (BIFF8)",[60,111],{"x":79,"y":112,"width":81,"height":82,"rx":83,"fill":113,"stroke":114,"style":86},"152","#fee8f2","var(--accent,#f43f8f)",[67,116,31],{"x":69,"y":117,"style":118},"174","font-size:13px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle",[67,120,122],{"x":69,"y":121,"style":95},"190","binary OOXML",[60,124],{"x":79,"y":81,"width":81,"height":80,"rx":83,"fill":125,"stroke":126,"style":86},"#d9f4f1","var(--teal,#0f9488)",[67,128,131],{"x":69,"y":129,"style":130},"233","font-size:13px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle",".ods",[60,133],{"x":79,"y":134,"width":81,"height":80,"rx":83,"fill":135,"stroke":136,"style":86},"256","#f0f2f5","var(--line,#cdd5e6)",[67,138,141],{"x":69,"y":139,"style":140},"281","font-size:13px;font-weight:700;fill:var(--text,#172033);text-anchor:middle",".csv",[60,143],{"x":144,"y":80,"width":145,"height":82,"rx":83,"fill":146,"stroke":85,"style":86},"496","308","#f0f4ff",[67,148,151],{"x":149,"y":89,"style":150},"650","font-size:12.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","openpyxl · xlsxwriter",[67,153,154],{"x":149,"y":94,"style":95},"read and write · styles, charts, formulas",[60,156],{"x":144,"y":99,"width":145,"height":82,"rx":83,"fill":100,"stroke":101,"style":86},[67,158,160],{"x":149,"y":104,"style":159},"font-size:12.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","xlrd · python-calamine",[67,162,163],{"x":149,"y":108,"style":95},"read only — convert to write",[60,165],{"x":144,"y":112,"width":145,"height":82,"rx":83,"fill":113,"stroke":114,"style":86},[67,167,169],{"x":149,"y":117,"style":168},"font-size:12.5px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","pyxlsb · python-calamine",[67,171,172],{"x":149,"y":121,"style":95},"read only",[60,174],{"x":144,"y":81,"width":145,"height":80,"rx":83,"fill":125,"stroke":126,"style":86},[67,176,178],{"x":149,"y":129,"style":177},"font-size:12.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","odfpy — read and write",[60,180],{"x":144,"y":134,"width":145,"height":80,"rx":83,"fill":135,"stroke":136,"style":86},[67,182,184],{"x":149,"y":139,"style":183},"font-size:12.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","stdlib csv · pandas",[186,187,188,189,188,195,188,198,188,201,188,203,50],"g",{"stroke":85,"style":86},"\n    ",[190,191],"line",{"x1":192,"y1":193,"x2":194,"y2":193},"228","63","488",[190,196],{"x1":192,"y1":197,"x2":194,"y2":197},"119",[190,199],{"x1":192,"y1":200,"x2":194,"y2":200},"175",[190,202],{"x1":192,"y1":192,"x2":194,"y2":192},[190,204],{"x1":192,"y1":205,"x2":194,"y2":205},"276",[186,207,188,209,188,213,188,216,188,219,188,222,50],{"fill":208},"#5b5cf0",[210,211],"polygon",{"points":212},"496,63 484,57 484,69",[210,214],{"points":215},"496,119 484,113 484,125",[210,217],{"points":218},"496,175 484,169 484,181",[210,220],{"points":221},"496,228 484,222 484,234",[210,223],{"points":224},"496,276 484,270 484,282",[226,227,229],"h2",{"id":228},"the-five-formats-you-will-actually-meet","The five formats you will actually meet",[10,231,232],{},"Every spreadsheet that lands on a Python script belongs to one of a handful of families, and the family — not the file name — decides which package can open it.",[10,234,235,240,241,244,245,249,250,254],{},[236,237,238],"strong",{},[14,239,16],{}," is the modern default: an OOXML workbook, which is really a zip archive full of XML parts. Unzip one and you find ",[14,242,243],{},"xl\u002Fworksheets\u002Fsheet1.xml",", a styles part, a shared-strings table. Because it is an open, documented format, Python support is excellent — ",[34,246,248],{"href":247},"\u002Fgetting-started-with-python-excel-automation\u002Fusing-openpyxl-for-excel-file-manipulation\u002F","openpyxl"," reads and writes it, ",[34,251,253],{"href":252},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fbuilding-excel-reports-with-xlsxwriter\u002F","xlsxwriter"," writes it very fast, and pandas sits on top of both.",[10,256,257,261,262,265,266,268,269,271],{},[236,258,259],{},[14,260,24],{}," is byte-for-byte the same container with one extra part inside: ",[14,263,264],{},"xl\u002FvbaProject.bin",", holding the macros. Everything that reads ",[14,267,16],{}," reads ",[14,270,24],{}," too. The catch is writing, which is covered in its own section below.",[10,273,274,278,279,282,283,285,286,288,289,38],{},[236,275,276],{},[14,277,20],{}," is the pre-2007 binary format. It caps out at 65,536 rows and 256 columns, stores no OOXML at all, and needs a completely separate reader. Modern ",[14,280,281],{},"xlrd"," reads only this format — support for ",[14,284,16],{}," was removed in ",[14,287,281],{}," 2.0, which is why so much old tutorial code now fails with ",[14,290,291],{},"XLRDError: Excel xlsx file; not supported",[10,293,294,298,299,301,302,306,307,310,311,38],{},[236,295,296],{},[14,297,31],{}," is Excel's binary workbook: the same logical structure as ",[14,300,16],{}," but with the XML replaced by a compact binary encoding. Excel opens it faster and the files are smaller; Python can only ",[303,304,305],"em",{},"read"," it, via ",[14,308,309],{},"pyxlsb"," or ",[14,312,313],{},"python-calamine",[10,315,316,320,321,324],{},[236,317,318],{},[14,319,131],{}," is the OpenDocument spreadsheet used by LibreOffice and Google Sheets exports. pandas reads and writes it through ",[14,322,323],{},"odfpy",", but styling support is thin.",[326,327,328,350],"table",{},[329,330,331],"thead",{},[332,333,334,338,341,344,347],"tr",{},[335,336,337],"th",{},"Format",[335,339,340],{},"Read with",[335,342,343],{},"Write with",[335,345,346],{},"Keeps styles",[335,348,349],{},"Row limit",[351,352,353,372,392,410,426,441],"tbody",{},[332,354,355,360,363,366,369],{},[356,357,358],"td",{},[14,359,16],{},[356,361,362],{},"openpyxl, calamine",[356,364,365],{},"openpyxl, xlsxwriter",[356,367,368],{},"Yes",[356,370,371],{},"1,048,576",[332,373,374,378,380,387,390],{},[356,375,376],{},[14,377,24],{},[356,379,362],{},[356,381,382,383,386],{},"openpyxl (",[14,384,385],{},"keep_vba=True",")",[356,388,389],{},"Yes, plus macros",[356,391,371],{},[332,393,394,398,401,404,407],{},[356,395,396],{},[14,397,20],{},[356,399,400],{},"xlrd, calamine",[356,402,403],{},"— (convert first)",[356,405,406],{},"On read only",[356,408,409],{},"65,536",[332,411,412,416,419,422,424],{},[356,413,414],{},[14,415,31],{},[356,417,418],{},"pyxlsb, calamine",[356,420,421],{},"—",[356,423,406],{},[356,425,371],{},[332,427,428,432,434,436,439],{},[356,429,430],{},[14,431,131],{},[356,433,323],{},[356,435,323],{},[356,437,438],{},"Minimal",[356,440,371],{},[332,442,443,447,454,458,461],{},[356,444,445],{},[14,446,141],{},[356,448,449,450,453],{},"stdlib ",[14,451,452],{},"csv",", pandas",[356,455,449,456,453],{},[14,457,452],{},[356,459,460],{},"No",[356,462,463],{},"none",[10,465,466],{},"Install what you need up front so the failure is a missing package at setup time rather than a traceback at 3 a.m. in a scheduled job:",[468,469,474],"pre",{"className":470,"code":471,"language":472,"meta":473,"style":473},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install pandas openpyxl xlsxwriter    # the everyday set\npip install xlrd                          # legacy .xls\npip install pyxlsb                         # binary .xlsb\npip install odfpy                          # OpenDocument .ods\npip install python-calamine                # one fast reader for xlsx\u002Fxls\u002Fxlsb\u002Fods\n","bash","",[14,475,476,501,514,527,540],{"__ignoreMap":473},[477,478,480,484,488,491,494,497],"span",{"class":190,"line":479},1,[477,481,483],{"class":482},"sMTad","pip",[477,485,487],{"class":486},"srMev"," install",[477,489,490],{"class":486}," pandas",[477,492,493],{"class":486}," openpyxl",[477,495,496],{"class":486}," xlsxwriter",[477,498,500],{"class":499},"s-wDw","    # the everyday set\n",[477,502,504,506,508,511],{"class":190,"line":503},2,[477,505,483],{"class":482},[477,507,487],{"class":486},[477,509,510],{"class":486}," xlrd",[477,512,513],{"class":499},"                          # legacy .xls\n",[477,515,517,519,521,524],{"class":190,"line":516},3,[477,518,483],{"class":482},[477,520,487],{"class":486},[477,522,523],{"class":486}," pyxlsb",[477,525,526],{"class":499},"                         # binary .xlsb\n",[477,528,530,532,534,537],{"class":190,"line":529},4,[477,531,483],{"class":482},[477,533,487],{"class":486},[477,535,536],{"class":486}," odfpy",[477,538,539],{"class":499},"                          # OpenDocument .ods\n",[477,541,543,545,547,550],{"class":190,"line":542},5,[477,544,483],{"class":482},[477,546,487],{"class":486},[477,548,549],{"class":486}," python-calamine",[477,551,552],{"class":499},"                # one fast reader for xlsx\u002Fxls\u002Fxlsb\u002Fods\n",[10,554,555,557,558,560,561,560,563,565,566,568,569,572],{},[14,556,313],{}," deserves a note: it is a Rust-backed reader that handles ",[14,559,16],{},", ",[14,562,20],{},[14,564,31],{}," and ",[14,567,131],{}," behind a single interface, and pandas ships an official ",[14,570,571],{},"engine=\"calamine\""," for it. It reads only — no writing, no styles — but for the \"just get the values into a DataFrame\" case it is both the fastest option and the one that spares you from installing four packages.",[226,574,576],{"id":575},"how-pandas-chooses-an-engine","How pandas chooses an engine",[10,578,579,582],{},[14,580,581],{},"pd.read_excel"," looks at the file extension and maps it to an engine. That mapping is the source of most confusing errors, because the extension can be wrong and the engine can be missing.",[40,584,50,591,50,594,50,597,50,601,50,606,50,611,50,615,50,619,50,622,50,625,50,630,50,633,50,638,50,642,50,645,50,649,50,664,50,679,50,683,50,688,50,692,50,695,50,697,50,699,50,702,50,704,50,706,50,709,50,712,50,715,50,721],{"viewBox":585,"role":43,"ariaLabel":586,"ariaLabelledBy":587,"xmlns":48,"style":590},"0 0 800 296","Decision flow showing how pandas read_excel picks an engine: an explicit engine argument wins, otherwise the file extension selects openpyxl, xlrd, pyxlsb or odf, and a missing package raises ImportError.",[588,589],"engine-t","engine-d","width:100%;max-width:800px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[52,592,593],{"id":588},"How pandas resolves the engine for read_excel",[56,595,596],{"id":589},"A decision tree. If the engine argument is given, pandas uses it directly. Otherwise it inspects the file suffix: xlsx and xlsm route to openpyxl, xls routes to xlrd, xlsb routes to pyxlsb, and ods routes to odf. If the chosen package is not installed the call raises ImportError naming the missing package, which is the most common failure.",[60,598],{"x":62,"y":62,"width":599,"height":600,"fill":65},"800","296",[60,602],{"x":603,"y":604,"width":192,"height":82,"rx":605,"fill":84,"stroke":85,"style":86},"286","14","11",[67,607,610],{"x":608,"y":609,"style":90},"400","35","pd.read_excel(path)",[67,612,614],{"x":608,"y":613,"style":95},"52","engine= given?",[190,616],{"x1":608,"y1":617,"x2":608,"y2":618,"stroke":85,"style":86},"60","82",[210,620],{"points":621,"fill":208},"400,90 393,78 407,78",[60,623],{"x":624,"y":604,"width":121,"height":82,"rx":605,"fill":125,"stroke":126,"style":86},"596",[67,626,629],{"x":627,"y":609,"style":628},"691","font-size:12px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","yes → use it",[67,631,632],{"x":627,"y":613,"style":95},"extension ignored",[190,634],{"x1":635,"y1":636,"x2":637,"y2":636,"stroke":126,"style":86},"514","37","588",[210,639],{"points":640,"fill":641},"596,37 584,31 584,43","#0f9488",[60,643],{"x":603,"y":644,"width":192,"height":80,"rx":605,"fill":100,"stroke":101,"style":86},"90",[67,646,648],{"x":608,"y":647,"style":159},"115","no → inspect the suffix",[186,650,188,651,188,655,188,658,188,661,50],{"stroke":101,"style":86,"fill":463},[652,653],"path",{"d":654},"M400 130 V 152 H 96 V 178",[652,656],{"d":657},"M400 130 V 152 H 296 V 178",[652,659],{"d":660},"M400 130 V 152 H 496 V 178",[652,662],{"d":663},"M400 130 V 152 H 700 V 178",[186,665,188,667,188,670,188,673,188,676,50],{"fill":666},"#b4740a",[210,668],{"points":669},"96,186 89,174 103,174",[210,671],{"points":672},"296,186 289,174 303,174",[210,674],{"points":675},"496,186 489,174 503,174",[210,677],{"points":678},"700,186 693,174 707,174",[60,680],{"x":79,"y":681,"width":682,"height":613,"rx":83,"fill":146,"stroke":85,"style":86},"186","160",[67,684,687],{"x":99,"y":685,"style":686},"206","font-size:12px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle",".xlsx \u002F .xlsm",[67,689,248],{"x":99,"y":690,"style":691},"226","font-size:11.5px;fill:var(--text,#172033);text-anchor:middle",[60,693],{"x":694,"y":681,"width":682,"height":613,"rx":83,"fill":146,"stroke":85,"style":86},"216",[67,696,20],{"x":600,"y":685,"style":686},[67,698,281],{"x":600,"y":690,"style":691},[60,700],{"x":701,"y":681,"width":682,"height":613,"rx":83,"fill":146,"stroke":85,"style":86},"416",[67,703,31],{"x":144,"y":685,"style":686},[67,705,309],{"x":144,"y":690,"style":691},[60,707],{"x":708,"y":681,"width":682,"height":613,"rx":83,"fill":146,"stroke":85,"style":86},"620",[67,710,131],{"x":711,"y":685,"style":686},"700",[67,713,714],{"x":711,"y":690,"style":691},"odf",[60,716],{"x":694,"y":717,"width":718,"height":719,"rx":720,"fill":113,"stroke":114,"style":86},"252","368","34","9",[67,722,725],{"x":608,"y":723,"style":724},"274","font-size:11.5px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","package missing → ImportError naming it",[10,727,728,729,732],{},"You can always override the guess. Passing ",[14,730,731],{},"engine="," explicitly is the fix when a file has the wrong extension, and it is how you opt into the fast calamine reader:",[468,734,738],{"className":735,"code":736,"language":737,"meta":473,"style":473},"language-python shiki shiki-themes github-light github-dark-high-contrast","import pandas as pd\n\n# Let the extension decide (the usual case).\ndf = pd.read_excel(\"sales.xlsx\")\n\n# Force an engine — useful when the extension lies, or for speed.\ndf = pd.read_excel(\"sales.xlsx\", engine=\"calamine\")\ndf = pd.read_excel(\"legacy.xls\", engine=\"xlrd\")\ndf = pd.read_excel(\"archive.xlsb\", engine=\"pyxlsb\")\ndf = pd.read_excel(\"export.ods\", engine=\"odf\")\n","python",[14,739,740,756,762,767,784,788,794,818,841,864],{"__ignoreMap":473},[477,741,742,746,750,753],{"class":190,"line":479},[477,743,745],{"class":744},"s-kum","import",[477,747,749],{"class":748},"skGVy"," pandas ",[477,751,752],{"class":744},"as",[477,754,755],{"class":748}," pd\n",[477,757,758],{"class":190,"line":503},[477,759,761],{"emptyLinePlaceholder":760},true,"\n",[477,763,764],{"class":190,"line":516},[477,765,766],{"class":499},"# Let the extension decide (the usual case).\n",[477,768,769,772,775,778,781],{"class":190,"line":529},[477,770,771],{"class":748},"df ",[477,773,774],{"class":744},"=",[477,776,777],{"class":748}," pd.read_excel(",[477,779,780],{"class":486},"\"sales.xlsx\"",[477,782,783],{"class":748},")\n",[477,785,786],{"class":190,"line":542},[477,787,761],{"emptyLinePlaceholder":760},[477,789,791],{"class":190,"line":790},6,[477,792,793],{"class":499},"# Force an engine — useful when the extension lies, or for speed.\n",[477,795,797,799,801,803,805,807,811,813,816],{"class":190,"line":796},7,[477,798,771],{"class":748},[477,800,774],{"class":744},[477,802,777],{"class":748},[477,804,780],{"class":486},[477,806,560],{"class":748},[477,808,810],{"class":809},"sa561","engine",[477,812,774],{"class":744},[477,814,815],{"class":486},"\"calamine\"",[477,817,783],{"class":748},[477,819,821,823,825,827,830,832,834,836,839],{"class":190,"line":820},8,[477,822,771],{"class":748},[477,824,774],{"class":744},[477,826,777],{"class":748},[477,828,829],{"class":486},"\"legacy.xls\"",[477,831,560],{"class":748},[477,833,810],{"class":809},[477,835,774],{"class":744},[477,837,838],{"class":486},"\"xlrd\"",[477,840,783],{"class":748},[477,842,844,846,848,850,853,855,857,859,862],{"class":190,"line":843},9,[477,845,771],{"class":748},[477,847,774],{"class":744},[477,849,777],{"class":748},[477,851,852],{"class":486},"\"archive.xlsb\"",[477,854,560],{"class":748},[477,856,810],{"class":809},[477,858,774],{"class":744},[477,860,861],{"class":486},"\"pyxlsb\"",[477,863,783],{"class":748},[477,865,867,869,871,873,876,878,880,882,885],{"class":190,"line":866},10,[477,868,771],{"class":748},[477,870,774],{"class":744},[477,872,777],{"class":748},[477,874,875],{"class":486},"\"export.ods\"",[477,877,560],{"class":748},[477,879,810],{"class":809},[477,881,774],{"class":744},[477,883,884],{"class":486},"\"odf\"",[477,886,783],{"class":748},[10,888,889,890,893],{},"Writing follows the same pattern through ",[14,891,892],{},"ExcelWriter",", where the engine choice also decides which formatting features you get:",[468,895,897],{"className":735,"code":896,"language":737,"meta":473,"style":473},"import pandas as pd\n\ndf = pd.DataFrame({\"region\": [\"North\", \"South\"], \"revenue\": [159.92, 247.50]})\n\nwith pd.ExcelWriter(\"out.xlsx\", engine=\"openpyxl\") as writer:\n    df.to_excel(writer, sheet_name=\"Summary\", index=False)\n\nwith pd.ExcelWriter(\"out_fast.xlsx\", engine=\"xlsxwriter\") as writer:\n    df.to_excel(writer, sheet_name=\"Summary\", index=False)\n\nwith pd.ExcelWriter(\"out.ods\", engine=\"odf\") as writer:\n    df.to_excel(writer, sheet_name=\"Summary\", index=False)\n",[14,898,899,909,913,956,960,988,1013,1017,1041,1061,1065,1089],{"__ignoreMap":473},[477,900,901,903,905,907],{"class":190,"line":479},[477,902,745],{"class":744},[477,904,749],{"class":748},[477,906,752],{"class":744},[477,908,755],{"class":748},[477,910,911],{"class":190,"line":503},[477,912,761],{"emptyLinePlaceholder":760},[477,914,915,917,919,922,925,928,931,933,936,939,942,944,948,950,953],{"class":190,"line":516},[477,916,771],{"class":748},[477,918,774],{"class":744},[477,920,921],{"class":748}," pd.DataFrame({",[477,923,924],{"class":486},"\"region\"",[477,926,927],{"class":748},": [",[477,929,930],{"class":486},"\"North\"",[477,932,560],{"class":748},[477,934,935],{"class":486},"\"South\"",[477,937,938],{"class":748},"], ",[477,940,941],{"class":486},"\"revenue\"",[477,943,927],{"class":748},[477,945,947],{"class":946},"sP0c6","159.92",[477,949,560],{"class":748},[477,951,952],{"class":946},"247.50",[477,954,955],{"class":748},"]})\n",[477,957,958],{"class":190,"line":529},[477,959,761],{"emptyLinePlaceholder":760},[477,961,962,965,968,971,973,975,977,980,983,985],{"class":190,"line":542},[477,963,964],{"class":744},"with",[477,966,967],{"class":748}," pd.ExcelWriter(",[477,969,970],{"class":486},"\"out.xlsx\"",[477,972,560],{"class":748},[477,974,810],{"class":809},[477,976,774],{"class":744},[477,978,979],{"class":486},"\"openpyxl\"",[477,981,982],{"class":748},") ",[477,984,752],{"class":744},[477,986,987],{"class":748}," writer:\n",[477,989,990,993,996,998,1001,1003,1006,1008,1011],{"class":190,"line":790},[477,991,992],{"class":748},"    df.to_excel(writer, ",[477,994,995],{"class":809},"sheet_name",[477,997,774],{"class":744},[477,999,1000],{"class":486},"\"Summary\"",[477,1002,560],{"class":748},[477,1004,1005],{"class":809},"index",[477,1007,774],{"class":744},[477,1009,1010],{"class":946},"False",[477,1012,783],{"class":748},[477,1014,1015],{"class":190,"line":796},[477,1016,761],{"emptyLinePlaceholder":760},[477,1018,1019,1021,1023,1026,1028,1030,1032,1035,1037,1039],{"class":190,"line":820},[477,1020,964],{"class":744},[477,1022,967],{"class":748},[477,1024,1025],{"class":486},"\"out_fast.xlsx\"",[477,1027,560],{"class":748},[477,1029,810],{"class":809},[477,1031,774],{"class":744},[477,1033,1034],{"class":486},"\"xlsxwriter\"",[477,1036,982],{"class":748},[477,1038,752],{"class":744},[477,1040,987],{"class":748},[477,1042,1043,1045,1047,1049,1051,1053,1055,1057,1059],{"class":190,"line":843},[477,1044,992],{"class":748},[477,1046,995],{"class":809},[477,1048,774],{"class":744},[477,1050,1000],{"class":486},[477,1052,560],{"class":748},[477,1054,1005],{"class":809},[477,1056,774],{"class":744},[477,1058,1010],{"class":946},[477,1060,783],{"class":748},[477,1062,1063],{"class":190,"line":866},[477,1064,761],{"emptyLinePlaceholder":760},[477,1066,1068,1070,1072,1075,1077,1079,1081,1083,1085,1087],{"class":190,"line":1067},11,[477,1069,964],{"class":744},[477,1071,967],{"class":748},[477,1073,1074],{"class":486},"\"out.ods\"",[477,1076,560],{"class":748},[477,1078,810],{"class":809},[477,1080,774],{"class":744},[477,1082,884],{"class":486},[477,1084,982],{"class":748},[477,1086,752],{"class":744},[477,1088,987],{"class":748},[477,1090,1092,1094,1096,1098,1100,1102,1104,1106,1108],{"class":190,"line":1091},12,[477,1093,992],{"class":748},[477,1095,995],{"class":809},[477,1097,774],{"class":744},[477,1099,1000],{"class":486},[477,1101,560],{"class":748},[477,1103,1005],{"class":809},[477,1105,774],{"class":744},[477,1107,1010],{"class":946},[477,1109,783],{"class":748},[10,1111,1112,1113,1115,1116,1120,1121,1124],{},"The trade-off between the two ",[14,1114,16],{}," writers is covered in detail in ",[34,1117,1119],{"href":1118},"\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 short version: xlsxwriter is faster and has richer formatting, openpyxl is the only one that can ",[303,1122,1123],{},"modify"," an existing file.",[226,1126,1128],{"id":1127},"reading-the-legacy-binary-format","Reading the legacy binary format",[10,1130,1131,1132,1134],{},"A ",[14,1133,20],{}," file cannot be opened by openpyxl at all — the error is blunt and confuses people who expect one library to do everything:",[468,1136,1138],{"className":735,"code":1137,"language":737,"meta":473,"style":473},"from openpyxl import load_workbook\n\nload_workbook(\"legacy.xls\")\n# InvalidFileException: openpyxl does not support the old .xls file format,\n# please use xlrd to read this file, or convert it to the more recent\n# .xlsx file format.\n",[14,1139,1140,1153,1157,1166,1171,1176],{"__ignoreMap":473},[477,1141,1142,1145,1148,1150],{"class":190,"line":479},[477,1143,1144],{"class":744},"from",[477,1146,1147],{"class":748}," openpyxl ",[477,1149,745],{"class":744},[477,1151,1152],{"class":748}," load_workbook\n",[477,1154,1155],{"class":190,"line":503},[477,1156,761],{"emptyLinePlaceholder":760},[477,1158,1159,1162,1164],{"class":190,"line":516},[477,1160,1161],{"class":748},"load_workbook(",[477,1163,829],{"class":486},[477,1165,783],{"class":748},[477,1167,1168],{"class":190,"line":529},[477,1169,1170],{"class":499},"# InvalidFileException: openpyxl does not support the old .xls file format,\n",[477,1172,1173],{"class":190,"line":542},[477,1174,1175],{"class":499},"# please use xlrd to read this file, or convert it to the more recent\n",[477,1177,1178],{"class":190,"line":790},[477,1179,1180],{"class":499},"# .xlsx file format.\n",[10,1182,1183,1184,1186],{},"The pragmatic answer is to read it once with a reader that understands BIFF, then work in ",[14,1185,16],{}," from there:",[468,1188,1190],{"className":735,"code":1189,"language":737,"meta":473,"style":473},"import pandas as pd\n\n# One call, one engine, straight into a DataFrame.\ndf = pd.read_excel(\"legacy.xls\", engine=\"xlrd\")\n\n# Or read every sheet at once.\nsheets = pd.read_excel(\"legacy.xls\", sheet_name=None, engine=\"xlrd\")\nprint(list(sheets))          # ['Q1', 'Q2', 'Notes']\n",[14,1191,1192,1202,1206,1211,1231,1235,1240,1270],{"__ignoreMap":473},[477,1193,1194,1196,1198,1200],{"class":190,"line":479},[477,1195,745],{"class":744},[477,1197,749],{"class":748},[477,1199,752],{"class":744},[477,1201,755],{"class":748},[477,1203,1204],{"class":190,"line":503},[477,1205,761],{"emptyLinePlaceholder":760},[477,1207,1208],{"class":190,"line":516},[477,1209,1210],{"class":499},"# One call, one engine, straight into a DataFrame.\n",[477,1212,1213,1215,1217,1219,1221,1223,1225,1227,1229],{"class":190,"line":529},[477,1214,771],{"class":748},[477,1216,774],{"class":744},[477,1218,777],{"class":748},[477,1220,829],{"class":486},[477,1222,560],{"class":748},[477,1224,810],{"class":809},[477,1226,774],{"class":744},[477,1228,838],{"class":486},[477,1230,783],{"class":748},[477,1232,1233],{"class":190,"line":542},[477,1234,761],{"emptyLinePlaceholder":760},[477,1236,1237],{"class":190,"line":790},[477,1238,1239],{"class":499},"# Or read every sheet at once.\n",[477,1241,1242,1245,1247,1249,1251,1253,1255,1257,1260,1262,1264,1266,1268],{"class":190,"line":796},[477,1243,1244],{"class":748},"sheets ",[477,1246,774],{"class":744},[477,1248,777],{"class":748},[477,1250,829],{"class":486},[477,1252,560],{"class":748},[477,1254,995],{"class":809},[477,1256,774],{"class":744},[477,1258,1259],{"class":946},"None",[477,1261,560],{"class":748},[477,1263,810],{"class":809},[477,1265,774],{"class":744},[477,1267,838],{"class":486},[477,1269,783],{"class":748},[477,1271,1272,1275,1278,1281,1284],{"class":190,"line":820},[477,1273,1274],{"class":946},"print",[477,1276,1277],{"class":748},"(",[477,1279,1280],{"class":946},"list",[477,1282,1283],{"class":748},"(sheets))          ",[477,1285,1286],{"class":499},"# ['Q1', 'Q2', 'Notes']\n",[10,1288,1289,1290,1292,1293,1295,1296,1299,1300,1302,1303,38],{},"Remember the row cap. A ",[14,1291,20],{}," sheet holds at most 65,536 rows, so if an upstream system exports to ",[14,1294,20],{}," and your data has outgrown that, rows are being lost ",[303,1297,1298],{},"before"," Python ever sees the file. The full walkthrough — including the pitfalls of dates and the ",[14,1301,281],{}," 2.0 breaking change — is in ",[34,1304,1306],{"href":1305},"\u002Fgetting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions\u002Fread-xls-files-in-python-with-xlrd-and-pandas\u002F","reading .xls files in Python",[226,1308,1310],{"id":1309},"keeping-macros-alive-in-xlsm","Keeping macros alive in .xlsm",[10,1312,1313,1314,1316],{},"This is the trap that costs people the most time. Load an ",[14,1315,24],{},", change one cell, save it — and the macros are gone, with no warning at all.",[40,1318,50,1325,50,1328,50,1331,50,1335,50,1340,50,1346,50,1349,50,1354,50,1357,50,1365,50,1370,50,1374,50,1378,50,1381,50,1385,50,1389,50,1393,50,1397,50,1399,50,1402,50,1406,50,1411,50,1414,50,1416,50,1419],{"viewBox":1319,"role":43,"ariaLabel":1320,"ariaLabelledBy":1321,"xmlns":48,"style":1324},"0 0 780 250","Two paths for editing a macro-enabled workbook: loading without keep_vba drops the vbaProject part silently, while loading with keep_vba equals True and saving as .xlsm preserves it.",[1322,1323],"vba-t","vba-d","width:100%;max-width:780px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[52,1326,1327],{"id":1322},"What happens to vbaProject.bin when openpyxl saves an .xlsm",[56,1329,1330],{"id":1323},"The source workbook contains worksheets plus a vbaProject binary part. The upper path loads it with default options and saves, producing a file whose macros have been dropped without any error. The lower path passes keep_vba equals True and writes back to an .xlsm name, so the vbaProject part survives into the output.",[60,1332],{"x":62,"y":62,"width":1333,"height":1334,"fill":65},"780","250",[60,1336],{"x":604,"y":1337,"width":1338,"height":618,"rx":1339,"fill":84,"stroke":85,"style":86},"86","150","12",[67,1341,1345],{"x":1342,"y":1343,"style":1344},"89","70","font-size:11px;font-weight:700;fill:var(--muted,#5b6780);text-anchor:middle","source",[67,1347,1348],{"x":1342,"y":647,"style":90},"report.xlsm",[67,1350,1353],{"x":1342,"y":1351,"style":1352},"136","font-size:10.5px;fill:var(--text,#172033);text-anchor:middle","sheets +",[67,1355,1356],{"x":1342,"y":112,"style":1352},"vbaProject.bin",[186,1358,188,1359,188,1362,50],{"stroke":136,"style":86,"fill":463},[652,1360],{"d":1361},"M164 112 H 200 V 62 H 232",[652,1363],{"d":1364},"M164 142 H 200 V 190 H 232",[60,1366],{"x":1367,"y":719,"width":1368,"height":1369,"rx":605,"fill":113,"stroke":114,"style":86},"240","222","56",[67,1371,1373],{"x":1372,"y":1369,"style":724},"351","load_workbook(path)",[67,1375,1377],{"x":1372,"y":1376,"style":95},"76","default — VBA not loaded",[60,1379],{"x":1367,"y":1380,"width":1368,"height":1369,"rx":605,"fill":125,"stroke":126,"style":86},"162",[67,1382,385],{"x":1372,"y":1383,"style":1384},"184","font-size:11.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle",[67,1386,1388],{"x":1372,"y":1387,"style":95},"204","VBA part held in memory",[190,1390],{"x1":1391,"y1":89,"x2":1392,"y2":89,"stroke":114,"style":86},"462","530",[210,1394],{"points":1395,"fill":1396},"538,62 526,56 526,68","#f43f8f",[190,1398],{"x1":1391,"y1":121,"x2":1392,"y2":121,"stroke":126,"style":86},[210,1400],{"points":1401,"fill":641},"538,190 526,184 526,196",[60,1403],{"x":1404,"y":719,"width":1405,"height":1369,"rx":605,"fill":113,"stroke":114,"style":86},"546","220",[67,1407,1410],{"x":1408,"y":1369,"style":1409},"656","font-size:12px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","macros gone",[67,1412,1413],{"x":1408,"y":1376,"style":95},"no error, no warning",[60,1415],{"x":1404,"y":1380,"width":1405,"height":1369,"rx":605,"fill":125,"stroke":126,"style":86},[67,1417,1418],{"x":1408,"y":1383,"style":628},"macros intact",[67,1420,1421],{"x":1408,"y":1387,"style":95},"saved back as .xlsm",[10,1423,1424],{},"The rule has two halves, and both are required:",[468,1426,1428],{"className":735,"code":1427,"language":737,"meta":473,"style":473},"from openpyxl import load_workbook\n\n# 1. Tell openpyxl to carry the VBA project through.\nwb = load_workbook(\"report.xlsm\", keep_vba=True)\n\nws = wb[\"Data\"]\nws[\"B2\"] = 4821\n\n# 2. Save with an .xlsm name. Saving as .xlsx discards the macros\n#    even when keep_vba was set.\nwb.save(\"report_updated.xlsm\")\n",[14,1429,1430,1440,1444,1449,1474,1478,1494,1510,1514,1519,1524],{"__ignoreMap":473},[477,1431,1432,1434,1436,1438],{"class":190,"line":479},[477,1433,1144],{"class":744},[477,1435,1147],{"class":748},[477,1437,745],{"class":744},[477,1439,1152],{"class":748},[477,1441,1442],{"class":190,"line":503},[477,1443,761],{"emptyLinePlaceholder":760},[477,1445,1446],{"class":190,"line":516},[477,1447,1448],{"class":499},"# 1. Tell openpyxl to carry the VBA project through.\n",[477,1450,1451,1454,1456,1459,1462,1464,1467,1469,1472],{"class":190,"line":529},[477,1452,1453],{"class":748},"wb ",[477,1455,774],{"class":744},[477,1457,1458],{"class":748}," load_workbook(",[477,1460,1461],{"class":486},"\"report.xlsm\"",[477,1463,560],{"class":748},[477,1465,1466],{"class":809},"keep_vba",[477,1468,774],{"class":744},[477,1470,1471],{"class":946},"True",[477,1473,783],{"class":748},[477,1475,1476],{"class":190,"line":542},[477,1477,761],{"emptyLinePlaceholder":760},[477,1479,1480,1483,1485,1488,1491],{"class":190,"line":790},[477,1481,1482],{"class":748},"ws ",[477,1484,774],{"class":744},[477,1486,1487],{"class":748}," wb[",[477,1489,1490],{"class":486},"\"Data\"",[477,1492,1493],{"class":748},"]\n",[477,1495,1496,1499,1502,1505,1507],{"class":190,"line":796},[477,1497,1498],{"class":748},"ws[",[477,1500,1501],{"class":486},"\"B2\"",[477,1503,1504],{"class":748},"] ",[477,1506,774],{"class":744},[477,1508,1509],{"class":946}," 4821\n",[477,1511,1512],{"class":190,"line":820},[477,1513,761],{"emptyLinePlaceholder":760},[477,1515,1516],{"class":190,"line":843},[477,1517,1518],{"class":499},"# 2. Save with an .xlsm name. Saving as .xlsx discards the macros\n",[477,1520,1521],{"class":190,"line":866},[477,1522,1523],{"class":499},"#    even when keep_vba was set.\n",[477,1525,1526,1529,1532],{"class":190,"line":1067},[477,1527,1528],{"class":748},"wb.save(",[477,1530,1531],{"class":486},"\"report_updated.xlsm\"",[477,1533,783],{"class":748},[10,1535,1536,1537,1540,1541,1545],{},"There is a second, subtler half to the rule: openpyxl preserves the VBA ",[303,1538,1539],{},"binary"," but does not understand it. If a macro refers to a sheet you renamed or a named range you deleted, the file still opens and the macro still exists — it just fails at run time inside Excel. Treat macro-enabled workbooks as templates whose structure you fill, not restructure; the template-filling pattern in ",[34,1542,1544],{"href":1543},"\u002Fautomating-reporting-workflows\u002Fgenerating-excel-reports-from-templates\u002Fpopulate-excel-template-without-losing-formatting\u002F","populate an Excel template without losing formatting"," applies directly.",[226,1547,1549],{"id":1548},"converting-between-formats-in-bulk","Converting between formats in bulk",[10,1551,1552,1553,1555],{},"Conversion is the pressure valve for every format problem: read once with whatever engine understands the input, write out ",[14,1554,16],{},", and let the rest of your pipeline assume one format. Because the read side is uniform through pandas, a converter is short.",[468,1557,1559],{"className":735,"code":1558,"language":737,"meta":473,"style":473},"from pathlib import Path\nimport pandas as pd\n\nREADERS = {\n    \".xls\": \"xlrd\",\n    \".xlsb\": \"pyxlsb\",\n    \".ods\": \"odf\",\n    \".xlsx\": \"openpyxl\",\n    \".xlsm\": \"openpyxl\",\n}\n\ndef convert_to_xlsx(src, out_dir=\"converted\"):\n    \"\"\"Read any supported spreadsheet and write every sheet to one .xlsx.\"\"\"\n    src = Path(src)\n    engine = READERS.get(src.suffix.lower())\n    if engine is None:\n        raise ValueError(f\"Unsupported input format: {src.suffix}\")\n\n    sheets = pd.read_excel(src, sheet_name=None, engine=engine)\n\n    out_dir = Path(out_dir)\n    out_dir.mkdir(parents=True, exist_ok=True)\n    dest = out_dir \u002F (src.stem + \".xlsx\")\n\n    with pd.ExcelWriter(dest, engine=\"xlsxwriter\") as writer:\n        for name, frame in sheets.items():\n            # Excel sheet names cap at 31 characters.\n            frame.to_excel(writer, sheet_name=name[:31], index=False)\n    return dest\n\nif __name__ == \"__main__\":\n    for path in Path(\"inbox\").glob(\"*.*\"):\n        if path.suffix.lower() in READERS:\n            print(\"converted\", convert_to_xlsx(path))\n",[14,1560,1561,1573,1583,1587,1598,1611,1622,1633,1644,1655,1660,1664,1684,1690,1701,1715,1733,1765,1770,1796,1801,1812,1836,1861,1866,1887,1902,1908,1934,1943,1948,1965,1990,2005],{"__ignoreMap":473},[477,1562,1563,1565,1568,1570],{"class":190,"line":479},[477,1564,1144],{"class":744},[477,1566,1567],{"class":748}," pathlib ",[477,1569,745],{"class":744},[477,1571,1572],{"class":748}," Path\n",[477,1574,1575,1577,1579,1581],{"class":190,"line":503},[477,1576,745],{"class":744},[477,1578,749],{"class":748},[477,1580,752],{"class":744},[477,1582,755],{"class":748},[477,1584,1585],{"class":190,"line":516},[477,1586,761],{"emptyLinePlaceholder":760},[477,1588,1589,1592,1595],{"class":190,"line":529},[477,1590,1591],{"class":946},"READERS",[477,1593,1594],{"class":744}," =",[477,1596,1597],{"class":748}," {\n",[477,1599,1600,1603,1606,1608],{"class":190,"line":542},[477,1601,1602],{"class":486},"    \".xls\"",[477,1604,1605],{"class":748},": ",[477,1607,838],{"class":486},[477,1609,1610],{"class":748},",\n",[477,1612,1613,1616,1618,1620],{"class":190,"line":790},[477,1614,1615],{"class":486},"    \".xlsb\"",[477,1617,1605],{"class":748},[477,1619,861],{"class":486},[477,1621,1610],{"class":748},[477,1623,1624,1627,1629,1631],{"class":190,"line":796},[477,1625,1626],{"class":486},"    \".ods\"",[477,1628,1605],{"class":748},[477,1630,884],{"class":486},[477,1632,1610],{"class":748},[477,1634,1635,1638,1640,1642],{"class":190,"line":820},[477,1636,1637],{"class":486},"    \".xlsx\"",[477,1639,1605],{"class":748},[477,1641,979],{"class":486},[477,1643,1610],{"class":748},[477,1645,1646,1649,1651,1653],{"class":190,"line":843},[477,1647,1648],{"class":486},"    \".xlsm\"",[477,1650,1605],{"class":748},[477,1652,979],{"class":486},[477,1654,1610],{"class":748},[477,1656,1657],{"class":190,"line":866},[477,1658,1659],{"class":748},"}\n",[477,1661,1662],{"class":190,"line":1067},[477,1663,761],{"emptyLinePlaceholder":760},[477,1665,1666,1669,1673,1676,1678,1681],{"class":190,"line":1091},[477,1667,1668],{"class":744},"def",[477,1670,1672],{"class":1671},"s_Opv"," convert_to_xlsx",[477,1674,1675],{"class":748},"(src, out_dir",[477,1677,774],{"class":744},[477,1679,1680],{"class":486},"\"converted\"",[477,1682,1683],{"class":748},"):\n",[477,1685,1687],{"class":190,"line":1686},13,[477,1688,1689],{"class":486},"    \"\"\"Read any supported spreadsheet and write every sheet to one .xlsx.\"\"\"\n",[477,1691,1693,1696,1698],{"class":190,"line":1692},14,[477,1694,1695],{"class":748},"    src ",[477,1697,774],{"class":744},[477,1699,1700],{"class":748}," Path(src)\n",[477,1702,1704,1707,1709,1712],{"class":190,"line":1703},15,[477,1705,1706],{"class":748},"    engine ",[477,1708,774],{"class":744},[477,1710,1711],{"class":946}," READERS",[477,1713,1714],{"class":748},".get(src.suffix.lower())\n",[477,1716,1718,1721,1724,1727,1730],{"class":190,"line":1717},16,[477,1719,1720],{"class":744},"    if",[477,1722,1723],{"class":748}," engine ",[477,1725,1726],{"class":744},"is",[477,1728,1729],{"class":946}," None",[477,1731,1732],{"class":748},":\n",[477,1734,1736,1739,1742,1744,1747,1750,1754,1757,1760,1763],{"class":190,"line":1735},17,[477,1737,1738],{"class":744},"        raise",[477,1740,1741],{"class":946}," ValueError",[477,1743,1277],{"class":748},[477,1745,1746],{"class":744},"f",[477,1748,1749],{"class":486},"\"Unsupported input format: ",[477,1751,1753],{"class":1752},"sSjpA","{",[477,1755,1756],{"class":748},"src.suffix",[477,1758,1759],{"class":1752},"}",[477,1761,1762],{"class":486},"\"",[477,1764,783],{"class":748},[477,1766,1768],{"class":190,"line":1767},18,[477,1769,761],{"emptyLinePlaceholder":760},[477,1771,1773,1776,1778,1781,1783,1785,1787,1789,1791,1793],{"class":190,"line":1772},19,[477,1774,1775],{"class":748},"    sheets ",[477,1777,774],{"class":744},[477,1779,1780],{"class":748}," pd.read_excel(src, ",[477,1782,995],{"class":809},[477,1784,774],{"class":744},[477,1786,1259],{"class":946},[477,1788,560],{"class":748},[477,1790,810],{"class":809},[477,1792,774],{"class":744},[477,1794,1795],{"class":748},"engine)\n",[477,1797,1799],{"class":190,"line":1798},20,[477,1800,761],{"emptyLinePlaceholder":760},[477,1802,1804,1807,1809],{"class":190,"line":1803},21,[477,1805,1806],{"class":748},"    out_dir ",[477,1808,774],{"class":744},[477,1810,1811],{"class":748}," Path(out_dir)\n",[477,1813,1815,1818,1821,1823,1825,1827,1830,1832,1834],{"class":190,"line":1814},22,[477,1816,1817],{"class":748},"    out_dir.mkdir(",[477,1819,1820],{"class":809},"parents",[477,1822,774],{"class":744},[477,1824,1471],{"class":946},[477,1826,560],{"class":748},[477,1828,1829],{"class":809},"exist_ok",[477,1831,774],{"class":744},[477,1833,1471],{"class":946},[477,1835,783],{"class":748},[477,1837,1839,1842,1844,1847,1850,1853,1856,1859],{"class":190,"line":1838},23,[477,1840,1841],{"class":748},"    dest ",[477,1843,774],{"class":744},[477,1845,1846],{"class":748}," out_dir ",[477,1848,1849],{"class":744},"\u002F",[477,1851,1852],{"class":748}," (src.stem ",[477,1854,1855],{"class":744},"+",[477,1857,1858],{"class":486}," \".xlsx\"",[477,1860,783],{"class":748},[477,1862,1864],{"class":190,"line":1863},24,[477,1865,761],{"emptyLinePlaceholder":760},[477,1867,1869,1872,1875,1877,1879,1881,1883,1885],{"class":190,"line":1868},25,[477,1870,1871],{"class":744},"    with",[477,1873,1874],{"class":748}," pd.ExcelWriter(dest, ",[477,1876,810],{"class":809},[477,1878,774],{"class":744},[477,1880,1034],{"class":486},[477,1882,982],{"class":748},[477,1884,752],{"class":744},[477,1886,987],{"class":748},[477,1888,1890,1893,1896,1899],{"class":190,"line":1889},26,[477,1891,1892],{"class":744},"        for",[477,1894,1895],{"class":748}," name, frame ",[477,1897,1898],{"class":744},"in",[477,1900,1901],{"class":748}," sheets.items():\n",[477,1903,1905],{"class":190,"line":1904},27,[477,1906,1907],{"class":499},"            # Excel sheet names cap at 31 characters.\n",[477,1909,1911,1914,1916,1918,1921,1924,1926,1928,1930,1932],{"class":190,"line":1910},28,[477,1912,1913],{"class":748},"            frame.to_excel(writer, ",[477,1915,995],{"class":809},[477,1917,774],{"class":744},[477,1919,1920],{"class":748},"name[:",[477,1922,1923],{"class":946},"31",[477,1925,938],{"class":748},[477,1927,1005],{"class":809},[477,1929,774],{"class":744},[477,1931,1010],{"class":946},[477,1933,783],{"class":748},[477,1935,1937,1940],{"class":190,"line":1936},29,[477,1938,1939],{"class":744},"    return",[477,1941,1942],{"class":748}," dest\n",[477,1944,1946],{"class":190,"line":1945},30,[477,1947,761],{"emptyLinePlaceholder":760},[477,1949,1951,1954,1957,1960,1963],{"class":190,"line":1950},31,[477,1952,1953],{"class":744},"if",[477,1955,1956],{"class":946}," __name__",[477,1958,1959],{"class":744}," ==",[477,1961,1962],{"class":486}," \"__main__\"",[477,1964,1732],{"class":748},[477,1966,1968,1971,1974,1976,1979,1982,1985,1988],{"class":190,"line":1967},32,[477,1969,1970],{"class":744},"    for",[477,1972,1973],{"class":748}," path ",[477,1975,1898],{"class":744},[477,1977,1978],{"class":748}," Path(",[477,1980,1981],{"class":486},"\"inbox\"",[477,1983,1984],{"class":748},").glob(",[477,1986,1987],{"class":486},"\"*.*\"",[477,1989,1683],{"class":748},[477,1991,1993,1996,1999,2001,2003],{"class":190,"line":1992},33,[477,1994,1995],{"class":744},"        if",[477,1997,1998],{"class":748}," path.suffix.lower() ",[477,2000,1898],{"class":744},[477,2002,1711],{"class":946},[477,2004,1732],{"class":748},[477,2006,2008,2011,2013,2015],{"class":190,"line":2007},34,[477,2009,2010],{"class":946},"            print",[477,2012,1277],{"class":748},[477,2014,1680],{"class":486},[477,2016,2017],{"class":748},", convert_to_xlsx(path))\n",[10,2019,2020,2021,2024,2025,2028,2029,2033,2034,2037],{},"Two things to be clear-eyed about. First, this converts ",[236,2022,2023],{},"values, not appearance"," — pandas reads cells, so colours, merged cells, charts and formulas do not survive. If the visual fidelity matters, convert with LibreOffice instead (",[14,2026,2027],{},"soffice --headless --convert-to xlsx","), the same tool used in ",[34,2030,2032],{"href":2031},"\u002Fautomating-reporting-workflows\u002Fexporting-excel-reports-to-pdf\u002Fconvert-excel-file-to-pdf-with-python\u002F","converting Excel to PDF",". Second, formulas come back as their last cached ",[303,2035,2036],{},"result"," under most engines, which is usually what you want for a data pipeline and never what you want if you were trying to preserve a live model.",[10,2039,2040,2041,2045],{},"For the reverse direction — going down to CSV for a system that will not read spreadsheets — see ",[34,2042,2044],{"href":2043},"\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-large-excel-files-in-python\u002Fconvert-excel-to-csv-with-python\u002F","converting Excel to CSV with Python",", which also covers the encoding and delimiter details that trip up exports.",[226,2047,2049],{"id":2048},"detecting-the-real-format-of-a-file","Detecting the real format of a file",[10,2051,2052,2053,2055,2056,2058,2059,2062],{},"Extensions lie. A system exports \"Excel\" that is actually tab-separated text; a user renames ",[14,2054,20],{}," to ",[14,2057,16],{}," because a form insisted on it; a download arrives as ",[14,2060,2061],{},"report.xlsx"," and is an HTML table. Sniffing the first bytes tells you the truth in microseconds.",[40,2064,50,2071,50,2074,50,2077,50,2081,50,2086,50,2090,50,2096,50,2100,50,2105,50,2108,50,2113,50,2116,50,2119,50,2123,50,2127,50,2131,50,2134,50,2136,50,2140,50,2143],{"viewBox":2065,"role":43,"ariaLabel":2066,"ariaLabelledBy":2067,"xmlns":48,"style":2070},"0 0 760 214","File signature table: PK zero-three zero-four means an OOXML zip, D0 CF 11 E0 means the legacy OLE2 binary format, less-than sign means HTML, and printable text means CSV.",[2068,2069],"magic-t","magic-d","width:100%;max-width:760px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[52,2072,2073],{"id":2068},"Leading bytes that identify a spreadsheet file",[56,2075,2076],{"id":2069},"Four signatures read from the first four bytes of a file. PK 03 04 identifies a zip archive, which means an xlsx, xlsm or ods workbook. D0 CF 11 E0 identifies the OLE2 compound document used by legacy xls and xlsb. An opening angle bracket means the file is HTML masquerading as a spreadsheet. Anything else that is printable text is most likely a CSV.",[60,2078],{"x":62,"y":62,"width":2079,"height":2080,"fill":65},"760","214",[67,2082,2085],{"x":2083,"y":2084,"style":71},"380","24","first bytes → what the file really is",[60,2087],{"x":79,"y":2088,"width":2089,"height":1376,"rx":1339,"fill":84,"stroke":85,"style":86},"38","356",[67,2091,2095],{"x":2092,"y":2093,"style":2094},"194","64","font-size:14px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","50 4B 03 04",[67,2097,2099],{"x":2092,"y":2098,"style":691},"84","a zip archive — \"PK\"",[67,2101,2104],{"x":2092,"y":2102,"style":2103},"102","font-size:11px;fill:var(--muted,#5b6780);text-anchor:middle",".xlsx · .xlsm · .ods",[60,2106],{"x":2107,"y":2088,"width":2089,"height":1376,"rx":1339,"fill":100,"stroke":101,"style":86},"388",[67,2109,2112],{"x":2110,"y":2093,"style":2111},"566","font-size:14px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","D0 CF 11 E0",[67,2114,2115],{"x":2110,"y":2098,"style":691},"OLE2 compound document",[67,2117,2118],{"x":2110,"y":2102,"style":2103},".xls · .xlsb · .doc",[60,2120],{"x":79,"y":2121,"width":2089,"height":2122,"rx":1339,"fill":113,"stroke":114,"style":86},"126","72",[67,2124,2126],{"x":2092,"y":112,"style":2125},"font-size:14px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","3C (\"\u003C\")",[67,2128,2130],{"x":2092,"y":2129,"style":691},"172","HTML pretending to be Excel",[67,2132,2133],{"x":2092,"y":121,"style":2103},"read_html, not read_excel",[60,2135],{"x":2107,"y":2121,"width":2089,"height":2122,"rx":1339,"fill":125,"stroke":126,"style":86},[67,2137,2139],{"x":2110,"y":112,"style":2138},"font-size:14px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","printable text",[67,2141,2142],{"x":2110,"y":2129,"style":691},"delimited plain text",[67,2144,2145],{"x":2110,"y":121,"style":2103},"read_csv with a sniffed sep",[468,2147,2149],{"className":735,"code":2148,"language":737,"meta":473,"style":473},"from pathlib import Path\nimport zipfile\n\nOLE2 = b\"\\xd0\\xcf\\x11\\xe0\"\n\ndef detect_format(path):\n    \"\"\"Identify a spreadsheet by content, not by file name.\"\"\"\n    path = Path(path)\n    head = path.open(\"rb\").read(8)\n\n    if head.startswith(b\"PK\\x03\\x04\"):\n        # A zip — look inside to tell xlsx\u002Fxlsm from ods.\n        with zipfile.ZipFile(path) as zf:\n            names = set(zf.namelist())\n        if \"xl\u002FvbaProject.bin\" in names:\n            return \"xlsm\"\n        if any(n.startswith(\"xl\u002F\") for n in names):\n            return \"xlsx\"\n        if \"content.xml\" in names:\n            return \"ods\"\n        return \"zip\"\n\n    if head.startswith(OLE2):\n        return \"xls-or-xlsb\"        # OLE2 container; both live here\n    if head.lstrip()[:1] == b\"\u003C\":\n        return \"html\"\n    return \"text\"                   # csv \u002F tsv \u002F something delimited\n\nfor name in [\"sales.xlsx\", \"legacy.xls\", \"export.csv\"]:\n    print(name, \"->\", detect_format(name))\n",[14,2150,2151,2161,2168,2172,2190,2194,2204,2209,2219,2240,2244,2264,2269,2282,2295,2308,2316,2342,2349,2360,2367,2375,2379,2389,2399,2421,2428,2438,2442,2468],{"__ignoreMap":473},[477,2152,2153,2155,2157,2159],{"class":190,"line":479},[477,2154,1144],{"class":744},[477,2156,1567],{"class":748},[477,2158,745],{"class":744},[477,2160,1572],{"class":748},[477,2162,2163,2165],{"class":190,"line":503},[477,2164,745],{"class":744},[477,2166,2167],{"class":748}," zipfile\n",[477,2169,2170],{"class":190,"line":516},[477,2171,761],{"emptyLinePlaceholder":760},[477,2173,2174,2177,2179,2182,2184,2187],{"class":190,"line":529},[477,2175,2176],{"class":946},"OLE2",[477,2178,1594],{"class":744},[477,2180,2181],{"class":744}," b",[477,2183,1762],{"class":486},[477,2185,2186],{"class":1752},"\\xd0\\xcf\\x11\\xe0",[477,2188,2189],{"class":486},"\"\n",[477,2191,2192],{"class":190,"line":542},[477,2193,761],{"emptyLinePlaceholder":760},[477,2195,2196,2198,2201],{"class":190,"line":790},[477,2197,1668],{"class":744},[477,2199,2200],{"class":1671}," detect_format",[477,2202,2203],{"class":748},"(path):\n",[477,2205,2206],{"class":190,"line":796},[477,2207,2208],{"class":486},"    \"\"\"Identify a spreadsheet by content, not by file name.\"\"\"\n",[477,2210,2211,2214,2216],{"class":190,"line":820},[477,2212,2213],{"class":748},"    path ",[477,2215,774],{"class":744},[477,2217,2218],{"class":748}," Path(path)\n",[477,2220,2221,2224,2226,2229,2232,2235,2238],{"class":190,"line":843},[477,2222,2223],{"class":748},"    head ",[477,2225,774],{"class":744},[477,2227,2228],{"class":748}," path.open(",[477,2230,2231],{"class":486},"\"rb\"",[477,2233,2234],{"class":748},").read(",[477,2236,2237],{"class":946},"8",[477,2239,783],{"class":748},[477,2241,2242],{"class":190,"line":866},[477,2243,761],{"emptyLinePlaceholder":760},[477,2245,2246,2248,2251,2254,2257,2260,2262],{"class":190,"line":1067},[477,2247,1720],{"class":744},[477,2249,2250],{"class":748}," head.startswith(",[477,2252,2253],{"class":744},"b",[477,2255,2256],{"class":486},"\"PK",[477,2258,2259],{"class":1752},"\\x03\\x04",[477,2261,1762],{"class":486},[477,2263,1683],{"class":748},[477,2265,2266],{"class":190,"line":1091},[477,2267,2268],{"class":499},"        # A zip — look inside to tell xlsx\u002Fxlsm from ods.\n",[477,2270,2271,2274,2277,2279],{"class":190,"line":1686},[477,2272,2273],{"class":744},"        with",[477,2275,2276],{"class":748}," zipfile.ZipFile(path) ",[477,2278,752],{"class":744},[477,2280,2281],{"class":748}," zf:\n",[477,2283,2284,2287,2289,2292],{"class":190,"line":1692},[477,2285,2286],{"class":748},"            names ",[477,2288,774],{"class":744},[477,2290,2291],{"class":946}," set",[477,2293,2294],{"class":748},"(zf.namelist())\n",[477,2296,2297,2299,2302,2305],{"class":190,"line":1703},[477,2298,1995],{"class":744},[477,2300,2301],{"class":486}," \"xl\u002FvbaProject.bin\"",[477,2303,2304],{"class":744}," in",[477,2306,2307],{"class":748}," names:\n",[477,2309,2310,2313],{"class":190,"line":1717},[477,2311,2312],{"class":744},"            return",[477,2314,2315],{"class":486}," \"xlsm\"\n",[477,2317,2318,2320,2323,2326,2329,2331,2334,2337,2339],{"class":190,"line":1735},[477,2319,1995],{"class":744},[477,2321,2322],{"class":946}," any",[477,2324,2325],{"class":748},"(n.startswith(",[477,2327,2328],{"class":486},"\"xl\u002F\"",[477,2330,982],{"class":748},[477,2332,2333],{"class":744},"for",[477,2335,2336],{"class":748}," n ",[477,2338,1898],{"class":744},[477,2340,2341],{"class":748}," names):\n",[477,2343,2344,2346],{"class":190,"line":1767},[477,2345,2312],{"class":744},[477,2347,2348],{"class":486}," \"xlsx\"\n",[477,2350,2351,2353,2356,2358],{"class":190,"line":1772},[477,2352,1995],{"class":744},[477,2354,2355],{"class":486}," \"content.xml\"",[477,2357,2304],{"class":744},[477,2359,2307],{"class":748},[477,2361,2362,2364],{"class":190,"line":1798},[477,2363,2312],{"class":744},[477,2365,2366],{"class":486}," \"ods\"\n",[477,2368,2369,2372],{"class":190,"line":1803},[477,2370,2371],{"class":744},"        return",[477,2373,2374],{"class":486}," \"zip\"\n",[477,2376,2377],{"class":190,"line":1814},[477,2378,761],{"emptyLinePlaceholder":760},[477,2380,2381,2383,2385,2387],{"class":190,"line":1838},[477,2382,1720],{"class":744},[477,2384,2250],{"class":748},[477,2386,2176],{"class":946},[477,2388,1683],{"class":748},[477,2390,2391,2393,2396],{"class":190,"line":1863},[477,2392,2371],{"class":744},[477,2394,2395],{"class":486}," \"xls-or-xlsb\"",[477,2397,2398],{"class":499},"        # OLE2 container; both live here\n",[477,2400,2401,2403,2406,2409,2411,2414,2416,2419],{"class":190,"line":1868},[477,2402,1720],{"class":744},[477,2404,2405],{"class":748}," head.lstrip()[:",[477,2407,2408],{"class":946},"1",[477,2410,1504],{"class":748},[477,2412,2413],{"class":744},"==",[477,2415,2181],{"class":744},[477,2417,2418],{"class":486},"\"\u003C\"",[477,2420,1732],{"class":748},[477,2422,2423,2425],{"class":190,"line":1889},[477,2424,2371],{"class":744},[477,2426,2427],{"class":486}," \"html\"\n",[477,2429,2430,2432,2435],{"class":190,"line":1904},[477,2431,1939],{"class":744},[477,2433,2434],{"class":486}," \"text\"",[477,2436,2437],{"class":499},"                   # csv \u002F tsv \u002F something delimited\n",[477,2439,2440],{"class":190,"line":1910},[477,2441,761],{"emptyLinePlaceholder":760},[477,2443,2444,2446,2449,2451,2454,2456,2458,2460,2462,2465],{"class":190,"line":1936},[477,2445,2333],{"class":744},[477,2447,2448],{"class":748}," name ",[477,2450,1898],{"class":744},[477,2452,2453],{"class":748}," [",[477,2455,780],{"class":486},[477,2457,560],{"class":748},[477,2459,829],{"class":486},[477,2461,560],{"class":748},[477,2463,2464],{"class":486},"\"export.csv\"",[477,2466,2467],{"class":748},"]:\n",[477,2469,2470,2473,2476,2479],{"class":190,"line":1945},[477,2471,2472],{"class":946},"    print",[477,2474,2475],{"class":748},"(name, ",[477,2477,2478],{"class":486},"\"->\"",[477,2480,2481],{"class":748},", detect_format(name))\n",[10,2483,2484,2485,2489],{},"Wiring that into an ingest step turns a mystifying traceback into a clear message. It pairs well with the column and type checks in ",[34,2486,2488],{"href":2487},"\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fvalidate-excel-columns-before-import-with-pandas\u002F","validating Excel columns before import"," — sniff the container first, then validate the contents.",[468,2491,2493],{"className":735,"code":2492,"language":737,"meta":473,"style":473},"import pandas as pd\n\nREAD = {\"xlsx\": \"openpyxl\", \"xlsm\": \"openpyxl\", \"ods\": \"odf\"}\n\ndef read_any(path):\n    kind = detect_format(path)\n    if kind in READ:\n        return pd.read_excel(path, engine=READ[kind])\n    if kind == \"xls-or-xlsb\":\n        # calamine reads both binary families behind one engine.\n        return pd.read_excel(path, engine=\"calamine\")\n    if kind == \"html\":\n        return pd.read_html(path)[0]\n    if kind == \"text\":\n        return pd.read_csv(path, sep=None, engine=\"python\")\n    raise ValueError(f\"Cannot read {path}: unrecognised format {kind}\")\n",[14,2494,2495,2505,2509,2546,2550,2559,2569,2583,2599,2611,2616,2630,2643,2654,2666,2691],{"__ignoreMap":473},[477,2496,2497,2499,2501,2503],{"class":190,"line":479},[477,2498,745],{"class":744},[477,2500,749],{"class":748},[477,2502,752],{"class":744},[477,2504,755],{"class":748},[477,2506,2507],{"class":190,"line":503},[477,2508,761],{"emptyLinePlaceholder":760},[477,2510,2511,2514,2516,2519,2522,2524,2526,2528,2531,2533,2535,2537,2540,2542,2544],{"class":190,"line":516},[477,2512,2513],{"class":946},"READ",[477,2515,1594],{"class":744},[477,2517,2518],{"class":748}," {",[477,2520,2521],{"class":486},"\"xlsx\"",[477,2523,1605],{"class":748},[477,2525,979],{"class":486},[477,2527,560],{"class":748},[477,2529,2530],{"class":486},"\"xlsm\"",[477,2532,1605],{"class":748},[477,2534,979],{"class":486},[477,2536,560],{"class":748},[477,2538,2539],{"class":486},"\"ods\"",[477,2541,1605],{"class":748},[477,2543,884],{"class":486},[477,2545,1659],{"class":748},[477,2547,2548],{"class":190,"line":529},[477,2549,761],{"emptyLinePlaceholder":760},[477,2551,2552,2554,2557],{"class":190,"line":542},[477,2553,1668],{"class":744},[477,2555,2556],{"class":1671}," read_any",[477,2558,2203],{"class":748},[477,2560,2561,2564,2566],{"class":190,"line":790},[477,2562,2563],{"class":748},"    kind ",[477,2565,774],{"class":744},[477,2567,2568],{"class":748}," detect_format(path)\n",[477,2570,2571,2573,2576,2578,2581],{"class":190,"line":796},[477,2572,1720],{"class":744},[477,2574,2575],{"class":748}," kind ",[477,2577,1898],{"class":744},[477,2579,2580],{"class":946}," READ",[477,2582,1732],{"class":748},[477,2584,2585,2587,2590,2592,2594,2596],{"class":190,"line":820},[477,2586,2371],{"class":744},[477,2588,2589],{"class":748}," pd.read_excel(path, ",[477,2591,810],{"class":809},[477,2593,774],{"class":744},[477,2595,2513],{"class":946},[477,2597,2598],{"class":748},"[kind])\n",[477,2600,2601,2603,2605,2607,2609],{"class":190,"line":843},[477,2602,1720],{"class":744},[477,2604,2575],{"class":748},[477,2606,2413],{"class":744},[477,2608,2395],{"class":486},[477,2610,1732],{"class":748},[477,2612,2613],{"class":190,"line":866},[477,2614,2615],{"class":499},"        # calamine reads both binary families behind one engine.\n",[477,2617,2618,2620,2622,2624,2626,2628],{"class":190,"line":1067},[477,2619,2371],{"class":744},[477,2621,2589],{"class":748},[477,2623,810],{"class":809},[477,2625,774],{"class":744},[477,2627,815],{"class":486},[477,2629,783],{"class":748},[477,2631,2632,2634,2636,2638,2641],{"class":190,"line":1091},[477,2633,1720],{"class":744},[477,2635,2575],{"class":748},[477,2637,2413],{"class":744},[477,2639,2640],{"class":486}," \"html\"",[477,2642,1732],{"class":748},[477,2644,2645,2647,2650,2652],{"class":190,"line":1686},[477,2646,2371],{"class":744},[477,2648,2649],{"class":748}," pd.read_html(path)[",[477,2651,62],{"class":946},[477,2653,1493],{"class":748},[477,2655,2656,2658,2660,2662,2664],{"class":190,"line":1692},[477,2657,1720],{"class":744},[477,2659,2575],{"class":748},[477,2661,2413],{"class":744},[477,2663,2434],{"class":486},[477,2665,1732],{"class":748},[477,2667,2668,2670,2673,2676,2678,2680,2682,2684,2686,2689],{"class":190,"line":1703},[477,2669,2371],{"class":744},[477,2671,2672],{"class":748}," pd.read_csv(path, ",[477,2674,2675],{"class":809},"sep",[477,2677,774],{"class":744},[477,2679,1259],{"class":946},[477,2681,560],{"class":748},[477,2683,810],{"class":809},[477,2685,774],{"class":744},[477,2687,2688],{"class":486},"\"python\"",[477,2690,783],{"class":748},[477,2692,2693,2696,2698,2700,2702,2705,2707,2709,2711,2714,2716,2719,2721,2723],{"class":190,"line":1717},[477,2694,2695],{"class":744},"    raise",[477,2697,1741],{"class":946},[477,2699,1277],{"class":748},[477,2701,1746],{"class":744},[477,2703,2704],{"class":486},"\"Cannot read ",[477,2706,1753],{"class":1752},[477,2708,652],{"class":748},[477,2710,1759],{"class":1752},[477,2712,2713],{"class":486},": unrecognised format ",[477,2715,1753],{"class":1752},[477,2717,2718],{"class":748},"kind",[477,2720,1759],{"class":1752},[477,2722,1762],{"class":486},[477,2724,783],{"class":748},[226,2726,2728],{"id":2727},"what-actually-lives-inside-an-xlsx","What actually lives inside an .xlsx",[10,2730,2731,2732,2734],{},"Because ",[14,2733,16],{}," is a zip archive, you can inspect it with nothing but the standard library — which is genuinely useful when a file misbehaves and you want to know whether the problem is your code or the file.",[468,2736,2738],{"className":735,"code":2737,"language":737,"meta":473,"style":473},"import zipfile\n\nwith zipfile.ZipFile(\"sales.xlsx\") as zf:\n    for info in sorted(zf.infolist(), key=lambda i: -i.file_size)[:8]:\n        print(f\"{info.file_size:>10,}  {info.filename}\")\n",[14,2739,2740,2746,2750,2765,2799],{"__ignoreMap":473},[477,2741,2742,2744],{"class":190,"line":479},[477,2743,745],{"class":744},[477,2745,2167],{"class":748},[477,2747,2748],{"class":190,"line":503},[477,2749,761],{"emptyLinePlaceholder":760},[477,2751,2752,2754,2757,2759,2761,2763],{"class":190,"line":516},[477,2753,964],{"class":744},[477,2755,2756],{"class":748}," zipfile.ZipFile(",[477,2758,780],{"class":486},[477,2760,982],{"class":748},[477,2762,752],{"class":744},[477,2764,2281],{"class":748},[477,2766,2767,2769,2772,2774,2777,2780,2783,2786,2789,2792,2795,2797],{"class":190,"line":529},[477,2768,1970],{"class":744},[477,2770,2771],{"class":748}," info ",[477,2773,1898],{"class":744},[477,2775,2776],{"class":946}," sorted",[477,2778,2779],{"class":748},"(zf.infolist(), ",[477,2781,2782],{"class":809},"key",[477,2784,2785],{"class":744},"=lambda",[477,2787,2788],{"class":748}," i: ",[477,2790,2791],{"class":744},"-",[477,2793,2794],{"class":748},"i.file_size)[:",[477,2796,2237],{"class":946},[477,2798,2467],{"class":748},[477,2800,2801,2804,2806,2808,2810,2812,2815,2818,2820,2823,2826,2828,2830],{"class":190,"line":542},[477,2802,2803],{"class":946},"        print",[477,2805,1277],{"class":748},[477,2807,1746],{"class":744},[477,2809,1762],{"class":486},[477,2811,1753],{"class":1752},[477,2813,2814],{"class":748},"info.file_size",[477,2816,2817],{"class":744},":>10,",[477,2819,1759],{"class":1752},[477,2821,2822],{"class":1752},"  {",[477,2824,2825],{"class":748},"info.filename",[477,2827,1759],{"class":1752},[477,2829,1762],{"class":486},[477,2831,783],{"class":748},[10,2833,2834],{},"A typical report prints something like this:",[468,2836,2840],{"className":2837,"code":2839,"language":67,"meta":473},[2838],"language-text","   482,104  xl\u002Fworksheets\u002Fsheet1.xml\n   118,940  xl\u002FsharedStrings.xml\n     9,220  xl\u002Fstyles.xml\n     2,118  xl\u002Fworkbook.xml\n     1,004  xl\u002Ftheme\u002Ftheme1.xml\n       872  [Content_Types].xml\n       610  xl\u002F_rels\u002Fworkbook.xml.rels\n       412  docProps\u002Fapp.xml\n",[14,2841,2839],{"__ignoreMap":473},[10,2843,2844],{},"Three of those parts explain most of the surprises people hit.",[10,2846,2847,2852],{},[236,2848,2849],{},[14,2850,2851],{},"sharedStrings.xml"," is a deduplication table. Every distinct piece of text in the workbook is stored once here, and cells reference it by index. That is why a sheet with a million repeated category names is far smaller than you would expect — and why writing that same sheet with a tool that does not use shared strings produces a much larger file.",[10,2854,2855,2860,2861,38],{},[236,2856,2857],{},[14,2858,2859],{},"styles.xml"," holds every distinct combination of font, fill, border and number format in the workbook. Excel caps this at roughly 64,000 unique cell formats, which sounds generous until a script applies a style object inside a loop and creates a fresh entry every iteration. The result is the \"too many different cell formats\" error, and the fix — define each style once outside the loop and reuse it — is covered in ",[34,2862,2864],{"href":2863},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fstyling-excel-cells-with-openpyxl\u002F","styling Excel cells with openpyxl",[10,2866,2867,2872,2873,2875,2876,2878,2879,2055,2881,2883],{},[236,2868,2869],{},[14,2870,2871],{},"[Content_Types].xml"," declares what each part is. This is the file that makes an ",[14,2874,24],{}," an ",[14,2877,24],{},": it carries the macro-enabled content type. Rename an ",[14,2880,24],{},[14,2882,16],{}," on disk and Excel still treats it as macro-enabled, because the extension was never what decided it.",[10,2885,2886],{},"Knowing the layout also gives you a fast structural check that needs no spreadsheet library at all — handy in CI, where installing openpyxl just to assert a file is well-formed is overkill:",[468,2888,2890],{"className":735,"code":2889,"language":737,"meta":473,"style":473},"import zipfile\n\nREQUIRED = {\"[Content_Types].xml\", \"xl\u002Fworkbook.xml\"}\n\ndef looks_like_a_workbook(path):\n    \"\"\"Cheap structural validation of an OOXML file.\"\"\"\n    try:\n        with zipfile.ZipFile(path) as zf:\n            names = set(zf.namelist())\n            if bad := zf.testzip():          # first corrupt member, or None\n                return False, f\"corrupt archive member: {bad}\"\n    except zipfile.BadZipFile:\n        return False, \"not a zip archive — wrong format or truncated download\"\n\n    missing = REQUIRED - names\n    if missing:\n        return False, f\"missing parts: {', '.join(sorted(missing))}\"\n    sheets = [n for n in names if n.startswith(\"xl\u002Fworksheets\u002Fsheet\")]\n    return True, f\"{len(sheets)} sheet(s)\"\n\nprint(looks_like_a_workbook(\"sales.xlsx\"))\n",[14,2891,2892,2898,2902,2921,2925,2934,2939,2946,2956,2966,2983,3007,3015,3026,3030,3046,3053,3084,3113,3139,3143],{"__ignoreMap":473},[477,2893,2894,2896],{"class":190,"line":479},[477,2895,745],{"class":744},[477,2897,2167],{"class":748},[477,2899,2900],{"class":190,"line":503},[477,2901,761],{"emptyLinePlaceholder":760},[477,2903,2904,2907,2909,2911,2914,2916,2919],{"class":190,"line":516},[477,2905,2906],{"class":946},"REQUIRED",[477,2908,1594],{"class":744},[477,2910,2518],{"class":748},[477,2912,2913],{"class":486},"\"[Content_Types].xml\"",[477,2915,560],{"class":748},[477,2917,2918],{"class":486},"\"xl\u002Fworkbook.xml\"",[477,2920,1659],{"class":748},[477,2922,2923],{"class":190,"line":529},[477,2924,761],{"emptyLinePlaceholder":760},[477,2926,2927,2929,2932],{"class":190,"line":542},[477,2928,1668],{"class":744},[477,2930,2931],{"class":1671}," looks_like_a_workbook",[477,2933,2203],{"class":748},[477,2935,2936],{"class":190,"line":790},[477,2937,2938],{"class":486},"    \"\"\"Cheap structural validation of an OOXML file.\"\"\"\n",[477,2940,2941,2944],{"class":190,"line":796},[477,2942,2943],{"class":744},"    try",[477,2945,1732],{"class":748},[477,2947,2948,2950,2952,2954],{"class":190,"line":820},[477,2949,2273],{"class":744},[477,2951,2276],{"class":748},[477,2953,752],{"class":744},[477,2955,2281],{"class":748},[477,2957,2958,2960,2962,2964],{"class":190,"line":843},[477,2959,2286],{"class":748},[477,2961,774],{"class":744},[477,2963,2291],{"class":946},[477,2965,2294],{"class":748},[477,2967,2968,2971,2974,2977,2980],{"class":190,"line":866},[477,2969,2970],{"class":744},"            if",[477,2972,2973],{"class":748}," bad ",[477,2975,2976],{"class":744},":=",[477,2978,2979],{"class":748}," zf.testzip():          ",[477,2981,2982],{"class":499},"# first corrupt member, or None\n",[477,2984,2985,2988,2991,2993,2995,2998,3000,3003,3005],{"class":190,"line":1067},[477,2986,2987],{"class":744},"                return",[477,2989,2990],{"class":946}," False",[477,2992,560],{"class":748},[477,2994,1746],{"class":744},[477,2996,2997],{"class":486},"\"corrupt archive member: ",[477,2999,1753],{"class":1752},[477,3001,3002],{"class":748},"bad",[477,3004,1759],{"class":1752},[477,3006,2189],{"class":486},[477,3008,3009,3012],{"class":190,"line":1091},[477,3010,3011],{"class":744},"    except",[477,3013,3014],{"class":748}," zipfile.BadZipFile:\n",[477,3016,3017,3019,3021,3023],{"class":190,"line":1686},[477,3018,2371],{"class":744},[477,3020,2990],{"class":946},[477,3022,560],{"class":748},[477,3024,3025],{"class":486},"\"not a zip archive — wrong format or truncated download\"\n",[477,3027,3028],{"class":190,"line":1692},[477,3029,761],{"emptyLinePlaceholder":760},[477,3031,3032,3035,3037,3040,3043],{"class":190,"line":1703},[477,3033,3034],{"class":748},"    missing ",[477,3036,774],{"class":744},[477,3038,3039],{"class":946}," REQUIRED",[477,3041,3042],{"class":744}," -",[477,3044,3045],{"class":748}," names\n",[477,3047,3048,3050],{"class":190,"line":1717},[477,3049,1720],{"class":744},[477,3051,3052],{"class":748}," missing:\n",[477,3054,3055,3057,3059,3061,3063,3066,3068,3071,3074,3077,3080,3082],{"class":190,"line":1735},[477,3056,2371],{"class":744},[477,3058,2990],{"class":946},[477,3060,560],{"class":748},[477,3062,1746],{"class":744},[477,3064,3065],{"class":486},"\"missing parts: ",[477,3067,1753],{"class":1752},[477,3069,3070],{"class":486},"', '",[477,3072,3073],{"class":748},".join(",[477,3075,3076],{"class":946},"sorted",[477,3078,3079],{"class":748},"(missing))",[477,3081,1759],{"class":1752},[477,3083,2189],{"class":486},[477,3085,3086,3088,3090,3093,3095,3097,3099,3102,3104,3107,3110],{"class":190,"line":1767},[477,3087,1775],{"class":748},[477,3089,774],{"class":744},[477,3091,3092],{"class":748}," [n ",[477,3094,2333],{"class":744},[477,3096,2336],{"class":748},[477,3098,1898],{"class":744},[477,3100,3101],{"class":748}," names ",[477,3103,1953],{"class":744},[477,3105,3106],{"class":748}," n.startswith(",[477,3108,3109],{"class":486},"\"xl\u002Fworksheets\u002Fsheet\"",[477,3111,3112],{"class":748},")]\n",[477,3114,3115,3117,3120,3122,3124,3126,3128,3131,3134,3136],{"class":190,"line":1772},[477,3116,1939],{"class":744},[477,3118,3119],{"class":946}," True",[477,3121,560],{"class":748},[477,3123,1746],{"class":744},[477,3125,1762],{"class":486},[477,3127,1753],{"class":1752},[477,3129,3130],{"class":946},"len",[477,3132,3133],{"class":748},"(sheets)",[477,3135,1759],{"class":1752},[477,3137,3138],{"class":486}," sheet(s)\"\n",[477,3140,3141],{"class":190,"line":1798},[477,3142,761],{"emptyLinePlaceholder":760},[477,3144,3145,3147,3150,3152],{"class":190,"line":1803},[477,3146,1274],{"class":946},[477,3148,3149],{"class":748},"(looks_like_a_workbook(",[477,3151,780],{"class":486},[477,3153,3154],{"class":748},"))\n",[10,3156,3157,3158,3161,3162,3164],{},"Truncated downloads are the single most common cause of ",[14,3159,3160],{},"BadZipFile"," in production. A file that stopped arriving halfway through is still a plausible-looking ",[14,3163,16],{}," on disk, and this check catches it before a confusing parser error does.",[226,3166,3168],{"id":3167},"reading-from-bytes-streams-and-urls","Reading from bytes, streams and URLs",[10,3170,3171],{},"Not every workbook arrives as a path on disk. It comes back from an HTTP request, out of an object store, or as an upload in a web handler. Every reader in this ecosystem accepts a file-like object, so none of those cases needs a temporary file.",[40,3173,50,3179,50,3182,50,3185,50,3188,50,3193,50,3196,50,3201,50,3203,50,3208,50,3211,50,3216,50,3219,50,3224,50,3238,50,3241,50,3247,50,3251,50,3255,50,3263,50,3271,50,3275,50,3279,50,3283,50,3286,50,3289],{"viewBox":3174,"role":43,"ariaLabel":3175,"ariaLabelledBy":3176,"xmlns":48,"style":590},"0 0 800 238","Four sources feeding one reader: a filesystem path, HTTP response bytes, an object-store download, and an upload stream all become a BytesIO buffer that pandas and openpyxl read identically.",[3177,3178],"src-t","src-d",[52,3180,3181],{"id":3177},"Any source of workbook bytes reads the same way",[56,3183,3184],{"id":3178},"Four sources on the left — a path on disk, the bytes of an HTTP response, an object-store download, and an uploaded file stream — all converge on a BytesIO buffer in the middle. From there pandas read_excel and openpyxl load_workbook consume it exactly as they would a path, so no temporary file is needed anywhere in the chain.",[60,3186],{"x":62,"y":62,"width":599,"height":3187,"fill":65},"238",[67,3189,3192],{"x":3190,"y":70,"style":3191},"98","font-size:11.5px;font-weight:700;fill:var(--muted,#5b6780);text-anchor:middle","where the bytes came from",[60,3194],{"x":79,"y":2088,"width":3195,"height":2088,"rx":720,"fill":135,"stroke":136,"style":86},"166",[67,3197,3200],{"x":3198,"y":89,"style":3199},"99","font-size:11.5px;font-weight:600;fill:var(--text,#172033);text-anchor:middle","a path on disk",[60,3202],{"x":79,"y":2098,"width":3195,"height":2088,"rx":720,"fill":84,"stroke":85,"style":86},[67,3204,3207],{"x":3198,"y":3205,"style":3206},"108","font-size:11.5px;font-weight:600;fill:var(--brand-strong,#4338ca);text-anchor:middle","response.content",[60,3209],{"x":79,"y":3210,"width":3195,"height":2088,"rx":720,"fill":125,"stroke":126,"style":86},"130",[67,3212,3215],{"x":3198,"y":3213,"style":3214},"154","font-size:11.5px;font-weight:600;fill:var(--teal-ink,#0b6157);text-anchor:middle","object store",[60,3217],{"x":79,"y":3218,"width":3195,"height":2088,"rx":720,"fill":100,"stroke":101,"style":86},"176",[67,3220,3223],{"x":3198,"y":3221,"style":3222},"200","font-size:11.5px;font-weight:600;fill:var(--gold-ink,#7a4e06);text-anchor:middle","upload stream",[186,3225,188,3226,188,3229,188,3232,188,3235,50],{"stroke":136,"style":86,"fill":463},[652,3227],{"d":3228},"M182 57 H 226 V 122",[652,3230],{"d":3231},"M182 103 H 226 V 122",[652,3233],{"d":3234},"M182 149 H 226 V 122",[652,3236],{"d":3237},"M182 195 H 226 V 122",[210,3239],{"points":3240,"fill":208},"234,122 222,116 222,128",[60,3242],{"x":3243,"y":644,"width":3244,"height":3245,"rx":3246,"fill":146,"stroke":85,"style":86},"242","196","66","13",[67,3248,3250],{"x":3249,"y":104,"style":150},"340","io.BytesIO(data)",[67,3252,3254],{"x":3249,"y":3253,"style":95},"139","an in-memory file object",[186,3256,188,3257,188,3260,50],{"stroke":85,"style":86,"fill":463},[652,3258],{"d":3259},"M438 123 H 482 V 76 H 518",[652,3261],{"d":3262},"M438 123 H 482 V 170 H 518",[186,3264,188,3265,188,3268,50],{"fill":208},[210,3266],{"points":3267},"526,76 514,70 514,82",[210,3269],{"points":3270},"526,170 514,164 514,176",[60,3272],{"x":3273,"y":3274,"width":1334,"height":613,"rx":1339,"fill":84,"stroke":85,"style":86},"534","50",[67,3276,3278],{"x":3277,"y":2122,"style":686},"659","pd.read_excel(buf)",[67,3280,3282],{"x":3277,"y":3281,"style":95},"91","values into a DataFrame",[60,3284],{"x":3273,"y":3285,"width":1334,"height":613,"rx":1339,"fill":125,"stroke":126,"style":86},"144",[67,3287,3288],{"x":3277,"y":3195,"style":628},"load_workbook(buf)",[67,3290,3292],{"x":3277,"y":3291,"style":95},"185","styles, formulas, sheets",[468,3294,3296],{"className":735,"code":3295,"language":737,"meta":473,"style":473},"import io\nimport pandas as pd\nimport requests\nfrom openpyxl import load_workbook\n\nresponse = requests.get(\n    \"https:\u002F\u002Fexample.com\u002Fexports\u002Faugust.xlsx\", timeout=30\n)\nresponse.raise_for_status()\n\nbuffer = io.BytesIO(response.content)\n\n# pandas takes the buffer directly — no temp file.\ndf = pd.read_excel(buffer)\n\n# Rewind before handing the same bytes to another reader.\nbuffer.seek(0)\nwb = load_workbook(buffer)\nprint(wb.sheetnames)\n",[14,3297,3298,3305,3315,3322,3332,3336,3346,3361,3365,3370,3374,3384,3388,3393,3402,3406,3411,3420,3429],{"__ignoreMap":473},[477,3299,3300,3302],{"class":190,"line":479},[477,3301,745],{"class":744},[477,3303,3304],{"class":748}," io\n",[477,3306,3307,3309,3311,3313],{"class":190,"line":503},[477,3308,745],{"class":744},[477,3310,749],{"class":748},[477,3312,752],{"class":744},[477,3314,755],{"class":748},[477,3316,3317,3319],{"class":190,"line":516},[477,3318,745],{"class":744},[477,3320,3321],{"class":748}," requests\n",[477,3323,3324,3326,3328,3330],{"class":190,"line":529},[477,3325,1144],{"class":744},[477,3327,1147],{"class":748},[477,3329,745],{"class":744},[477,3331,1152],{"class":748},[477,3333,3334],{"class":190,"line":542},[477,3335,761],{"emptyLinePlaceholder":760},[477,3337,3338,3341,3343],{"class":190,"line":790},[477,3339,3340],{"class":748},"response ",[477,3342,774],{"class":744},[477,3344,3345],{"class":748}," requests.get(\n",[477,3347,3348,3351,3353,3356,3358],{"class":190,"line":796},[477,3349,3350],{"class":486},"    \"https:\u002F\u002Fexample.com\u002Fexports\u002Faugust.xlsx\"",[477,3352,560],{"class":748},[477,3354,3355],{"class":809},"timeout",[477,3357,774],{"class":744},[477,3359,3360],{"class":946},"30\n",[477,3362,3363],{"class":190,"line":820},[477,3364,783],{"class":748},[477,3366,3367],{"class":190,"line":843},[477,3368,3369],{"class":748},"response.raise_for_status()\n",[477,3371,3372],{"class":190,"line":866},[477,3373,761],{"emptyLinePlaceholder":760},[477,3375,3376,3379,3381],{"class":190,"line":1067},[477,3377,3378],{"class":748},"buffer ",[477,3380,774],{"class":744},[477,3382,3383],{"class":748}," io.BytesIO(response.content)\n",[477,3385,3386],{"class":190,"line":1091},[477,3387,761],{"emptyLinePlaceholder":760},[477,3389,3390],{"class":190,"line":1686},[477,3391,3392],{"class":499},"# pandas takes the buffer directly — no temp file.\n",[477,3394,3395,3397,3399],{"class":190,"line":1692},[477,3396,771],{"class":748},[477,3398,774],{"class":744},[477,3400,3401],{"class":748}," pd.read_excel(buffer)\n",[477,3403,3404],{"class":190,"line":1703},[477,3405,761],{"emptyLinePlaceholder":760},[477,3407,3408],{"class":190,"line":1717},[477,3409,3410],{"class":499},"# Rewind before handing the same bytes to another reader.\n",[477,3412,3413,3416,3418],{"class":190,"line":1735},[477,3414,3415],{"class":748},"buffer.seek(",[477,3417,62],{"class":946},[477,3419,783],{"class":748},[477,3421,3422,3424,3426],{"class":190,"line":1767},[477,3423,1453],{"class":748},[477,3425,774],{"class":744},[477,3427,3428],{"class":748}," load_workbook(buffer)\n",[477,3430,3431,3433],{"class":190,"line":1772},[477,3432,1274],{"class":946},[477,3434,3435],{"class":748},"(wb.sheetnames)\n",[10,3437,3438,3439,3442,3443,3446],{},"That ",[14,3440,3441],{},"seek(0)"," is the detail people miss. Reading a buffer leaves the cursor at the end, and the second reader sees zero bytes and reports a corrupt file. Rewind between consumers, or build a fresh ",[14,3444,3445],{},"BytesIO"," for each.",[10,3448,3449],{},"Writing to a buffer works the same way in reverse, which is how you return a workbook from a web endpoint or hand one to an upload call without ever touching the filesystem:",[468,3451,3453],{"className":735,"code":3452,"language":737,"meta":473,"style":473},"import io\nimport pandas as pd\n\ndf = pd.DataFrame({\"region\": [\"North\", \"South\"], \"revenue\": [159.92, 247.50]})\n\nbuffer = io.BytesIO()\nwith pd.ExcelWriter(buffer, engine=\"xlsxwriter\") as writer:\n    df.to_excel(writer, sheet_name=\"Summary\", index=False)\n\nbuffer.seek(0)\npayload = buffer.getvalue()      # bytes, ready to upload or return\nprint(f\"{len(payload):,} bytes in memory\")\n",[14,3454,3455,3461,3471,3475,3507,3511,3520,3539,3559,3563,3571,3584],{"__ignoreMap":473},[477,3456,3457,3459],{"class":190,"line":479},[477,3458,745],{"class":744},[477,3460,3304],{"class":748},[477,3462,3463,3465,3467,3469],{"class":190,"line":503},[477,3464,745],{"class":744},[477,3466,749],{"class":748},[477,3468,752],{"class":744},[477,3470,755],{"class":748},[477,3472,3473],{"class":190,"line":516},[477,3474,761],{"emptyLinePlaceholder":760},[477,3476,3477,3479,3481,3483,3485,3487,3489,3491,3493,3495,3497,3499,3501,3503,3505],{"class":190,"line":529},[477,3478,771],{"class":748},[477,3480,774],{"class":744},[477,3482,921],{"class":748},[477,3484,924],{"class":486},[477,3486,927],{"class":748},[477,3488,930],{"class":486},[477,3490,560],{"class":748},[477,3492,935],{"class":486},[477,3494,938],{"class":748},[477,3496,941],{"class":486},[477,3498,927],{"class":748},[477,3500,947],{"class":946},[477,3502,560],{"class":748},[477,3504,952],{"class":946},[477,3506,955],{"class":748},[477,3508,3509],{"class":190,"line":542},[477,3510,761],{"emptyLinePlaceholder":760},[477,3512,3513,3515,3517],{"class":190,"line":790},[477,3514,3378],{"class":748},[477,3516,774],{"class":744},[477,3518,3519],{"class":748}," io.BytesIO()\n",[477,3521,3522,3524,3527,3529,3531,3533,3535,3537],{"class":190,"line":796},[477,3523,964],{"class":744},[477,3525,3526],{"class":748}," pd.ExcelWriter(buffer, ",[477,3528,810],{"class":809},[477,3530,774],{"class":744},[477,3532,1034],{"class":486},[477,3534,982],{"class":748},[477,3536,752],{"class":744},[477,3538,987],{"class":748},[477,3540,3541,3543,3545,3547,3549,3551,3553,3555,3557],{"class":190,"line":820},[477,3542,992],{"class":748},[477,3544,995],{"class":809},[477,3546,774],{"class":744},[477,3548,1000],{"class":486},[477,3550,560],{"class":748},[477,3552,1005],{"class":809},[477,3554,774],{"class":744},[477,3556,1010],{"class":946},[477,3558,783],{"class":748},[477,3560,3561],{"class":190,"line":843},[477,3562,761],{"emptyLinePlaceholder":760},[477,3564,3565,3567,3569],{"class":190,"line":866},[477,3566,3415],{"class":748},[477,3568,62],{"class":946},[477,3570,783],{"class":748},[477,3572,3573,3576,3578,3581],{"class":190,"line":1067},[477,3574,3575],{"class":748},"payload ",[477,3577,774],{"class":744},[477,3579,3580],{"class":748}," buffer.getvalue()      ",[477,3582,3583],{"class":499},"# bytes, ready to upload or return\n",[477,3585,3586,3588,3590,3592,3594,3596,3598,3601,3604,3606,3609],{"class":190,"line":1091},[477,3587,1274],{"class":946},[477,3589,1277],{"class":748},[477,3591,1746],{"class":744},[477,3593,1762],{"class":486},[477,3595,1753],{"class":1752},[477,3597,3130],{"class":946},[477,3599,3600],{"class":748},"(payload)",[477,3602,3603],{"class":744},":,",[477,3605,1759],{"class":1752},[477,3607,3608],{"class":486}," bytes in memory\"",[477,3610,783],{"class":748},[10,3612,3613,3614,3616,3617,3620,3621,38],{},"One caveat worth knowing: ",[14,3615,581],{}," accepts a URL string directly, and it works — but it downloads with no timeout, no retry and no status check. In a scheduled job that is a hang waiting to happen, so fetch with ",[14,3618,3619],{},"requests"," yourself and pass the bytes, as above. The same reasoning applies to API sources generally; see ",[34,3622,3624],{"href":3623},"\u002Fadvanced-data-transformation-and-cleaning\u002Fmoving-data-between-excel-and-databases\u002Ffetch-api-data-into-excel-with-python-requests\u002F","fetching API data into Excel with Python requests",[226,3626,3628],{"id":3627},"choosing-the-output-format-for-your-readers","Choosing the output format for your readers",[10,3630,3631],{},"Input format is decided for you. Output format is a choice, and it is worth making deliberately rather than defaulting to whatever the first tutorial used.",[326,3633,3634,3647],{},[329,3635,3636],{},[332,3637,3638,3641,3644],{},[335,3639,3640],{},"If the recipient...",[335,3642,3643],{},"Write",[335,3645,3646],{},"Because",[351,3648,3649,3661,3673,3685,3700,3713],{},[332,3650,3651,3654,3658],{},[356,3652,3653],{},"opens it in Excel and reads it",[356,3655,3656],{},[14,3657,16],{},[356,3659,3660],{},"Full formatting, charts, and universal support.",[332,3662,3663,3666,3670],{},[356,3664,3665],{},"runs macros against it",[356,3667,3668],{},[14,3669,24],{},[356,3671,3672],{},"Only format that can hold a VBA project.",[332,3674,3675,3678,3682],{},[356,3676,3677],{},"loads it into another system",[356,3679,3680],{},[14,3681,141],{},[356,3683,3684],{},"No parsing dependency; no ambiguity about types.",[332,3686,3687,3690,3694],{},[356,3688,3689],{},"uses LibreOffice exclusively",[356,3691,3692],{},[14,3693,16],{},[356,3695,3696,3697,3699],{},"LibreOffice reads it perfectly; ",[14,3698,131],{}," gains nothing.",[332,3701,3702,3705,3710],{},[356,3703,3704],{},"just needs the numbers, at volume",[356,3706,3707,3709],{},[14,3708,141],{}," or Parquet",[356,3711,3712],{},"Orders of magnitude faster to write and read.",[332,3714,3715,3718,3721],{},[356,3716,3717],{},"will not edit it at all",[356,3719,3720],{},"PDF",[356,3722,3723],{},"Nobody can accidentally change a figure.",[10,3725,3726,3727,38],{},"The last row deserves more weight than it usually gets. A surprising share of \"Excel reports\" are never edited — they are read, and then filed. Sending a PDF removes the whole class of problems where two people hold different edited copies of the same figures, and the conversion is one step, described in ",[34,3728,3730],{"href":3729},"\u002Fautomating-reporting-workflows\u002Fexporting-excel-reports-to-pdf\u002F","exporting Excel reports to PDF",[10,3732,3733,3734,3736],{},"When the answer is ",[14,3735,16],{},", one more decision remains: which writer. The trade-off is not subtle:",[468,3738,3740],{"className":735,"code":3739,"language":737,"meta":473,"style":473},"import time\nimport pandas as pd\n\ndf = pd.DataFrame({\"id\": range(200_000), \"amount\": range(200_000)})\n\nfor engine in (\"openpyxl\", \"xlsxwriter\"):\n    start = time.perf_counter()\n    df.to_excel(f\"bench_{engine}.xlsx\", index=False, engine=engine)\n    print(f\"{engine:\u003C12} {time.perf_counter() - start:6.2f}s\")\n",[14,3741,3742,3749,3759,3763,3801,3805,3824,3834,3869],{"__ignoreMap":473},[477,3743,3744,3746],{"class":190,"line":479},[477,3745,745],{"class":744},[477,3747,3748],{"class":748}," time\n",[477,3750,3751,3753,3755,3757],{"class":190,"line":503},[477,3752,745],{"class":744},[477,3754,749],{"class":748},[477,3756,752],{"class":744},[477,3758,755],{"class":748},[477,3760,3761],{"class":190,"line":516},[477,3762,761],{"emptyLinePlaceholder":760},[477,3764,3765,3767,3769,3771,3774,3776,3779,3781,3784,3787,3790,3792,3794,3796,3798],{"class":190,"line":529},[477,3766,771],{"class":748},[477,3768,774],{"class":744},[477,3770,921],{"class":748},[477,3772,3773],{"class":486},"\"id\"",[477,3775,1605],{"class":748},[477,3777,3778],{"class":946},"range",[477,3780,1277],{"class":748},[477,3782,3783],{"class":946},"200_000",[477,3785,3786],{"class":748},"), ",[477,3788,3789],{"class":486},"\"amount\"",[477,3791,1605],{"class":748},[477,3793,3778],{"class":946},[477,3795,1277],{"class":748},[477,3797,3783],{"class":946},[477,3799,3800],{"class":748},")})\n",[477,3802,3803],{"class":190,"line":542},[477,3804,761],{"emptyLinePlaceholder":760},[477,3806,3807,3809,3811,3813,3816,3818,3820,3822],{"class":190,"line":790},[477,3808,2333],{"class":744},[477,3810,1723],{"class":748},[477,3812,1898],{"class":744},[477,3814,3815],{"class":748}," (",[477,3817,979],{"class":486},[477,3819,560],{"class":748},[477,3821,1034],{"class":486},[477,3823,1683],{"class":748},[477,3825,3826,3829,3831],{"class":190,"line":796},[477,3827,3828],{"class":748},"    start ",[477,3830,774],{"class":744},[477,3832,3833],{"class":748}," time.perf_counter()\n",[477,3835,3836,3839,3841,3844,3846,3848,3850,3853,3855,3857,3859,3861,3863,3865,3867],{"class":190,"line":820},[477,3837,3838],{"class":748},"    df.to_excel(",[477,3840,1746],{"class":744},[477,3842,3843],{"class":486},"\"bench_",[477,3845,1753],{"class":1752},[477,3847,810],{"class":748},[477,3849,1759],{"class":1752},[477,3851,3852],{"class":486},".xlsx\"",[477,3854,560],{"class":748},[477,3856,1005],{"class":809},[477,3858,774],{"class":744},[477,3860,1010],{"class":946},[477,3862,560],{"class":748},[477,3864,810],{"class":809},[477,3866,774],{"class":744},[477,3868,1795],{"class":748},[477,3870,3871,3873,3875,3877,3879,3881,3883,3886,3888,3890,3893,3895,3898,3901,3903,3906],{"class":190,"line":843},[477,3872,2472],{"class":946},[477,3874,1277],{"class":748},[477,3876,1746],{"class":744},[477,3878,1762],{"class":486},[477,3880,1753],{"class":1752},[477,3882,810],{"class":748},[477,3884,3885],{"class":744},":\u003C12",[477,3887,1759],{"class":1752},[477,3889,2518],{"class":1752},[477,3891,3892],{"class":748},"time.perf_counter() ",[477,3894,2791],{"class":744},[477,3896,3897],{"class":748}," start",[477,3899,3900],{"class":744},":6.2f",[477,3902,1759],{"class":1752},[477,3904,3905],{"class":486},"s\"",[477,3907,783],{"class":748},[10,3909,3910,3911,38],{},"xlsxwriter wins on write speed and has the richer formatting API, but it can only create new files. openpyxl is the only one of the two that can open an existing workbook and change part of it — which is exactly what template filling requires. The full comparison is in ",[34,3912,1119],{"href":1118},[10,3914,3915,3916,3919],{},"A practical rule that covers nearly every case: ",[236,3917,3918],{},"create with xlsxwriter, modify with openpyxl",". If a job does both — builds a fresh workbook and then edits a template — use each library for the half it is good at rather than forcing one to do everything.",[226,3921,3923],{"id":3922},"key-takeaways","Key takeaways",[3925,3926,3927,3958,3971,3982,3988],"ul",{},[3928,3929,3930,3933,3934,3936,3937,1849,3939,560,3941,3936,3943,560,3945,3936,3947,560,3949,3936,3951,3953,3954,3957],"li",{},[236,3931,3932],{},"The extension picks the engine, and you can override it."," ",[14,3935,248],{}," for ",[14,3938,16],{},[14,3940,24],{},[14,3942,281],{},[14,3944,20],{},[14,3946,309],{},[14,3948,31],{},[14,3950,714],{},[14,3952,131],{}," — or ",[14,3955,3956],{},"calamine"," for all of them at once when you only need values.",[3928,3959,3960,3967,3968,3970],{},[236,3961,3962,565,3964,3966],{},[14,3963,20],{},[14,3965,31],{}," are read-only from Python."," Convert to ",[14,3969,16],{}," at the edge of your pipeline and never think about them again.",[3928,3972,3973,3981],{},[236,3974,3975,3977,3978,3980],{},[14,3976,385],{}," plus an ",[14,3979,24],{}," filename"," is the only way macros survive an openpyxl round-trip. Both halves are required.",[3928,3983,3984,3987],{},[236,3985,3986],{},"Converting through pandas keeps values, not appearance."," For visual fidelity, shell out to headless LibreOffice instead.",[3928,3989,3990,3993,3994,3997,3998,4000],{},[236,3991,3992],{},"Sniff the leading bytes"," before trusting a file name; ",[14,3995,3996],{},"PK"," means a zip-based workbook and ",[14,3999,2112],{}," means a legacy binary one.",[226,4002,4004],{"id":4003},"frequently-asked-questions","Frequently asked questions",[10,4006,4007,4013,4014,4016,4017,1849,4019,4021,4022,4024,4025,4027,4028,4030,4031,38],{},[236,4008,4009,4010,4012],{},"Which engine does pandas use for an ",[14,4011,16],{}," file?","\nopenpyxl. pandas picks the engine from the file extension unless you pass ",[14,4015,731],{}," explicitly — openpyxl for ",[14,4018,16],{},[14,4020,24],{},", calamine or xlrd for ",[14,4023,20],{},", pyxlsb for ",[14,4026,31],{},", and odfpy for ",[14,4029,131],{},". Install the matching package or the read raises ",[14,4032,4033],{},"ImportError",[10,4035,4036,4041,4042,560,4044,4046,4047,4049,4050,310,4052,4054,4055,4057],{},[236,4037,4038,4039,4012],{},"Why does openpyxl refuse to open my ",[14,4040,20],{},"\nopenpyxl only handles the OOXML zip formats (",[14,4043,16],{},[14,4045,24],{},"). A ",[14,4048,20],{}," file is the older binary BIFF format and needs ",[14,4051,281],{},[14,4053,313],{}," instead, or a conversion step to ",[14,4056,16],{}," first.",[10,4059,4060,4066,4067,4069,4070,4072],{},[236,4061,4062,4063,4065],{},"Will saving an ",[14,4064,24],{}," with openpyxl keep the macros?","\nOnly if you load it with ",[14,4068,385],{}," and save it back with an ",[14,4071,24],{}," extension. Without that flag openpyxl drops the VBA project silently and you get a macro-free workbook with no error.",[10,4074,4075,4081,4082,310,4084,4086,4087,4089,4090,4092],{},[236,4076,4077,4078,4080],{},"Is ",[14,4079,31],{}," worth using for large files?","\nIt reads and writes faster in Excel itself and the files are smaller, but Python support is read-only through ",[14,4083,309],{},[14,4085,313],{},". If a job needs to write output, produce ",[14,4088,16],{}," and let Excel users save as ",[14,4091,31],{}," if they want.",[10,4094,4095,4098,4099,4101,4102,4104],{},[236,4096,4097],{},"How do I know what a file really is when the extension is wrong?","\nRead the first few bytes. An OOXML workbook starts with ",[14,4100,3996],{}," (a zip), the legacy binary formats start with the OLE2 signature ",[14,4103,2112],{},", and a CSV is plain text. Sniffing the header is far more reliable than trusting the name.",[10,4106,4107,4113,4114,4116,4117,4119,4120,4122,4123,4125,4126,4128],{},[236,4108,4109,4110,4112],{},"Can Python write ",[14,4111,131],{}," files?","\nYes — pandas writes OpenDocument spreadsheets through the ",[14,4115,714],{}," engine when the output path ends in ",[14,4118,131],{},", and reads them through ",[14,4121,323],{},". Formatting support is much thinner than for ",[14,4124,16],{},", so treat ",[14,4127,131],{}," as a data interchange format rather than a report target.",[226,4130,4132],{"id":4131},"related","Related",[3925,4134,4135,4141,4147,4154,4161,4168,4174],{},[3928,4136,4137,4138,4140],{},"Up to the parent: ",[34,4139,37],{"href":36}," — the foundations this topic sits inside.",[3928,4142,4143,4146],{},[34,4144,4145],{"href":1305},"Read .xls Files in Python with xlrd and pandas"," — the legacy binary format end to end.",[3928,4148,4149,4153],{},[34,4150,4152],{"href":4151},"\u002Fgetting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions\u002Fconvert-xls-to-xlsx-with-python\u002F","Convert .xls to .xlsx with Python"," — a batch converter with the pitfalls handled.",[3928,4155,4156,4160],{},[34,4157,4159],{"href":4158},"\u002Fgetting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions\u002Fwork-with-macro-enabled-xlsm-files-in-openpyxl\u002F","Work with Macro-Enabled .xlsm Files in openpyxl"," — keeping the VBA project intact.",[3928,4162,4163,4167],{},[34,4164,4166],{"href":4165},"\u002Fgetting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions\u002Fread-and-write-ods-files-with-python\u002F","Read and Write .ods Files with Python"," — the OpenDocument side.",[3928,4169,4170,4173],{},[34,4171,4172],{"href":247},"Using openpyxl for Excel File Manipulation"," — the library behind the OOXML formats.",[3928,4175,4176,4180],{},[34,4177,4179],{"href":4178},"\u002Fgetting-started-with-python-excel-automation\u002Freading-excel-files-with-pandas\u002F","Reading Excel Files with pandas"," — the reading layer that sits on top of every engine here.",[4182,4183,4184],"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 .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 .sP0c6, html code.shiki .sP0c6{--shiki-default:#005CC5;--shiki-dark:#91CBFF}html pre.shiki code .s_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}html pre.shiki code .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}html pre.shiki code .sMTad, html code.shiki .sMTad{--shiki-default:#6F42C1;--shiki-dark:#FFB757}",{"title":473,"searchDepth":503,"depth":503,"links":4186},[4187,4188,4189,4190,4191,4192,4193,4194,4195,4196,4197,4198],{"id":228,"depth":503,"text":229},{"id":575,"depth":503,"text":576},{"id":1127,"depth":503,"text":1128},{"id":1309,"depth":503,"text":1310},{"id":1548,"depth":503,"text":1549},{"id":2048,"depth":503,"text":2049},{"id":2727,"depth":503,"text":2728},{"id":3167,"depth":503,"text":3168},{"id":3627,"depth":503,"text":3628},{"id":3922,"depth":503,"text":3923},{"id":4003,"depth":503,"text":4004},{"id":4131,"depth":503,"text":4132},"2026-08-15","Work with .xlsx, .xlsm, .xls, .xlsb, .ods and .csv from Python: which engine reads each, how to convert between them safely, and how to detect the real format of a file.","md",[4203,4206,4209,4212,4215,4217],{"q":4204,"a":4205},"Which engine does pandas use for an .xlsx file?","openpyxl. pandas picks the engine from the file extension unless you pass engine= explicitly — openpyxl for .xlsx\u002F.xlsm, calamine or xlrd for .xls, pyxlsb for .xlsb, and odfpy for .ods. Install the matching package or the read raises ImportError.",{"q":4207,"a":4208},"Why does openpyxl refuse to open my .xls file?","openpyxl only handles the OOXML zip formats (.xlsx, .xlsm). A .xls file is the older binary BIFF format and needs xlrd or python-calamine instead, or a conversion step to .xlsx first.",{"q":4210,"a":4211},"Will saving an .xlsm with openpyxl keep the macros?","Only if you load it with keep_vba=True and save it back with an .xlsm extension. Without that flag openpyxl drops the VBA project silently and you get a macro-free workbook with no error.",{"q":4213,"a":4214},"Is .xlsb worth using for large files?","It reads and writes faster in Excel itself and the files are smaller, but Python support is read-only through pyxlsb or python-calamine. If a job needs to write output, produce .xlsx and let Excel users save as .xlsb if they want.",{"q":4097,"a":4216},"Read the first few bytes. An OOXML workbook starts with PK (a zip), the legacy binary formats start with the OLE2 signature D0 CF 11 E0, and a CSV is plain text. Sniffing the header is far more reliable than trusting the name.",{"q":4218,"a":4219},"Can Python write .ods files?","Yes — pandas writes OpenDocument spreadsheets through the odf engine when the output path ends in .ods, and reads them through odfpy. Formatting support is much thinner than for .xlsx, so treat .ods as a data interchange format rather than a report target.",{},"\u002Fgetting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions",{"title":4223,"description":4224},"Excel File Formats in Python: xlsx, xls, xlsm, ods","A practical map of the spreadsheet formats Python can read and write — the engine behind each, converting between them in bulk, and identifying a file whose extension lies.","handling-excel-file-formats-and-conversions","getting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions\u002Findex","guide","rg_qF1T79RLbfxX0v36BwRYFi4BjU__rDlDeJJcspiI",[4230,4234],{"title":4231,"path":4232,"stem":4233,"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":4152,"path":4235,"stem":4236,"children":-1},"\u002Fgetting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions\u002Fconvert-xls-to-xlsx-with-python","getting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions\u002Fconvert-xls-to-xlsx-with-python\u002Findex",1786800026511]