[{"data":1,"prerenderedAt":2361},["ShallowReactive",2],{"doc:\u002Fgetting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions\u002Fread-and-write-ods-files-with-python":3,"surround:\u002Fgetting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions\u002Fread-and-write-ods-files-with-python":2353},{"id":4,"title":5,"body":6,"dateModified":2325,"datePublished":2325,"description":2326,"extension":2327,"faq":2328,"meta":2344,"navigation":265,"path":2345,"seo":2346,"slug":2349,"stem":2350,"type":2351,"__hash__":2352},"docs\u002Fgetting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions\u002Fread-and-write-ods-files-with-python\u002Findex.md","Read and Write .ods Files with Python",{"type":7,"value":8,"toc":2313},"minimark",[9,24,174,179,224,227,231,238,295,298,327,333,529,539,546,572,576,590,693,703,880,898,902,908,1015,1029,1178,1185,1195,1273,1287,1366,1373,1377,1495,1498,1768,1782,1810,1814,1951,1955,1967,1970,2009,2012,2136,2139,2143,2163,2167,2195,2213,2222,2252,2268,2272,2309],[10,11,12,13,17,18,23],"p",{},"OpenDocument spreadsheets arrive from LibreOffice users, from Google Sheets exports, and from public-sector data portals where ODF is the mandated format. Python reads and writes them, but the support is shallower than for ",[14,15,16],"code",{},".xlsx"," in a way that matters: you get the values, and very little else. This guide covers the reading and writing calls, the formatting ceiling you will hit, and the conversion step that is usually the right answer for a reporting pipeline. It completes the format coverage in ",[19,20,22],"a",{"href":21},"\u002Fgetting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions\u002F","Handling Excel File Formats and Conversions",".",[25,26,35,36,35,40,35,44,35,51,35,58,35,63,35,67,35,75,35,81,35,90,35,94,35,99,35,102,35,105,35,109,35,112,35,114,35,116,35,118,35,121,35,125,35,130,35,134,35,136,35,138,35,141,35,145,35,150,35,154,35,156,35,158,35,161,35,165,35,168,35,170,35,172],"svg",{"viewBox":27,"role":28,"ariaLabel":29,"ariaLabelledBy":30,"xmlns":33,"style":34},"0 0 800 240","img","Comparison of what survives in ods versus xlsx from Python: values and sheet structure work in both, number formats are partial in ods, and styles, charts and conditional formatting are unavailable in ods.",[31,32],"ods-cap-t","ods-cap-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:800px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[37,38,39],"title",{"id":31},"What Python can write to .ods compared with .xlsx",[41,42,43],"desc",{"id":32},"A five-row capability matrix. Cell values and multiple sheets are fully supported in both formats. Number formats are partial in ods and full in xlsx. Cell styling, charts and conditional formatting are unavailable through the ods writer but fully available in xlsx through openpyxl and xlsxwriter.",[45,46],"rect",{"x":47,"y":47,"width":48,"height":49,"fill":50},"0","800","240","#ffffff",[52,53,57],"text",{"x":54,"y":55,"style":56},"300","28","font-size:12px;font-weight:700;fill:var(--muted,#5b6780);text-anchor:middle","capability",[52,59,62],{"x":60,"y":55,"style":61},"560","font-size:12px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle",".ods (odfpy)",[52,64,16],{"x":65,"y":55,"style":66},"712","font-size:12px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle",[45,68],{"x":69,"y":70,"width":71,"height":72,"rx":73,"fill":74},"16","38","768","36","8","#f0f2f5",[52,76,80],{"x":77,"y":78,"style":79},"34","61","font-size:12px;font-weight:600;fill:var(--text,#172033)","cell values and sheet structure",[45,82],{"x":83,"y":84,"width":85,"height":86,"rx":87,"fill":88,"stroke":89},"500","44","120","24","6","#d9f4f1","var(--teal,#0f9488)",[52,91,93],{"x":60,"y":78,"style":92},"font-size:11px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","full",[45,95],{"x":96,"y":84,"width":85,"height":86,"rx":87,"fill":97,"stroke":98},"652","#ebebfd","var(--brand,#5b5cf0)",[52,100,93],{"x":65,"y":78,"style":101},"font-size:11px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle",[45,103],{"x":69,"y":104,"width":71,"height":72,"rx":73,"fill":50},"78",[52,106,108],{"x":77,"y":107,"style":79},"101","multiple sheets in one file",[45,110],{"x":83,"y":111,"width":85,"height":86,"rx":87,"fill":88,"stroke":89},"84",[52,113,93],{"x":60,"y":107,"style":92},[45,115],{"x":96,"y":111,"width":85,"height":86,"rx":87,"fill":97,"stroke":98},[52,117,93],{"x":65,"y":107,"style":101},[45,119],{"x":69,"y":120,"width":71,"height":72,"rx":73,"fill":74},"118",[52,122,124],{"x":77,"y":123,"style":79},"141","number and date formats",[45,126],{"x":83,"y":127,"width":85,"height":86,"rx":87,"fill":128,"stroke":129},"124","#fdefd8","var(--gold,#b4740a)",[52,131,133],{"x":60,"y":123,"style":132},"font-size:11px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","partial",[45,135],{"x":96,"y":127,"width":85,"height":86,"rx":87,"fill":97,"stroke":98},[52,137,93],{"x":65,"y":123,"style":101},[45,139],{"x":69,"y":140,"width":71,"height":72,"rx":73,"fill":50},"158",[52,142,144],{"x":77,"y":143,"style":79},"181","fills, fonts, borders, column widths",[45,146],{"x":83,"y":147,"width":85,"height":86,"rx":87,"fill":148,"stroke":149},"164","#fee8f2","var(--accent,#f43f8f)",[52,151,153],{"x":60,"y":143,"style":152},"font-size:11px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","none",[45,155],{"x":96,"y":147,"width":85,"height":86,"rx":87,"fill":97,"stroke":98},[52,157,93],{"x":65,"y":143,"style":101},[45,159],{"x":69,"y":160,"width":71,"height":72,"rx":73,"fill":74},"198",[52,162,164],{"x":77,"y":163,"style":79},"221","charts and conditional formatting",[45,166],{"x":83,"y":167,"width":85,"height":86,"rx":87,"fill":148,"stroke":149},"204",[52,169,153],{"x":60,"y":163,"style":152},[45,171],{"x":96,"y":167,"width":85,"height":86,"rx":87,"fill":97,"stroke":98},[52,173,93],{"x":65,"y":163,"style":101},[175,176,178],"h2",{"id":177},"prerequisites","Prerequisites",[180,181,186],"pre",{"className":182,"code":183,"language":184,"meta":185,"style":185},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install pandas odfpy               # read and write .ods\npip install python-calamine            # optional: faster reads, no writes\n","bash","",[14,187,188,211],{"__ignoreMap":185},[189,190,193,197,201,204,207],"span",{"class":191,"line":192},"line",1,[189,194,196],{"class":195},"sMTad","pip",[189,198,200],{"class":199},"srMev"," install",[189,202,203],{"class":199}," pandas",[189,205,206],{"class":199}," odfpy",[189,208,210],{"class":209},"s-wDw","               # read and write .ods\n",[189,212,214,216,218,221],{"class":191,"line":213},2,[189,215,196],{"class":195},[189,217,200],{"class":199},[189,219,220],{"class":199}," python-calamine",[189,222,223],{"class":209},"            # optional: faster reads, no writes\n",[10,225,226],{},"To produce a test file, save any spreadsheet from LibreOffice Calc as \"ODF Spreadsheet (.ods)\", or from Google Sheets via File → Download → OpenDocument.",[175,228,230],{"id":229},"step-1-read-an-ods-into-a-dataframe","Step 1 — Read an .ods into a DataFrame",[10,232,233,234,237],{},"pandas routes on the extension, so the plain call already works once ",[14,235,236],{},"odfpy"," is installed:",[180,239,243],{"className":240,"code":241,"language":242,"meta":185,"style":185},"language-python shiki shiki-themes github-light github-dark-high-contrast","import pandas as pd\n\ndf = pd.read_excel(\"budget.ods\")\nprint(df.head())\n","python",[14,244,245,261,267,285],{"__ignoreMap":185},[189,246,247,251,255,258],{"class":191,"line":192},[189,248,250],{"class":249},"s-kum","import",[189,252,254],{"class":253},"skGVy"," pandas ",[189,256,257],{"class":249},"as",[189,259,260],{"class":253}," pd\n",[189,262,263],{"class":191,"line":213},[189,264,266],{"emptyLinePlaceholder":265},true,"\n",[189,268,270,273,276,279,282],{"class":191,"line":269},3,[189,271,272],{"class":253},"df ",[189,274,275],{"class":249},"=",[189,277,278],{"class":253}," pd.read_excel(",[189,280,281],{"class":199},"\"budget.ods\"",[189,283,284],{"class":253},")\n",[189,286,288,292],{"class":191,"line":287},4,[189,289,291],{"class":290},"sP0c6","print",[189,293,294],{"class":253},"(df.head())\n",[10,296,297],{},"Being explicit costs nothing and documents the dependency for whoever reads the script next:",[180,299,301],{"className":240,"code":300,"language":242,"meta":185,"style":185},"df = pd.read_excel(\"budget.ods\", engine=\"odf\")\n",[14,302,303],{"__ignoreMap":185},[189,304,305,307,309,311,313,316,320,322,325],{"class":191,"line":192},[189,306,272],{"class":253},[189,308,275],{"class":249},[189,310,278],{"class":253},[189,312,281],{"class":199},[189,314,315],{"class":253},", ",[189,317,319],{"class":318},"sa561","engine",[189,321,275],{"class":249},[189,323,324],{"class":199},"\"odf\"",[189,326,284],{"class":253},[10,328,329,330,332],{},"Every option you know from ",[14,331,16],{}," reads applies unchanged — sheet selection, header rows, column subsets:",[180,334,336],{"className":240,"code":335,"language":242,"meta":185,"style":185},"# One named sheet, skipping a two-row title block above the header.\ndf = pd.read_excel(\"budget.ods\", sheet_name=\"Q3\", skiprows=2, engine=\"odf\")\n\n# Every sheet at once, as {name: DataFrame}.\nsheets = pd.read_excel(\"budget.ods\", sheet_name=None, engine=\"odf\")\nfor name, frame in sheets.items():\n    print(f\"{name:\u003C16} {frame.shape}\")\n\n# Only the columns you need.\ndf = pd.read_excel(\"budget.ods\", usecols=[\"region\", \"amount\"], engine=\"odf\")\n",[14,337,338,343,383,387,392,423,438,478,483,489],{"__ignoreMap":185},[189,339,340],{"class":191,"line":192},[189,341,342],{"class":209},"# One named sheet, skipping a two-row title block above the header.\n",[189,344,345,347,349,351,353,355,358,360,363,365,368,370,373,375,377,379,381],{"class":191,"line":213},[189,346,272],{"class":253},[189,348,275],{"class":249},[189,350,278],{"class":253},[189,352,281],{"class":199},[189,354,315],{"class":253},[189,356,357],{"class":318},"sheet_name",[189,359,275],{"class":249},[189,361,362],{"class":199},"\"Q3\"",[189,364,315],{"class":253},[189,366,367],{"class":318},"skiprows",[189,369,275],{"class":249},[189,371,372],{"class":290},"2",[189,374,315],{"class":253},[189,376,319],{"class":318},[189,378,275],{"class":249},[189,380,324],{"class":199},[189,382,284],{"class":253},[189,384,385],{"class":191,"line":269},[189,386,266],{"emptyLinePlaceholder":265},[189,388,389],{"class":191,"line":287},[189,390,391],{"class":209},"# Every sheet at once, as {name: DataFrame}.\n",[189,393,395,398,400,402,404,406,408,410,413,415,417,419,421],{"class":191,"line":394},5,[189,396,397],{"class":253},"sheets ",[189,399,275],{"class":249},[189,401,278],{"class":253},[189,403,281],{"class":199},[189,405,315],{"class":253},[189,407,357],{"class":318},[189,409,275],{"class":249},[189,411,412],{"class":290},"None",[189,414,315],{"class":253},[189,416,319],{"class":318},[189,418,275],{"class":249},[189,420,324],{"class":199},[189,422,284],{"class":253},[189,424,426,429,432,435],{"class":191,"line":425},6,[189,427,428],{"class":249},"for",[189,430,431],{"class":253}," name, frame ",[189,433,434],{"class":249},"in",[189,436,437],{"class":253}," sheets.items():\n",[189,439,441,444,447,450,453,457,460,463,466,469,472,474,476],{"class":191,"line":440},7,[189,442,443],{"class":290},"    print",[189,445,446],{"class":253},"(",[189,448,449],{"class":249},"f",[189,451,452],{"class":199},"\"",[189,454,456],{"class":455},"sSjpA","{",[189,458,459],{"class":253},"name",[189,461,462],{"class":249},":\u003C16",[189,464,465],{"class":455},"}",[189,467,468],{"class":455}," {",[189,470,471],{"class":253},"frame.shape",[189,473,465],{"class":455},[189,475,452],{"class":199},[189,477,284],{"class":253},[189,479,481],{"class":191,"line":480},8,[189,482,266],{"emptyLinePlaceholder":265},[189,484,486],{"class":191,"line":485},9,[189,487,488],{"class":209},"# Only the columns you need.\n",[189,490,492,494,496,498,500,502,505,507,510,513,515,518,521,523,525,527],{"class":191,"line":491},10,[189,493,272],{"class":253},[189,495,275],{"class":249},[189,497,278],{"class":253},[189,499,281],{"class":199},[189,501,315],{"class":253},[189,503,504],{"class":318},"usecols",[189,506,275],{"class":249},[189,508,509],{"class":253},"[",[189,511,512],{"class":199},"\"region\"",[189,514,315],{"class":253},[189,516,517],{"class":199},"\"amount\"",[189,519,520],{"class":253},"], ",[189,522,319],{"class":318},[189,524,275],{"class":249},[189,526,324],{"class":199},[189,528,284],{"class":253},[10,530,531,532,534,535,23],{},"The header and skip-row mechanics are identical to the ",[14,533,16],{}," case covered in ",[19,536,538],{"href":537},"\u002Fgetting-started-with-python-excel-automation\u002Freading-excel-files-with-pandas\u002Fskip-rows-and-set-header-when-reading-excel-with-pandas\u002F","skipping rows and setting the header when reading Excel",[10,540,541,542,545],{},"For a large ",[14,543,544],{},".ods"," where you only want the numbers, the calamine engine is markedly faster because it is a Rust parser rather than a Python XML walk:",[180,547,549],{"className":240,"code":548,"language":242,"meta":185,"style":185},"df = pd.read_excel(\"budget.ods\", engine=\"calamine\")\n",[14,550,551],{"__ignoreMap":185},[189,552,553,555,557,559,561,563,565,567,570],{"class":191,"line":192},[189,554,272],{"class":253},[189,556,275],{"class":249},[189,558,278],{"class":253},[189,560,281],{"class":199},[189,562,315],{"class":253},[189,564,319],{"class":318},[189,566,275],{"class":249},[189,568,569],{"class":199},"\"calamine\"",[189,571,284],{"class":253},[175,573,575],{"id":574},"step-2-write-an-ods","Step 2 — Write an .ods",[10,577,578,579,582,583,585,586,589],{},"Writing is the same ",[14,580,581],{},"to_excel"," call with an ",[14,584,544],{}," destination. pandas selects the ",[14,587,588],{},"odf"," engine from the suffix:",[180,591,593],{"className":240,"code":592,"language":242,"meta":185,"style":185},"import pandas as pd\n\ndf = pd.DataFrame({\n    \"region\": [\"North\", \"South\", \"West\"],\n    \"revenue\": [159.92, 247.50, 137.44],\n})\n\ndf.to_excel(\"summary.ods\", index=False)\n",[14,594,595,605,609,618,642,664,669,673],{"__ignoreMap":185},[189,596,597,599,601,603],{"class":191,"line":192},[189,598,250],{"class":249},[189,600,254],{"class":253},[189,602,257],{"class":249},[189,604,260],{"class":253},[189,606,607],{"class":191,"line":213},[189,608,266],{"emptyLinePlaceholder":265},[189,610,611,613,615],{"class":191,"line":269},[189,612,272],{"class":253},[189,614,275],{"class":249},[189,616,617],{"class":253}," pd.DataFrame({\n",[189,619,620,623,626,629,631,634,636,639],{"class":191,"line":287},[189,621,622],{"class":199},"    \"region\"",[189,624,625],{"class":253},": [",[189,627,628],{"class":199},"\"North\"",[189,630,315],{"class":253},[189,632,633],{"class":199},"\"South\"",[189,635,315],{"class":253},[189,637,638],{"class":199},"\"West\"",[189,640,641],{"class":253},"],\n",[189,643,644,647,649,652,654,657,659,662],{"class":191,"line":394},[189,645,646],{"class":199},"    \"revenue\"",[189,648,625],{"class":253},[189,650,651],{"class":290},"159.92",[189,653,315],{"class":253},[189,655,656],{"class":290},"247.50",[189,658,315],{"class":253},[189,660,661],{"class":290},"137.44",[189,663,641],{"class":253},[189,665,666],{"class":191,"line":425},[189,667,668],{"class":253},"})\n",[189,670,671],{"class":191,"line":440},[189,672,266],{"emptyLinePlaceholder":265},[189,674,675,678,681,683,686,688,691],{"class":191,"line":480},[189,676,677],{"class":253},"df.to_excel(",[189,679,680],{"class":199},"\"summary.ods\"",[189,682,315],{"class":253},[189,684,685],{"class":318},"index",[189,687,275],{"class":249},[189,689,690],{"class":290},"False",[189,692,284],{"class":253},[10,694,695,696,699,700,702],{},"Multiple sheets work through ",[14,697,698],{},"ExcelWriter",", exactly as with ",[14,701,16],{},":",[180,704,706],{"className":240,"code":705,"language":242,"meta":185,"style":185},"import pandas as pd\n\nsummary = pd.DataFrame({\"region\": [\"North\", \"South\"], \"revenue\": [159.92, 247.50]})\ndetail = pd.DataFrame({\"order\": [1, 2, 3], \"amount\": [40.0, 61.5, 58.42]})\n\nwith pd.ExcelWriter(\"report.ods\", engine=\"odf\") as writer:\n    summary.to_excel(writer, sheet_name=\"Summary\", index=False)\n    detail.to_excel(writer, sheet_name=\"Detail\", index=False)\n",[14,707,708,718,722,758,805,809,836,858],{"__ignoreMap":185},[189,709,710,712,714,716],{"class":191,"line":192},[189,711,250],{"class":249},[189,713,254],{"class":253},[189,715,257],{"class":249},[189,717,260],{"class":253},[189,719,720],{"class":191,"line":213},[189,721,266],{"emptyLinePlaceholder":265},[189,723,724,727,729,732,734,736,738,740,742,744,747,749,751,753,755],{"class":191,"line":269},[189,725,726],{"class":253},"summary ",[189,728,275],{"class":249},[189,730,731],{"class":253}," pd.DataFrame({",[189,733,512],{"class":199},[189,735,625],{"class":253},[189,737,628],{"class":199},[189,739,315],{"class":253},[189,741,633],{"class":199},[189,743,520],{"class":253},[189,745,746],{"class":199},"\"revenue\"",[189,748,625],{"class":253},[189,750,651],{"class":290},[189,752,315],{"class":253},[189,754,656],{"class":290},[189,756,757],{"class":253},"]})\n",[189,759,760,763,765,767,770,772,775,777,779,781,784,786,788,790,793,795,798,800,803],{"class":191,"line":287},[189,761,762],{"class":253},"detail ",[189,764,275],{"class":249},[189,766,731],{"class":253},[189,768,769],{"class":199},"\"order\"",[189,771,625],{"class":253},[189,773,774],{"class":290},"1",[189,776,315],{"class":253},[189,778,372],{"class":290},[189,780,315],{"class":253},[189,782,783],{"class":290},"3",[189,785,520],{"class":253},[189,787,517],{"class":199},[189,789,625],{"class":253},[189,791,792],{"class":290},"40.0",[189,794,315],{"class":253},[189,796,797],{"class":290},"61.5",[189,799,315],{"class":253},[189,801,802],{"class":290},"58.42",[189,804,757],{"class":253},[189,806,807],{"class":191,"line":394},[189,808,266],{"emptyLinePlaceholder":265},[189,810,811,814,817,820,822,824,826,828,831,833],{"class":191,"line":425},[189,812,813],{"class":249},"with",[189,815,816],{"class":253}," pd.ExcelWriter(",[189,818,819],{"class":199},"\"report.ods\"",[189,821,315],{"class":253},[189,823,319],{"class":318},[189,825,275],{"class":249},[189,827,324],{"class":199},[189,829,830],{"class":253},") ",[189,832,257],{"class":249},[189,834,835],{"class":253}," writer:\n",[189,837,838,841,843,845,848,850,852,854,856],{"class":191,"line":440},[189,839,840],{"class":253},"    summary.to_excel(writer, ",[189,842,357],{"class":318},[189,844,275],{"class":249},[189,846,847],{"class":199},"\"Summary\"",[189,849,315],{"class":253},[189,851,685],{"class":318},[189,853,275],{"class":249},[189,855,690],{"class":290},[189,857,284],{"class":253},[189,859,860,863,865,867,870,872,874,876,878],{"class":191,"line":480},[189,861,862],{"class":253},"    detail.to_excel(writer, ",[189,864,357],{"class":318},[189,866,275],{"class":249},[189,868,869],{"class":199},"\"Detail\"",[189,871,315],{"class":253},[189,873,685],{"class":318},[189,875,275],{"class":249},[189,877,690],{"class":290},[189,879,284],{"class":253},[10,881,882,883,885,886,889,890,894,895,897],{},"What you cannot do is style it. There is no ",[14,884,544],{}," equivalent of the ",[14,887,888],{},"openpyxl"," styling in ",[19,891,893],{"href":892},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fstyling-excel-cells-with-openpyxl\u002F","styling Excel cells with openpyxl",", no conditional formatting, no charts, no frozen panes, no column widths. The ",[14,896,588],{}," writer emits data.",[175,899,901],{"id":900},"step-3-handle-the-ods-quirks","Step 3 — Handle the .ods quirks",[10,903,904,905,907],{},"Three behaviours differ from ",[14,906,16],{}," in ways that bite.",[25,909,35,916,35,919,35,922,35,926,35,931,35,937,35,942,35,946,35,951,35,955,35,959,35,963,35,966,35,971,35,974,35,977,35,980,35,983,35,986,35,989,35,992,35,997,35,1000,35,1003,35,1006,35,1009,35,1012],{"viewBox":910,"role":28,"ariaLabel":911,"ariaLabelledBy":912,"xmlns":33,"style":915},"0 0 780 226","Three ods reading quirks and their fixes: repeated empty cells expand into wide sparse frames, formulas return their cached result or the formula text, and dates may arrive as strings needing explicit parsing.",[913,914],"odsq-t","odsq-d","width:100%;max-width:780px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[37,917,918],{"id":913},"Three OpenDocument quirks and how to handle them",[41,920,921],{"id":914},"Three paired panels. Repeated empty cells in the XML expand into hundreds of unnamed trailing columns, fixed by dropping columns whose names start with Unnamed. Formulas return the cached result when one exists and the formula text otherwise, fixed by recalculating in LibreOffice. Dates sometimes arrive as strings, fixed by passing parse dates or calling to datetime afterwards.",[45,923],{"x":47,"y":47,"width":924,"height":925,"fill":50},"780","226",[45,927],{"x":928,"y":86,"width":49,"height":929,"rx":928,"fill":128,"stroke":129,"style":930},"14","188","stroke-width:2px",[52,932,936],{"x":933,"y":934,"style":935},"134","52","font-size:12.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","phantom columns",[52,938,941],{"x":933,"y":939,"style":940},"80","font-size:11px;fill:var(--text,#172033);text-anchor:middle","ODF encodes runs of blanks",[52,943,945],{"x":933,"y":944,"style":940},"100","as a repeat count",[52,947,950],{"x":933,"y":948,"style":949},"126","font-size:11px;fill:var(--muted,#5b6780);text-anchor:middle","they expand into",[52,952,954],{"x":933,"y":953,"style":949},"146","Unnamed: 7, 8, 9 …",[45,956],{"x":77,"y":957,"width":958,"height":77,"rx":73,"fill":50,"stroke":129},"162","200",[52,960,962],{"x":933,"y":961,"style":132},"184","drop Unnamed columns",[45,964],{"x":965,"y":86,"width":49,"height":929,"rx":928,"fill":97,"stroke":98,"style":930},"270",[52,967,970],{"x":968,"y":934,"style":969},"390","font-size:12.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","formula cells",[52,972,973],{"x":968,"y":939,"style":940},"ODF stores the formula",[52,975,976],{"x":968,"y":944,"style":940},"and a cached result",[52,978,979],{"x":968,"y":948,"style":949},"no cache means you get",[52,981,982],{"x":968,"y":953,"style":949},"the formula text back",[45,984],{"x":985,"y":957,"width":958,"height":77,"rx":73,"fill":50,"stroke":98},"290",[52,987,988],{"x":968,"y":961,"style":101},"recalculate once, then read",[45,990],{"x":991,"y":86,"width":49,"height":929,"rx":928,"fill":88,"stroke":89,"style":930},"526",[52,993,996],{"x":994,"y":934,"style":995},"646","font-size:12.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","date columns",[52,998,999],{"x":994,"y":939,"style":940},"typed dates parse cleanly",[52,1001,1002],{"x":994,"y":944,"style":940},"text dates do not",[52,1004,1005],{"x":994,"y":948,"style":949},"object dtype where you",[52,1007,1008],{"x":994,"y":953,"style":949},"expected datetime64",[45,1010],{"x":1011,"y":957,"width":958,"height":77,"rx":73,"fill":50,"stroke":89},"546",[52,1013,1014],{"x":994,"y":961,"style":92},"pd.to_datetime after read",[10,1016,1017,1021,1022,1025,1026,702],{},[1018,1019,1020],"strong",{},"Phantom trailing columns."," OpenDocument compresses runs of empty cells into a single element with a repeat count. Some writers emit generous counts, so a six-column sheet reads back with two hundred ",[14,1023,1024],{},"Unnamed: n"," columns full of ",[14,1027,1028],{},"NaN",[180,1030,1032],{"className":240,"code":1031,"language":242,"meta":185,"style":185},"import pandas as pd\n\ndf = pd.read_excel(\"export.ods\", engine=\"odf\")\n\n# Drop the artefacts: unnamed AND entirely empty.\njunk = [c for c in df.columns\n        if str(c).startswith(\"Unnamed:\") and df[c].isna().all()]\ndf = df.drop(columns=junk)\nprint(f\"dropped {len(junk)} phantom columns; {df.shape[1]} remain\")\n",[14,1033,1034,1044,1048,1069,1073,1078,1098,1120,1137],{"__ignoreMap":185},[189,1035,1036,1038,1040,1042],{"class":191,"line":192},[189,1037,250],{"class":249},[189,1039,254],{"class":253},[189,1041,257],{"class":249},[189,1043,260],{"class":253},[189,1045,1046],{"class":191,"line":213},[189,1047,266],{"emptyLinePlaceholder":265},[189,1049,1050,1052,1054,1056,1059,1061,1063,1065,1067],{"class":191,"line":269},[189,1051,272],{"class":253},[189,1053,275],{"class":249},[189,1055,278],{"class":253},[189,1057,1058],{"class":199},"\"export.ods\"",[189,1060,315],{"class":253},[189,1062,319],{"class":318},[189,1064,275],{"class":249},[189,1066,324],{"class":199},[189,1068,284],{"class":253},[189,1070,1071],{"class":191,"line":287},[189,1072,266],{"emptyLinePlaceholder":265},[189,1074,1075],{"class":191,"line":394},[189,1076,1077],{"class":209},"# Drop the artefacts: unnamed AND entirely empty.\n",[189,1079,1080,1083,1085,1088,1090,1093,1095],{"class":191,"line":425},[189,1081,1082],{"class":253},"junk ",[189,1084,275],{"class":249},[189,1086,1087],{"class":253}," [c ",[189,1089,428],{"class":249},[189,1091,1092],{"class":253}," c ",[189,1094,434],{"class":249},[189,1096,1097],{"class":253}," df.columns\n",[189,1099,1100,1103,1106,1109,1112,1114,1117],{"class":191,"line":440},[189,1101,1102],{"class":249},"        if",[189,1104,1105],{"class":290}," str",[189,1107,1108],{"class":253},"(c).startswith(",[189,1110,1111],{"class":199},"\"Unnamed:\"",[189,1113,830],{"class":253},[189,1115,1116],{"class":249},"and",[189,1118,1119],{"class":253}," df[c].isna().all()]\n",[189,1121,1122,1124,1126,1129,1132,1134],{"class":191,"line":480},[189,1123,272],{"class":253},[189,1125,275],{"class":249},[189,1127,1128],{"class":253}," df.drop(",[189,1130,1131],{"class":318},"columns",[189,1133,275],{"class":249},[189,1135,1136],{"class":253},"junk)\n",[189,1138,1139,1141,1143,1145,1148,1150,1153,1156,1158,1161,1163,1166,1168,1171,1173,1176],{"class":191,"line":485},[189,1140,291],{"class":290},[189,1142,446],{"class":253},[189,1144,449],{"class":249},[189,1146,1147],{"class":199},"\"dropped ",[189,1149,456],{"class":455},[189,1151,1152],{"class":290},"len",[189,1154,1155],{"class":253},"(junk)",[189,1157,465],{"class":455},[189,1159,1160],{"class":199}," phantom columns; ",[189,1162,456],{"class":455},[189,1164,1165],{"class":253},"df.shape[",[189,1167,774],{"class":290},[189,1169,1170],{"class":253},"]",[189,1172,465],{"class":455},[189,1174,1175],{"class":199}," remain\"",[189,1177,284],{"class":253},[10,1179,1180,1181,23],{},"The same trick removes all-blank rows; the fuller treatment is in ",[19,1182,1184],{"href":1183},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcleaning-excel-data-with-pandas\u002Fremove-blank-rows-from-excel-with-pandas\u002F","removing blank rows from Excel with pandas",[10,1186,1187,1190,1191,1194],{},[1018,1188,1189],{},"Formula cells."," ODF stores a formula alongside its last computed result. pandas reads the cached result when one exists. Files written by scripts or by portals that never opened the sheet may have no cache, and then the formula string comes through as text. Detect it rather than letting a ",[14,1192,1193],{},"\"=SUM(B2:B9)\""," string reach a numeric aggregation:",[180,1196,1198],{"className":240,"code":1197,"language":242,"meta":185,"style":185},"suspect = df[\"total\"].astype(str).str.startswith(\"=\")\nif suspect.any():\n    raise ValueError(\n        f\"{suspect.sum()} formula cells have no cached value — \"\n        \"open the file in LibreOffice, recalculate, and re-save.\"\n    )\n",[14,1199,1200,1227,1235,1246,1263,1268],{"__ignoreMap":185},[189,1201,1202,1205,1207,1210,1213,1216,1219,1222,1225],{"class":191,"line":192},[189,1203,1204],{"class":253},"suspect ",[189,1206,275],{"class":249},[189,1208,1209],{"class":253}," df[",[189,1211,1212],{"class":199},"\"total\"",[189,1214,1215],{"class":253},"].astype(",[189,1217,1218],{"class":290},"str",[189,1220,1221],{"class":253},").str.startswith(",[189,1223,1224],{"class":199},"\"=\"",[189,1226,284],{"class":253},[189,1228,1229,1232],{"class":191,"line":213},[189,1230,1231],{"class":249},"if",[189,1233,1234],{"class":253}," suspect.any():\n",[189,1236,1237,1240,1243],{"class":191,"line":269},[189,1238,1239],{"class":249},"    raise",[189,1241,1242],{"class":290}," ValueError",[189,1244,1245],{"class":253},"(\n",[189,1247,1248,1251,1253,1255,1258,1260],{"class":191,"line":287},[189,1249,1250],{"class":249},"        f",[189,1252,452],{"class":199},[189,1254,456],{"class":455},[189,1256,1257],{"class":253},"suspect.sum()",[189,1259,465],{"class":455},[189,1261,1262],{"class":199}," formula cells have no cached value — \"\n",[189,1264,1265],{"class":191,"line":394},[189,1266,1267],{"class":199},"        \"open the file in LibreOffice, recalculate, and re-save.\"\n",[189,1269,1270],{"class":191,"line":425},[189,1271,1272],{"class":253},"    )\n",[10,1274,1275,1278,1279,1282,1283,1286],{},[1018,1276,1277],{},"Dates."," A properly typed date cell parses to ",[14,1280,1281],{},"datetime64",". A date somebody typed as text does not, and you get ",[14,1284,1285],{},"object"," dtype:",[180,1288,1290],{"className":240,"code":1289,"language":242,"meta":185,"style":185},"df[\"invoiced\"] = pd.to_datetime(df[\"invoiced\"], errors=\"coerce\")\nbad = df[\"invoiced\"].isna().sum()\nif bad:\n    print(f\"warning: {bad} unparseable dates coerced to NaT\")\n",[14,1291,1292,1322,1336,1343],{"__ignoreMap":185},[189,1293,1294,1297,1300,1303,1305,1308,1310,1312,1315,1317,1320],{"class":191,"line":192},[189,1295,1296],{"class":253},"df[",[189,1298,1299],{"class":199},"\"invoiced\"",[189,1301,1302],{"class":253},"] ",[189,1304,275],{"class":249},[189,1306,1307],{"class":253}," pd.to_datetime(df[",[189,1309,1299],{"class":199},[189,1311,520],{"class":253},[189,1313,1314],{"class":318},"errors",[189,1316,275],{"class":249},[189,1318,1319],{"class":199},"\"coerce\"",[189,1321,284],{"class":253},[189,1323,1324,1327,1329,1331,1333],{"class":191,"line":213},[189,1325,1326],{"class":253},"bad ",[189,1328,275],{"class":249},[189,1330,1209],{"class":253},[189,1332,1299],{"class":199},[189,1334,1335],{"class":253},"].isna().sum()\n",[189,1337,1338,1340],{"class":191,"line":269},[189,1339,1231],{"class":249},[189,1341,1342],{"class":253}," bad:\n",[189,1344,1345,1347,1349,1351,1354,1356,1359,1361,1364],{"class":191,"line":287},[189,1346,443],{"class":290},[189,1348,446],{"class":253},[189,1350,449],{"class":249},[189,1352,1353],{"class":199},"\"warning: ",[189,1355,456],{"class":455},[189,1357,1358],{"class":253},"bad",[189,1360,465],{"class":455},[189,1362,1363],{"class":199}," unparseable dates coerced to NaT\"",[189,1365,284],{"class":253},[10,1367,1368,1369,23],{},"The full set of date-handling techniques lives in ",[19,1370,1372],{"href":1371},"\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-dates-and-times-in-excel-data\u002F","working with dates and times in Excel data",[175,1374,1376],{"id":1375},"step-4-convert-ods-to-xlsx-at-the-boundary","Step 4 — Convert .ods to .xlsx at the boundary",[25,1378,35,1384,35,1387,35,1390,35,1393,35,1398,35,1403,35,1406,35,1409,35,1414,35,1418,35,1423,35,1437,35,1442,35,1449,35,1454,35,1457,35,1461,35,1466,35,1469,35,1474,35,1479,35,1483,35,1487,35,1490],{"viewBox":1379,"role":28,"ariaLabel":1380,"ariaLabelledBy":1381,"xmlns":33,"style":34},"0 0 800 216","Ingest boundary pattern: mixed ods, xls and csv inputs are normalised to xlsx at the edge of the pipeline so every downstream stage works with one format.",[1382,1383],"ingest-t","ingest-d",[37,1385,1386],{"id":1382},"Normalising every input format at the ingest boundary",[41,1388,1389],{"id":1383},"Three input formats — ods from LibreOffice, legacy xls, and csv — all pass through a single normalise step that writes xlsx. Everything downstream of that boundary, including cleaning, formatting, charting and delivery, sees only xlsx. The alternative, where each downstream stage handles three formats, multiplies the special cases.",[45,1391],{"x":47,"y":47,"width":48,"height":1392,"fill":50},"216",[52,1394,1397],{"x":939,"y":1395,"style":1396},"26","font-size:11.5px;font-weight:700;fill:var(--muted,#5b6780);text-anchor:middle","whatever arrives",[45,1399],{"x":69,"y":1400,"width":1401,"height":1400,"rx":1402,"fill":88,"stroke":89,"style":930},"40","128","9",[52,1404,544],{"x":939,"y":1405,"style":61},"65",[45,1407],{"x":69,"y":1408,"width":1401,"height":1400,"rx":1402,"fill":128,"stroke":129,"style":930},"90",[52,1410,1413],{"x":939,"y":1411,"style":1412},"115","font-size:12px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle",".xls",[45,1415],{"x":69,"y":1416,"width":1401,"height":1400,"rx":1402,"fill":74,"stroke":1417,"style":930},"140","var(--line,#cdd5e6)",[52,1419,1422],{"x":939,"y":1420,"style":1421},"165","font-size:12px;font-weight:700;fill:var(--text,#172033);text-anchor:middle",".csv",[1424,1425,1426,1427,1426,1431,1426,1434,35],"g",{"stroke":1417,"style":930,"fill":153},"\n    ",[1428,1429],"path",{"d":1430},"M144 60 H 184 V 110",[1428,1432],{"d":1433},"M144 110 H 192",[1428,1435],{"d":1436},"M144 160 H 184 V 110",[1438,1439],"polygon",{"points":1440,"fill":1441},"200,110 188,104 188,116","#5b5cf0",[45,1443],{"x":1444,"y":1445,"width":1446,"height":1447,"rx":1448,"fill":97,"stroke":98,"style":930},"208","66","192","88","13",[52,1450,1453],{"x":1451,"y":1452,"style":969},"304","96","normalise",[52,1455,1456],{"x":1451,"y":120,"style":940},"read with the right engine",[52,1458,1460],{"x":1451,"y":1459,"style":940},"136","write .xlsx",[191,1462],{"x1":1463,"y1":1464,"x2":1465,"y2":1464,"stroke":98,"style":930},"400","110","440",[1438,1467],{"points":1468,"fill":1441},"448,110 436,104 436,116",[45,1470],{"x":1471,"y":1400,"width":1472,"height":1416,"rx":928,"fill":1473,"stroke":98,"style":930},"456","328","#f0f4ff",[52,1475,1478],{"x":1476,"y":1477,"style":969},"620","68","everything downstream: .xlsx only",[52,1480,1482],{"x":1476,"y":1481,"style":940},"98","clean · validate · aggregate",[52,1484,1486],{"x":1476,"y":1485,"style":940},"122","style · chart · export to PDF",[52,1488,1489],{"x":1476,"y":953,"style":940},"schedule · email · archive",[52,1491,1494],{"x":1476,"y":1492,"style":1493},"168","font-size:10.5px;fill:var(--muted,#5b6780);text-anchor:middle","one format, no per-stage special cases",[10,1496,1497],{},"For anything beyond a one-off read, convert. Your pipeline then has one format to reason about, and everything downstream gains styling, charts and the streaming writers:",[180,1499,1501],{"className":240,"code":1500,"language":242,"meta":185,"style":185},"from pathlib import Path\nimport pandas as pd\n\ndef ods_to_xlsx(src, out_dir=\"converted\"):\n    \"\"\"Convert an OpenDocument spreadsheet to .xlsx, all sheets.\"\"\"\n    src = Path(src)\n    out = Path(out_dir)\n    out.mkdir(parents=True, exist_ok=True)\n    dest = out \u002F (src.stem + \".xlsx\")\n\n    sheets = pd.read_excel(src, sheet_name=None, engine=\"odf\")\n    with pd.ExcelWriter(dest, engine=\"xlsxwriter\") as writer:\n        for name, frame in sheets.items():\n            frame.to_excel(writer, sheet_name=str(name)[:31], index=False)\n    return dest\n\nfor path in Path(\"inbox\").glob(\"*.ods\"):\n    print(path.name, \"->\", ods_to_xlsx(path).name)\n",[14,1502,1503,1516,1526,1530,1550,1555,1565,1575,1599,1623,1627,1654,1676,1688,1716,1725,1730,1754],{"__ignoreMap":185},[189,1504,1505,1508,1511,1513],{"class":191,"line":192},[189,1506,1507],{"class":249},"from",[189,1509,1510],{"class":253}," pathlib ",[189,1512,250],{"class":249},[189,1514,1515],{"class":253}," Path\n",[189,1517,1518,1520,1522,1524],{"class":191,"line":213},[189,1519,250],{"class":249},[189,1521,254],{"class":253},[189,1523,257],{"class":249},[189,1525,260],{"class":253},[189,1527,1528],{"class":191,"line":269},[189,1529,266],{"emptyLinePlaceholder":265},[189,1531,1532,1535,1539,1542,1544,1547],{"class":191,"line":287},[189,1533,1534],{"class":249},"def",[189,1536,1538],{"class":1537},"s_Opv"," ods_to_xlsx",[189,1540,1541],{"class":253},"(src, out_dir",[189,1543,275],{"class":249},[189,1545,1546],{"class":199},"\"converted\"",[189,1548,1549],{"class":253},"):\n",[189,1551,1552],{"class":191,"line":394},[189,1553,1554],{"class":199},"    \"\"\"Convert an OpenDocument spreadsheet to .xlsx, all sheets.\"\"\"\n",[189,1556,1557,1560,1562],{"class":191,"line":425},[189,1558,1559],{"class":253},"    src ",[189,1561,275],{"class":249},[189,1563,1564],{"class":253}," Path(src)\n",[189,1566,1567,1570,1572],{"class":191,"line":440},[189,1568,1569],{"class":253},"    out ",[189,1571,275],{"class":249},[189,1573,1574],{"class":253}," Path(out_dir)\n",[189,1576,1577,1580,1583,1585,1588,1590,1593,1595,1597],{"class":191,"line":480},[189,1578,1579],{"class":253},"    out.mkdir(",[189,1581,1582],{"class":318},"parents",[189,1584,275],{"class":249},[189,1586,1587],{"class":290},"True",[189,1589,315],{"class":253},[189,1591,1592],{"class":318},"exist_ok",[189,1594,275],{"class":249},[189,1596,1587],{"class":290},[189,1598,284],{"class":253},[189,1600,1601,1604,1606,1609,1612,1615,1618,1621],{"class":191,"line":485},[189,1602,1603],{"class":253},"    dest ",[189,1605,275],{"class":249},[189,1607,1608],{"class":253}," out ",[189,1610,1611],{"class":249},"\u002F",[189,1613,1614],{"class":253}," (src.stem ",[189,1616,1617],{"class":249},"+",[189,1619,1620],{"class":199}," \".xlsx\"",[189,1622,284],{"class":253},[189,1624,1625],{"class":191,"line":491},[189,1626,266],{"emptyLinePlaceholder":265},[189,1628,1630,1633,1635,1638,1640,1642,1644,1646,1648,1650,1652],{"class":191,"line":1629},11,[189,1631,1632],{"class":253},"    sheets ",[189,1634,275],{"class":249},[189,1636,1637],{"class":253}," pd.read_excel(src, ",[189,1639,357],{"class":318},[189,1641,275],{"class":249},[189,1643,412],{"class":290},[189,1645,315],{"class":253},[189,1647,319],{"class":318},[189,1649,275],{"class":249},[189,1651,324],{"class":199},[189,1653,284],{"class":253},[189,1655,1657,1660,1663,1665,1667,1670,1672,1674],{"class":191,"line":1656},12,[189,1658,1659],{"class":249},"    with",[189,1661,1662],{"class":253}," pd.ExcelWriter(dest, ",[189,1664,319],{"class":318},[189,1666,275],{"class":249},[189,1668,1669],{"class":199},"\"xlsxwriter\"",[189,1671,830],{"class":253},[189,1673,257],{"class":249},[189,1675,835],{"class":253},[189,1677,1679,1682,1684,1686],{"class":191,"line":1678},13,[189,1680,1681],{"class":249},"        for",[189,1683,431],{"class":253},[189,1685,434],{"class":249},[189,1687,437],{"class":253},[189,1689,1691,1694,1696,1698,1700,1703,1706,1708,1710,1712,1714],{"class":191,"line":1690},14,[189,1692,1693],{"class":253},"            frame.to_excel(writer, ",[189,1695,357],{"class":318},[189,1697,275],{"class":249},[189,1699,1218],{"class":290},[189,1701,1702],{"class":253},"(name)[:",[189,1704,1705],{"class":290},"31",[189,1707,520],{"class":253},[189,1709,685],{"class":318},[189,1711,275],{"class":249},[189,1713,690],{"class":290},[189,1715,284],{"class":253},[189,1717,1719,1722],{"class":191,"line":1718},15,[189,1720,1721],{"class":249},"    return",[189,1723,1724],{"class":253}," dest\n",[189,1726,1728],{"class":191,"line":1727},16,[189,1729,266],{"emptyLinePlaceholder":265},[189,1731,1733,1735,1738,1740,1743,1746,1749,1752],{"class":191,"line":1732},17,[189,1734,428],{"class":249},[189,1736,1737],{"class":253}," path ",[189,1739,434],{"class":249},[189,1741,1742],{"class":253}," Path(",[189,1744,1745],{"class":199},"\"inbox\"",[189,1747,1748],{"class":253},").glob(",[189,1750,1751],{"class":199},"\"*.ods\"",[189,1753,1549],{"class":253},[189,1755,1757,1759,1762,1765],{"class":191,"line":1756},18,[189,1758,443],{"class":290},[189,1760,1761],{"class":253},"(path.name, ",[189,1763,1764],{"class":199},"\"->\"",[189,1766,1767],{"class":253},", ods_to_xlsx(path).name)\n",[10,1769,1770,1771,1773,1774,1778,1779,1781],{},"That is a values-only conversion, with the same caveats as the ",[14,1772,1413],{}," converter in ",[19,1775,1777],{"href":1776},"\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",". If the ",[14,1780,544],{}," is a formatted document rather than a data dump, use headless LibreOffice instead — it is the native application for the format and preserves everything:",[180,1783,1785],{"className":182,"code":1784,"language":184,"meta":185,"style":185},"soffice --headless --convert-to xlsx --outdir converted budget.ods\n",[14,1786,1787],{"__ignoreMap":185},[189,1788,1789,1792,1795,1798,1801,1804,1807],{"class":191,"line":192},[189,1790,1791],{"class":195},"soffice",[189,1793,1794],{"class":290}," --headless",[189,1796,1797],{"class":290}," --convert-to",[189,1799,1800],{"class":199}," xlsx",[189,1802,1803],{"class":290}," --outdir",[189,1805,1806],{"class":199}," converted",[189,1808,1809],{"class":199}," budget.ods\n",[175,1811,1813],{"id":1812},"common-pitfalls-and-fixes","Common pitfalls and fixes",[1815,1816,1817,1833],"table",{},[1818,1819,1820],"thead",{},[1821,1822,1823,1827,1830],"tr",{},[1824,1825,1826],"th",{},"Symptom",[1824,1828,1829],{},"Cause",[1824,1831,1832],{},"Fix",[1834,1835,1836,1852,1870,1881,1900,1917,1938],"tbody",{},[1821,1837,1838,1844,1847],{},[1839,1840,1841],"td",{},[14,1842,1843],{},"ImportError: Missing optional dependency 'odf'",[1839,1845,1846],{},"odfpy not installed",[1839,1848,1849],{},[14,1850,1851],{},"pip install odfpy",[1821,1853,1854,1861,1864],{},[1839,1855,1856,1857,1860],{},"Hundreds of ",[14,1858,1859],{},"Unnamed:"," columns",[1839,1862,1863],{},"ODF repeat-count encoding of blank cells",[1839,1865,1866,1867,1869],{},"Drop unnamed, all-",[14,1868,1028],{}," columns after the read.",[1821,1871,1872,1875,1878],{},[1839,1873,1874],{},"Formulas come back as text",[1839,1876,1877],{},"No cached result in the file",[1839,1879,1880],{},"Open and recalculate in LibreOffice, then re-save.",[1821,1882,1883,1888,1894],{},[1839,1884,1885,1887],{},[14,1886,581],{}," ignores styling arguments",[1839,1889,1890,1891,1893],{},"The ",[14,1892,588],{}," writer supports values only",[1839,1895,1896,1897,1899],{},"Write ",[14,1898,16],{}," if the output must be formatted.",[1821,1901,1902,1908,1911],{},[1839,1903,1904,1905,1907],{},"Dates are ",[14,1906,1285],{}," dtype",[1839,1909,1910],{},"Text dates, not typed date cells",[1839,1912,1913,1916],{},[14,1914,1915],{},"pd.to_datetime(col, errors=\"coerce\")"," after reading.",[1821,1918,1919,1922,1927],{},[1839,1920,1921],{},"File opens as a zip, not a spreadsheet",[1839,1923,1924,1925],{},"Extension renamed from ",[14,1926,16],{},[1839,1928,1929,1930,1933,1934,1937],{},"Sniff the archive contents: ",[14,1931,1932],{},"content.xml"," means ODF, an ",[14,1935,1936],{},"xl\u002F"," prefix means OOXML.",[1821,1939,1940,1943,1946],{},[1839,1941,1942],{},"Sheet name rejected on conversion",[1839,1944,1945],{},"Excel's 31-character and character limits",[1839,1947,1948,1949,23],{},"Truncate and sanitise before ",[14,1950,581],{},[175,1952,1954],{"id":1953},"performance-and-scale-notes","Performance and scale notes",[10,1956,1957,1959,1960,1962,1963,23],{},[14,1958,236],{}," parses the whole ",[14,1961,1932],{}," into a DOM before pandas sees a single row, so memory scales with file size and then some — expect several times the on-disk size in peak RSS. There is no streaming or read-only mode equivalent to ",[19,1964,1966],{"href":1965},"\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-large-excel-files-in-python\u002Fspeed-up-openpyxl-with-read-only-mode\u002F","openpyxl's read-only mode",[10,1968,1969],{},"Two practical consequences. Read one sheet, not all of them, when you only need one:",[180,1971,1973],{"className":240,"code":1972,"language":242,"meta":185,"style":185},"# Parses and materialises only \"Detail\".\ndf = pd.read_excel(\"large.ods\", sheet_name=\"Detail\", engine=\"odf\")\n",[14,1974,1975,1980],{"__ignoreMap":185},[189,1976,1977],{"class":191,"line":192},[189,1978,1979],{"class":209},"# Parses and materialises only \"Detail\".\n",[189,1981,1982,1984,1986,1988,1991,1993,1995,1997,1999,2001,2003,2005,2007],{"class":191,"line":213},[189,1983,272],{"class":253},[189,1985,275],{"class":249},[189,1987,278],{"class":253},[189,1989,1990],{"class":199},"\"large.ods\"",[189,1992,315],{"class":253},[189,1994,357],{"class":318},[189,1996,275],{"class":249},[189,1998,869],{"class":199},[189,2000,315],{"class":253},[189,2002,319],{"class":318},[189,2004,275],{"class":249},[189,2006,324],{"class":199},[189,2008,284],{"class":253},[10,2010,2011],{},"And for files above a few tens of megabytes, use calamine or convert first:",[180,2013,2015],{"className":240,"code":2014,"language":242,"meta":185,"style":185},"import time\nimport pandas as pd\n\nfor engine in (\"odf\", \"calamine\"):\n    start = time.perf_counter()\n    frame = pd.read_excel(\"large.ods\", engine=engine)\n    print(f\"{engine:\u003C10} {time.perf_counter() - start:6.2f}s  {frame.shape}\")\n",[14,2016,2017,2024,2034,2038,2058,2068,2088],{"__ignoreMap":185},[189,2018,2019,2021],{"class":191,"line":192},[189,2020,250],{"class":249},[189,2022,2023],{"class":253}," time\n",[189,2025,2026,2028,2030,2032],{"class":191,"line":213},[189,2027,250],{"class":249},[189,2029,254],{"class":253},[189,2031,257],{"class":249},[189,2033,260],{"class":253},[189,2035,2036],{"class":191,"line":269},[189,2037,266],{"emptyLinePlaceholder":265},[189,2039,2040,2042,2045,2047,2050,2052,2054,2056],{"class":191,"line":287},[189,2041,428],{"class":249},[189,2043,2044],{"class":253}," engine ",[189,2046,434],{"class":249},[189,2048,2049],{"class":253}," (",[189,2051,324],{"class":199},[189,2053,315],{"class":253},[189,2055,569],{"class":199},[189,2057,1549],{"class":253},[189,2059,2060,2063,2065],{"class":191,"line":394},[189,2061,2062],{"class":253},"    start ",[189,2064,275],{"class":249},[189,2066,2067],{"class":253}," time.perf_counter()\n",[189,2069,2070,2073,2075,2077,2079,2081,2083,2085],{"class":191,"line":425},[189,2071,2072],{"class":253},"    frame ",[189,2074,275],{"class":249},[189,2076,278],{"class":253},[189,2078,1990],{"class":199},[189,2080,315],{"class":253},[189,2082,319],{"class":318},[189,2084,275],{"class":249},[189,2086,2087],{"class":253},"engine)\n",[189,2089,2090,2092,2094,2096,2098,2100,2102,2105,2107,2109,2112,2115,2118,2121,2123,2126,2128,2130,2132,2134],{"class":191,"line":440},[189,2091,443],{"class":290},[189,2093,446],{"class":253},[189,2095,449],{"class":249},[189,2097,452],{"class":199},[189,2099,456],{"class":455},[189,2101,319],{"class":253},[189,2103,2104],{"class":249},":\u003C10",[189,2106,465],{"class":455},[189,2108,468],{"class":455},[189,2110,2111],{"class":253},"time.perf_counter() ",[189,2113,2114],{"class":249},"-",[189,2116,2117],{"class":253}," start",[189,2119,2120],{"class":249},":6.2f",[189,2122,465],{"class":455},[189,2124,2125],{"class":199},"s  ",[189,2127,456],{"class":455},[189,2129,471],{"class":253},[189,2131,465],{"class":455},[189,2133,452],{"class":199},[189,2135,284],{"class":253},[10,2137,2138],{},"On a typical multi-megabyte export, calamine finishes in a fraction of the odfpy time and holds far less memory, because it streams the archive instead of building a document tree. The trade is that it returns values only — no cell-level introspection — which for an ingest step is exactly what you want anyway.",[175,2140,2142],{"id":2141},"conclusion","Conclusion",[10,2144,2145,2147,2148,2152,2153,2156,2157,2159,2160,2162],{},[14,2146,544],{}," is a first-class ",[2149,2150,2151],"em",{},"input"," format in Python and a second-class output one. Reading is a plain ",[14,2154,2155],{},"pd.read_excel"," with ",[14,2158,236],{}," installed, and every familiar option works; writing produces correct data with no formatting at all. Watch for phantom columns from the repeat-count encoding, formula cells with no cached result, and text dates. Then convert to ",[14,2161,16],{}," at ingest so that the styling, charting and streaming tools the rest of this site covers are available to you.",[175,2164,2166],{"id":2165},"frequently-asked-questions","Frequently asked questions",[10,2168,2169,2175,2177,2178,2180,2181,2183,2184,2187,2188,2191,2192,2194],{},[1018,2170,2171,2172,2174],{},"Which package do I need to read ",[14,2173,544],{}," in pandas?",[14,2176,236],{},". Install it with ",[14,2179,1851],{},", then pandas routes ",[14,2182,544],{}," files to ",[14,2185,2186],{},"engine=\"odf\""," automatically. ",[14,2189,2190],{},"python-calamine"," also reads ",[14,2193,544],{}," and is faster, but it cannot write.",[10,2196,2197,2206,2207,2209,2210,2212],{},[1018,2198,2199,2200,2202,2203,2205],{},"Can I style ",[14,2201,544],{}," output the way I style ",[14,2204,16],{},"?","\nNot through pandas. The ",[14,2208,588],{}," writer emits values and basic number formats only — no fills, borders, conditional formatting or charts. If the output needs to look like a report, write ",[14,2211,16],{}," instead.",[10,2214,2215,2221],{},[1018,2216,2217,2218,2220],{},"Why do my ",[14,2219,544],{}," formulas come back as text?","\nOpenDocument stores both the formula and its cached result. pandas reads the cached value where one exists; where the file was written by a tool that saved no cache, you get the formula string. Recalculate the file in LibreOffice once to populate the cache.",[10,2223,2224,2232,2233,2235,2236,315,2238,2241,2242,2245,2246,2248,2249,2251],{},[1018,2225,2226,2227,2229,2230,2205],{},"Is ",[14,2228,544],{}," a zip file like ",[14,2231,16],{},"\nYes. Both are zip archives of XML. An ",[14,2234,544],{}," contains ",[14,2237,1932],{},[14,2239,2240],{},"styles.xml"," and a ",[14,2243,2244],{},"mimetype"," entry, whereas an ",[14,2247,16],{}," has an ",[14,2250,1936],{}," directory. That difference is how you tell them apart by content rather than extension.",[10,2253,2254,2262,2264,2265,2267],{},[1018,2255,2256,2257,2259,2260,2205],{},"Should I standardise on ",[14,2258,544],{}," or ",[14,2261,16],{},[14,2263,16],{},", in almost every case. It has far richer Python tooling for formatting and charts, and LibreOffice reads it perfectly. Treat ",[14,2266,544],{}," as an input format you convert at ingest.",[175,2269,2271],{"id":2270},"related","Related",[2273,2274,2275,2282,2288,2295,2301],"ul",{},[2276,2277,2278,2279,2281],"li",{},"Up to the parent: ",[19,2280,22],{"href":21}," — the full format map.",[2276,2283,2284,2287],{},[19,2285,2286],{"href":1776},"Convert .xls to .xlsx with Python"," — the same conversion pattern for the legacy binary format.",[2276,2289,2290,2294],{},[19,2291,2293],{"href":2292},"\u002Fgetting-started-with-python-excel-automation\u002Freading-excel-files-with-pandas\u002F","Reading Excel Files with pandas"," — the reading options that apply to every engine.",[2276,2296,2297,2300],{},[19,2298,2299],{"href":1183},"Remove Blank Rows from Excel with pandas"," — cleaning up the sparse frames ODF exports produce.",[2276,2302,2303,2306,2307,23],{},[19,2304,2305],{"href":1965},"Speed up openpyxl with Read-Only Mode"," — the streaming option you gain by converting to ",[14,2308,16],{},[2310,2311,2312],"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 .sP0c6, html code.shiki .sP0c6{--shiki-default:#005CC5;--shiki-dark:#91CBFF}html pre.shiki code .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}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":185,"searchDepth":213,"depth":213,"links":2314},[2315,2316,2317,2318,2319,2320,2321,2322,2323,2324],{"id":177,"depth":213,"text":178},{"id":229,"depth":213,"text":230},{"id":574,"depth":213,"text":575},{"id":900,"depth":213,"text":901},{"id":1375,"depth":213,"text":1376},{"id":1812,"depth":213,"text":1813},{"id":1953,"depth":213,"text":1954},{"id":2141,"depth":213,"text":2142},{"id":2165,"depth":213,"text":2166},{"id":2270,"depth":213,"text":2271},"2026-08-15","Handle OpenDocument spreadsheets from Python with pandas and odfpy — reading sheets, writing output, the formatting limits, and converting .ods to .xlsx when you need more.","md",[2329,2332,2335,2338,2341],{"q":2330,"a":2331},"Which package do I need to read .ods in pandas?","odfpy. Install it with pip install odfpy, then pandas routes .ods files to engine=\"odf\" automatically. python-calamine also reads .ods and is faster, but it cannot write.",{"q":2333,"a":2334},"Can I style .ods output the way I style .xlsx?","Not through pandas. The odf writer emits values and basic number formats only — no fills, borders, conditional formatting or charts. If the output needs to look like a report, write .xlsx instead.",{"q":2336,"a":2337},"Why do my .ods formulas come back as text?","OpenDocument stores both the formula and its cached result. pandas reads the cached value where one exists; where the file was written by a tool that saved no cache, you get the formula string. Recalculate the file in LibreOffice once to populate the cache.",{"q":2339,"a":2340},"Is .ods a zip file like .xlsx?","Yes. Both are zip archives of XML. An .ods contains content.xml, styles.xml and a mimetype entry, whereas an .xlsx has an xl\u002F directory. That difference is how you tell them apart by content rather than extension.",{"q":2342,"a":2343},"Should I standardise on .ods or .xlsx?",".xlsx, in almost every case. It has far richer Python tooling for formatting and charts, and LibreOffice reads it perfectly. Treat .ods as an input format you convert at ingest.",{},"\u002Fgetting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions\u002Fread-and-write-ods-files-with-python",{"title":2347,"description":2348},"Read and Write .ods Files in Python (pandas + odfpy)","Work with LibreOffice and Google Sheets .ods exports in Python: the odf engine, multi-sheet reads, writing OpenDocument output, and when to convert to .xlsx instead.","read-and-write-ods-files-with-python","getting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions\u002Fread-and-write-ods-files-with-python\u002Findex","how-to","gGwjr1x9-t1IWbL6og-Q79T1h3puFyai9AjtX8JUGaY",[2354,2357],{"title":2286,"path":2355,"stem":2356,"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",{"title":2358,"path":2359,"stem":2360,"children":-1},"Read .xls Files in Python with xlrd and pandas","\u002Fgetting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions\u002Fread-xls-files-in-python-with-xlrd-and-pandas","getting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions\u002Fread-xls-files-in-python-with-xlrd-and-pandas\u002Findex",1786800027102]