[{"data":1,"prerenderedAt":2579},["ShallowReactive",2],{"doc:\u002Fgetting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions\u002Fconvert-xls-to-xlsx-with-python":3,"surround:\u002Fgetting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions\u002Fconvert-xls-to-xlsx-with-python":2571},{"id":4,"title":5,"body":6,"dateModified":2547,"datePublished":2547,"description":2548,"extension":2549,"faq":2550,"meta":2562,"navigation":292,"path":2563,"seo":2564,"slug":2567,"stem":2568,"type":2569,"__hash__":2570},"docs\u002Fgetting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions\u002Fconvert-xls-to-xlsx-with-python\u002Findex.md","Convert .xls to .xlsx with Python",{"type":7,"value":8,"toc":2535},"minimark",[9,28,163,168,203,211,228,243,247,250,504,507,511,525,766,773,937,940,944,958,1079,1090,1134,1137,1231,1244,1248,1251,1683,1693,1697,1837,1841,1968,1975,1978,2239,2255,2258,2420,2424,2436,2440,2446,2457,2475,2484,2490,2494,2531],[10,11,12,13,17,18,21,22,27],"p",{},"Legacy ",[14,15,16],"code",{},".xls"," files are a slow leak in a reporting pipeline: every downstream step needs a special reader, nothing can write back to them, and they cap out at 65,536 rows. The durable fix is to convert them to ",[14,19,20],{},".xlsx"," once at the ingest boundary. This guide gives you two converters — a fast values-only one built on pandas, and a full-fidelity one that shells out to headless LibreOffice — plus the sheet-name and dtype traps that break naive scripts on real files. It builds on the reading techniques in ",[23,24,26],"a",{"href":25},"\u002Fgetting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions\u002Fread-xls-files-in-python-with-xlrd-and-pandas\u002F","reading .xls files with xlrd and pandas",".",[29,30,39,40,39,44,39,48,39,55,39,65,39,72,39,77,39,90,39,98,39,104,39,109,39,113,39,118,39,123,39,127,39,131,39,136,39,141,39,146,39,153,39,159],"svg",{"viewBox":31,"role":32,"ariaLabel":33,"ariaLabelledBy":34,"xmlns":37,"style":38},"0 0 800 246","img","Two conversion routes from xls to xlsx: a pandas route that carries values only and is fast, and a LibreOffice route that preserves formatting, formulas and charts but pays a process startup cost.",[35,36],"conv-route-t","conv-route-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  ",[41,42,43],"title",{"id":35},"Choosing between the pandas and LibreOffice conversion routes",[45,46,47],"desc",{"id":36},"A legacy xls file can take two paths to xlsx. The pandas route reads cell values with xlrd and writes them with xlsxwriter: fast, in-process, values only. The LibreOffice route runs the soffice binary headless: slower to start but it preserves styles, merged cells, formulas and charts. Both end at the same xlsx output.",[49,50],"rect",{"x":51,"y":51,"width":52,"height":53,"fill":54},"0","800","246","#ffffff",[49,56],{"x":57,"y":58,"width":59,"height":60,"rx":61,"fill":62,"stroke":63,"style":64},"14","94","128","60","12","#fdefd8","var(--gold,#b4740a)","stroke-width:2px",[66,67,71],"text",{"x":68,"y":69,"style":70},"78","120","font-size:13px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","legacy.xls",[66,73,76],{"x":68,"y":74,"style":75},"139","font-size:10.5px;fill:var(--muted,#5b6780);text-anchor:middle","BIFF8",[78,79,82,83,82,87,39],"g",{"stroke":80,"style":64,"fill":81},"var(--line,#cdd5e6)","none","\n    ",[84,85],"path",{"d":86},"M142 112 H 176 V 58 H 206",[84,88],{"d":89},"M142 136 H 176 V 190 H 206",[49,91],{"x":92,"y":93,"width":94,"height":95,"rx":61,"fill":96,"stroke":97,"style":64},"214","24","290","70","#ebebfd","var(--brand,#5b5cf0)",[66,99,103],{"x":100,"y":101,"style":102},"359","48","font-size:12.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","pandas → xlsxwriter",[66,105,108],{"x":100,"y":106,"style":107},"68","font-size:11px;fill:var(--text,#172033);text-anchor:middle","values only · in-process · fast",[66,110,112],{"x":100,"y":111,"style":75},"85","styles, charts and formulas dropped",[49,114],{"x":92,"y":115,"width":94,"height":95,"rx":61,"fill":116,"stroke":117,"style":64},"156","#d9f4f1","var(--teal,#0f9488)",[66,119,122],{"x":100,"y":120,"style":121},"180","font-size:12.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","soffice --convert-to xlsx",[66,124,126],{"x":100,"y":125,"style":107},"200","full fidelity · separate process",[66,128,130],{"x":100,"y":129,"style":75},"217","styles, merges, formulas, charts kept",[78,132,82,133,39],{"stroke":97,"style":64,"fill":81},[84,134],{"d":135},"M504 59 H 560 V 122 H 606",[78,137,82,138,39],{"stroke":117,"style":64,"fill":81},[84,139],{"d":140},"M504 191 H 560 V 122 H 606",[142,143],"polygon",{"points":144,"fill":145},"614,122 602,116 602,128","#5b5cf0",[49,147],{"x":148,"y":149,"width":150,"height":151,"rx":61,"fill":152,"stroke":97,"style":64},"622","92","162","64","#f0f4ff",[66,154,158],{"x":155,"y":156,"style":157},"703","118","font-size:13px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","legacy.xlsx",[66,160,162],{"x":155,"y":161,"style":75},"138","1,048,576 row ceiling",[164,165,167],"h2",{"id":166},"prerequisites","Prerequisites",[169,170,175],"pre",{"className":171,"code":172,"language":173,"meta":174,"style":174},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install pandas xlrd xlsxwriter     # the values-only converter\n","bash","",[14,176,177],{"__ignoreMap":174},[178,179,182,186,190,193,196,199],"span",{"class":180,"line":181},"line",1,[178,183,185],{"class":184},"sMTad","pip",[178,187,189],{"class":188},"srMev"," install",[178,191,192],{"class":188}," pandas",[178,194,195],{"class":188}," xlrd",[178,197,198],{"class":188}," xlsxwriter",[178,200,202],{"class":201},"s-wDw","     # the values-only converter\n",[10,204,205,206,210],{},"For the fidelity-preserving route you also need LibreOffice installed as a program (not a pip package), exactly as in ",[23,207,209],{"href":208},"\u002Fautomating-reporting-workflows\u002Fexporting-excel-reports-to-pdf\u002Fconvert-excel-file-to-pdf-with-python\u002F","converting Excel to PDF",":",[169,212,214],{"className":171,"code":213,"language":173,"meta":174,"style":174},"soffice --version      # Debian\u002FUbuntu: sudo apt install libreoffice-calc\n",[14,215,216],{"__ignoreMap":174},[178,217,218,221,225],{"class":180,"line":181},[178,219,220],{"class":184},"soffice",[178,222,224],{"class":223},"sP0c6"," --version",[178,226,227],{"class":201},"      # Debian\u002FUbuntu: sudo apt install libreoffice-calc\n",[10,229,230,231,233,234,238,239,242],{},"Decide which you need before writing code. If the ",[14,232,16],{}," is a ",[235,236,237],"strong",{},"data export"," that your pipeline reads and reshapes, the pandas route is right and simpler. If it is a ",[235,240,241],{},"formatted report"," somebody will open and look at, use LibreOffice.",[164,244,246],{"id":245},"step-1-convert-one-file-values-only","Step 1 — Convert one file, values only",[10,248,249],{},"The core is three lines: read every sheet, open a writer, write each sheet back. The rest is the guard rails.",[169,251,255],{"className":252,"code":253,"language":254,"meta":174,"style":174},"language-python shiki shiki-themes github-light github-dark-high-contrast","from pathlib import Path\nimport pandas as pd\n\ndef xls_to_xlsx(src, dest=None):\n    \"\"\"Convert one .xls to .xlsx, preserving every sheet's values.\"\"\"\n    src = Path(src)\n    dest = Path(dest) if dest else src.with_suffix(\".xlsx\")\n\n    # sheet_name=None -> an ordered dict of {sheet name: DataFrame}\n    sheets = pd.read_excel(src, sheet_name=None, engine=\"xlrd\")\n\n    with pd.ExcelWriter(dest, engine=\"xlsxwriter\") as writer:\n        for name, frame in sheets.items():\n            frame.to_excel(writer, sheet_name=name, index=False)\n    return dest\n\nprint(xls_to_xlsx(\"legacy.xls\"))     # legacy.xlsx\n","python",[14,256,257,273,287,294,316,322,333,362,367,373,405,410,434,449,472,481,486],{"__ignoreMap":174},[178,258,259,263,267,270],{"class":180,"line":181},[178,260,262],{"class":261},"s-kum","from",[178,264,266],{"class":265},"skGVy"," pathlib ",[178,268,269],{"class":261},"import",[178,271,272],{"class":265}," Path\n",[178,274,276,278,281,284],{"class":180,"line":275},2,[178,277,269],{"class":261},[178,279,280],{"class":265}," pandas ",[178,282,283],{"class":261},"as",[178,285,286],{"class":265}," pd\n",[178,288,290],{"class":180,"line":289},3,[178,291,293],{"emptyLinePlaceholder":292},true,"\n",[178,295,297,300,304,307,310,313],{"class":180,"line":296},4,[178,298,299],{"class":261},"def",[178,301,303],{"class":302},"s_Opv"," xls_to_xlsx",[178,305,306],{"class":265},"(src, dest",[178,308,309],{"class":261},"=",[178,311,312],{"class":223},"None",[178,314,315],{"class":265},"):\n",[178,317,319],{"class":180,"line":318},5,[178,320,321],{"class":188},"    \"\"\"Convert one .xls to .xlsx, preserving every sheet's values.\"\"\"\n",[178,323,325,328,330],{"class":180,"line":324},6,[178,326,327],{"class":265},"    src ",[178,329,309],{"class":261},[178,331,332],{"class":265}," Path(src)\n",[178,334,336,339,341,344,347,350,353,356,359],{"class":180,"line":335},7,[178,337,338],{"class":265},"    dest ",[178,340,309],{"class":261},[178,342,343],{"class":265}," Path(dest) ",[178,345,346],{"class":261},"if",[178,348,349],{"class":265}," dest ",[178,351,352],{"class":261},"else",[178,354,355],{"class":265}," src.with_suffix(",[178,357,358],{"class":188},"\".xlsx\"",[178,360,361],{"class":265},")\n",[178,363,365],{"class":180,"line":364},8,[178,366,293],{"emptyLinePlaceholder":292},[178,368,370],{"class":180,"line":369},9,[178,371,372],{"class":201},"    # sheet_name=None -> an ordered dict of {sheet name: DataFrame}\n",[178,374,376,379,381,384,388,390,392,395,398,400,403],{"class":180,"line":375},10,[178,377,378],{"class":265},"    sheets ",[178,380,309],{"class":261},[178,382,383],{"class":265}," pd.read_excel(src, ",[178,385,387],{"class":386},"sa561","sheet_name",[178,389,309],{"class":261},[178,391,312],{"class":223},[178,393,394],{"class":265},", ",[178,396,397],{"class":386},"engine",[178,399,309],{"class":261},[178,401,402],{"class":188},"\"xlrd\"",[178,404,361],{"class":265},[178,406,408],{"class":180,"line":407},11,[178,409,293],{"emptyLinePlaceholder":292},[178,411,413,416,419,421,423,426,429,431],{"class":180,"line":412},12,[178,414,415],{"class":261},"    with",[178,417,418],{"class":265}," pd.ExcelWriter(dest, ",[178,420,397],{"class":386},[178,422,309],{"class":261},[178,424,425],{"class":188},"\"xlsxwriter\"",[178,427,428],{"class":265},") ",[178,430,283],{"class":261},[178,432,433],{"class":265}," writer:\n",[178,435,437,440,443,446],{"class":180,"line":436},13,[178,438,439],{"class":261},"        for",[178,441,442],{"class":265}," name, frame ",[178,444,445],{"class":261},"in",[178,447,448],{"class":265}," sheets.items():\n",[178,450,452,455,457,459,462,465,467,470],{"class":180,"line":451},14,[178,453,454],{"class":265},"            frame.to_excel(writer, ",[178,456,387],{"class":386},[178,458,309],{"class":261},[178,460,461],{"class":265},"name, ",[178,463,464],{"class":386},"index",[178,466,309],{"class":261},[178,468,469],{"class":223},"False",[178,471,361],{"class":265},[178,473,475,478],{"class":180,"line":474},15,[178,476,477],{"class":261},"    return",[178,479,480],{"class":265}," dest\n",[178,482,484],{"class":180,"line":483},16,[178,485,293],{"emptyLinePlaceholder":292},[178,487,489,492,495,498,501],{"class":180,"line":488},17,[178,490,491],{"class":223},"print",[178,493,494],{"class":265},"(xls_to_xlsx(",[178,496,497],{"class":188},"\"legacy.xls\"",[178,499,500],{"class":265},"))     ",[178,502,503],{"class":201},"# legacy.xlsx\n",[10,505,506],{},"Run that on a real corpus of legacy files and it will fail, usually on the second or third one. Two things break it.",[164,508,510],{"id":509},"step-2-sanitise-sheet-names","Step 2 — Sanitise sheet names",[10,512,513,514,517,518,520,521,524],{},"Excel forbids ",[14,515,516],{},": \\ \u002F ? * [ ]"," in sheet names and caps them at 31 characters, but ",[14,519,16],{}," files created by other tools routinely violate both. ",[14,522,523],{},"xlsxwriter"," raises rather than silently truncating, so the converter dies mid-batch.",[169,526,528],{"className":252,"code":527,"language":254,"meta":174,"style":174},"import re\n\nINVALID = re.compile(r\"[:\\\\\u002F?*\\[\\]]\")\n\ndef safe_sheet_name(name, used):\n    \"\"\"Return a name Excel will accept, unique within the workbook.\"\"\"\n    clean = INVALID.sub(\"_\", str(name)).strip() or \"Sheet\"\n    clean = clean[:31]\n\n    # Uniqueness after truncation: \"Regional summary north\" and\n    # \"Regional summary south\" can collide once cut to 31 chars.\n    base, n = clean, 2\n    while clean.lower() in used:\n        suffix = f\"_{n}\"\n        clean = base[: 31 - len(suffix)] + suffix\n        n += 1\n\n    used.add(clean.lower())\n    return clean\n",[14,529,530,537,541,581,585,595,600,630,645,649,654,659,672,685,710,737,748,752,758],{"__ignoreMap":174},[178,531,532,534],{"class":180,"line":181},[178,533,269],{"class":261},[178,535,536],{"class":265}," re\n",[178,538,539],{"class":180,"line":275},[178,540,293],{"emptyLinePlaceholder":292},[178,542,543,546,549,552,555,558,561,564,568,571,574,577,579],{"class":180,"line":289},[178,544,545],{"class":223},"INVALID",[178,547,548],{"class":261}," =",[178,550,551],{"class":265}," re.compile(",[178,553,554],{"class":261},"r",[178,556,557],{"class":188},"\"",[178,559,560],{"class":223},"[",[178,562,210],{"class":563},"sSjpA",[178,565,567],{"class":566},"s_b0D","\\\\",[178,569,570],{"class":563},"\u002F?*",[178,572,573],{"class":566},"\\[\\]",[178,575,576],{"class":223},"]",[178,578,557],{"class":188},[178,580,361],{"class":265},[178,582,583],{"class":180,"line":296},[178,584,293],{"emptyLinePlaceholder":292},[178,586,587,589,592],{"class":180,"line":318},[178,588,299],{"class":261},[178,590,591],{"class":302}," safe_sheet_name",[178,593,594],{"class":265},"(name, used):\n",[178,596,597],{"class":180,"line":324},[178,598,599],{"class":188},"    \"\"\"Return a name Excel will accept, unique within the workbook.\"\"\"\n",[178,601,602,605,607,610,613,616,618,621,624,627],{"class":180,"line":335},[178,603,604],{"class":265},"    clean ",[178,606,309],{"class":261},[178,608,609],{"class":223}," INVALID",[178,611,612],{"class":265},".sub(",[178,614,615],{"class":188},"\"_\"",[178,617,394],{"class":265},[178,619,620],{"class":223},"str",[178,622,623],{"class":265},"(name)).strip() ",[178,625,626],{"class":261},"or",[178,628,629],{"class":188}," \"Sheet\"\n",[178,631,632,634,636,639,642],{"class":180,"line":364},[178,633,604],{"class":265},[178,635,309],{"class":261},[178,637,638],{"class":265}," clean[:",[178,640,641],{"class":223},"31",[178,643,644],{"class":265},"]\n",[178,646,647],{"class":180,"line":369},[178,648,293],{"emptyLinePlaceholder":292},[178,650,651],{"class":180,"line":375},[178,652,653],{"class":201},"    # Uniqueness after truncation: \"Regional summary north\" and\n",[178,655,656],{"class":180,"line":407},[178,657,658],{"class":201},"    # \"Regional summary south\" can collide once cut to 31 chars.\n",[178,660,661,664,666,669],{"class":180,"line":412},[178,662,663],{"class":265},"    base, n ",[178,665,309],{"class":261},[178,667,668],{"class":265}," clean, ",[178,670,671],{"class":223},"2\n",[178,673,674,677,680,682],{"class":180,"line":436},[178,675,676],{"class":261},"    while",[178,678,679],{"class":265}," clean.lower() ",[178,681,445],{"class":261},[178,683,684],{"class":265}," used:\n",[178,686,687,690,692,695,698,701,704,707],{"class":180,"line":451},[178,688,689],{"class":265},"        suffix ",[178,691,309],{"class":261},[178,693,694],{"class":261}," f",[178,696,697],{"class":188},"\"_",[178,699,700],{"class":563},"{",[178,702,703],{"class":265},"n",[178,705,706],{"class":563},"}",[178,708,709],{"class":188},"\"\n",[178,711,712,715,717,720,722,725,728,731,734],{"class":180,"line":474},[178,713,714],{"class":265},"        clean ",[178,716,309],{"class":261},[178,718,719],{"class":265}," base[: ",[178,721,641],{"class":223},[178,723,724],{"class":261}," -",[178,726,727],{"class":223}," len",[178,729,730],{"class":265},"(suffix)] ",[178,732,733],{"class":261},"+",[178,735,736],{"class":265}," suffix\n",[178,738,739,742,745],{"class":180,"line":483},[178,740,741],{"class":265},"        n ",[178,743,744],{"class":261},"+=",[178,746,747],{"class":223}," 1\n",[178,749,750],{"class":180,"line":488},[178,751,293],{"emptyLinePlaceholder":292},[178,753,755],{"class":180,"line":754},18,[178,756,757],{"class":265},"    used.add(clean.lower())\n",[178,759,761,763],{"class":180,"line":760},19,[178,762,477],{"class":261},[178,764,765],{"class":265}," clean\n",[10,767,768,769,772],{},"Excel also treats sheet names case-insensitively, which is why the ",[14,770,771],{},"used"," set is keyed on the lowercase form. Wire it in:",[169,774,776],{"className":252,"code":775,"language":254,"meta":174,"style":174},"def xls_to_xlsx(src, dest=None):\n    src = Path(src)\n    dest = Path(dest) if dest else src.with_suffix(\".xlsx\")\n    sheets = pd.read_excel(src, sheet_name=None, engine=\"xlrd\")\n\n    used, report = set(), {}\n    with pd.ExcelWriter(dest, engine=\"xlsxwriter\") as writer:\n        for name, frame in sheets.items():\n            target = safe_sheet_name(name, used)\n            frame.to_excel(writer, sheet_name=target, index=False)\n            report[target] = len(frame)\n    return dest, report\n",[14,777,778,792,800,820,844,848,861,879,889,899,918,930],{"__ignoreMap":174},[178,779,780,782,784,786,788,790],{"class":180,"line":181},[178,781,299],{"class":261},[178,783,303],{"class":302},[178,785,306],{"class":265},[178,787,309],{"class":261},[178,789,312],{"class":223},[178,791,315],{"class":265},[178,793,794,796,798],{"class":180,"line":275},[178,795,327],{"class":265},[178,797,309],{"class":261},[178,799,332],{"class":265},[178,801,802,804,806,808,810,812,814,816,818],{"class":180,"line":289},[178,803,338],{"class":265},[178,805,309],{"class":261},[178,807,343],{"class":265},[178,809,346],{"class":261},[178,811,349],{"class":265},[178,813,352],{"class":261},[178,815,355],{"class":265},[178,817,358],{"class":188},[178,819,361],{"class":265},[178,821,822,824,826,828,830,832,834,836,838,840,842],{"class":180,"line":296},[178,823,378],{"class":265},[178,825,309],{"class":261},[178,827,383],{"class":265},[178,829,387],{"class":386},[178,831,309],{"class":261},[178,833,312],{"class":223},[178,835,394],{"class":265},[178,837,397],{"class":386},[178,839,309],{"class":261},[178,841,402],{"class":188},[178,843,361],{"class":265},[178,845,846],{"class":180,"line":318},[178,847,293],{"emptyLinePlaceholder":292},[178,849,850,853,855,858],{"class":180,"line":324},[178,851,852],{"class":265},"    used, report ",[178,854,309],{"class":261},[178,856,857],{"class":223}," set",[178,859,860],{"class":265},"(), {}\n",[178,862,863,865,867,869,871,873,875,877],{"class":180,"line":335},[178,864,415],{"class":261},[178,866,418],{"class":265},[178,868,397],{"class":386},[178,870,309],{"class":261},[178,872,425],{"class":188},[178,874,428],{"class":265},[178,876,283],{"class":261},[178,878,433],{"class":265},[178,880,881,883,885,887],{"class":180,"line":364},[178,882,439],{"class":261},[178,884,442],{"class":265},[178,886,445],{"class":261},[178,888,448],{"class":265},[178,890,891,894,896],{"class":180,"line":369},[178,892,893],{"class":265},"            target ",[178,895,309],{"class":261},[178,897,898],{"class":265}," safe_sheet_name(name, used)\n",[178,900,901,903,905,907,910,912,914,916],{"class":180,"line":375},[178,902,454],{"class":265},[178,904,387],{"class":386},[178,906,309],{"class":261},[178,908,909],{"class":265},"target, ",[178,911,464],{"class":386},[178,913,309],{"class":261},[178,915,469],{"class":223},[178,917,361],{"class":265},[178,919,920,923,925,927],{"class":180,"line":407},[178,921,922],{"class":265},"            report[target] ",[178,924,309],{"class":261},[178,926,727],{"class":223},[178,928,929],{"class":265},"(frame)\n",[178,931,932,934],{"class":180,"line":412},[178,933,477],{"class":261},[178,935,936],{"class":265}," dest, report\n",[10,938,939],{},"Returning the per-sheet row count gives you something to assert on, which is the cheapest possible defence against a conversion that silently produced an empty workbook.",[164,941,943],{"id":942},"step-3-keep-the-values-honest","Step 3 — Keep the values honest",[10,945,946,947,950,951,954,955,27],{},"pandas infers dtypes on read, and its inferences are wrong in ways that matter for identifiers. A column of zero-padded account codes becomes integers and loses the padding; a mixed text\u002Fnumber column becomes ",[14,948,949],{},"object"," and writes fine; a numeric column with one stray blank becomes float, so ",[14,952,953],{},"1042"," writes as ",[14,956,957],{},"1042.0",[29,959,39,966,39,969,39,972,39,976,39,981,39,985,39,989,39,995,39,999,39,1003,39,1009,39,1014,39,1017,39,1021,39,1027,39,1030,39,1033,39,1037,39,1039,39,1041,39,1044,39,1046,39,1050,39,1053,39,1057,39,1061,39,1063,39,1066,39,1069,39,1071,39,1074],{"viewBox":960,"role":32,"ariaLabel":961,"ariaLabelledBy":962,"xmlns":37,"style":965},"0 0 780 236","Three dtype hazards when converting: zero-padded codes losing their padding, integer columns becoming floats because of one blank cell, and dates arriving as serial numbers.",[963,964],"dtype-t","dtype-d","width:100%;max-width:780px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[41,967,968],{"id":963},"Three ways inferred dtypes corrupt a conversion",[45,970,971],{"id":964},"Three before-and-after pairs. A zero-padded code 00742 is inferred as an integer and written as 742, losing the padding. An integer column containing one blank is promoted to float, so 1042 becomes 1042.0. A date cell that was not date-formatted arrives as the serial 45292 rather than a date. Each is fixed by passing an explicit dtype or converter on read.",[49,973],{"x":51,"y":51,"width":974,"height":975,"fill":54},"780","236",[66,977,980],{"x":120,"y":978,"style":979},"28","font-size:12px;font-weight:700;fill:var(--muted,#5b6780);text-anchor:middle","in the .xls",[66,982,984],{"x":983,"y":978,"style":979},"470","after a naive read",[66,986,988],{"x":987,"y":978,"style":979},"674","fix",[49,990],{"x":93,"y":991,"width":992,"height":993,"rx":994,"fill":116,"stroke":117,"style":64},"42","312","52","10",[66,996,998],{"x":120,"y":997,"style":121},"66","account_code = 00742",[66,1000,1002],{"x":120,"y":1001,"style":75},"84","text, zero-padded",[49,1004],{"x":1005,"y":991,"width":1006,"height":993,"rx":994,"fill":1007,"stroke":1008,"style":64},"356","230","#fee8f2","var(--accent,#f43f8f)",[66,1010,1013],{"x":1011,"y":997,"style":1012},"471","font-size:12.5px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","742",[66,1015,1016],{"x":1011,"y":1001,"style":75},"padding gone",[49,1018],{"x":1019,"y":991,"width":1020,"height":993,"rx":994,"fill":96,"stroke":97,"style":64},"600","164",[66,1022,1026],{"x":1023,"y":1024,"style":1025},"682","73","font-size:11px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","dtype=str",[49,1028],{"x":93,"y":1029,"width":992,"height":993,"rx":994,"fill":116,"stroke":117,"style":64},"104",[66,1031,1032],{"x":120,"y":59,"style":121},"units = 1042, blank, 88",[66,1034,1036],{"x":120,"y":1035,"style":75},"146","integers with one gap",[49,1038],{"x":1005,"y":1029,"width":1006,"height":993,"rx":994,"fill":1007,"stroke":1008,"style":64},[66,1040,957],{"x":1011,"y":59,"style":1012},[66,1042,1043],{"x":1011,"y":1035,"style":75},"promoted to float",[49,1045],{"x":1019,"y":1029,"width":1020,"height":993,"rx":994,"fill":96,"stroke":97,"style":64},[66,1047,1049],{"x":1023,"y":1048,"style":1025},"135","Int64 dtype",[49,1051],{"x":93,"y":1052,"width":992,"height":993,"rx":994,"fill":116,"stroke":117,"style":64},"166",[66,1054,1056],{"x":120,"y":1055,"style":121},"190","invoiced = 2024-01-01",[66,1058,1060],{"x":120,"y":1059,"style":75},"208","cell not date-formatted",[49,1062],{"x":1005,"y":1052,"width":1006,"height":993,"rx":994,"fill":1007,"stroke":1008,"style":64},[66,1064,1065],{"x":1011,"y":1055,"style":1012},"45292",[66,1067,1068],{"x":1011,"y":1059,"style":75},"raw day serial",[49,1070],{"x":1019,"y":1052,"width":1020,"height":993,"rx":994,"fill":96,"stroke":97,"style":64},[66,1072,1073],{"x":1023,"y":1055,"style":1025},"to_datetime",[66,1075,1078],{"x":1023,"y":1076,"style":1077},"207","font-size:10px;fill:var(--muted,#5b6780);text-anchor:middle","origin 1899-12-30",[10,1080,1081,1082,1086,1087,1089],{},"For a ",[1083,1084,1085],"em",{},"pure"," conversion — where the goal is a faithful ",[14,1088,20],{}," copy and any analysis happens later — read everything as text. Nothing is inferred, so nothing is corrupted:",[169,1091,1093],{"className":252,"code":1092,"language":254,"meta":174,"style":174},"# dtype=str: every cell becomes a string, exactly as stored.\nsheets = pd.read_excel(src, sheet_name=None, engine=\"xlrd\", dtype=str)\n",[14,1094,1095,1100],{"__ignoreMap":174},[178,1096,1097],{"class":180,"line":181},[178,1098,1099],{"class":201},"# dtype=str: every cell becomes a string, exactly as stored.\n",[178,1101,1102,1105,1107,1109,1111,1113,1115,1117,1119,1121,1123,1125,1128,1130,1132],{"class":180,"line":275},[178,1103,1104],{"class":265},"sheets ",[178,1106,309],{"class":261},[178,1108,383],{"class":265},[178,1110,387],{"class":386},[178,1112,309],{"class":261},[178,1114,312],{"class":223},[178,1116,394],{"class":265},[178,1118,397],{"class":386},[178,1120,309],{"class":261},[178,1122,402],{"class":188},[178,1124,394],{"class":265},[178,1126,1127],{"class":386},"dtype",[178,1129,309],{"class":261},[178,1131,620],{"class":223},[178,1133,361],{"class":265},[10,1135,1136],{},"That is the safe default for archival conversions. When you know the schema and want typed output, name the columns you care about instead:",[169,1138,1140],{"className":252,"code":1139,"language":254,"meta":174,"style":174},"sheets = pd.read_excel(\n    src,\n    sheet_name=None,\n    engine=\"xlrd\",\n    dtype={\"account_code\": str, \"units\": \"Int64\"},   # Int64 tolerates blanks\n    parse_dates=[\"invoiced\"],\n)\n",[14,1141,1142,1151,1156,1168,1179,1212,1227],{"__ignoreMap":174},[178,1143,1144,1146,1148],{"class":180,"line":181},[178,1145,1104],{"class":265},[178,1147,309],{"class":261},[178,1149,1150],{"class":265}," pd.read_excel(\n",[178,1152,1153],{"class":180,"line":275},[178,1154,1155],{"class":265},"    src,\n",[178,1157,1158,1161,1163,1165],{"class":180,"line":289},[178,1159,1160],{"class":386},"    sheet_name",[178,1162,309],{"class":261},[178,1164,312],{"class":223},[178,1166,1167],{"class":265},",\n",[178,1169,1170,1173,1175,1177],{"class":180,"line":296},[178,1171,1172],{"class":386},"    engine",[178,1174,309],{"class":261},[178,1176,402],{"class":188},[178,1178,1167],{"class":265},[178,1180,1181,1184,1186,1188,1191,1194,1196,1198,1201,1203,1206,1209],{"class":180,"line":318},[178,1182,1183],{"class":386},"    dtype",[178,1185,309],{"class":261},[178,1187,700],{"class":265},[178,1189,1190],{"class":188},"\"account_code\"",[178,1192,1193],{"class":265},": ",[178,1195,620],{"class":223},[178,1197,394],{"class":265},[178,1199,1200],{"class":188},"\"units\"",[178,1202,1193],{"class":265},[178,1204,1205],{"class":188},"\"Int64\"",[178,1207,1208],{"class":265},"},   ",[178,1210,1211],{"class":201},"# Int64 tolerates blanks\n",[178,1213,1214,1217,1219,1221,1224],{"class":180,"line":324},[178,1215,1216],{"class":386},"    parse_dates",[178,1218,309],{"class":261},[178,1220,560],{"class":265},[178,1222,1223],{"class":188},"\"invoiced\"",[178,1225,1226],{"class":265},"],\n",[178,1228,1229],{"class":180,"line":335},[178,1230,361],{"class":265},[10,1232,1233,1236,1237,1239,1240,27],{},[14,1234,1235],{},"Int64"," (capital I) is pandas' nullable integer type — it holds a missing value without promoting the column to float, which is exactly the ",[14,1238,957],{}," problem. The wider treatment of type coercion on import is in ",[23,1241,1243],{"href":1242},"\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fcheck-excel-data-types-with-pandas\u002F","checking Excel data types with pandas",[164,1245,1247],{"id":1246},"step-4-the-full-fidelity-route","Step 4 — The full-fidelity route",[10,1249,1250],{},"When the file is a formatted report, pandas is the wrong tool — it will hand you a plain grid where you had merged headers, colours and a chart. LibreOffice re-saves the workbook in the new format, keeping all of it:",[169,1252,1254],{"className":252,"code":1253,"language":254,"meta":174,"style":174},"import shutil, subprocess, tempfile\nfrom pathlib import Path\n\ndef xls_to_xlsx_libreoffice(paths, out_dir=\"converted\", timeout=600):\n    \"\"\"Convert .xls files to .xlsx with headless LibreOffice, keeping formatting.\"\"\"\n    soffice = shutil.which(\"soffice\") or shutil.which(\"libreoffice\")\n    if soffice is None:\n        raise RuntimeError(\"LibreOffice not found; install it or use the pandas route\")\n\n    out = Path(out_dir).resolve()\n    out.mkdir(parents=True, exist_ok=True)\n    srcs = [str(Path(p).resolve()) for p in paths]\n\n    # A throwaway profile keeps concurrent runs from deadlocking on the lock file.\n    with tempfile.TemporaryDirectory() as profile:\n        result = subprocess.run(\n            [soffice, f\"-env:UserInstallation=file:\u002F\u002F{profile}\",\n             \"--headless\", \"--convert-to\", \"xlsx\", \"--outdir\", str(out), *srcs],\n            capture_output=True, text=True, timeout=timeout,\n        )\n    if result.returncode != 0:\n        raise RuntimeError(result.stderr.strip())\n\n    produced = [out \u002F (Path(s).stem + \".xlsx\") for s in srcs]\n    missing = [p.name for p in produced if not p.is_file()]\n    if missing:\n        raise RuntimeError(f\"no output for: {', '.join(missing)}\")\n    return produced\n",[14,1255,1256,1263,1273,1277,1301,1306,1330,1347,1363,1367,1377,1401,1427,1431,1436,1448,1458,1480,1513,1540,1546,1562,1572,1577,1611,1639,1647,1675],{"__ignoreMap":174},[178,1257,1258,1260],{"class":180,"line":181},[178,1259,269],{"class":261},[178,1261,1262],{"class":265}," shutil, subprocess, tempfile\n",[178,1264,1265,1267,1269,1271],{"class":180,"line":275},[178,1266,262],{"class":261},[178,1268,266],{"class":265},[178,1270,269],{"class":261},[178,1272,272],{"class":265},[178,1274,1275],{"class":180,"line":289},[178,1276,293],{"emptyLinePlaceholder":292},[178,1278,1279,1281,1284,1287,1289,1292,1295,1297,1299],{"class":180,"line":296},[178,1280,299],{"class":261},[178,1282,1283],{"class":302}," xls_to_xlsx_libreoffice",[178,1285,1286],{"class":265},"(paths, out_dir",[178,1288,309],{"class":261},[178,1290,1291],{"class":188},"\"converted\"",[178,1293,1294],{"class":265},", timeout",[178,1296,309],{"class":261},[178,1298,1019],{"class":223},[178,1300,315],{"class":265},[178,1302,1303],{"class":180,"line":318},[178,1304,1305],{"class":188},"    \"\"\"Convert .xls files to .xlsx with headless LibreOffice, keeping formatting.\"\"\"\n",[178,1307,1308,1311,1313,1316,1319,1321,1323,1325,1328],{"class":180,"line":324},[178,1309,1310],{"class":265},"    soffice ",[178,1312,309],{"class":261},[178,1314,1315],{"class":265}," shutil.which(",[178,1317,1318],{"class":188},"\"soffice\"",[178,1320,428],{"class":265},[178,1322,626],{"class":261},[178,1324,1315],{"class":265},[178,1326,1327],{"class":188},"\"libreoffice\"",[178,1329,361],{"class":265},[178,1331,1332,1335,1338,1341,1344],{"class":180,"line":335},[178,1333,1334],{"class":261},"    if",[178,1336,1337],{"class":265}," soffice ",[178,1339,1340],{"class":261},"is",[178,1342,1343],{"class":223}," None",[178,1345,1346],{"class":265},":\n",[178,1348,1349,1352,1355,1358,1361],{"class":180,"line":364},[178,1350,1351],{"class":261},"        raise",[178,1353,1354],{"class":223}," RuntimeError",[178,1356,1357],{"class":265},"(",[178,1359,1360],{"class":188},"\"LibreOffice not found; install it or use the pandas route\"",[178,1362,361],{"class":265},[178,1364,1365],{"class":180,"line":369},[178,1366,293],{"emptyLinePlaceholder":292},[178,1368,1369,1372,1374],{"class":180,"line":375},[178,1370,1371],{"class":265},"    out ",[178,1373,309],{"class":261},[178,1375,1376],{"class":265}," Path(out_dir).resolve()\n",[178,1378,1379,1382,1385,1387,1390,1392,1395,1397,1399],{"class":180,"line":407},[178,1380,1381],{"class":265},"    out.mkdir(",[178,1383,1384],{"class":386},"parents",[178,1386,309],{"class":261},[178,1388,1389],{"class":223},"True",[178,1391,394],{"class":265},[178,1393,1394],{"class":386},"exist_ok",[178,1396,309],{"class":261},[178,1398,1389],{"class":223},[178,1400,361],{"class":265},[178,1402,1403,1406,1408,1411,1413,1416,1419,1422,1424],{"class":180,"line":412},[178,1404,1405],{"class":265},"    srcs ",[178,1407,309],{"class":261},[178,1409,1410],{"class":265}," [",[178,1412,620],{"class":223},[178,1414,1415],{"class":265},"(Path(p).resolve()) ",[178,1417,1418],{"class":261},"for",[178,1420,1421],{"class":265}," p ",[178,1423,445],{"class":261},[178,1425,1426],{"class":265}," paths]\n",[178,1428,1429],{"class":180,"line":436},[178,1430,293],{"emptyLinePlaceholder":292},[178,1432,1433],{"class":180,"line":451},[178,1434,1435],{"class":201},"    # A throwaway profile keeps concurrent runs from deadlocking on the lock file.\n",[178,1437,1438,1440,1443,1445],{"class":180,"line":474},[178,1439,415],{"class":261},[178,1441,1442],{"class":265}," tempfile.TemporaryDirectory() ",[178,1444,283],{"class":261},[178,1446,1447],{"class":265}," profile:\n",[178,1449,1450,1453,1455],{"class":180,"line":483},[178,1451,1452],{"class":265},"        result ",[178,1454,309],{"class":261},[178,1456,1457],{"class":265}," subprocess.run(\n",[178,1459,1460,1463,1466,1469,1471,1474,1476,1478],{"class":180,"line":488},[178,1461,1462],{"class":265},"            [soffice, ",[178,1464,1465],{"class":261},"f",[178,1467,1468],{"class":188},"\"-env:UserInstallation=file:\u002F\u002F",[178,1470,700],{"class":563},[178,1472,1473],{"class":265},"profile",[178,1475,706],{"class":563},[178,1477,557],{"class":188},[178,1479,1167],{"class":265},[178,1481,1482,1485,1487,1490,1492,1495,1497,1500,1502,1504,1507,1510],{"class":180,"line":754},[178,1483,1484],{"class":188},"             \"--headless\"",[178,1486,394],{"class":265},[178,1488,1489],{"class":188},"\"--convert-to\"",[178,1491,394],{"class":265},[178,1493,1494],{"class":188},"\"xlsx\"",[178,1496,394],{"class":265},[178,1498,1499],{"class":188},"\"--outdir\"",[178,1501,394],{"class":265},[178,1503,620],{"class":223},[178,1505,1506],{"class":265},"(out), ",[178,1508,1509],{"class":261},"*",[178,1511,1512],{"class":265},"srcs],\n",[178,1514,1515,1518,1520,1522,1524,1526,1528,1530,1532,1535,1537],{"class":180,"line":760},[178,1516,1517],{"class":386},"            capture_output",[178,1519,309],{"class":261},[178,1521,1389],{"class":223},[178,1523,394],{"class":265},[178,1525,66],{"class":386},[178,1527,309],{"class":261},[178,1529,1389],{"class":223},[178,1531,394],{"class":265},[178,1533,1534],{"class":386},"timeout",[178,1536,309],{"class":261},[178,1538,1539],{"class":265},"timeout,\n",[178,1541,1543],{"class":180,"line":1542},20,[178,1544,1545],{"class":265},"        )\n",[178,1547,1549,1551,1554,1557,1560],{"class":180,"line":1548},21,[178,1550,1334],{"class":261},[178,1552,1553],{"class":265}," result.returncode ",[178,1555,1556],{"class":261},"!=",[178,1558,1559],{"class":223}," 0",[178,1561,1346],{"class":265},[178,1563,1565,1567,1569],{"class":180,"line":1564},22,[178,1566,1351],{"class":261},[178,1568,1354],{"class":223},[178,1570,1571],{"class":265},"(result.stderr.strip())\n",[178,1573,1575],{"class":180,"line":1574},23,[178,1576,293],{"emptyLinePlaceholder":292},[178,1578,1580,1583,1585,1588,1591,1594,1596,1599,1601,1603,1606,1608],{"class":180,"line":1579},24,[178,1581,1582],{"class":265},"    produced ",[178,1584,309],{"class":261},[178,1586,1587],{"class":265}," [out ",[178,1589,1590],{"class":261},"\u002F",[178,1592,1593],{"class":265}," (Path(s).stem ",[178,1595,733],{"class":261},[178,1597,1598],{"class":188}," \".xlsx\"",[178,1600,428],{"class":265},[178,1602,1418],{"class":261},[178,1604,1605],{"class":265}," s ",[178,1607,445],{"class":261},[178,1609,1610],{"class":265}," srcs]\n",[178,1612,1614,1617,1619,1622,1624,1626,1628,1631,1633,1636],{"class":180,"line":1613},25,[178,1615,1616],{"class":265},"    missing ",[178,1618,309],{"class":261},[178,1620,1621],{"class":265}," [p.name ",[178,1623,1418],{"class":261},[178,1625,1421],{"class":265},[178,1627,445],{"class":261},[178,1629,1630],{"class":265}," produced ",[178,1632,346],{"class":261},[178,1634,1635],{"class":261}," not",[178,1637,1638],{"class":265}," p.is_file()]\n",[178,1640,1642,1644],{"class":180,"line":1641},26,[178,1643,1334],{"class":261},[178,1645,1646],{"class":265}," missing:\n",[178,1648,1650,1652,1654,1656,1658,1661,1663,1666,1669,1671,1673],{"class":180,"line":1649},27,[178,1651,1351],{"class":261},[178,1653,1354],{"class":223},[178,1655,1357],{"class":265},[178,1657,1465],{"class":261},[178,1659,1660],{"class":188},"\"no output for: ",[178,1662,700],{"class":563},[178,1664,1665],{"class":188},"', '",[178,1667,1668],{"class":265},".join(missing)",[178,1670,706],{"class":563},[178,1672,557],{"class":188},[178,1674,361],{"class":265},[178,1676,1678,1680],{"class":180,"line":1677},28,[178,1679,477],{"class":261},[178,1681,1682],{"class":265}," produced\n",[10,1684,1685,1686,1689,1690,1692],{},"Passing every path to ",[235,1687,1688],{},"one"," ",[14,1691,220],{}," call matters: LibreOffice takes a second or two to start, and a per-file loop pays that tax on every iteration.",[164,1694,1696],{"id":1695},"common-pitfalls-and-fixes","Common pitfalls and fixes",[1698,1699,1700,1716],"table",{},[1701,1702,1703],"thead",{},[1704,1705,1706,1710,1713],"tr",{},[1707,1708,1709],"th",{},"Symptom",[1707,1711,1712],{},"Cause",[1707,1714,1715],{},"Fix",[1717,1718,1719,1740,1757,1775,1790,1805,1820],"tbody",{},[1704,1720,1721,1727,1730],{},[1722,1723,1724],"td",{},[14,1725,1726],{},"InvalidWorksheetName",[1722,1728,1729],{},"Sheet name too long or has forbidden characters",[1722,1731,1732,1733,1736,1737,27],{},"Sanitise with ",[14,1734,1735],{},"safe_sheet_name"," before ",[14,1738,1739],{},"to_excel",[1704,1741,1742,1745,1748],{},[1722,1743,1744],{},"Codes lost their leading zeros",[1722,1746,1747],{},"pandas inferred an integer dtype",[1722,1749,1750,1751,1753,1754,27],{},"Read with ",[14,1752,1026],{},", or name the column in ",[14,1755,1756],{},"dtype={...}",[1704,1758,1759,1766,1769],{},[1722,1760,1761,1763,1764],{},[14,1762,953],{}," became ",[14,1765,957],{},[1722,1767,1768],{},"One blank promoted the column to float",[1722,1770,1771,1772,1774],{},"Use the nullable ",[14,1773,1235],{}," dtype.",[1704,1776,1777,1780,1783],{},[1722,1778,1779],{},"Output opens but is empty",[1722,1781,1782],{},"Header row misdetected, all rows read as header",[1722,1784,1785,1786,1789],{},"Pass ",[14,1787,1788],{},"header=None"," and set names yourself.",[1704,1791,1792,1797,1800],{},[1722,1793,1794],{},[14,1795,1796],{},"ImportError: Missing optional dependency 'xlrd'",[1722,1798,1799],{},"Reader not installed",[1722,1801,1802],{},[14,1803,1804],{},"pip install xlrd",[1704,1806,1807,1810,1813],{},[1722,1808,1809],{},"LibreOffice hangs under cron",[1722,1811,1812],{},"Profile lock held by another instance",[1722,1814,1815,1816,1819],{},"Pass a fresh ",[14,1817,1818],{},"-env:UserInstallation"," per run, as above.",[1704,1821,1822,1825,1834],{},[1722,1823,1824],{},"Converted file rejected by Excel",[1722,1826,1827,1828,1830,1831,1833],{},"Wrote ",[14,1829,20],{}," bytes to a ",[14,1832,16],{}," filename",[1722,1835,1836],{},"Always write the new suffix; do not convert in place.",[164,1838,1840],{"id":1839},"performance-and-scale-notes","Performance and scale notes",[29,1842,39,1848,39,1851,39,1854,39,1857,39,1861,39,1866,39,1871,39,1875,39,1889,39,1895,39,1901,39,1903,39,1907,39,1909,39,1913,39,1916,39,1920,39,1931,39,1935,39,1938,39,1942,39,1948,39,1953,39,1957,39,1959,39,1964],{"viewBox":1843,"role":32,"ariaLabel":1844,"ariaLabelledBy":1845,"xmlns":37,"style":965},"0 0 780 240","Batch converter architecture: a directory of legacy files fans out to worker processes that each convert one file, failures are captured per file rather than aborting the run, and a summary report records rows converted and errors.",[1846,1847],"batcharch-t","batcharch-d",[41,1849,1850],{"id":1846},"A batch converter that survives one bad file",[45,1852,1853],{"id":1847},"A directory of legacy xls files feeds a process pool of four workers. Each worker converts one file independently and returns either a row count or an error string. Successes flow to a converted directory; failures flow to a report rather than aborting the run. A final summary lists both, so a single corrupt file never costs the other three hundred conversions.",[49,1855],{"x":51,"y":51,"width":974,"height":1856,"fill":54},"240",[49,1858],{"x":57,"y":1859,"width":1860,"height":95,"rx":61,"fill":62,"stroke":63,"style":64},"86","130",[66,1862,1865],{"x":1863,"y":95,"style":1864},"79","font-size:11px;font-weight:700;fill:var(--muted,#5b6780);text-anchor:middle","input",[66,1867,1870],{"x":1863,"y":1868,"style":1869},"114","font-size:12.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","legacy\u002F*.xls",[66,1872,1874],{"x":1863,"y":1873,"style":75},"134","300 files",[78,1876,82,1877,82,1880,82,1883,82,1886,39],{"stroke":80,"style":64,"fill":81},[84,1878],{"d":1879},"M144 121 H 176 V 46 H 208",[84,1881],{"d":1882},"M144 121 H 176 V 96 H 208",[84,1884],{"d":1885},"M144 121 H 176 V 146 H 208",[84,1887],{"d":1888},"M144 121 H 176 V 196 H 208",[49,1890],{"x":1891,"y":978,"width":1892,"height":1893,"rx":1894,"fill":96,"stroke":97,"style":64},"212","150","36","9",[66,1896,1900],{"x":1897,"y":1898,"style":1899},"287","51","font-size:11.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","worker 1",[49,1902],{"x":1891,"y":68,"width":1892,"height":1893,"rx":1894,"fill":96,"stroke":97,"style":64},[66,1904,1906],{"x":1897,"y":1905,"style":1899},"101","worker 2",[49,1908],{"x":1891,"y":59,"width":1892,"height":1893,"rx":1894,"fill":96,"stroke":97,"style":64},[66,1910,1912],{"x":1897,"y":1911,"style":1899},"151","worker 3",[49,1914],{"x":1891,"y":1915,"width":1892,"height":1893,"rx":1894,"fill":96,"stroke":97,"style":64},"178",[66,1917,1919],{"x":1897,"y":1918,"style":1899},"201","worker 4",[78,1921,82,1922,82,1925,82,1928,39],{"stroke":117,"style":64,"fill":81},[84,1923],{"d":1924},"M362 46 H 402 V 74 H 442",[84,1926],{"d":1927},"M362 96 H 402 V 74 H 442",[84,1929],{"d":1930},"M362 146 H 402 V 74 H 442",[142,1932],{"points":1933,"fill":1934},"450,74 438,68 438,80","#0f9488",[84,1936],{"d":1937,"fill":81,"stroke":1008,"style":64},"M362 196 H 402 V 176 H 442",[142,1939],{"points":1940,"fill":1941},"450,176 438,170 438,182","#f43f8f",[49,1943],{"x":1944,"y":1945,"width":1946,"height":1947,"rx":61,"fill":116,"stroke":117,"style":64},"458","44","304","62",[66,1949,1952],{"x":1950,"y":95,"style":1951},"610","font-size:12px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","converted\u002F*.xlsx",[66,1954,1956],{"x":1950,"y":1955,"style":75},"90","row count returned per file",[49,1958],{"x":1944,"y":1035,"width":1946,"height":1947,"rx":61,"fill":1007,"stroke":1008,"style":64},[66,1960,1963],{"x":1950,"y":1961,"style":1962},"172","font-size:12px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","failure report",[66,1965,1967],{"x":1950,"y":1966,"style":75},"192","the run continues regardless",[10,1969,1970,1971,1974],{},"The pandas converter is I\u002FO and parse bound, and ",[14,1972,1973],{},"xlrd"," is pure Python — expect roughly a second per megabyte of input on ordinary hardware. LibreOffice is slower per file but amortises well when batched.",[10,1976,1977],{},"For a directory of a few hundred files, run the conversions concurrently. The work releases the GIL only partially, so processes beat threads here:",[169,1979,1981],{"className":252,"code":1980,"language":254,"meta":174,"style":174},"from concurrent.futures import ProcessPoolExecutor\nfrom pathlib import Path\n\ndef convert_one(path):\n    try:\n        dest, report = xls_to_xlsx(path)\n        return path.name, sum(report.values()), None\n    except Exception as exc:               # keep the batch alive\n        return path.name, 0, str(exc)\n\nif __name__ == \"__main__\":\n    files = sorted(Path(\"legacy\").glob(\"*.xls\"))\n    with ProcessPoolExecutor(max_workers=4) as pool:\n        for name, rows, error in pool.map(convert_one, files):\n            status = f\"FAILED: {error}\" if error else f\"{rows} rows\"\n            print(f\"{name:\u003C40} {status}\")\n",[14,1982,1983,1995,2005,2009,2019,2026,2036,2053,2070,2085,2089,2104,2129,2151,2163,2206],{"__ignoreMap":174},[178,1984,1985,1987,1990,1992],{"class":180,"line":181},[178,1986,262],{"class":261},[178,1988,1989],{"class":265}," concurrent.futures ",[178,1991,269],{"class":261},[178,1993,1994],{"class":265}," ProcessPoolExecutor\n",[178,1996,1997,1999,2001,2003],{"class":180,"line":275},[178,1998,262],{"class":261},[178,2000,266],{"class":265},[178,2002,269],{"class":261},[178,2004,272],{"class":265},[178,2006,2007],{"class":180,"line":289},[178,2008,293],{"emptyLinePlaceholder":292},[178,2010,2011,2013,2016],{"class":180,"line":296},[178,2012,299],{"class":261},[178,2014,2015],{"class":302}," convert_one",[178,2017,2018],{"class":265},"(path):\n",[178,2020,2021,2024],{"class":180,"line":318},[178,2022,2023],{"class":261},"    try",[178,2025,1346],{"class":265},[178,2027,2028,2031,2033],{"class":180,"line":324},[178,2029,2030],{"class":265},"        dest, report ",[178,2032,309],{"class":261},[178,2034,2035],{"class":265}," xls_to_xlsx(path)\n",[178,2037,2038,2041,2044,2047,2050],{"class":180,"line":335},[178,2039,2040],{"class":261},"        return",[178,2042,2043],{"class":265}," path.name, ",[178,2045,2046],{"class":223},"sum",[178,2048,2049],{"class":265},"(report.values()), ",[178,2051,2052],{"class":223},"None\n",[178,2054,2055,2058,2061,2064,2067],{"class":180,"line":364},[178,2056,2057],{"class":261},"    except",[178,2059,2060],{"class":223}," Exception",[178,2062,2063],{"class":261}," as",[178,2065,2066],{"class":265}," exc:               ",[178,2068,2069],{"class":201},"# keep the batch alive\n",[178,2071,2072,2074,2076,2078,2080,2082],{"class":180,"line":369},[178,2073,2040],{"class":261},[178,2075,2043],{"class":265},[178,2077,51],{"class":223},[178,2079,394],{"class":265},[178,2081,620],{"class":223},[178,2083,2084],{"class":265},"(exc)\n",[178,2086,2087],{"class":180,"line":375},[178,2088,293],{"emptyLinePlaceholder":292},[178,2090,2091,2093,2096,2099,2102],{"class":180,"line":407},[178,2092,346],{"class":261},[178,2094,2095],{"class":223}," __name__",[178,2097,2098],{"class":261}," ==",[178,2100,2101],{"class":188}," \"__main__\"",[178,2103,1346],{"class":265},[178,2105,2106,2109,2111,2114,2117,2120,2123,2126],{"class":180,"line":412},[178,2107,2108],{"class":265},"    files ",[178,2110,309],{"class":261},[178,2112,2113],{"class":223}," sorted",[178,2115,2116],{"class":265},"(Path(",[178,2118,2119],{"class":188},"\"legacy\"",[178,2121,2122],{"class":265},").glob(",[178,2124,2125],{"class":188},"\"*.xls\"",[178,2127,2128],{"class":265},"))\n",[178,2130,2131,2133,2136,2139,2141,2144,2146,2148],{"class":180,"line":436},[178,2132,415],{"class":261},[178,2134,2135],{"class":265}," ProcessPoolExecutor(",[178,2137,2138],{"class":386},"max_workers",[178,2140,309],{"class":261},[178,2142,2143],{"class":223},"4",[178,2145,428],{"class":265},[178,2147,283],{"class":261},[178,2149,2150],{"class":265}," pool:\n",[178,2152,2153,2155,2158,2160],{"class":180,"line":451},[178,2154,439],{"class":261},[178,2156,2157],{"class":265}," name, rows, error ",[178,2159,445],{"class":261},[178,2161,2162],{"class":265}," pool.map(convert_one, files):\n",[178,2164,2165,2168,2170,2172,2175,2177,2180,2182,2184,2187,2190,2192,2194,2196,2198,2201,2203],{"class":180,"line":474},[178,2166,2167],{"class":265},"            status ",[178,2169,309],{"class":261},[178,2171,694],{"class":261},[178,2173,2174],{"class":188},"\"FAILED: ",[178,2176,700],{"class":563},[178,2178,2179],{"class":265},"error",[178,2181,706],{"class":563},[178,2183,557],{"class":188},[178,2185,2186],{"class":261}," if",[178,2188,2189],{"class":265}," error ",[178,2191,352],{"class":261},[178,2193,694],{"class":261},[178,2195,557],{"class":188},[178,2197,700],{"class":563},[178,2199,2200],{"class":265},"rows",[178,2202,706],{"class":563},[178,2204,2205],{"class":188}," rows\"\n",[178,2207,2208,2211,2213,2215,2217,2219,2222,2225,2227,2230,2233,2235,2237],{"class":180,"line":483},[178,2209,2210],{"class":223},"            print",[178,2212,1357],{"class":265},[178,2214,1465],{"class":261},[178,2216,557],{"class":188},[178,2218,700],{"class":563},[178,2220,2221],{"class":265},"name",[178,2223,2224],{"class":261},":\u003C40",[178,2226,706],{"class":563},[178,2228,2229],{"class":563}," {",[178,2231,2232],{"class":265},"status",[178,2234,706],{"class":563},[178,2236,557],{"class":188},[178,2238,361],{"class":265},[10,2240,2241,2242,2246,2247,2250,2251,2254],{},"Catching per-file exceptions and reporting them is not optional in a batch job — a single corrupt legacy file should not abort a run over three hundred others. The same reasoning drives the retry and reporting patterns in ",[23,2243,2245],{"href":2244},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002F","error handling and logging in Excel automation",". Note the ",[14,2248,2249],{},"if __name__ == \"__main__\":"," guard: ",[14,2252,2253],{},"ProcessPoolExecutor"," re-imports the module in each worker, and without the guard the pool spawns recursively.",[10,2256,2257],{},"Finally, verify rather than assume. A conversion that produced a valid but empty workbook passes every exception check:",[169,2259,2261],{"className":252,"code":2260,"language":254,"meta":174,"style":174},"before = pd.read_excel(\"legacy.xls\", sheet_name=None, engine=\"xlrd\")\nafter = pd.read_excel(\"legacy.xlsx\", sheet_name=None, engine=\"openpyxl\")\n\nassert len(before) == len(after), \"sheet count changed\"\nfor (a_name, a), (b_name, b) in zip(before.items(), after.items()):\n    assert a.shape == b.shape, f\"{a_name}: {a.shape} -> {b.shape}\"\nprint(\"conversion verified\")\n",[14,2262,2263,2293,2324,2328,2349,2364,2409],{"__ignoreMap":174},[178,2264,2265,2268,2270,2273,2275,2277,2279,2281,2283,2285,2287,2289,2291],{"class":180,"line":181},[178,2266,2267],{"class":265},"before ",[178,2269,309],{"class":261},[178,2271,2272],{"class":265}," pd.read_excel(",[178,2274,497],{"class":188},[178,2276,394],{"class":265},[178,2278,387],{"class":386},[178,2280,309],{"class":261},[178,2282,312],{"class":223},[178,2284,394],{"class":265},[178,2286,397],{"class":386},[178,2288,309],{"class":261},[178,2290,402],{"class":188},[178,2292,361],{"class":265},[178,2294,2295,2298,2300,2302,2305,2307,2309,2311,2313,2315,2317,2319,2322],{"class":180,"line":275},[178,2296,2297],{"class":265},"after ",[178,2299,309],{"class":261},[178,2301,2272],{"class":265},[178,2303,2304],{"class":188},"\"legacy.xlsx\"",[178,2306,394],{"class":265},[178,2308,387],{"class":386},[178,2310,309],{"class":261},[178,2312,312],{"class":223},[178,2314,394],{"class":265},[178,2316,397],{"class":386},[178,2318,309],{"class":261},[178,2320,2321],{"class":188},"\"openpyxl\"",[178,2323,361],{"class":265},[178,2325,2326],{"class":180,"line":289},[178,2327,293],{"emptyLinePlaceholder":292},[178,2329,2330,2333,2335,2338,2341,2343,2346],{"class":180,"line":296},[178,2331,2332],{"class":261},"assert",[178,2334,727],{"class":223},[178,2336,2337],{"class":265},"(before) ",[178,2339,2340],{"class":261},"==",[178,2342,727],{"class":223},[178,2344,2345],{"class":265},"(after), ",[178,2347,2348],{"class":188},"\"sheet count changed\"\n",[178,2350,2351,2353,2356,2358,2361],{"class":180,"line":318},[178,2352,1418],{"class":261},[178,2354,2355],{"class":265}," (a_name, a), (b_name, b) ",[178,2357,445],{"class":261},[178,2359,2360],{"class":223}," zip",[178,2362,2363],{"class":265},"(before.items(), after.items()):\n",[178,2365,2366,2369,2372,2374,2377,2379,2381,2383,2386,2388,2390,2392,2395,2397,2400,2402,2405,2407],{"class":180,"line":324},[178,2367,2368],{"class":261},"    assert",[178,2370,2371],{"class":265}," a.shape ",[178,2373,2340],{"class":261},[178,2375,2376],{"class":265}," b.shape, ",[178,2378,1465],{"class":261},[178,2380,557],{"class":188},[178,2382,700],{"class":563},[178,2384,2385],{"class":265},"a_name",[178,2387,706],{"class":563},[178,2389,1193],{"class":188},[178,2391,700],{"class":563},[178,2393,2394],{"class":265},"a.shape",[178,2396,706],{"class":563},[178,2398,2399],{"class":188}," -> ",[178,2401,700],{"class":563},[178,2403,2404],{"class":265},"b.shape",[178,2406,706],{"class":563},[178,2408,709],{"class":188},[178,2410,2411,2413,2415,2418],{"class":180,"line":335},[178,2412,491],{"class":223},[178,2414,1357],{"class":265},[178,2416,2417],{"class":188},"\"conversion verified\"",[178,2419,361],{"class":265},[164,2421,2423],{"id":2422},"conclusion","Conclusion",[10,2425,2426,2427,2429,2430,2432,2433,2435],{},"Converting ",[14,2428,16],{}," to ",[14,2431,20],{}," is a one-line idea wrapped in three practical concerns: sheet names Excel will reject, dtypes pandas will guess wrong, and the question of whether you need the values or the whole formatted document. Read with ",[14,2434,1026],{}," for archival copies, sanitise sheet names, and verify shapes afterwards. When the file is a report rather than a data dump, hand it to headless LibreOffice and let it preserve what pandas cannot see. Convert once at ingest and the rest of your pipeline never has to know the legacy format existed.",[164,2437,2439],{"id":2438},"frequently-asked-questions","Frequently asked questions",[10,2441,2442,2445],{},[235,2443,2444],{},"Does the pandas converter keep colours, merged cells and formulas?","\nNo. pandas reads cell values, so the output is clean data with default formatting. Use the headless LibreOffice path when the appearance or the live formulas matter.",[10,2447,2448,2451,2452,2454,2455,27],{},[235,2449,2450],{},"Why does my conversion fail with \"Invalid Excel character\" on a sheet name?","\nExcel forbids ",[14,2453,516],{}," in sheet names and caps them at 31 characters. Sanitise each name before passing it to ",[14,2456,1739],{},[10,2458,2459,2465,2466,2468,2469,2471,2472,2474],{},[235,2460,2461,2462,2464],{},"Can I convert in place and overwrite the ",[14,2463,16],{},"?","\nYou cannot write ",[14,2467,20],{}," content into a ",[14,2470,16],{}," filename and expect Excel to open it. Write a new file with the ",[14,2473,20],{}," suffix and keep the original until the conversion is verified — legacy files are often the only copy.",[10,2476,2477,2480,2481,2483],{},[235,2478,2479],{},"Which is faster for a hundred files?","\nThe pandas path, comfortably. LibreOffice pays a process startup cost, though passing every file to one ",[14,2482,220],{}," invocation amortises it. pandas stays in-process and converts a typical report in well under a second.",[10,2485,2486,2489],{},[235,2487,2488],{},"How do I check the conversion did not lose rows?","\nCompare shapes sheet by sheet before and after. The converter above returns a per-sheet row count so you can assert on it, which catches both truncation and an accidentally empty sheet.",[164,2491,2493],{"id":2492},"related","Related",[2495,2496,2497,2506,2512,2519,2525],"ul",{},[2498,2499,2500,2501,2505],"li",{},"Up to the parent: ",[23,2502,2504],{"href":2503},"\u002Fgetting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions\u002F","Handling Excel File Formats and Conversions"," — the format map and engine table.",[2498,2507,2508,2511],{},[23,2509,2510],{"href":25},"Read .xls Files in Python with xlrd and pandas"," — the reading half of this workflow.",[2498,2513,2514,2518],{},[23,2515,2517],{"href":2516},"\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-large-excel-files-in-python\u002Fconvert-excel-to-csv-with-python\u002F","Convert Excel to CSV with Python"," — when the target is plain text rather than a workbook.",[2498,2520,2521,2524],{},[23,2522,2523],{"href":208},"Convert an Excel File to PDF with Python"," — the same headless LibreOffice technique aimed at PDF.",[2498,2526,2527,2530],{},[23,2528,2529],{"href":1242},"Check Excel Data Types with pandas"," — going deeper on the dtype traps above.",[2532,2533,2534],"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 .sP0c6, html code.shiki .sP0c6{--shiki-default:#005CC5;--shiki-dark:#91CBFF}html pre.shiki code .s-kum, html code.shiki .s-kum{--shiki-default:#D73A49;--shiki-dark:#FF9492}html pre.shiki code .skGVy, html code.shiki .skGVy{--shiki-default:#24292E;--shiki-dark:#F0F3F6}html pre.shiki code .s_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}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_b0D, html code.shiki .s_b0D{--shiki-default:#22863A;--shiki-default-font-weight:bold;--shiki-dark:#72F088;--shiki-dark-font-weight:bold}",{"title":174,"searchDepth":275,"depth":275,"links":2536},[2537,2538,2539,2540,2541,2542,2543,2544,2545,2546],{"id":166,"depth":275,"text":167},{"id":245,"depth":275,"text":246},{"id":509,"depth":275,"text":510},{"id":942,"depth":275,"text":943},{"id":1246,"depth":275,"text":1247},{"id":1695,"depth":275,"text":1696},{"id":1839,"depth":275,"text":1840},{"id":2422,"depth":275,"text":2423},{"id":2438,"depth":275,"text":2439},{"id":2492,"depth":275,"text":2493},"2026-08-15","Batch-convert legacy .xls workbooks to .xlsx in Python — a values-only pandas converter, a formatting-preserving LibreOffice path, and the sheet-name and dtype traps.","md",[2551,2553,2555,2558,2560],{"q":2444,"a":2552},"No. pandas reads cell values, so the output is clean data with default formatting. Use the headless LibreOffice path when the appearance or the live formulas matter.",{"q":2450,"a":2554},"Excel forbids the characters colon, backslash, forward slash, question mark, asterisk and square brackets in sheet names, and caps them at 31 characters. Sanitise each name before passing it to to_excel.",{"q":2556,"a":2557},"Can I convert in place and overwrite the .xls?","You cannot write .xlsx content into a .xls filename and expect Excel to open it. Write a new file with the .xlsx suffix and keep the original until the conversion is verified — legacy files are often the only copy.",{"q":2479,"a":2559},"The pandas path, comfortably. LibreOffice pays a process startup cost, though passing every file to one soffice invocation amortises it. pandas stays in-process and converts a typical report in well under a second.",{"q":2488,"a":2561},"Compare shapes sheet by sheet before and after. The converter below returns a per-sheet row count so you can assert on it, which catches both truncation and an accidentally empty sheet.",{},"\u002Fgetting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions\u002Fconvert-xls-to-xlsx-with-python",{"title":2565,"description":2566},"Convert .xls to .xlsx in Python (Batch Script)","Two converters for legacy Excel files: pandas plus xlsxwriter for data, headless LibreOffice for full fidelity. Includes sheet-name sanitising and a safe batch runner.","convert-xls-to-xlsx-with-python","getting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions\u002Fconvert-xls-to-xlsx-with-python\u002Findex","how-to","I-VqUdXnxk6aYe6lFKEa2QspaRRwmKOWg9djSeweAbU",[2572,2575],{"title":2504,"path":2573,"stem":2574,"children":-1},"\u002Fgetting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions","getting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions\u002Findex",{"title":2576,"path":2577,"stem":2578,"children":-1},"Read and Write .ods Files with Python","\u002Fgetting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions\u002Fread-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",1786800027102]