[{"data":1,"prerenderedAt":2229},["ShallowReactive",2],{"doc:\u002Fgetting-started-with-python-excel-automation\u002Fchoosing-a-python-excel-library\u002Fpick-an-excel-engine-for-xlsx-xlsm-xls-xlsb-and-ods":3,"surround:\u002Fgetting-started-with-python-excel-automation\u002Fchoosing-a-python-excel-library\u002Fpick-an-excel-engine-for-xlsx-xlsm-xls-xlsb-and-ods":2220},{"id":4,"title":5,"body":6,"dateModified":2190,"datePublished":2190,"description":2191,"extension":2192,"faq":2193,"meta":2204,"navigation":452,"path":2213,"seo":2214,"slug":2216,"stem":2217,"type":2218,"__hash__":2219},"docs\u002Fgetting-started-with-python-excel-automation\u002Fchoosing-a-python-excel-library\u002Fpick-an-excel-engine-for-xlsx-xlsm-xls-xlsb-and-ods\u002Findex.md","Pick an Excel Engine for .xlsx, .xlsm, .xls, .xlsb and .ods",{"type":7,"value":8,"toc":2175},"minimark",[9,23,149,154,157,250,254,400,425,600,604,610,693,708,712,715,816,820,829,1060,1068,1072,1079,1178,1189,1193,1196,1592,1604,1608,1734,1738,1747,1910,1921,1929,1933,1939,2068,2076,2080,2096,2100,2107,2113,2119,2125,2131,2135,2171],[10,11,12,16,17,22],"p",{},[13,14,15],"code",{},"pandas.read_excel()"," looks like one function, but it is really a dispatcher over four or five\nindependent parsers, and most of the confusing errors in Python Excel work come from the wrong one\nbeing selected. The extension decides the default, the default is sometimes not installed, and two\nof the formats are not related to each other at all despite both being \"Excel files\". This guide,\npart of ",[18,19,21],"a",{"href":20},"\u002Fgetting-started-with-python-excel-automation\u002Fchoosing-a-python-excel-library\u002F","Choosing a Python Excel Library",",\nis the map from extension to engine.",[24,25,33,34,33,38,33,42,33,49,33,59,33,66,33,73,33,79,33,84,33,89,33,94,33,100,33,105,33,110,33,113,33,115,33,118,33,122,33,127,33,132,33,135,33,137,33,140,33,144],"svg",{"viewBox":26,"role":27,"ariaLabelledBy":28,"xmlns":31,"style":32},"0 0 760 264","img",[29,30],"eng-pick-t","eng-pick-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:760px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[35,36,37],"title",{"id":29},"Picking a parser from the file extension",[39,40,41],"desc",{"id":30},"OOXML files go to openpyxl or calamine, the legacy binary format needs xlrd 1.2.0 or calamine, and OpenDocument files need odfpy. calamine covers every read case except .ods.",[43,44],"rect",{"x":45,"y":45,"width":46,"height":47,"fill":48},"0","760","264","#ffffff",[43,50],{"x":51,"y":52,"width":53,"height":54,"rx":55,"fill":56,"stroke":57,"style":58},"230.0","26","300","56","12","#ebebfd","var(--brand,#5b5cf0)","stroke-width:2px",[60,61,65],"text",{"x":62,"y":63,"style":64},"380.0","60","font-size:13.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","What is the file extension?",[43,67],{"x":68,"y":69,"width":70,"height":71,"rx":55,"fill":72,"stroke":57,"style":58},"18.0","148","226.7","84","#f0f4ff",[60,74,78],{"x":75,"y":76,"style":77},"131.35","182","font-size:13px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","openpyxl",[60,80,83],{"x":75,"y":81,"style":82},"204","font-size:11.5px;font-weight:400;fill:var(--muted,#5b6780);text-anchor:middle","read, write, edit",[85,86],"line",{"x1":75,"y1":87,"x2":75,"y2":88,"stroke":57,"style":58},"87","141",[90,91],"polygon",{"points":92,"fill":93},"131.35,141 126.35,132 136.35,132","#5b5cf0",[60,95,99],{"x":96,"y":97,"style":98},"137.35","116","font-size:11px;font-weight:600;fill:var(--muted,#5b6780);text-anchor:start",".xlsx \u002F .xlsm",[43,101],{"x":102,"y":69,"width":70,"height":71,"rx":55,"fill":103,"stroke":104,"style":58},"266.7","#d9f4f1","var(--teal,#0f9488)",[60,106,109],{"x":107,"y":76,"style":108},"380.04999999999995","font-size:13px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","calamine",[60,111,112],{"x":107,"y":81,"style":82},"read only, fast",[85,114],{"x1":107,"y1":87,"x2":107,"y2":88,"stroke":57,"style":58},[90,116],{"points":117,"fill":93},"380.04999999999995,141 375.04999999999995,132 385.04999999999995,132",[60,119,121],{"x":120,"y":97,"style":98},"386.04999999999995",".xls \u002F .xlsb",[43,123],{"x":124,"y":69,"width":70,"height":71,"rx":55,"fill":125,"stroke":126,"style":58},"515.4","#fdefd8","var(--gold,#b4740a)",[60,128,131],{"x":129,"y":76,"style":130},"628.75","font-size:13px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","odfpy",[60,133,134],{"x":129,"y":81,"style":82},"read and write",[85,136],{"x1":129,"y1":87,"x2":129,"y2":88,"stroke":57,"style":58},[90,138],{"points":139,"fill":93},"628.75,141 623.75,132 633.75,132",[60,141,143],{"x":142,"y":97,"style":98},"634.75",".ods",[60,145,148],{"x":62,"y":146,"style":147},"252","font-size:12.5px;font-weight:400;fill:var(--muted,#5b6780);text-anchor:middle","the extension names the container, not a version of one format",[150,151,153],"h2",{"id":152},"prerequisites","Prerequisites",[10,155,156],{},"Install only what your formats need:",[158,159,164],"pre",{"className":160,"code":161,"language":162,"meta":163,"style":163},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install openpyxl          # .xlsx and .xlsm, read and write\npip install python-calamine   # .xlsx, .xlsm, .xls, .xlsb — read only, fast\npip install pyxlsb            # .xlsb only, read only\npip install \"xlrd==1.2.0\"     # legacy .xls only (xlrd 2.0 dropped it)\npip install odfpy             # .ods, read and write\npip install XlsxWriter        # .xlsx write only, richest formatting\n","bash","",[13,165,166,185,198,211,224,237],{"__ignoreMap":163},[167,168,170,174,178,181],"span",{"class":85,"line":169},1,[167,171,173],{"class":172},"sMTad","pip",[167,175,177],{"class":176},"srMev"," install",[167,179,180],{"class":176}," openpyxl",[167,182,184],{"class":183},"s-wDw","          # .xlsx and .xlsm, read and write\n",[167,186,188,190,192,195],{"class":85,"line":187},2,[167,189,173],{"class":172},[167,191,177],{"class":176},[167,193,194],{"class":176}," python-calamine",[167,196,197],{"class":183},"   # .xlsx, .xlsm, .xls, .xlsb — read only, fast\n",[167,199,201,203,205,208],{"class":85,"line":200},3,[167,202,173],{"class":172},[167,204,177],{"class":176},[167,206,207],{"class":176}," pyxlsb",[167,209,210],{"class":183},"            # .xlsb only, read only\n",[167,212,214,216,218,221],{"class":85,"line":213},4,[167,215,173],{"class":172},[167,217,177],{"class":176},[167,219,220],{"class":176}," \"xlrd==1.2.0\"",[167,222,223],{"class":183},"     # legacy .xls only (xlrd 2.0 dropped it)\n",[167,225,227,229,231,234],{"class":85,"line":226},5,[167,228,173],{"class":172},[167,230,177],{"class":176},[167,232,233],{"class":176}," odfpy",[167,235,236],{"class":183},"             # .ods, read and write\n",[167,238,240,242,244,247],{"class":85,"line":239},6,[167,241,173],{"class":172},[167,243,177],{"class":176},[167,245,246],{"class":176}," XlsxWriter",[167,248,249],{"class":183},"        # .xlsx write only, richest formatting\n",[150,251,253],{"id":252},"the-map-from-extension-to-engine","The map from extension to engine",[24,255,33,260,33,263,33,266,33,269,33,278,33,284,33,287,33,291,33,295,33,298,33,301,33,305,33,309,33,312,33,314,33,316,33,319,33,323,33,326,33,328,33,330,33,332,33,335,33,339,33,341,33,343,33,345,33,347,33,350,33,354,33,356,33,358,33,360,33,362,33,365,33,369,33,371,33,373,33,375,33,377,33,382,33,387,33,390,33,392,33,394,33,396],{"viewBox":256,"role":27,"ariaLabelledBy":257,"xmlns":31,"style":32},"0 0 760 336",[258,259],"eng-grid-t","eng-grid-d",[35,261,262],{"id":258},"Which engine handles which format, and in which direction",[39,264,265],{"id":259},"A grid of engines against formats: openpyxl reads and writes OOXML, calamine reads four formats quickly but writes none, pyxlsb reads binary workbooks, xlrd reads only legacy files, and odfpy handles OpenDocument.",[43,267],{"x":45,"y":45,"width":46,"height":268,"fill":48},"336",[43,270],{"x":271,"y":272,"width":273,"height":274,"rx":275,"fill":276,"stroke":277},"18","30","724","34","8","#e7ebef","var(--line,#cdd5e6)",[60,279,283],{"x":280,"y":281,"style":282},"126.6","52","font-size:12px;font-weight:700;fill:var(--muted,#5b6780);text-anchor:middle","Engine",[60,285,99],{"x":286,"y":281,"style":282},"298.55",[60,288,290],{"x":289,"y":281,"style":282},"425.25",".xls",[60,292,294],{"x":293,"y":281,"style":282},"551.95",".xlsb",[60,296,143],{"x":297,"y":281,"style":282},"678.65",[43,299],{"x":271,"y":300,"width":273,"height":274,"rx":275,"fill":72,"stroke":57},"67",[60,302,78],{"x":280,"y":303,"style":304},"88.0","font-size:12px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle",[60,306,308],{"x":286,"y":303,"style":307},"font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","read + write",[60,310,311],{"x":289,"y":303,"style":307},"no",[60,313,311],{"x":293,"y":303,"style":307},[60,315,311],{"x":297,"y":303,"style":307},[43,317],{"x":271,"y":318,"width":273,"height":274,"rx":275,"fill":103,"stroke":104},"107",[60,320,109],{"x":280,"y":321,"style":322},"128.0","font-size:12px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle",[60,324,325],{"x":286,"y":321,"style":307},"read",[60,327,325],{"x":289,"y":321,"style":307},[60,329,325],{"x":293,"y":321,"style":307},[60,331,311],{"x":297,"y":321,"style":307},[43,333],{"x":271,"y":334,"width":273,"height":274,"rx":275,"fill":276,"stroke":277},"147",[60,336,338],{"x":280,"y":337,"style":282},"168.0","pyxlsb",[60,340,311],{"x":286,"y":337,"style":307},[60,342,311],{"x":289,"y":337,"style":307},[60,344,325],{"x":293,"y":337,"style":307},[60,346,311],{"x":297,"y":337,"style":307},[43,348],{"x":271,"y":349,"width":273,"height":274,"rx":275,"fill":276,"stroke":277},"187",[60,351,353],{"x":280,"y":352,"style":282},"208.0","xlrd 1.2.0",[60,355,311],{"x":286,"y":352,"style":307},[60,357,325],{"x":289,"y":352,"style":307},[60,359,311],{"x":293,"y":352,"style":307},[60,361,311],{"x":297,"y":352,"style":307},[43,363],{"x":271,"y":364,"width":273,"height":274,"rx":275,"fill":125,"stroke":126},"227",[60,366,131],{"x":280,"y":367,"style":368},"248.0","font-size:12px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle",[60,370,311],{"x":286,"y":367,"style":307},[60,372,311],{"x":289,"y":367,"style":307},[60,374,311],{"x":293,"y":367,"style":307},[60,376,308],{"x":297,"y":367,"style":307},[43,378],{"x":271,"y":379,"width":273,"height":274,"rx":275,"fill":380,"stroke":381},"267","#fee8f2","var(--accent,#d81b73)",[60,383,386],{"x":280,"y":384,"style":385},"288.0","font-size:12px;font-weight:700;fill:var(--accent,#d81b73);text-anchor:middle","XlsxWriter",[60,388,389],{"x":286,"y":384,"style":307},"write",[60,391,311],{"x":289,"y":384,"style":307},[60,393,311],{"x":293,"y":384,"style":307},[60,395,311],{"x":297,"y":384,"style":307},[60,397,399],{"x":62,"y":398,"style":147},"324","half of these read only — check the direction before you pin one",[10,401,402,405,406,409,410,412,413,415,416,418,419,421,422,424],{},[13,403,404],{},".xlsx"," and ",[13,407,408],{},".xlsm"," are the same OOXML container; the difference is that ",[13,411,408],{}," carries a VBA\nproject. ",[13,414,290],{}," is the pre-2007 binary format and shares nothing with them. ",[13,417,294],{}," is a modern\nbinary format — the same logical model as ",[13,420,404],{},", stored as binary records instead of XML. ",[13,423,143],{},"\nis OpenDocument, produced by LibreOffice. Treating those four as variations of one format is the\nroot of most engine errors.",[158,426,430],{"className":427,"code":428,"language":429,"meta":163,"style":163},"language-python shiki shiki-themes github-light github-dark-high-contrast","import pandas as pd\n\nmodern = pd.read_excel(\"book.xlsx\")                          # openpyxl by default\nmacro = pd.read_excel(\"book.xlsm\")                           # openpyxl too\nlegacy = pd.read_excel(\"archive.xls\", engine=\"xlrd\")         # xlrd 1.2.0\nbinary = pd.read_excel(\"large.xlsb\", engine=\"pyxlsb\")        # or \"calamine\"\nopendoc = pd.read_excel(\"sheet.ods\", engine=\"odf\")\nfast = pd.read_excel(\"book.xlsx\", engine=\"calamine\")         # any of the four, quickly\n","python",[13,431,432,448,454,474,492,522,549,574],{"__ignoreMap":163},[167,433,434,438,442,445],{"class":85,"line":169},[167,435,437],{"class":436},"s-kum","import",[167,439,441],{"class":440},"skGVy"," pandas ",[167,443,444],{"class":436},"as",[167,446,447],{"class":440}," pd\n",[167,449,450],{"class":85,"line":187},[167,451,453],{"emptyLinePlaceholder":452},true,"\n",[167,455,456,459,462,465,468,471],{"class":85,"line":200},[167,457,458],{"class":440},"modern ",[167,460,461],{"class":436},"=",[167,463,464],{"class":440}," pd.read_excel(",[167,466,467],{"class":176},"\"book.xlsx\"",[167,469,470],{"class":440},")                          ",[167,472,473],{"class":183},"# openpyxl by default\n",[167,475,476,479,481,483,486,489],{"class":85,"line":213},[167,477,478],{"class":440},"macro ",[167,480,461],{"class":436},[167,482,464],{"class":440},[167,484,485],{"class":176},"\"book.xlsm\"",[167,487,488],{"class":440},")                           ",[167,490,491],{"class":183},"# openpyxl too\n",[167,493,494,497,499,501,504,507,511,513,516,519],{"class":85,"line":226},[167,495,496],{"class":440},"legacy ",[167,498,461],{"class":436},[167,500,464],{"class":440},[167,502,503],{"class":176},"\"archive.xls\"",[167,505,506],{"class":440},", ",[167,508,510],{"class":509},"sa561","engine",[167,512,461],{"class":436},[167,514,515],{"class":176},"\"xlrd\"",[167,517,518],{"class":440},")         ",[167,520,521],{"class":183},"# xlrd 1.2.0\n",[167,523,524,527,529,531,534,536,538,540,543,546],{"class":85,"line":239},[167,525,526],{"class":440},"binary ",[167,528,461],{"class":436},[167,530,464],{"class":440},[167,532,533],{"class":176},"\"large.xlsb\"",[167,535,506],{"class":440},[167,537,510],{"class":509},[167,539,461],{"class":436},[167,541,542],{"class":176},"\"pyxlsb\"",[167,544,545],{"class":440},")        ",[167,547,548],{"class":183},"# or \"calamine\"\n",[167,550,552,555,557,559,562,564,566,568,571],{"class":85,"line":551},7,[167,553,554],{"class":440},"opendoc ",[167,556,461],{"class":436},[167,558,464],{"class":440},[167,560,561],{"class":176},"\"sheet.ods\"",[167,563,506],{"class":440},[167,565,510],{"class":509},[167,567,461],{"class":436},[167,569,570],{"class":176},"\"odf\"",[167,572,573],{"class":440},")\n",[167,575,577,580,582,584,586,588,590,592,595,597],{"class":85,"line":576},8,[167,578,579],{"class":440},"fast ",[167,581,461],{"class":436},[167,583,464],{"class":440},[167,585,467],{"class":176},[167,587,506],{"class":440},[167,589,510],{"class":509},[167,591,461],{"class":436},[167,593,594],{"class":176},"\"calamine\"",[167,596,518],{"class":440},[167,598,599],{"class":183},"# any of the four, quickly\n",[150,601,603],{"id":602},"preserving-macros-when-editing-xlsm","Preserving macros when editing .xlsm",[10,605,606,607,609],{},"The ",[13,608,408],{}," trap is quiet and expensive: openpyxl opens the file happily, saves it happily, and the\nresult has no macros in it. The VBA project is a separate part of the container that openpyxl only\ncarries across when told to.",[158,611,613],{"className":427,"code":612,"language":429,"meta":163,"style":163},"from openpyxl import load_workbook\n\nbook = load_workbook(\"dashboard.xlsm\", keep_vba=True)\nbook[\"Data\"][\"B2\"] = 128400\nbook.save(\"dashboard-updated.xlsm\")        # extension must stay .xlsm\n",[13,614,615,628,632,658,680],{"__ignoreMap":163},[167,616,617,620,623,625],{"class":85,"line":169},[167,618,619],{"class":436},"from",[167,621,622],{"class":440}," openpyxl ",[167,624,437],{"class":436},[167,626,627],{"class":440}," load_workbook\n",[167,629,630],{"class":85,"line":187},[167,631,453],{"emptyLinePlaceholder":452},[167,633,634,637,639,642,645,647,650,652,656],{"class":85,"line":200},[167,635,636],{"class":440},"book ",[167,638,461],{"class":436},[167,640,641],{"class":440}," load_workbook(",[167,643,644],{"class":176},"\"dashboard.xlsm\"",[167,646,506],{"class":440},[167,648,649],{"class":509},"keep_vba",[167,651,461],{"class":436},[167,653,655],{"class":654},"sP0c6","True",[167,657,573],{"class":440},[167,659,660,663,666,669,672,675,677],{"class":85,"line":213},[167,661,662],{"class":440},"book[",[167,664,665],{"class":176},"\"Data\"",[167,667,668],{"class":440},"][",[167,670,671],{"class":176},"\"B2\"",[167,673,674],{"class":440},"] ",[167,676,461],{"class":436},[167,678,679],{"class":654}," 128400\n",[167,681,682,685,688,690],{"class":85,"line":226},[167,683,684],{"class":440},"book.save(",[167,686,687],{"class":176},"\"dashboard-updated.xlsm\"",[167,689,545],{"class":440},[167,691,692],{"class":183},"# extension must stay .xlsm\n",[10,694,695,696,699,700,702,703,707],{},"Both halves are required. ",[13,697,698],{},"keep_vba=True"," preserves the project; saving under ",[13,701,404],{}," discards it\nregardless. ",[18,704,706],{"href":705},"\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","\ncovers what else survives that round trip.",[150,709,711],{"id":710},"choosing-between-a-reader-and-a-writer","Choosing between a reader and a writer",[10,713,714],{},"Half the engines in the list read only, and the split is not obvious from their names. calamine and\npyxlsb never write. xlsxwriter never reads. openpyxl and odfpy do both. That matters for the shape\nof a job: a fast read with calamine and a formatted write with xlsxwriter is a perfectly ordinary\npairing, and neither one could do the other's half.",[158,716,718],{"className":427,"code":717,"language":429,"meta":163,"style":163},"import pandas as pd\n\nframe = pd.read_excel(\"source.xlsb\", engine=\"calamine\")      # fast read, binary format\nwith pd.ExcelWriter(\"report.xlsx\", engine=\"xlsxwriter\") as writer:\n    frame.to_excel(writer, sheet_name=\"Data\", index=False)   # formatted write\n",[13,719,720,730,734,760,788],{"__ignoreMap":163},[167,721,722,724,726,728],{"class":85,"line":169},[167,723,437],{"class":436},[167,725,441],{"class":440},[167,727,444],{"class":436},[167,729,447],{"class":440},[167,731,732],{"class":85,"line":187},[167,733,453],{"emptyLinePlaceholder":452},[167,735,736,739,741,743,746,748,750,752,754,757],{"class":85,"line":200},[167,737,738],{"class":440},"frame ",[167,740,461],{"class":436},[167,742,464],{"class":440},[167,744,745],{"class":176},"\"source.xlsb\"",[167,747,506],{"class":440},[167,749,510],{"class":509},[167,751,461],{"class":436},[167,753,594],{"class":176},[167,755,756],{"class":440},")      ",[167,758,759],{"class":183},"# fast read, binary format\n",[167,761,762,765,768,771,773,775,777,780,783,785],{"class":85,"line":213},[167,763,764],{"class":436},"with",[167,766,767],{"class":440}," pd.ExcelWriter(",[167,769,770],{"class":176},"\"report.xlsx\"",[167,772,506],{"class":440},[167,774,510],{"class":509},[167,776,461],{"class":436},[167,778,779],{"class":176},"\"xlsxwriter\"",[167,781,782],{"class":440},") ",[167,784,444],{"class":436},[167,786,787],{"class":440}," writer:\n",[167,789,790,793,796,798,800,802,805,807,810,813],{"class":85,"line":226},[167,791,792],{"class":440},"    frame.to_excel(writer, ",[167,794,795],{"class":509},"sheet_name",[167,797,461],{"class":436},[167,799,665],{"class":176},[167,801,506],{"class":440},[167,803,804],{"class":509},"index",[167,806,461],{"class":436},[167,808,809],{"class":654},"False",[167,811,812],{"class":440},")   ",[167,814,815],{"class":183},"# formatted write\n",[150,817,819],{"id":818},"detecting-the-real-format-before-trusting-the-extension","Detecting the real format before trusting the extension",[10,821,822,823,825,826,828],{},"Files arrive misnamed constantly — a ",[13,824,290],{}," that is really a CSV, an ",[13,827,404],{}," that is really an HTML\ntable saved by a reporting tool. Checking the magic bytes turns a confusing parser error into a\nclear message.",[158,830,832],{"className":427,"code":831,"language":429,"meta":163,"style":163},"from pathlib import Path\n\nSIGNATURES = {\n    b\"PK\\x03\\x04\": \"OOXML (.xlsx\u002F.xlsm\u002F.ods)\",\n    b\"\\xd0\\xcf\\x11\\xe0\": \"OLE2 (legacy .xls)\",\n}\n\ndef sniff(path: Path) -> str:\n    head = path.open(\"rb\").read(8)\n    for magic, name in SIGNATURES.items():\n        if head.startswith(magic):\n            return name\n    if head.lstrip()[:1] in (b\"\u003C\", b\"{\"):\n        return \"text — HTML or JSON, not a workbook\"\n    return \"text — probably CSV\"\n\nprint(sniff(Path(\"suspect.xls\")))\n",[13,833,834,846,850,861,885,903,908,912,930,951,969,978,987,1022,1031,1040,1045],{"__ignoreMap":163},[167,835,836,838,841,843],{"class":85,"line":169},[167,837,619],{"class":436},[167,839,840],{"class":440}," pathlib ",[167,842,437],{"class":436},[167,844,845],{"class":440}," Path\n",[167,847,848],{"class":85,"line":187},[167,849,453],{"emptyLinePlaceholder":452},[167,851,852,855,858],{"class":85,"line":200},[167,853,854],{"class":654},"SIGNATURES",[167,856,857],{"class":436}," =",[167,859,860],{"class":440}," {\n",[167,862,863,866,869,873,876,879,882],{"class":85,"line":213},[167,864,865],{"class":436},"    b",[167,867,868],{"class":176},"\"PK",[167,870,872],{"class":871},"sSjpA","\\x03\\x04",[167,874,875],{"class":176},"\"",[167,877,878],{"class":440},": ",[167,880,881],{"class":176},"\"OOXML (.xlsx\u002F.xlsm\u002F.ods)\"",[167,883,884],{"class":440},",\n",[167,886,887,889,891,894,896,898,901],{"class":85,"line":226},[167,888,865],{"class":436},[167,890,875],{"class":176},[167,892,893],{"class":871},"\\xd0\\xcf\\x11\\xe0",[167,895,875],{"class":176},[167,897,878],{"class":440},[167,899,900],{"class":176},"\"OLE2 (legacy .xls)\"",[167,902,884],{"class":440},[167,904,905],{"class":85,"line":239},[167,906,907],{"class":440},"}\n",[167,909,910],{"class":85,"line":551},[167,911,453],{"emptyLinePlaceholder":452},[167,913,914,917,921,924,927],{"class":85,"line":576},[167,915,916],{"class":436},"def",[167,918,920],{"class":919},"s_Opv"," sniff",[167,922,923],{"class":440},"(path: Path) -> ",[167,925,926],{"class":654},"str",[167,928,929],{"class":440},":\n",[167,931,933,936,938,941,944,947,949],{"class":85,"line":932},9,[167,934,935],{"class":440},"    head ",[167,937,461],{"class":436},[167,939,940],{"class":440}," path.open(",[167,942,943],{"class":176},"\"rb\"",[167,945,946],{"class":440},").read(",[167,948,275],{"class":654},[167,950,573],{"class":440},[167,952,954,957,960,963,966],{"class":85,"line":953},10,[167,955,956],{"class":436},"    for",[167,958,959],{"class":440}," magic, name ",[167,961,962],{"class":436},"in",[167,964,965],{"class":654}," SIGNATURES",[167,967,968],{"class":440},".items():\n",[167,970,972,975],{"class":85,"line":971},11,[167,973,974],{"class":436},"        if",[167,976,977],{"class":440}," head.startswith(magic):\n",[167,979,981,984],{"class":85,"line":980},12,[167,982,983],{"class":436},"            return",[167,985,986],{"class":440}," name\n",[167,988,990,993,996,999,1001,1003,1006,1009,1012,1014,1016,1019],{"class":85,"line":989},13,[167,991,992],{"class":436},"    if",[167,994,995],{"class":440}," head.lstrip()[:",[167,997,998],{"class":654},"1",[167,1000,674],{"class":440},[167,1002,962],{"class":436},[167,1004,1005],{"class":440}," (",[167,1007,1008],{"class":436},"b",[167,1010,1011],{"class":176},"\"\u003C\"",[167,1013,506],{"class":440},[167,1015,1008],{"class":436},[167,1017,1018],{"class":176},"\"{\"",[167,1020,1021],{"class":440},"):\n",[167,1023,1025,1028],{"class":85,"line":1024},14,[167,1026,1027],{"class":436},"        return",[167,1029,1030],{"class":176}," \"text — HTML or JSON, not a workbook\"\n",[167,1032,1034,1037],{"class":85,"line":1033},15,[167,1035,1036],{"class":436},"    return",[167,1038,1039],{"class":176}," \"text — probably CSV\"\n",[167,1041,1043],{"class":85,"line":1042},16,[167,1044,453],{"emptyLinePlaceholder":452},[167,1046,1048,1051,1054,1057],{"class":85,"line":1047},17,[167,1049,1050],{"class":654},"print",[167,1052,1053],{"class":440},"(sniff(Path(",[167,1055,1056],{"class":176},"\"suspect.xls\"",[167,1058,1059],{"class":440},")))\n",[10,1061,1062,1063,1067],{},"That check costs microseconds and removes a whole class of support question.\n",[18,1064,1066],{"href":1065},"\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-excel-file-format-cannot-be-determined-in-pandas\u002F","Fix \"Excel file format cannot be determined\" in pandas","\nwalks through the error it prevents.",[150,1069,1071],{"id":1070},"how-pandas-decides-and-why-the-error-is-confusing","How pandas decides, and why the error is confusing",[10,1073,1074,1075,1078],{},"Understanding the resolution order explains almost every engine error message. pandas takes an\nexplicit ",[13,1076,1077],{},"engine="," argument if you gave one. Otherwise it maps the extension to a default. Then it\nimports the parser it chose — and that import is where a missing dependency surfaces, which is why\nthe traceback names openpyxl when the real problem is that the file was a CSV all along.",[24,1080,33,1085,33,1088,33,1091,33,1094,33,1100,33,1105,33,1109,33,1115,33,1120,33,1123,33,1126,33,1130,33,1134,33,1138,33,1141,33,1144,33,1148,33,1152,33,1156,33,1159,33,1162,33,1166,33,1170,33,1174],{"viewBox":1081,"role":27,"ariaLabelledBy":1082,"xmlns":31,"style":32},"0 0 760 308",[1083,1084],"eng-order-t","eng-order-d",[35,1086,1087],{"id":1083},"The order pandas resolves an engine in",[39,1089,1090],{"id":1084},"pandas uses an explicitly passed engine first, otherwise it maps the file extension to a default parser, then imports it — and raises a missing-dependency error only at that last step.",[43,1092],{"x":45,"y":45,"width":46,"height":1093,"fill":48},"308",[43,1095],{"x":1096,"y":1097,"width":1098,"height":281,"rx":1099,"fill":103,"stroke":104},"22","28","716","10",[1101,1102],"circle",{"cx":281,"cy":1103,"r":1104,"fill":93},"54.0","15",[60,1106,998],{"x":281,"y":1107,"style":1108},"59.0","font-size:13px;font-weight:700;fill:#ffffff;text-anchor:middle",[60,1110,1114],{"x":1111,"y":1112,"style":1113},"82","50","font-size:13px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:start","An explicit engine argument wins",[60,1116,1119],{"x":1111,"y":1117,"style":1118},"68","font-size:11.5px;font-weight:400;fill:var(--muted,#5b6780);text-anchor:start","engine='calamine' overrides everything below",[43,1121],{"x":1096,"y":1122,"width":1098,"height":281,"rx":1099,"fill":103,"stroke":104},"90",[1101,1124],{"cx":281,"cy":1125,"r":1104,"fill":93},"116.0",[60,1127,1129],{"x":281,"y":1128,"style":1108},"121.0","2",[60,1131,1133],{"x":1111,"y":1132,"style":1113},"112","Otherwise the extension decides",[60,1135,1137],{"x":1111,"y":1136,"style":1118},"130",".xlsx and .xlsm map to openpyxl, .ods to odf",[43,1139],{"x":1096,"y":1140,"width":1098,"height":281,"rx":1099,"fill":103,"stroke":104},"152",[1101,1142],{"cx":281,"cy":1143,"r":1104,"fill":93},"178.0",[60,1145,1147],{"x":281,"y":1146,"style":1108},"183.0","3",[60,1149,1151],{"x":1111,"y":1150,"style":1113},"174","The chosen parser is imported",[60,1153,1155],{"x":1111,"y":1154,"style":1118},"192","this is where ModuleNotFoundError appears",[43,1157],{"x":1096,"y":1158,"width":1098,"height":281,"rx":1099,"fill":103,"stroke":104},"214",[1101,1160],{"cx":281,"cy":1161,"r":1104,"fill":93},"240.0",[60,1163,1165],{"x":281,"y":1164,"style":1108},"245.0","4",[60,1167,1169],{"x":1111,"y":1168,"style":1113},"236","The file's own bytes are never checked",[60,1171,1173],{"x":1111,"y":1172,"style":1118},"254","a misnamed file fails inside the parser",[60,1175,1177],{"x":62,"y":1176,"style":147},"296","the error you see names the parser, not the real problem",[10,1179,1180,1181,1183,1184,1188],{},"Nothing in that sequence inspects the file's contents. A ",[13,1182,404],{}," that is really an HTML table gets\nsent to openpyxl, which fails deep inside a zip reader with a message about a corrupt archive. That\nis the reasoning behind sniffing the bytes yourself before the read, and behind\n",[18,1185,1187],{"href":1186},"\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-badzipfile-error-when-reading-excel-in-python\u002F","Fix BadZipFile Error When Reading Excel in Python",",\nwhich is the same failure seen from the other side.",[150,1190,1192],{"id":1191},"a-defensive-reader-that-names-the-real-problem","A defensive reader that names the real problem",[10,1194,1195],{},"Wrapping the read once turns every one of these into a message an operator can act on, which\nmatters far more in a scheduled job than in an interactive session.",[158,1197,1199],{"className":427,"code":1198,"language":429,"meta":163,"style":163},"from pathlib import Path\nimport pandas as pd\n\nENGINE_BY_SUFFIX = {\n    \".xlsx\": \"calamine\", \".xlsm\": \"calamine\", \".xlsb\": \"calamine\",\n    \".xls\": \"calamine\", \".ods\": \"odf\",\n}\n\ndef read_any(path, **kwargs):\n    path = Path(path)\n    engine = ENGINE_BY_SUFFIX.get(path.suffix.lower())\n    if engine is None:\n        raise ValueError(f\"{path.name}: unsupported extension {path.suffix!r}\")\n    try:\n        return pd.read_excel(path, engine=engine, **kwargs)\n    except ImportError as exc:\n        raise RuntimeError(f\"{path.name} needs the {engine} engine: pip install python-calamine\") from exc\n    except Exception as exc:\n        head = path.open(\"rb\").read(4)\n        if not head.startswith((b\"PK\\x03\\x04\", b\"\\xd0\\xcf\\x11\\xe0\")):\n            raise ValueError(f\"{path.name} is not a workbook — it starts with {head!r}\") from exc\n        raise\n\nprint(read_any(\"monthly.xlsb\").shape)\n",[13,1200,1201,1211,1221,1225,1234,1263,1283,1287,1291,1307,1317,1330,1345,1387,1394,1413,1427,1465,1477,1495,1527,1567,1573,1578],{"__ignoreMap":163},[167,1202,1203,1205,1207,1209],{"class":85,"line":169},[167,1204,619],{"class":436},[167,1206,840],{"class":440},[167,1208,437],{"class":436},[167,1210,845],{"class":440},[167,1212,1213,1215,1217,1219],{"class":85,"line":187},[167,1214,437],{"class":436},[167,1216,441],{"class":440},[167,1218,444],{"class":436},[167,1220,447],{"class":440},[167,1222,1223],{"class":85,"line":200},[167,1224,453],{"emptyLinePlaceholder":452},[167,1226,1227,1230,1232],{"class":85,"line":213},[167,1228,1229],{"class":654},"ENGINE_BY_SUFFIX",[167,1231,857],{"class":436},[167,1233,860],{"class":440},[167,1235,1236,1239,1241,1243,1245,1248,1250,1252,1254,1257,1259,1261],{"class":85,"line":226},[167,1237,1238],{"class":176},"    \".xlsx\"",[167,1240,878],{"class":440},[167,1242,594],{"class":176},[167,1244,506],{"class":440},[167,1246,1247],{"class":176},"\".xlsm\"",[167,1249,878],{"class":440},[167,1251,594],{"class":176},[167,1253,506],{"class":440},[167,1255,1256],{"class":176},"\".xlsb\"",[167,1258,878],{"class":440},[167,1260,594],{"class":176},[167,1262,884],{"class":440},[167,1264,1265,1268,1270,1272,1274,1277,1279,1281],{"class":85,"line":239},[167,1266,1267],{"class":176},"    \".xls\"",[167,1269,878],{"class":440},[167,1271,594],{"class":176},[167,1273,506],{"class":440},[167,1275,1276],{"class":176},"\".ods\"",[167,1278,878],{"class":440},[167,1280,570],{"class":176},[167,1282,884],{"class":440},[167,1284,1285],{"class":85,"line":551},[167,1286,907],{"class":440},[167,1288,1289],{"class":85,"line":576},[167,1290,453],{"emptyLinePlaceholder":452},[167,1292,1293,1295,1298,1301,1304],{"class":85,"line":932},[167,1294,916],{"class":436},[167,1296,1297],{"class":919}," read_any",[167,1299,1300],{"class":440},"(path, ",[167,1302,1303],{"class":436},"**",[167,1305,1306],{"class":440},"kwargs):\n",[167,1308,1309,1312,1314],{"class":85,"line":953},[167,1310,1311],{"class":440},"    path ",[167,1313,461],{"class":436},[167,1315,1316],{"class":440}," Path(path)\n",[167,1318,1319,1322,1324,1327],{"class":85,"line":971},[167,1320,1321],{"class":440},"    engine ",[167,1323,461],{"class":436},[167,1325,1326],{"class":654}," ENGINE_BY_SUFFIX",[167,1328,1329],{"class":440},".get(path.suffix.lower())\n",[167,1331,1332,1334,1337,1340,1343],{"class":85,"line":980},[167,1333,992],{"class":436},[167,1335,1336],{"class":440}," engine ",[167,1338,1339],{"class":436},"is",[167,1341,1342],{"class":654}," None",[167,1344,929],{"class":440},[167,1346,1347,1350,1353,1356,1359,1361,1364,1367,1370,1373,1375,1378,1381,1383,1385],{"class":85,"line":989},[167,1348,1349],{"class":436},"        raise",[167,1351,1352],{"class":654}," ValueError",[167,1354,1355],{"class":440},"(",[167,1357,1358],{"class":436},"f",[167,1360,875],{"class":176},[167,1362,1363],{"class":871},"{",[167,1365,1366],{"class":440},"path.name",[167,1368,1369],{"class":871},"}",[167,1371,1372],{"class":176},": unsupported extension ",[167,1374,1363],{"class":871},[167,1376,1377],{"class":440},"path.suffix",[167,1379,1380],{"class":436},"!r",[167,1382,1369],{"class":871},[167,1384,875],{"class":176},[167,1386,573],{"class":440},[167,1388,1389,1392],{"class":85,"line":1024},[167,1390,1391],{"class":436},"    try",[167,1393,929],{"class":440},[167,1395,1396,1398,1401,1403,1405,1408,1410],{"class":85,"line":1033},[167,1397,1027],{"class":436},[167,1399,1400],{"class":440}," pd.read_excel(path, ",[167,1402,510],{"class":509},[167,1404,461],{"class":436},[167,1406,1407],{"class":440},"engine, ",[167,1409,1303],{"class":436},[167,1411,1412],{"class":440},"kwargs)\n",[167,1414,1415,1418,1421,1424],{"class":85,"line":1042},[167,1416,1417],{"class":436},"    except",[167,1419,1420],{"class":654}," ImportError",[167,1422,1423],{"class":436}," as",[167,1425,1426],{"class":440}," exc:\n",[167,1428,1429,1431,1434,1436,1438,1440,1442,1444,1446,1449,1451,1453,1455,1458,1460,1462],{"class":85,"line":1047},[167,1430,1349],{"class":436},[167,1432,1433],{"class":654}," RuntimeError",[167,1435,1355],{"class":440},[167,1437,1358],{"class":436},[167,1439,875],{"class":176},[167,1441,1363],{"class":871},[167,1443,1366],{"class":440},[167,1445,1369],{"class":871},[167,1447,1448],{"class":176}," needs the ",[167,1450,1363],{"class":871},[167,1452,510],{"class":440},[167,1454,1369],{"class":871},[167,1456,1457],{"class":176}," engine: pip install python-calamine\"",[167,1459,782],{"class":440},[167,1461,619],{"class":436},[167,1463,1464],{"class":440}," exc\n",[167,1466,1468,1470,1473,1475],{"class":85,"line":1467},18,[167,1469,1417],{"class":436},[167,1471,1472],{"class":654}," Exception",[167,1474,1423],{"class":436},[167,1476,1426],{"class":440},[167,1478,1480,1483,1485,1487,1489,1491,1493],{"class":85,"line":1479},19,[167,1481,1482],{"class":440},"        head ",[167,1484,461],{"class":436},[167,1486,940],{"class":440},[167,1488,943],{"class":176},[167,1490,946],{"class":440},[167,1492,1165],{"class":654},[167,1494,573],{"class":440},[167,1496,1498,1500,1503,1506,1508,1510,1512,1514,1516,1518,1520,1522,1524],{"class":85,"line":1497},20,[167,1499,974],{"class":436},[167,1501,1502],{"class":436}," not",[167,1504,1505],{"class":440}," head.startswith((",[167,1507,1008],{"class":436},[167,1509,868],{"class":176},[167,1511,872],{"class":871},[167,1513,875],{"class":176},[167,1515,506],{"class":440},[167,1517,1008],{"class":436},[167,1519,875],{"class":176},[167,1521,893],{"class":871},[167,1523,875],{"class":176},[167,1525,1526],{"class":440},")):\n",[167,1528,1530,1533,1535,1537,1539,1541,1543,1545,1547,1550,1552,1555,1557,1559,1561,1563,1565],{"class":85,"line":1529},21,[167,1531,1532],{"class":436},"            raise",[167,1534,1352],{"class":654},[167,1536,1355],{"class":440},[167,1538,1358],{"class":436},[167,1540,875],{"class":176},[167,1542,1363],{"class":871},[167,1544,1366],{"class":440},[167,1546,1369],{"class":871},[167,1548,1549],{"class":176}," is not a workbook — it starts with ",[167,1551,1363],{"class":871},[167,1553,1554],{"class":440},"head",[167,1556,1380],{"class":436},[167,1558,1369],{"class":871},[167,1560,875],{"class":176},[167,1562,782],{"class":440},[167,1564,619],{"class":436},[167,1566,1464],{"class":440},[167,1568,1570],{"class":85,"line":1569},22,[167,1571,1572],{"class":436},"        raise\n",[167,1574,1576],{"class":85,"line":1575},23,[167,1577,453],{"emptyLinePlaceholder":452},[167,1579,1581,1583,1586,1589],{"class":85,"line":1580},24,[167,1582,1050],{"class":654},[167,1584,1585],{"class":440},"(read_any(",[167,1587,1588],{"class":176},"\"monthly.xlsb\"",[167,1590,1591],{"class":440},").shape)\n",[10,1593,1594,1595,1598,1599,1603],{},"One dictionary and one ",[13,1596,1597],{},"try"," block replaces the guessing, and the failure message names the file and\nthe reason rather than a line inside somebody else's parser. That is the same principle applied in\n",[18,1600,1602],{"href":1601},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Flog-python-excel-script-output-to-a-file\u002F","Log Python Excel Script Output to a File"," —\nfail where the cause is, not three stages downstream.",[150,1605,1607],{"id":1606},"common-pitfalls","Common pitfalls",[1609,1610,1611,1627],"table",{},[1612,1613,1614],"thead",{},[1615,1616,1617,1621,1624],"tr",{},[1618,1619,1620],"th",{},"Symptom",[1618,1622,1623],{},"Cause",[1618,1625,1626],{},"Fix",[1628,1629,1630,1650,1665,1686,1699,1717],"tbody",{},[1615,1631,1632,1638,1641],{},[1633,1634,1635],"td",{},[13,1636,1637],{},"InvalidFileException: openpyxl does not support the old .xls format",[1633,1639,1640],{},"Legacy binary file sent to an OOXML parser",[1633,1642,1643,1646,1647],{},[13,1644,1645],{},"engine=\"calamine\"",", or ",[13,1648,1649],{},"xlrd==1.2.0",[1615,1651,1652,1657,1662],{},[1633,1653,1654],{},[13,1655,1656],{},"XLRDError: Excel xlsx file; not supported",[1633,1658,1659,1660],{},"xlrd 2.0 asked to read ",[13,1661,404],{},[1633,1663,1664],{},"Let pandas pick openpyxl, or install calamine",[1615,1666,1667,1670,1677],{},[1633,1668,1669],{},"Macros vanish after an edit",[1633,1671,1672,1674,1675],{},[13,1673,649],{}," not set, or saved as ",[13,1676,404],{},[1633,1678,1679,1682,1683,1685],{},[13,1680,1681],{},"load_workbook(path, keep_vba=True)"," and keep the ",[13,1684,408],{}," extension",[1615,1687,1688,1693,1696],{},[1633,1689,1690],{},[13,1691,1692],{},"ValueError: Excel file format cannot be determined",[1633,1694,1695],{},"The file is not a workbook at all",[1633,1697,1698],{},"Sniff the magic bytes as above",[1615,1700,1701,1706,1711],{},[1633,1702,1703,1705],{},[13,1704,294],{}," write fails",[1633,1707,1708,1709],{},"No mainstream library writes ",[13,1710,294],{},[1633,1712,1713,1714,1716],{},"Write ",[13,1715,404],{},", or save through Excel itself",[1615,1718,1719,1724,1729],{},[1633,1720,1721],{},[13,1722,1723],{},"ModuleNotFoundError: odf",[1633,1725,1726,1728],{},[13,1727,143],{}," needs odfpy",[1633,1730,1731],{},[13,1732,1733],{},"pip install odfpy",[150,1735,1737],{"id":1736},"converting-between-formats-once-you-can-read-them","Converting between formats once you can read them",[10,1739,1740,1741,1743,1744,1746],{},"Once the right engine is reading the file, converting it is usually the next step — a ",[13,1742,290],{}," archive\nthat nobody can process, or an ",[13,1745,294],{}," export that has to become a shareable workbook. The pattern\nis the same in every direction: read with whatever handles the source, write with whatever produces\nthe target.",[158,1748,1750],{"className":427,"code":1749,"language":429,"meta":163,"style":163},"from pathlib import Path\nimport pandas as pd\n\ndef to_xlsx(source: Path) -> Path:\n    target = source.with_suffix(\".xlsx\")\n    tabs = pd.read_excel(source, sheet_name=None, engine=\"calamine\")\n    with pd.ExcelWriter(target, engine=\"xlsxwriter\") as writer:\n        for name, frame in tabs.items():\n            frame.to_excel(writer, sheet_name=name[:31], index=False)\n    return target\n\nprint(to_xlsx(Path(\"archive-2011.xls\")))\n",[13,1751,1752,1762,1772,1776,1786,1801,1828,1848,1861,1887,1894,1898],{"__ignoreMap":163},[167,1753,1754,1756,1758,1760],{"class":85,"line":169},[167,1755,619],{"class":436},[167,1757,840],{"class":440},[167,1759,437],{"class":436},[167,1761,845],{"class":440},[167,1763,1764,1766,1768,1770],{"class":85,"line":187},[167,1765,437],{"class":436},[167,1767,441],{"class":440},[167,1769,444],{"class":436},[167,1771,447],{"class":440},[167,1773,1774],{"class":85,"line":200},[167,1775,453],{"emptyLinePlaceholder":452},[167,1777,1778,1780,1783],{"class":85,"line":213},[167,1779,916],{"class":436},[167,1781,1782],{"class":919}," to_xlsx",[167,1784,1785],{"class":440},"(source: Path) -> Path:\n",[167,1787,1788,1791,1793,1796,1799],{"class":85,"line":226},[167,1789,1790],{"class":440},"    target ",[167,1792,461],{"class":436},[167,1794,1795],{"class":440}," source.with_suffix(",[167,1797,1798],{"class":176},"\".xlsx\"",[167,1800,573],{"class":440},[167,1802,1803,1806,1808,1811,1813,1815,1818,1820,1822,1824,1826],{"class":85,"line":239},[167,1804,1805],{"class":440},"    tabs ",[167,1807,461],{"class":436},[167,1809,1810],{"class":440}," pd.read_excel(source, ",[167,1812,795],{"class":509},[167,1814,461],{"class":436},[167,1816,1817],{"class":654},"None",[167,1819,506],{"class":440},[167,1821,510],{"class":509},[167,1823,461],{"class":436},[167,1825,594],{"class":176},[167,1827,573],{"class":440},[167,1829,1830,1833,1836,1838,1840,1842,1844,1846],{"class":85,"line":551},[167,1831,1832],{"class":436},"    with",[167,1834,1835],{"class":440}," pd.ExcelWriter(target, ",[167,1837,510],{"class":509},[167,1839,461],{"class":436},[167,1841,779],{"class":176},[167,1843,782],{"class":440},[167,1845,444],{"class":436},[167,1847,787],{"class":440},[167,1849,1850,1853,1856,1858],{"class":85,"line":576},[167,1851,1852],{"class":436},"        for",[167,1854,1855],{"class":440}," name, frame ",[167,1857,962],{"class":436},[167,1859,1860],{"class":440}," tabs.items():\n",[167,1862,1863,1866,1868,1870,1873,1876,1879,1881,1883,1885],{"class":85,"line":932},[167,1864,1865],{"class":440},"            frame.to_excel(writer, ",[167,1867,795],{"class":509},[167,1869,461],{"class":436},[167,1871,1872],{"class":440},"name[:",[167,1874,1875],{"class":654},"31",[167,1877,1878],{"class":440},"], ",[167,1880,804],{"class":509},[167,1882,461],{"class":436},[167,1884,809],{"class":654},[167,1886,573],{"class":440},[167,1888,1889,1891],{"class":85,"line":953},[167,1890,1036],{"class":436},[167,1892,1893],{"class":440}," target\n",[167,1895,1896],{"class":85,"line":971},[167,1897,453],{"emptyLinePlaceholder":452},[167,1899,1900,1902,1905,1908],{"class":85,"line":980},[167,1901,1050],{"class":654},[167,1903,1904],{"class":440},"(to_xlsx(Path(",[167,1906,1907],{"class":176},"\"archive-2011.xls\"",[167,1909,1059],{"class":440},[10,1911,1912,1913,1916,1917,1920],{},"Two details make that reliable on real archives. ",[13,1914,1915],{},"sheet_name=None"," reads every tab rather than only\nthe first, which is how multi-sheet legacy files quietly lose data during a conversion. And\n",[13,1918,1919],{},"name[:31]"," respects Excel's sheet-name limit — a tab called something longer raises on write, and\nthe error names the limit rather than the tab, which makes it slower to diagnose than it should be.",[10,1922,1923,1924,1928],{},"What no conversion carries across is formatting: this reads values and writes values, so styles,\nmerged cells and formulas from the source are gone. When those matter, the file has to be edited\nrather than rebuilt, and that means openpyxl for OOXML or Excel itself for anything else.\n",[18,1925,1927],{"href":1926},"\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","\ncovers the cases where the fidelity matters.",[150,1930,1932],{"id":1931},"performance-and-scale","Performance and scale",[10,1934,1935,1936,1938],{},"Engine choice is the largest read-side lever in this ecosystem, and it changes nothing else about\nyour code. calamine parses in Rust and typically finishes a wide sheet several times faster than\nopenpyxl; on binary ",[13,1937,294],{}," it beats pyxlsb comfortably too. The one caveat is that it returns\nvalues, not cells — no number formats, no fills, no comments — so it is a data reader rather than a\nfile reader.",[158,1940,1942],{"className":427,"code":1941,"language":429,"meta":163,"style":163},"import time\nimport pandas as pd\n\nfor engine in (\"openpyxl\", \"calamine\"):\n    start = time.perf_counter()\n    frame = pd.read_excel(\"wide.xlsx\", engine=engine)\n    print(f\"{engine:>9}: {time.perf_counter() - start:5.2f}s  {frame.shape}\")\n",[13,1943,1944,1951,1961,1965,1985,1995,2016],{"__ignoreMap":163},[167,1945,1946,1948],{"class":85,"line":169},[167,1947,437],{"class":436},[167,1949,1950],{"class":440}," time\n",[167,1952,1953,1955,1957,1959],{"class":85,"line":187},[167,1954,437],{"class":436},[167,1956,441],{"class":440},[167,1958,444],{"class":436},[167,1960,447],{"class":440},[167,1962,1963],{"class":85,"line":200},[167,1964,453],{"emptyLinePlaceholder":452},[167,1966,1967,1970,1972,1974,1976,1979,1981,1983],{"class":85,"line":213},[167,1968,1969],{"class":436},"for",[167,1971,1336],{"class":440},[167,1973,962],{"class":436},[167,1975,1005],{"class":440},[167,1977,1978],{"class":176},"\"openpyxl\"",[167,1980,506],{"class":440},[167,1982,594],{"class":176},[167,1984,1021],{"class":440},[167,1986,1987,1990,1992],{"class":85,"line":226},[167,1988,1989],{"class":440},"    start ",[167,1991,461],{"class":436},[167,1993,1994],{"class":440}," time.perf_counter()\n",[167,1996,1997,2000,2002,2004,2007,2009,2011,2013],{"class":85,"line":239},[167,1998,1999],{"class":440},"    frame ",[167,2001,461],{"class":436},[167,2003,464],{"class":440},[167,2005,2006],{"class":176},"\"wide.xlsx\"",[167,2008,506],{"class":440},[167,2010,510],{"class":509},[167,2012,461],{"class":436},[167,2014,2015],{"class":440},"engine)\n",[167,2017,2018,2021,2023,2025,2027,2029,2031,2034,2036,2038,2040,2043,2046,2049,2052,2054,2057,2059,2062,2064,2066],{"class":85,"line":551},[167,2019,2020],{"class":654},"    print",[167,2022,1355],{"class":440},[167,2024,1358],{"class":436},[167,2026,875],{"class":176},[167,2028,1363],{"class":871},[167,2030,510],{"class":440},[167,2032,2033],{"class":436},":>9",[167,2035,1369],{"class":871},[167,2037,878],{"class":176},[167,2039,1363],{"class":871},[167,2041,2042],{"class":440},"time.perf_counter() ",[167,2044,2045],{"class":436},"-",[167,2047,2048],{"class":440}," start",[167,2050,2051],{"class":436},":5.2f",[167,2053,1369],{"class":871},[167,2055,2056],{"class":176},"s  ",[167,2058,1363],{"class":871},[167,2060,2061],{"class":440},"frame.shape",[167,2063,1369],{"class":871},[167,2065,875],{"class":176},[167,2067,573],{"class":440},[10,2069,2070,2071,2075],{},"When the same file is read repeatedly, the engine stops being the interesting variable at all — see\n",[18,2072,2074],{"href":2073},"\u002Fgetting-started-with-python-excel-automation\u002Fchoosing-a-python-excel-library\u002Fexcel-vs-csv-vs-parquet-for-python-data-pipelines\u002F","Excel vs CSV vs Parquet for Python Data Pipelines",".",[150,2077,2079],{"id":2078},"conclusion","Conclusion",[10,2081,2082,2083,405,2085,2087,2088,405,2090,2092,2093,2095],{},"Pin the engine explicitly rather than relying on the extension, and install exactly the parsers your\nformats need. ",[13,2084,404],{},[13,2086,408],{}," are one format with an optional macro payload; ",[13,2089,290],{},[13,2091,294],{},"\nare unrelated binary formats that need their own readers; ",[13,2094,143],{}," needs odfpy. calamine collapses\nmost of that into one fast read-only dependency, leaving openpyxl for in-place edits and xlsxwriter\nfor formatted output — a three-package set that covers nearly every job.",[150,2097,2099],{"id":2098},"frequently-asked-questions","Frequently asked questions",[10,2101,2102,2106],{},[2103,2104,2105],"strong",{},"Which engine reads the most formats?","\npython-calamine. One install covers .xlsx, .xlsm, .xls and .xlsb, which is why it has largely replaced the pile of format-specific packages. It is read-only, so you still need a writer.",[10,2108,2109,2112],{},[2103,2110,2111],{},"Why does openpyxl refuse my .xls file?","\nBecause .xls is a completely different binary format, not an older version of .xlsx. openpyxl only implements OOXML and raises InvalidFileException rather than guessing. Use calamine, or xlrd pinned to 1.2.0.",[10,2114,2115,2118],{},[2103,2116,2117],{},"Do I lose macros when I edit an .xlsm file?","\nYes, unless you pass keep_vba=True to load_workbook and save with the .xlsm extension. Without it openpyxl writes a workbook with no VBA project and Excel opens it as a plain .xlsx.",[10,2120,2121,2124],{},[2103,2122,2123],{},"Can I write .xlsb from Python?","\nNot with the mainstream libraries — pyxlsb and calamine both read it and neither writes it. Write .xlsx instead, or drive Excel itself and save as .xlsb through the application.",[10,2126,2127,2130],{},[2103,2128,2129],{},"What reads OpenDocument .ods files?","\nodfpy, through pandas with engine='odf' for reading and to_excel(engine='odf') for writing. It is slower than the .xlsx path but it is the only pure-Python route to the format.",[150,2132,2134],{"id":2133},"related","Related",[2136,2137,2138,2145,2152,2159,2164],"ul",{},[2139,2140,2141,2142,2144],"li",{},"Up one level: ",[18,2143,21],{"href":20}," — the library comparison this engine map sits inside.",[2139,2146,2147,2151],{},[18,2148,2150],{"href":2149},"\u002Fgetting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions\u002F","Handling Excel File Formats and Conversions"," — converting between the formats once you can read them.",[2139,2153,2154,2158],{},[18,2155,2157],{"href":2156},"\u002Fgetting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions\u002Fread-xls-files-in-python-with-xlrd-and-pandas\u002F","Read .xls Files in Python with xlrd and pandas"," — the legacy format in detail, including the xlrd 2.0 break.",[2139,2160,2161,2163],{},[18,2162,706],{"href":705}," — keeping the VBA project through an edit.",[2139,2165,2166,2170],{},[18,2167,2169],{"href":2168},"\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002F","Troubleshooting Common Python Excel Errors"," — the errors an engine mismatch produces, decoded.",[2172,2173,2174],"style",{},"html pre.shiki code .sMTad, html code.shiki .sMTad{--shiki-default:#6F42C1;--shiki-dark:#FFB757}html pre.shiki code .srMev, html code.shiki .srMev{--shiki-default:#032F62;--shiki-dark:#ADDCFF}html pre.shiki code .s-wDw, html code.shiki .s-wDw{--shiki-default:#6A737D;--shiki-dark:#BDC4CC}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s-kum, html code.shiki .s-kum{--shiki-default:#D73A49;--shiki-dark:#FF9492}html pre.shiki code .skGVy, html code.shiki .skGVy{--shiki-default:#24292E;--shiki-dark:#F0F3F6}html pre.shiki code .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}html pre.shiki code .sP0c6, html code.shiki .sP0c6{--shiki-default:#005CC5;--shiki-dark:#91CBFF}html pre.shiki code .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}html pre.shiki code .s_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}",{"title":163,"searchDepth":187,"depth":187,"links":2176},[2177,2178,2179,2180,2181,2182,2183,2184,2185,2186,2187,2188,2189],{"id":152,"depth":187,"text":153},{"id":252,"depth":187,"text":253},{"id":602,"depth":187,"text":603},{"id":710,"depth":187,"text":711},{"id":818,"depth":187,"text":819},{"id":1070,"depth":187,"text":1071},{"id":1191,"depth":187,"text":1192},{"id":1606,"depth":187,"text":1607},{"id":1736,"depth":187,"text":1737},{"id":1931,"depth":187,"text":1932},{"id":2078,"depth":187,"text":2079},{"id":2098,"depth":187,"text":2099},{"id":2133,"depth":187,"text":2134},"2026-09-04","read_excel dispatches to five different parsers. Map each extension to the engine that reads it, keep macros through an edit, and sniff files that arrive misnamed.","md",[2194,2196,2198,2200,2202],{"q":2105,"a":2195},"python-calamine. One install covers .xlsx, .xlsm, .xls and .xlsb, which is why it has largely replaced the pile of format-specific packages. It is read-only, so you still need a writer.",{"q":2111,"a":2197},"Because .xls is a completely different binary format, not an older version of .xlsx. openpyxl only implements OOXML and raises InvalidFileException rather than guessing. Use calamine, or xlrd pinned to 1.2.0.",{"q":2117,"a":2199},"Yes, unless you pass keep_vba=True to load_workbook and save with the .xlsm extension. Without it openpyxl writes a workbook with no VBA project and Excel opens it as a plain .xlsx.",{"q":2123,"a":2201},"Not with the mainstream libraries — pyxlsb and calamine both read it and neither writes it. Write .xlsx instead, or drive Excel itself and save as .xlsb through the application.",{"q":2129,"a":2203},"odfpy, through pandas with engine='odf' for reading and to_excel(engine='odf') for writing. It is slower than the .xlsx path but it is the only pure-Python route to the format.",{"breadcrumb":2205},[2206,2209,2212],{"name":2207,"item":2208},"Home","\u002F",{"name":2210,"item":2211},"Getting Started with Python Excel Automation","\u002Fgetting-started-with-python-excel-automation\u002F",{"name":21,"item":20},"\u002Fgetting-started-with-python-excel-automation\u002Fchoosing-a-python-excel-library\u002Fpick-an-excel-engine-for-xlsx-xlsm-xls-xlsb-and-ods",{"title":5,"description":2215},"Map every Excel extension to the right Python engine — openpyxl, calamine, pyxlsb, xlrd 1.2.0, odfpy — with the install lines, the macro trap and a format sniffer.","pick-an-excel-engine-for-xlsx-xlsm-xls-xlsb-and-ods","getting-started-with-python-excel-automation\u002Fchoosing-a-python-excel-library\u002Fpick-an-excel-engine-for-xlsx-xlsm-xls-xlsb-and-ods\u002Findex","how-to","S0fkuyr8GguNfG5ZOcHIqhlVt4i1Yvwzjb6_UVErGmY",[2221,2225],{"title":2222,"path":2223,"stem":2224,"children":-1},"pandas vs Polars for Excel Workflows","\u002Fgetting-started-with-python-excel-automation\u002Fchoosing-a-python-excel-library\u002Fpandas-vs-polars-for-excel-workflows","getting-started-with-python-excel-automation\u002Fchoosing-a-python-excel-library\u002Fpandas-vs-polars-for-excel-workflows\u002Findex",{"title":2226,"path":2227,"stem":2228,"children":-1},"When to Use xlwings Instead of openpyxl","\u002Fgetting-started-with-python-excel-automation\u002Fchoosing-a-python-excel-library\u002Fwhen-to-use-xlwings-instead-of-openpyxl","getting-started-with-python-excel-automation\u002Fchoosing-a-python-excel-library\u002Fwhen-to-use-xlwings-instead-of-openpyxl\u002Findex",1788710154673]