[{"data":1,"prerenderedAt":2077},["ShallowReactive",2],{"doc:\u002Fadvanced-data-transformation-and-cleaning\u002Freading-excel-with-polars-and-arrow\u002Fread-multiple-excel-sheets-with-polars":3,"surround:\u002Fadvanced-data-transformation-and-cleaning\u002Freading-excel-with-polars-and-arrow\u002Fread-multiple-excel-sheets-with-polars":2045,"ancestors:\u002Fadvanced-data-transformation-and-cleaning\u002Freading-excel-with-polars-and-arrow\u002Fread-multiple-excel-sheets-with-polars":2054,"children:\u002Fadvanced-data-transformation-and-cleaning\u002Freading-excel-with-polars-and-arrow\u002Fread-multiple-excel-sheets-with-polars":2059,"related:\u002Fadvanced-data-transformation-and-cleaning\u002Freading-excel-with-polars-and-arrow\u002Fread-multiple-excel-sheets-with-polars":2060},{"id":4,"title":5,"body":6,"dateModified":2010,"datePublished":2010,"description":2011,"extension":2012,"faq":2013,"keyword":2022,"kind":2023,"meta":2024,"navigation":247,"path":2033,"seo":2034,"slug":2037,"stem":2038,"topics":2039,"type":2043,"__hash__":2044},"docs\u002Fadvanced-data-transformation-and-cleaning\u002Freading-excel-with-polars-and-arrow\u002Fread-multiple-excel-sheets-with-polars\u002Findex.md","Read Multiple Excel Sheets with Polars",{"type":7,"value":8,"toc":1996},"minimark",[9,24,163,168,203,206,554,558,565,693,700,704,710,777,792,849,1055,1059,1066,1156,1167,1171,1178,1261,1291,1391,1395,1398,1469,1476,1480,1483,1707,1714,1718,1721,1856,1859,1863,1918,1922,1931,1935,1941,1947,1953,1959,1963,1992],[10,11,12,13,18,19,23],"p",{},"A workbook with one sheet per month — plus a README tab and a pivot someone added — is one of the most common inputs to a reporting job. Polars reads each sheet quickly with its Rust-based engine, but stacking them is where problems appear: sheets that gained a column halfway through the year, an ID column that Polars infers as an integer in January and as text in February, and tabs that are not data at all. This guide, part of ",[14,15,17],"a",{"href":16},"\u002Fadvanced-data-transformation-and-cleaning\u002Freading-excel-with-polars-and-arrow\u002F","Reading Excel with Polars and Arrow",", builds a reliable multi-sheet read. The single-sheet basics are covered in ",[14,20,22],{"href":21},"\u002Fadvanced-data-transformation-and-cleaning\u002Freading-excel-with-polars-and-arrow\u002Fread-an-excel-file-with-polars-read-excel\u002F","Read an Excel File with Polars read_excel",".",[25,26,34,35,34,39,34,43,34,51,34,61,34,72,34,79,34,84,34,92,34,99,34,105,34,110,34,113,34,118,34,123,34,129,34,133,34,137,34,140,34,142,34,145,34,150,34,154,34,158,34,161],"svg",{"viewBox":27,"role":28,"ariaLabelledBy":29,"xmlns":32,"style":33},"4 0 752 332","img",[30,31],"pm-fan-t","pm-fan-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:752px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[36,37,38],"title",{"id":30},"Month sheets stacked into one frame, other tabs skipped",[40,41,42],"desc",{"id":31},"The workbook has README, Jan 2026, Feb 2026, Mar 2026 and Pivot tabs. Only the three month sheets are read, each tagged with its sheet name and read with the same column types, then stacked into a single Polars DataFrame.",[44,45],"rect",{"x":46,"y":47,"width":48,"height":49,"fill":50},"4","0","752","332","#ffffff",[44,52],{"x":53,"y":54,"width":55,"height":56,"rx":57,"fill":58,"stroke":59,"style":60},"520.0","119.5","220.0","93.0","12","#d9f4f1","var(--teal,#0f9488)","stroke-width:2px",[62,63,65],"g",{"transform":64},"translate(618.0,132.5) scale(1.000)",[66,67],"path",{"d":68,"fill":69,"stroke":70,"style":71},"M3 5h18v14H3z M3 10h18 M3 15h18 M9 5v14","none","var(--teal-ink,#0b6157)","stroke-width:1.80px;stroke-linecap:round;stroke-linejoin:round",[73,74,78],"text",{"x":75,"y":76,"style":77},"630.0","179.0","font-size:13.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","One DataFrame",[73,80,83],{"x":75,"y":81,"style":82},"198.5","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","+ sheet column",[44,85],{"x":86,"y":87,"width":88,"height":89,"rx":57,"fill":90,"stroke":91,"style":60},"20.0","16.0","250.0","92.0","#f0f4ff","var(--brand,#5b5cf0)",[62,93,95],{"transform":94},"translate(133.0,29.0) scale(1.000)",[66,96],{"d":97,"fill":69,"stroke":98,"style":71},"M4 4h16v16H4z M4 9h16 M4 14h16 M10 4v16","var(--brand-strong,#4338ca)",[73,100,104],{"x":101,"y":102,"style":103},"145.0","75.0","font-size:13px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Jan 2026",[73,106,109],{"x":101,"y":107,"style":108},"94.0","font-size:11px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","region, units, order_id",[66,111],{"d":112,"fill":69,"stroke":91,"style":60},"M274.0,62.0 C394.0,62.0 394.0,166.0 510.0,166.0",[114,115],"polygon",{"points":116,"fill":117},"514.0,166.0 505.0,171.0 505.0,161.0","#5b5cf0",[44,119],{"x":86,"y":120,"width":88,"height":89,"rx":57,"fill":121,"stroke":122,"style":60},"120.0","#fdefd8","var(--gold,#b4740a)",[62,124,126],{"transform":125},"translate(133.0,133.0) scale(1.000)",[66,127],{"d":97,"fill":69,"stroke":128,"style":71},"var(--gold-ink,#7a4e06)",[73,130,132],{"x":101,"y":76,"style":131},"font-size:13px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","Feb 2026",[73,134,136],{"x":101,"y":135,"style":108},"198.0","+ discount column",[66,138],{"d":139,"fill":69,"stroke":91,"style":60},"M274.0,166.0 C394.0,166.0 394.0,166.0 510.0,166.0",[114,141],{"points":116,"fill":117},[44,143],{"x":86,"y":144,"width":88,"height":89,"rx":57,"fill":90,"stroke":91,"style":60},"224.0",[62,146,148],{"transform":147},"translate(133.0,237.0) scale(1.000)",[66,149],{"d":97,"fill":69,"stroke":98,"style":71},[73,151,153],{"x":101,"y":152,"style":103},"283.0","Mar 2026",[73,155,157],{"x":101,"y":156,"style":108},"302.0","units with decimals",[66,159],{"d":160,"fill":69,"stroke":91,"style":60},"M274.0,270.0 C394.0,270.0 394.0,166.0 510.0,166.0",[114,162],{"points":116,"fill":117},[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 polars fastexcel pandas openpyxl\n","bash","",[176,177,178],"code",{"__ignoreMap":174},[179,180,183,187,191,194,197,200],"span",{"class":181,"line":182},"line",1,[179,184,186],{"class":185},"sMTad","pip",[179,188,190],{"class":189},"srMev"," install",[179,192,193],{"class":189}," polars",[179,195,196],{"class":189}," fastexcel",[179,198,199],{"class":189}," pandas",[179,201,202],{"class":189}," openpyxl\n",[10,204,205],{},"A sample workbook with the usual inconsistencies — written with pandas here, but in practice it comes from someone else:",[169,207,211],{"className":208,"code":209,"language":210,"meta":174,"style":174},"language-python shiki shiki-themes github-light github-dark-high-contrast","import pandas as pd\nimport polars as pl\n\nwith pd.ExcelWriter(\"year.xlsx\") as writer:\n    pd.DataFrame({\"Info\": [\"Monthly sales workbook\"]}).to_excel(writer, sheet_name=\"README\", index=False)\n    pd.DataFrame({\"region\": [\"North\", \"South\"], \"units\": [10, 4], \"order_id\": [101, 102]}) \\\n        .to_excel(writer, sheet_name=\"Jan 2026\", index=False)\n    pd.DataFrame({\"region\": [\"North\", \"East\"], \"units\": [7, 3], \"order_id\": [\"A-103\", 104],\n                  \"discount\": [0.1, None]}).to_excel(writer, sheet_name=\"Feb 2026\", index=False)\n    pd.DataFrame({\"region\": [\"West\"], \"units\": [5.5], \"order_id\": [105]}) \\\n        .to_excel(writer, sheet_name=\"Mar 2026\", index=False)\n    pd.DataFrame({\"x\": [1]}).to_excel(writer, sheet_name=\"Pivot\", index=False)\n","python",[176,212,213,229,242,249,269,312,363,386,433,468,500,522],{"__ignoreMap":174},[179,214,215,219,223,226],{"class":181,"line":182},[179,216,218],{"class":217},"s-kum","import",[179,220,222],{"class":221},"skGVy"," pandas ",[179,224,225],{"class":217},"as",[179,227,228],{"class":221}," pd\n",[179,230,232,234,237,239],{"class":181,"line":231},2,[179,233,218],{"class":217},[179,235,236],{"class":221}," polars ",[179,238,225],{"class":217},[179,240,241],{"class":221}," pl\n",[179,243,245],{"class":181,"line":244},3,[179,246,248],{"emptyLinePlaceholder":247},true,"\n",[179,250,252,255,258,261,264,266],{"class":181,"line":251},4,[179,253,254],{"class":217},"with",[179,256,257],{"class":221}," pd.ExcelWriter(",[179,259,260],{"class":189},"\"year.xlsx\"",[179,262,263],{"class":221},") ",[179,265,225],{"class":217},[179,267,268],{"class":221}," writer:\n",[179,270,272,275,278,281,284,287,291,294,297,300,303,305,309],{"class":181,"line":271},5,[179,273,274],{"class":221},"    pd.DataFrame({",[179,276,277],{"class":189},"\"Info\"",[179,279,280],{"class":221},": [",[179,282,283],{"class":189},"\"Monthly sales workbook\"",[179,285,286],{"class":221},"]}).to_excel(writer, ",[179,288,290],{"class":289},"sa561","sheet_name",[179,292,293],{"class":217},"=",[179,295,296],{"class":189},"\"README\"",[179,298,299],{"class":221},", ",[179,301,302],{"class":289},"index",[179,304,293],{"class":217},[179,306,308],{"class":307},"sP0c6","False",[179,310,311],{"class":221},")\n",[179,313,315,317,320,322,325,327,330,333,336,338,341,343,345,347,350,352,355,357,360],{"class":181,"line":314},6,[179,316,274],{"class":221},[179,318,319],{"class":189},"\"region\"",[179,321,280],{"class":221},[179,323,324],{"class":189},"\"North\"",[179,326,299],{"class":221},[179,328,329],{"class":189},"\"South\"",[179,331,332],{"class":221},"], ",[179,334,335],{"class":189},"\"units\"",[179,337,280],{"class":221},[179,339,340],{"class":307},"10",[179,342,299],{"class":221},[179,344,46],{"class":307},[179,346,332],{"class":221},[179,348,349],{"class":189},"\"order_id\"",[179,351,280],{"class":221},[179,353,354],{"class":307},"101",[179,356,299],{"class":221},[179,358,359],{"class":307},"102",[179,361,362],{"class":221},"]}) \\\n",[179,364,366,369,371,373,376,378,380,382,384],{"class":181,"line":365},7,[179,367,368],{"class":221},"        .to_excel(writer, ",[179,370,290],{"class":289},[179,372,293],{"class":217},[179,374,375],{"class":189},"\"Jan 2026\"",[179,377,299],{"class":221},[179,379,302],{"class":289},[179,381,293],{"class":217},[179,383,308],{"class":307},[179,385,311],{"class":221},[179,387,389,391,393,395,397,399,402,404,406,408,411,413,416,418,420,422,425,427,430],{"class":181,"line":388},8,[179,390,274],{"class":221},[179,392,319],{"class":189},[179,394,280],{"class":221},[179,396,324],{"class":189},[179,398,299],{"class":221},[179,400,401],{"class":189},"\"East\"",[179,403,332],{"class":221},[179,405,335],{"class":189},[179,407,280],{"class":221},[179,409,410],{"class":307},"7",[179,412,299],{"class":221},[179,414,415],{"class":307},"3",[179,417,332],{"class":221},[179,419,349],{"class":189},[179,421,280],{"class":221},[179,423,424],{"class":189},"\"A-103\"",[179,426,299],{"class":221},[179,428,429],{"class":307},"104",[179,431,432],{"class":221},"],\n",[179,434,436,439,441,444,446,449,451,453,455,458,460,462,464,466],{"class":181,"line":435},9,[179,437,438],{"class":189},"                  \"discount\"",[179,440,280],{"class":221},[179,442,443],{"class":307},"0.1",[179,445,299],{"class":221},[179,447,448],{"class":307},"None",[179,450,286],{"class":221},[179,452,290],{"class":289},[179,454,293],{"class":217},[179,456,457],{"class":189},"\"Feb 2026\"",[179,459,299],{"class":221},[179,461,302],{"class":289},[179,463,293],{"class":217},[179,465,308],{"class":307},[179,467,311],{"class":221},[179,469,471,473,475,477,480,482,484,486,489,491,493,495,498],{"class":181,"line":470},10,[179,472,274],{"class":221},[179,474,319],{"class":189},[179,476,280],{"class":221},[179,478,479],{"class":189},"\"West\"",[179,481,332],{"class":221},[179,483,335],{"class":189},[179,485,280],{"class":221},[179,487,488],{"class":307},"5.5",[179,490,332],{"class":221},[179,492,349],{"class":189},[179,494,280],{"class":221},[179,496,497],{"class":307},"105",[179,499,362],{"class":221},[179,501,503,505,507,509,512,514,516,518,520],{"class":181,"line":502},11,[179,504,368],{"class":221},[179,506,290],{"class":289},[179,508,293],{"class":217},[179,510,511],{"class":189},"\"Mar 2026\"",[179,513,299],{"class":221},[179,515,302],{"class":289},[179,517,293],{"class":217},[179,519,308],{"class":307},[179,521,311],{"class":221},[179,523,525,527,530,532,535,537,539,541,544,546,548,550,552],{"class":181,"line":524},12,[179,526,274],{"class":221},[179,528,529],{"class":189},"\"x\"",[179,531,280],{"class":221},[179,533,534],{"class":307},"1",[179,536,286],{"class":221},[179,538,290],{"class":289},[179,540,293],{"class":217},[179,542,543],{"class":189},"\"Pivot\"",[179,545,299],{"class":221},[179,547,302],{"class":289},[179,549,293],{"class":217},[179,551,308],{"class":307},[179,553,311],{"class":221},[164,555,557],{"id":556},"step-1-list-the-sheets-and-pick-the-data-ones","Step 1 — List the sheets and pick the data ones",[10,559,560,561,564],{},"Reading every sheet with ",[176,562,563],{},"sheet_id=None"," would also load the README and the pivot. List the names first — fastexcel, the engine Polars uses, reports them without reading any rows — and select the data sheets by a pattern:",[169,566,568],{"className":208,"code":567,"language":210,"meta":174,"style":174},"import re\n\nimport fastexcel\n\nnames = fastexcel.read_excel(\"year.xlsx\").sheet_names\nprint(names)             # ['README', 'Jan 2026', 'Feb 2026', 'Mar 2026', 'Pivot']\nmonth_sheets = [n for n in names if re.fullmatch(r\"[A-Z][a-z]{2} \\d{4}\", n.strip())]\nprint(month_sheets)      # ['Jan 2026', 'Feb 2026', 'Mar 2026']\n",[176,569,570,577,581,588,592,607,619,683],{"__ignoreMap":174},[179,571,572,574],{"class":181,"line":182},[179,573,218],{"class":217},[179,575,576],{"class":221}," re\n",[179,578,579],{"class":181,"line":231},[179,580,248],{"emptyLinePlaceholder":247},[179,582,583,585],{"class":181,"line":244},[179,584,218],{"class":217},[179,586,587],{"class":221}," fastexcel\n",[179,589,590],{"class":181,"line":251},[179,591,248],{"emptyLinePlaceholder":247},[179,593,594,597,599,602,604],{"class":181,"line":271},[179,595,596],{"class":221},"names ",[179,598,293],{"class":217},[179,600,601],{"class":221}," fastexcel.read_excel(",[179,603,260],{"class":189},[179,605,606],{"class":221},").sheet_names\n",[179,608,609,612,615],{"class":181,"line":314},[179,610,611],{"class":307},"print",[179,613,614],{"class":221},"(names)             ",[179,616,618],{"class":617},"s-wDw","# ['README', 'Jan 2026', 'Feb 2026', 'Mar 2026', 'Pivot']\n",[179,620,621,624,626,629,632,635,638,641,644,647,650,653,656,660,663,666,669,672,675,678,680],{"class":181,"line":365},[179,622,623],{"class":221},"month_sheets ",[179,625,293],{"class":217},[179,627,628],{"class":221}," [n ",[179,630,631],{"class":217},"for",[179,633,634],{"class":221}," n ",[179,636,637],{"class":217},"in",[179,639,640],{"class":221}," names ",[179,642,643],{"class":217},"if",[179,645,646],{"class":221}," re.fullmatch(",[179,648,649],{"class":217},"r",[179,651,652],{"class":189},"\"",[179,654,655],{"class":307},"[",[179,657,659],{"class":658},"sSjpA","A-Z",[179,661,662],{"class":307},"][",[179,664,665],{"class":658},"a-z",[179,667,668],{"class":307},"]",[179,670,671],{"class":217},"{2}",[179,673,674],{"class":307}," \\d",[179,676,677],{"class":217},"{4}",[179,679,652],{"class":189},[179,681,682],{"class":221},", n.strip())]\n",[179,684,685,687,690],{"class":181,"line":388},[179,686,611],{"class":307},[179,688,689],{"class":221},"(month_sheets)      ",[179,691,692],{"class":617},"# ['Jan 2026', 'Feb 2026', 'Mar 2026']\n",[10,694,695,696,699],{},"Selecting by pattern, rather than by position or a fixed list, keeps working when April is added and when someone inserts a notes tab in the middle. ",[176,697,698],{},"strip()"," guards against the trailing spaces that sheet names pick up surprisingly often.",[164,701,703],{"id":702},"step-2-read-the-sheets-and-see-why-they-do-not-stack","Step 2 — Read the sheets and see why they do not stack",[10,705,706,707,709],{},"Pass the list to ",[176,708,290],{}," and Polars returns a dictionary of DataFrames. Look at the schemas before combining:",[169,711,713],{"className":208,"code":712,"language":210,"meta":174,"style":174},"frames = pl.read_excel(\"year.xlsx\", sheet_name=month_sheets)\nfor name, frame in frames.items():\n    print(name, dict(frame.schema))\n# Jan 2026 {'region': String, 'units': Int64, 'order_id': Int64}\n# Feb 2026 {'region': String, 'units': Int64, 'order_id': String, 'discount': Float64}\n# Mar 2026 {'region': String, 'units': Float64, 'order_id': Int64}\n",[176,714,715,736,748,762,767,772],{"__ignoreMap":174},[179,716,717,720,722,725,727,729,731,733],{"class":181,"line":182},[179,718,719],{"class":221},"frames ",[179,721,293],{"class":217},[179,723,724],{"class":221}," pl.read_excel(",[179,726,260],{"class":189},[179,728,299],{"class":221},[179,730,290],{"class":289},[179,732,293],{"class":217},[179,734,735],{"class":221},"month_sheets)\n",[179,737,738,740,743,745],{"class":181,"line":231},[179,739,631],{"class":217},[179,741,742],{"class":221}," name, frame ",[179,744,637],{"class":217},[179,746,747],{"class":221}," frames.items():\n",[179,749,750,753,756,759],{"class":181,"line":244},[179,751,752],{"class":307},"    print",[179,754,755],{"class":221},"(name, ",[179,757,758],{"class":307},"dict",[179,760,761],{"class":221},"(frame.schema))\n",[179,763,764],{"class":181,"line":251},[179,765,766],{"class":617},"# Jan 2026 {'region': String, 'units': Int64, 'order_id': Int64}\n",[179,768,769],{"class":181,"line":271},[179,770,771],{"class":617},"# Feb 2026 {'region': String, 'units': Int64, 'order_id': String, 'discount': Float64}\n",[179,773,774],{"class":181,"line":314},[179,775,776],{"class":617},"# Mar 2026 {'region': String, 'units': Float64, 'order_id': Int64}\n",[10,778,779,780,783,784,787,788,791],{},"Three differences, each typical. February has an extra ",[176,781,782],{},"discount"," column. ",[176,785,786],{},"order_id"," is an integer in January and March but a string in February, because one February ID is \"A-103\". And ",[176,789,790],{},"units"," is a float in March because of the 5.5. A plain vertical concat refuses:",[169,793,795],{"className":208,"code":794,"language":210,"meta":174,"style":174},"try:\n    pl.concat(list(frames.values()), how=\"vertical\")\nexcept pl.exceptions.ShapeError as exc:\n    print(exc)   # unable to append to a DataFrame of width 3 with a DataFrame of width 4\n",[176,796,797,805,826,839],{"__ignoreMap":174},[179,798,799,802],{"class":181,"line":182},[179,800,801],{"class":217},"try",[179,803,804],{"class":221},":\n",[179,806,807,810,813,816,819,821,824],{"class":181,"line":231},[179,808,809],{"class":221},"    pl.concat(",[179,811,812],{"class":307},"list",[179,814,815],{"class":221},"(frames.values()), ",[179,817,818],{"class":289},"how",[179,820,293],{"class":217},[179,822,823],{"class":189},"\"vertical\"",[179,825,311],{"class":221},[179,827,828,831,834,836],{"class":181,"line":244},[179,829,830],{"class":217},"except",[179,832,833],{"class":221}," pl.exceptions.ShapeError ",[179,835,225],{"class":217},[179,837,838],{"class":221}," exc:\n",[179,840,841,843,846],{"class":181,"line":251},[179,842,752],{"class":307},[179,844,845],{"class":221},"(exc)   ",[179,847,848],{"class":617},"# unable to append to a DataFrame of width 3 with a DataFrame of width 4\n",[25,850,34,856,34,859,34,862,34,867,34,875,34,879,34,885,34,889,34,893,34,897,34,901,34,905,34,909,34,913,34,917,34,919,34,923,34,925,34,927,34,929,34,931,34,933,34,935,34,938,34,942,34,944,34,947,34,949,34,952,34,954,34,956,34,958,34,961,34,964,34,967,34,969,34,971,34,973,34,975,34,978,34,982,34,984,34,988,34,991,34,994,34,996,34,998,34,1000,34,1002,34,1004,34,1006,34,1008,34,1010,34,1015,34,1021,34,1025,34,1028,34,1032,34,1035,34,1039,34,1042,34,1046,34,1050],{"viewBox":851,"role":28,"ariaLabelledBy":852,"xmlns":32,"style":855},"123 0 514 218",[853,854],"pm-schemas-t","pm-schemas-d","width:100%;max-width:514px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[36,857,858],{"id":853},"Types inferred separately for each sheet",[40,860,861],{"id":854},"Polars infers the order_id column as Int64 in January and March but as String in February, where one ID is A-103. The units column is Int64 in January and February and Float64 in March. Only February has a discount column.",[44,863],{"x":864,"y":47,"width":865,"height":866,"fill":50},"123","514","218",[44,868],{"x":869,"y":87,"width":870,"height":871,"rx":47,"fill":872,"stroke":873,"style":874},"210.2","32.0","24.0","#e7ebef","var(--line,#cdd5e6)","stroke-width:1px",[44,876],{"x":877,"y":87,"width":878,"height":871,"rx":47,"fill":872,"stroke":873,"style":874},"242.2","83.7",[73,880,884],{"x":881,"y":882,"style":883},"284.1","33.0","font-size:11px;font-weight:600;fill:var(--muted,#5b6780);text-anchor:middle","A",[44,886],{"x":887,"y":87,"width":888,"height":871,"rx":47,"fill":872,"stroke":873,"style":874},"326.0","71.2",[73,890,892],{"x":891,"y":882,"style":883},"361.5","B",[44,894],{"x":895,"y":87,"width":896,"height":871,"rx":47,"fill":872,"stroke":873,"style":874},"397.1","74.8",[73,898,900],{"x":899,"y":882,"style":883},"434.5","C",[44,902],{"x":903,"y":87,"width":904,"height":871,"rx":47,"fill":872,"stroke":873,"style":874},"471.9","77.9",[73,906,908],{"x":907,"y":882,"style":883},"510.8","D",[44,910],{"x":869,"y":911,"width":870,"height":912,"rx":47,"fill":872,"stroke":873,"style":874},"40.0","28.0",[73,914,534],{"x":915,"y":916,"style":883},"226.2","59.0",[44,918],{"x":877,"y":911,"width":878,"height":912,"rx":47,"fill":50,"stroke":873,"style":874},[73,920,922],{"x":881,"y":916,"style":921},"font-size:12px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","sheet",[44,924],{"x":887,"y":911,"width":888,"height":912,"rx":47,"fill":50,"stroke":873,"style":874},[73,926,790],{"x":891,"y":916,"style":921},[44,928],{"x":895,"y":911,"width":896,"height":912,"rx":47,"fill":50,"stroke":873,"style":874},[73,930,786],{"x":899,"y":916,"style":921},[44,932],{"x":903,"y":911,"width":904,"height":912,"rx":47,"fill":50,"stroke":873,"style":874},[73,934,782],{"x":907,"y":916,"style":921},[44,936],{"x":869,"y":937,"width":870,"height":912,"rx":47,"fill":872,"stroke":873,"style":874},"68.0",[73,939,941],{"x":915,"y":940,"style":883},"87.0","2",[44,943],{"x":877,"y":937,"width":878,"height":912,"rx":47,"fill":50,"stroke":873,"style":874},[73,945,104],{"x":881,"y":940,"style":946},"font-size:12px;font-weight:400;fill:var(--text,#172033);text-anchor:middle",[44,948],{"x":887,"y":937,"width":888,"height":912,"rx":47,"fill":50,"stroke":873,"style":874},[73,950,951],{"x":891,"y":940,"style":946},"Int64",[44,953],{"x":895,"y":937,"width":896,"height":912,"rx":47,"fill":50,"stroke":873,"style":874},[73,955,951],{"x":899,"y":940,"style":946},[44,957],{"x":903,"y":937,"width":904,"height":912,"rx":47,"fill":50,"stroke":873,"style":874},[73,959,960],{"x":907,"y":940,"style":946},"—",[44,962],{"x":869,"y":963,"width":870,"height":912,"rx":47,"fill":872,"stroke":873,"style":874},"96.0",[73,965,415],{"x":915,"y":966,"style":883},"115.0",[44,968],{"x":877,"y":963,"width":878,"height":912,"rx":47,"fill":50,"stroke":873,"style":874},[73,970,132],{"x":881,"y":966,"style":946},[44,972],{"x":887,"y":963,"width":888,"height":912,"rx":47,"fill":50,"stroke":873,"style":874},[73,974,951],{"x":891,"y":966,"style":946},[44,976],{"x":895,"y":963,"width":896,"height":912,"rx":47,"fill":121,"stroke":122,"style":977},"stroke-width:1.5px",[73,979,981],{"x":899,"y":966,"style":980},"font-size:12px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","String",[44,983],{"x":903,"y":963,"width":904,"height":912,"rx":47,"fill":58,"stroke":59,"style":977},[73,985,987],{"x":907,"y":966,"style":986},"font-size:12px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","Float64",[44,989],{"x":869,"y":990,"width":870,"height":912,"rx":47,"fill":872,"stroke":873,"style":874},"124.0",[73,992,46],{"x":915,"y":993,"style":883},"143.0",[44,995],{"x":877,"y":990,"width":878,"height":912,"rx":47,"fill":50,"stroke":873,"style":874},[73,997,153],{"x":881,"y":993,"style":946},[44,999],{"x":887,"y":990,"width":888,"height":912,"rx":47,"fill":121,"stroke":122,"style":977},[73,1001,987],{"x":891,"y":993,"style":980},[44,1003],{"x":895,"y":990,"width":896,"height":912,"rx":47,"fill":50,"stroke":873,"style":874},[73,1005,951],{"x":899,"y":993,"style":946},[44,1007],{"x":903,"y":990,"width":904,"height":912,"rx":47,"fill":50,"stroke":873,"style":874},[73,1009,960],{"x":907,"y":993,"style":946},[44,1011],{"x":1012,"y":1013,"width":1014,"height":871,"rx":47,"fill":872,"stroke":873,"style":874},"218.2","152.0","74.2",[73,1016,1020],{"x":1017,"y":1018,"style":1019},"255.4","168.5","font-size:11.5px;font-weight:400;fill:var(--muted,#5b6780);text-anchor:middle","README",[44,1022],{"x":1023,"y":1013,"width":1024,"height":871,"rx":47,"fill":872,"stroke":873,"style":874},"294.5","78.0",[73,1026,104],{"x":1027,"y":1018,"style":1019},"333.5",[44,1029],{"x":1030,"y":1013,"width":1031,"height":871,"rx":47,"fill":872,"stroke":873,"style":874},"374.5","78.8",[73,1033,132],{"x":1034,"y":1018,"style":1019},"413.9",[44,1036],{"x":1037,"y":1013,"width":1038,"height":871,"rx":47,"fill":872,"stroke":873,"style":874},"455.3","79.5",[73,1040,153],{"x":1041,"y":1018,"style":1019},"495.0",[44,1043],{"x":1044,"y":1013,"width":1045,"height":871,"rx":47,"fill":872,"stroke":873,"style":874},"536.8","52.6",[73,1047,1049],{"x":1048,"y":1018,"style":1019},"563.1","Pivot",[73,1051,1054],{"x":1052,"y":135,"style":1053},"380.0","font-size:12.5px;font-weight:400;fill:var(--muted,#5b6780);text-anchor:middle","gold types disagree with the other sheets; teal is a column only one sheet has",[164,1056,1058],{"id":1057},"step-3-declare-the-types-once-for-every-sheet","Step 3 — Declare the types once for every sheet",[10,1060,1061,1062,1065],{},"The robust fix is to decide the types rather than let each sheet guess. ",[176,1063,1064],{},"schema_overrides"," applies to every sheet read in the call:",[169,1067,1069],{"className":208,"code":1068,"language":210,"meta":174,"style":174},"TYPES = {\"region\": pl.String, \"units\": pl.Float64, \"order_id\": pl.String}\nframes = pl.read_excel(\"year.xlsx\", sheet_name=month_sheets, schema_overrides=TYPES)\nfor name, frame in frames.items():\n    print(name, frame.schema[\"order_id\"], frame.schema[\"units\"])\n# Jan 2026 String Float64\n",[176,1070,1071,1097,1124,1134,1151],{"__ignoreMap":174},[179,1072,1073,1076,1079,1082,1084,1087,1089,1092,1094],{"class":181,"line":182},[179,1074,1075],{"class":307},"TYPES",[179,1077,1078],{"class":217}," =",[179,1080,1081],{"class":221}," {",[179,1083,319],{"class":189},[179,1085,1086],{"class":221},": pl.String, ",[179,1088,335],{"class":189},[179,1090,1091],{"class":221},": pl.Float64, ",[179,1093,349],{"class":189},[179,1095,1096],{"class":221},": pl.String}\n",[179,1098,1099,1101,1103,1105,1107,1109,1111,1113,1116,1118,1120,1122],{"class":181,"line":231},[179,1100,719],{"class":221},[179,1102,293],{"class":217},[179,1104,724],{"class":221},[179,1106,260],{"class":189},[179,1108,299],{"class":221},[179,1110,290],{"class":289},[179,1112,293],{"class":217},[179,1114,1115],{"class":221},"month_sheets, ",[179,1117,1064],{"class":289},[179,1119,293],{"class":217},[179,1121,1075],{"class":307},[179,1123,311],{"class":221},[179,1125,1126,1128,1130,1132],{"class":181,"line":244},[179,1127,631],{"class":217},[179,1129,742],{"class":221},[179,1131,637],{"class":217},[179,1133,747],{"class":221},[179,1135,1136,1138,1141,1143,1146,1148],{"class":181,"line":251},[179,1137,752],{"class":307},[179,1139,1140],{"class":221},"(name, frame.schema[",[179,1142,349],{"class":189},[179,1144,1145],{"class":221},"], frame.schema[",[179,1147,335],{"class":189},[179,1149,1150],{"class":221},"])\n",[179,1152,1153],{"class":181,"line":271},[179,1154,1155],{"class":617},"# Jan 2026 String Float64\n",[10,1157,1158,1159,1161,1162,1166],{},"IDs become text everywhere — \"101\" and \"A-103\" alike — which is right for identifiers: nobody adds order numbers together. Units become floats in every sheet. Declaring types keeps the column's type from changing month to month. It does not make bad values fail, though: a cell that cannot be converted — \"n\u002Fa\" in the ",[176,1160,790],{}," column, say — becomes null without an error, so count the nulls per column after reading and compare them with the source. Declaring types at read time is covered further in ",[14,1163,1165],{"href":1164},"\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fcheck-excel-data-types-with-pandas\u002F","Check Excel Data Types with pandas",", and the same reasoning applies to Polars.",[164,1168,1170],{"id":1169},"step-4-stack-with-the-right-concat-mode","Step 4 — Stack with the right concat mode",[10,1172,1173,1174,1177],{},"With the types aligned, only the extra column remains. ",[176,1175,1176],{},"how=\"diagonal\""," unions the columns and fills the gaps with nulls; tag each frame with its sheet first so every row keeps its origin:",[169,1179,1181],{"className":208,"code":1180,"language":210,"meta":174,"style":174},"combined = pl.concat(\n    [frame.with_columns(sheet=pl.lit(name)) for name, frame in frames.items()],\n    how=\"diagonal\",\n)\nprint(combined.select(\"sheet\", \"region\", \"units\", \"order_id\", \"discount\"))\n",[176,1182,1183,1193,1214,1227,1231],{"__ignoreMap":174},[179,1184,1185,1188,1190],{"class":181,"line":182},[179,1186,1187],{"class":221},"combined ",[179,1189,293],{"class":217},[179,1191,1192],{"class":221}," pl.concat(\n",[179,1194,1195,1198,1200,1202,1205,1207,1209,1211],{"class":181,"line":231},[179,1196,1197],{"class":221},"    [frame.with_columns(",[179,1199,922],{"class":289},[179,1201,293],{"class":217},[179,1203,1204],{"class":221},"pl.lit(name)) ",[179,1206,631],{"class":217},[179,1208,742],{"class":221},[179,1210,637],{"class":217},[179,1212,1213],{"class":221}," frames.items()],\n",[179,1215,1216,1219,1221,1224],{"class":181,"line":244},[179,1217,1218],{"class":289},"    how",[179,1220,293],{"class":217},[179,1222,1223],{"class":189},"\"diagonal\"",[179,1225,1226],{"class":221},",\n",[179,1228,1229],{"class":181,"line":251},[179,1230,311],{"class":221},[179,1232,1233,1235,1238,1241,1243,1245,1247,1249,1251,1253,1255,1258],{"class":181,"line":271},[179,1234,611],{"class":307},[179,1236,1237],{"class":221},"(combined.select(",[179,1239,1240],{"class":189},"\"sheet\"",[179,1242,299],{"class":221},[179,1244,319],{"class":189},[179,1246,299],{"class":221},[179,1248,335],{"class":189},[179,1250,299],{"class":221},[179,1252,349],{"class":189},[179,1254,299],{"class":221},[179,1256,1257],{"class":189},"\"discount\"",[179,1259,1260],{"class":221},"))\n",[10,1262,1263,1264,1267,1268,1271,1272,1275,1276,1278,1279,1281,1282,1284,1285,1287,1288,1290],{},"Choosing the concat mode is choosing what counts as an error. ",[176,1265,1266],{},"vertical"," demands identical columns and types. ",[176,1269,1270],{},"diagonal"," allows different columns but identical types. ",[176,1273,1274],{},"diagonal_relaxed"," also widens mismatched types to a common supertype — ",[176,1277,951],{}," and ",[176,1280,981],{}," become ",[176,1283,981],{}," — which works but hides the change. With ",[176,1286,1064],{}," in place, ",[176,1289,1270],{}," is the safest choice that still tolerates a new column.",[25,1292,34,1297,34,1300,34,1303,34,1306,34,1309,34,1314,34,1319,34,1322,34,1326,34,1330,34,1334,34,1336,34,1344,34,1346,34,1351,34,1354,34,1357,34,1359,34,1366,34,1368,34,1373,34,1375,34,1379,34,1381,34,1386,34,1388],{"viewBox":1293,"role":28,"ariaLabelledBy":1294,"xmlns":32,"style":33},"4 0 752 174",[1295,1296],"pm-modes-t","pm-modes-d",[36,1298,1299],{"id":1295},"What each concat mode accepts",[40,1301,1302],{"id":1296},"vertical requires identical columns and types. diagonal accepts different columns, filling gaps with nulls, but requires matching types. diagonal_relaxed accepts different columns and widens mismatched types to a common supertype, such as String for Int64 and String.",[44,1304],{"x":46,"y":47,"width":48,"height":1305,"fill":50},"174",[44,1307],{"x":86,"y":87,"width":1308,"height":911,"rx":47,"fill":50,"stroke":873,"style":874},"188.2",[44,1310],{"x":1311,"y":87,"width":1312,"height":911,"rx":47,"fill":90,"stroke":91,"style":1313},"208.2","265.9","stroke-width:1.2px",[73,1315,1318],{"x":1316,"y":1317,"style":103},"341.2","41.0","Different columns",[44,1320],{"x":1321,"y":87,"width":1312,"height":911,"rx":47,"fill":90,"stroke":91,"style":1313},"474.1",[73,1323,1325],{"x":1324,"y":1317,"style":103},"607.1","Different types",[44,1327],{"x":86,"y":1328,"width":1308,"height":1329,"rx":47,"fill":872,"stroke":873,"style":874},"56.0","34.0",[73,1331,1333],{"x":870,"y":1024,"style":1332},"font-size:12.5px;font-weight:700;fill:var(--text,#172033);text-anchor:start","how=\"vertical\"",[44,1335],{"x":1311,"y":1328,"width":1312,"height":1329,"rx":47,"fill":50,"stroke":873,"style":874},[62,1337,1339],{"transform":1338},"translate(333.2,65.0) scale(0.667)",[66,1340],{"d":1341,"fill":69,"stroke":1342,"style":1343},"M6 6l12 12 M18 6L6 18","var(--accent,#f43f8f)","stroke-width:2.70px;stroke-linecap:round;stroke-linejoin:round",[44,1345],{"x":1321,"y":1328,"width":1312,"height":1329,"rx":47,"fill":50,"stroke":873,"style":874},[62,1347,1349],{"transform":1348},"translate(599.1,65.0) scale(0.667)",[66,1350],{"d":1341,"fill":69,"stroke":1342,"style":1343},[44,1352],{"x":86,"y":1353,"width":1308,"height":1329,"rx":47,"fill":872,"stroke":873,"style":874},"90.0",[73,1355,1176],{"x":870,"y":1356,"style":1332},"112.0",[44,1358],{"x":1311,"y":1353,"width":1312,"height":1329,"rx":47,"fill":50,"stroke":873,"style":874},[62,1360,1362],{"transform":1361},"translate(332.2,98.0) scale(0.750)",[66,1363],{"d":1364,"fill":69,"stroke":59,"style":1365},"M5 12.5l4.5 4.5L19 7","stroke-width:2.40px;stroke-linecap:round;stroke-linejoin:round",[44,1367],{"x":1321,"y":1353,"width":1312,"height":1329,"rx":47,"fill":50,"stroke":873,"style":874},[62,1369,1371],{"transform":1370},"translate(599.1,99.0) scale(0.667)",[66,1372],{"d":1341,"fill":69,"stroke":1342,"style":1343},[44,1374],{"x":86,"y":990,"width":1308,"height":1329,"rx":47,"fill":872,"stroke":873,"style":874},[73,1376,1378],{"x":870,"y":1377,"style":1332},"146.0","how=\"diagonal_relaxed\"",[44,1380],{"x":1311,"y":990,"width":1312,"height":1329,"rx":47,"fill":50,"stroke":873,"style":874},[62,1382,1384],{"transform":1383},"translate(332.2,132.0) scale(0.750)",[66,1385],{"d":1364,"fill":69,"stroke":59,"style":1365},[44,1387],{"x":1321,"y":990,"width":1312,"height":1329,"rx":47,"fill":50,"stroke":873,"style":874},[73,1389,1390],{"x":1324,"y":101,"style":82},"yes, widened",[164,1392,1394],{"id":1393},"step-5-turn-the-sheet-name-into-a-real-date","Step 5 — Turn the sheet name into a real date",[10,1396,1397],{},"The sheet name is text; the report needs a month. Parse it once, after stacking:",[169,1399,1401],{"className":208,"code":1400,"language":210,"meta":174,"style":174},"combined = combined.with_columns(\n    month=pl.col(\"sheet\").str.strip_chars().str.to_date(\"%b %Y\")\n)\nprint(combined.group_by(\"month\").agg(pl.col(\"units\").sum()).sort(\"month\").rows())\n# [(datetime.date(2026, 1, 1), 14.0), (datetime.date(2026, 2, 1), 10.0), (datetime.date(2026, 3, 1), 5.5)]\n",[176,1402,1403,1412,1437,1441,1464],{"__ignoreMap":174},[179,1404,1405,1407,1409],{"class":181,"line":182},[179,1406,1187],{"class":221},[179,1408,293],{"class":217},[179,1410,1411],{"class":221}," combined.with_columns(\n",[179,1413,1414,1417,1419,1422,1424,1427,1429,1432,1435],{"class":181,"line":231},[179,1415,1416],{"class":289},"    month",[179,1418,293],{"class":217},[179,1420,1421],{"class":221},"pl.col(",[179,1423,1240],{"class":189},[179,1425,1426],{"class":221},").str.strip_chars().str.to_date(",[179,1428,652],{"class":189},[179,1430,1431],{"class":658},"%b",[179,1433,1434],{"class":189}," %Y\"",[179,1436,311],{"class":221},[179,1438,1439],{"class":181,"line":244},[179,1440,311],{"class":221},[179,1442,1443,1445,1448,1451,1454,1456,1459,1461],{"class":181,"line":251},[179,1444,611],{"class":307},[179,1446,1447],{"class":221},"(combined.group_by(",[179,1449,1450],{"class":189},"\"month\"",[179,1452,1453],{"class":221},").agg(pl.col(",[179,1455,335],{"class":189},[179,1457,1458],{"class":221},").sum()).sort(",[179,1460,1450],{"class":189},[179,1462,1463],{"class":221},").rows())\n",[179,1465,1466],{"class":181,"line":271},[179,1467,1468],{"class":617},"# [(datetime.date(2026, 1, 1), 14.0), (datetime.date(2026, 2, 1), 10.0), (datetime.date(2026, 3, 1), 5.5)]\n",[10,1470,1471,1472,1475],{},"A proper date sorts chronologically — text would put \"Feb\" before \"Jan\" — and works with Polars' date functions for quarters and fiscal years. If a sheet name does not match the format, ",[176,1473,1474],{},"to_date"," raises an error that names the value, which is the behaviour you want for a scheduled job.",[164,1477,1479],{"id":1478},"step-6-many-workbooks-many-sheets","Step 6 — Many workbooks, many sheets",[10,1481,1482],{},"The same function scales to a folder of yearly workbooks: list the files, select the sheets in each, read with the shared types and stack everything with a file column as well as a sheet column:",[169,1484,1486],{"className":208,"code":1485,"language":210,"meta":174,"style":174},"from pathlib import Path\n\ndef read_month_sheets(path):\n    names = [n for n in fastexcel.read_excel(path).sheet_names\n             if re.fullmatch(r\"[A-Z][a-z]{2} \\d{4}\", n.strip())]\n    frames = pl.read_excel(path, sheet_name=names, schema_overrides=TYPES)\n    return [f.with_columns(sheet=pl.lit(n), file=pl.lit(Path(path).name)) for n, f in frames.items()]\n\nall_frames = [f for path in sorted(Path(\".\").glob(\"year*.xlsx\")) for f in read_month_sheets(path)]\neverything = pl.concat(all_frames, how=\"diagonal\")\nprint(everything.height, everything[\"file\"].unique().to_list())   # 5 ['year.xlsx']\n",[176,1487,1488,1501,1505,1517,1535,1566,1591,1624,1628,1673,1691],{"__ignoreMap":174},[179,1489,1490,1493,1496,1498],{"class":181,"line":182},[179,1491,1492],{"class":217},"from",[179,1494,1495],{"class":221}," pathlib ",[179,1497,218],{"class":217},[179,1499,1500],{"class":221}," Path\n",[179,1502,1503],{"class":181,"line":231},[179,1504,248],{"emptyLinePlaceholder":247},[179,1506,1507,1510,1514],{"class":181,"line":244},[179,1508,1509],{"class":217},"def",[179,1511,1513],{"class":1512},"s_Opv"," read_month_sheets",[179,1515,1516],{"class":221},"(path):\n",[179,1518,1519,1522,1524,1526,1528,1530,1532],{"class":181,"line":251},[179,1520,1521],{"class":221},"    names ",[179,1523,293],{"class":217},[179,1525,628],{"class":221},[179,1527,631],{"class":217},[179,1529,634],{"class":221},[179,1531,637],{"class":217},[179,1533,1534],{"class":221}," fastexcel.read_excel(path).sheet_names\n",[179,1536,1537,1540,1542,1544,1546,1548,1550,1552,1554,1556,1558,1560,1562,1564],{"class":181,"line":271},[179,1538,1539],{"class":217},"             if",[179,1541,646],{"class":221},[179,1543,649],{"class":217},[179,1545,652],{"class":189},[179,1547,655],{"class":307},[179,1549,659],{"class":658},[179,1551,662],{"class":307},[179,1553,665],{"class":658},[179,1555,668],{"class":307},[179,1557,671],{"class":217},[179,1559,674],{"class":307},[179,1561,677],{"class":217},[179,1563,652],{"class":189},[179,1565,682],{"class":221},[179,1567,1568,1571,1573,1576,1578,1580,1583,1585,1587,1589],{"class":181,"line":314},[179,1569,1570],{"class":221},"    frames ",[179,1572,293],{"class":217},[179,1574,1575],{"class":221}," pl.read_excel(path, ",[179,1577,290],{"class":289},[179,1579,293],{"class":217},[179,1581,1582],{"class":221},"names, ",[179,1584,1064],{"class":289},[179,1586,293],{"class":217},[179,1588,1075],{"class":307},[179,1590,311],{"class":221},[179,1592,1593,1596,1599,1601,1603,1606,1609,1611,1614,1616,1619,1621],{"class":181,"line":365},[179,1594,1595],{"class":217},"    return",[179,1597,1598],{"class":221}," [f.with_columns(",[179,1600,922],{"class":289},[179,1602,293],{"class":217},[179,1604,1605],{"class":221},"pl.lit(n), ",[179,1607,1608],{"class":289},"file",[179,1610,293],{"class":217},[179,1612,1613],{"class":221},"pl.lit(Path(path).name)) ",[179,1615,631],{"class":217},[179,1617,1618],{"class":221}," n, f ",[179,1620,637],{"class":217},[179,1622,1623],{"class":221}," frames.items()]\n",[179,1625,1626],{"class":181,"line":388},[179,1627,248],{"emptyLinePlaceholder":247},[179,1629,1630,1633,1635,1638,1640,1643,1645,1648,1651,1654,1657,1660,1663,1665,1668,1670],{"class":181,"line":435},[179,1631,1632],{"class":221},"all_frames ",[179,1634,293],{"class":217},[179,1636,1637],{"class":221}," [f ",[179,1639,631],{"class":217},[179,1641,1642],{"class":221}," path ",[179,1644,637],{"class":217},[179,1646,1647],{"class":307}," sorted",[179,1649,1650],{"class":221},"(Path(",[179,1652,1653],{"class":189},"\".\"",[179,1655,1656],{"class":221},").glob(",[179,1658,1659],{"class":189},"\"year*.xlsx\"",[179,1661,1662],{"class":221},")) ",[179,1664,631],{"class":217},[179,1666,1667],{"class":221}," f ",[179,1669,637],{"class":217},[179,1671,1672],{"class":221}," read_month_sheets(path)]\n",[179,1674,1675,1678,1680,1683,1685,1687,1689],{"class":181,"line":470},[179,1676,1677],{"class":221},"everything ",[179,1679,293],{"class":217},[179,1681,1682],{"class":221}," pl.concat(all_frames, ",[179,1684,818],{"class":289},[179,1686,293],{"class":217},[179,1688,1223],{"class":189},[179,1690,311],{"class":221},[179,1692,1693,1695,1698,1701,1704],{"class":181,"line":502},[179,1694,611],{"class":307},[179,1696,1697],{"class":221},"(everything.height, everything[",[179,1699,1700],{"class":189},"\"file\"",[179,1702,1703],{"class":221},"].unique().to_list())   ",[179,1705,1706],{"class":617},"# 5 ['year.xlsx']\n",[10,1708,1709,1710,23],{},"Polars reads each sheet in native code, which makes this noticeably faster than a pandas loop for large workbooks; the Python overhead is only the loop over files and sheets. Converting the combined result to Parquet for later jobs is covered in ",[14,1711,1713],{"href":1712},"\u002Fadvanced-data-transformation-and-cleaning\u002Freading-excel-with-polars-and-arrow\u002Fconvert-excel-files-to-parquet-with-python\u002F","Convert Excel Files to Parquet with Python",[164,1715,1717],{"id":1716},"step-7-check-every-sheet-made-it-in","Step 7 — Check every sheet made it in",[10,1719,1720],{},"A stacked frame hides which sheets contributed. Before using it, compare the rows per sheet with what was read and flag sheets that are empty or missing a required column:",[169,1722,1724],{"className":208,"code":1723,"language":210,"meta":174,"style":174},"REQUIRED = {\"region\", \"units\", \"order_id\"}\nreport = []\nfor name, frame in frames.items():\n    report.append({\"sheet\": name, \"rows\": frame.height,\n                   \"missing\": \", \".join(sorted(REQUIRED - set(frame.columns)))})\nchecks = pl.DataFrame(report)\nprint(checks.rows())\n# [('Jan 2026', 2, ''), ('Feb 2026', 2, ''), ('Mar 2026', 1, '')]\nassert checks[\"rows\"].sum() == combined.height\n",[176,1725,1726,1748,1758,1768,1784,1815,1825,1832,1837],{"__ignoreMap":174},[179,1727,1728,1731,1733,1735,1737,1739,1741,1743,1745],{"class":181,"line":182},[179,1729,1730],{"class":307},"REQUIRED",[179,1732,1078],{"class":217},[179,1734,1081],{"class":221},[179,1736,319],{"class":189},[179,1738,299],{"class":221},[179,1740,335],{"class":189},[179,1742,299],{"class":221},[179,1744,349],{"class":189},[179,1746,1747],{"class":221},"}\n",[179,1749,1750,1753,1755],{"class":181,"line":231},[179,1751,1752],{"class":221},"report ",[179,1754,293],{"class":217},[179,1756,1757],{"class":221}," []\n",[179,1759,1760,1762,1764,1766],{"class":181,"line":244},[179,1761,631],{"class":217},[179,1763,742],{"class":221},[179,1765,637],{"class":217},[179,1767,747],{"class":221},[179,1769,1770,1773,1775,1778,1781],{"class":181,"line":251},[179,1771,1772],{"class":221},"    report.append({",[179,1774,1240],{"class":189},[179,1776,1777],{"class":221},": name, ",[179,1779,1780],{"class":189},"\"rows\"",[179,1782,1783],{"class":221},": frame.height,\n",[179,1785,1786,1789,1792,1795,1798,1801,1804,1806,1809,1812],{"class":181,"line":271},[179,1787,1788],{"class":189},"                   \"missing\"",[179,1790,1791],{"class":221},": ",[179,1793,1794],{"class":189},"\", \"",[179,1796,1797],{"class":221},".join(",[179,1799,1800],{"class":307},"sorted",[179,1802,1803],{"class":221},"(",[179,1805,1730],{"class":307},[179,1807,1808],{"class":217}," -",[179,1810,1811],{"class":307}," set",[179,1813,1814],{"class":221},"(frame.columns)))})\n",[179,1816,1817,1820,1822],{"class":181,"line":314},[179,1818,1819],{"class":221},"checks ",[179,1821,293],{"class":217},[179,1823,1824],{"class":221}," pl.DataFrame(report)\n",[179,1826,1827,1829],{"class":181,"line":365},[179,1828,611],{"class":307},[179,1830,1831],{"class":221},"(checks.rows())\n",[179,1833,1834],{"class":181,"line":388},[179,1835,1836],{"class":617},"# [('Jan 2026', 2, ''), ('Feb 2026', 2, ''), ('Mar 2026', 1, '')]\n",[179,1838,1839,1842,1845,1847,1850,1853],{"class":181,"line":435},[179,1840,1841],{"class":217},"assert",[179,1843,1844],{"class":221}," checks[",[179,1846,1780],{"class":189},[179,1848,1849],{"class":221},"].sum() ",[179,1851,1852],{"class":217},"==",[179,1854,1855],{"class":221}," combined.height\n",[10,1857,1858],{},"A sheet with zero rows usually means someone created next month's tab early; a missing required column means the layout changed. Both are easy to spot in this small table and hard to spot in the combined data, where a null column looks like missing values rather than a missing sheet.",[164,1860,1862],{"id":1861},"common-pitfalls","Common pitfalls",[1864,1865,1866,1886,1895,1908],"ul",{},[1867,1868,1869,1873,1874,1877,1878,1881,1882,1885],"li",{},[1870,1871,1872],"strong",{},"Zero-based habits."," ",[176,1875,1876],{},"sheet_id"," is one-based in Polars; pandas' ",[176,1879,1880],{},"sheet_name=0"," becomes ",[176,1883,1884],{},"sheet_id=1",". Selecting by name avoids the question.",[1867,1887,1888,1891,1892,1894],{},[1870,1889,1890],{},"Empty sheets."," A month sheet with only a header row reads as a zero-row frame with guessed types. ",[176,1893,1064],{}," keeps it compatible, so the concat does not fail on it.",[1867,1896,1897,1873,1900,1903,1904,1907],{},[1870,1898,1899],{},"Rows that vanish.",[176,1901,1902],{},"read_excel"," drops empty rows by default. A row whose only value failed to convert becomes entirely null and is dropped with the blanks; pass ",[176,1905,1906],{},"drop_empty_rows=False"," when you need to account for every source row.",[1867,1909,1910,1913,1914,1917],{},[1870,1911,1912],{},"Header rows that move."," If some sheets have a title above the table, pass ",[176,1915,1916],{},"read_options={\"header_row\": n}"," — but only when every selected sheet shares the layout; otherwise read those sheets separately.",[164,1919,1921],{"id":1920},"conclusion","Conclusion",[10,1923,1924,1925,1927,1928,1930],{},"To read many sheets with Polars, list the sheet names with fastexcel and select data sheets by pattern, read them in one call with ",[176,1926,1064],{}," so every sheet has the same types, tag each frame with its sheet and file, and stack with ",[176,1929,1176],{}," to tolerate new columns without hiding type changes. Parse sheet names into real dates once the data is combined.",[164,1932,1934],{"id":1933},"frequently-asked-questions","Frequently asked questions",[10,1936,1937,1940],{},[1870,1938,1939],{},"How do I read several sheets with Polars?","\nPass a list of names to sheet_name, or sheet_id=None for all sheets. Polars returns a dictionary keyed by sheet name with one DataFrame per sheet, which you can tag and concatenate.",[10,1942,1943,1946],{},[1870,1944,1945],{},"How do I list the sheet names without reading the data?","\nOpen the file with fastexcel, the engine Polars uses, and read its sheet_names attribute. It returns the names in workbook order without loading any rows.",[10,1948,1949,1952],{},[1870,1950,1951],{},"Why does pl.concat fail with a ShapeError or a schema error?","\nThe sheets do not have the same columns or the same types. Use how=\"diagonal_relaxed\" to union the columns and widen types, or better, fix the types at read time with schema_overrides so every sheet agrees.",[10,1954,1955,1958],{},[1870,1956,1957],{},"Why is an ID column an integer in one sheet and a string in another?","\nPolars infers types per sheet. A sheet whose IDs are all numbers gets Int64, while a sheet containing a value like A-103 gets String. Declare the type with schema_overrides so all sheets match.",[164,1960,1962],{"id":1961},"related","Related",[1864,1964,1965,1971,1978,1985],{},[1867,1966,1967,1968,1970],{},"Up one level: ",[14,1969,17],{"href":16}," — fast Excel reading.",[1867,1972,1973,1977],{},[14,1974,1976],{"href":1975},"\u002Fadvanced-data-transformation-and-cleaning\u002Freading-excel-with-polars-and-arrow\u002Fclean-and-aggregate-excel-data-with-polars\u002F","Clean and Aggregate Excel Data with Polars Expressions"," — what to do with the combined frame.",[1867,1979,1980,1984],{},[14,1981,1983],{"href":1982},"\u002Fgetting-started-with-python-excel-automation\u002Fworking-with-multiple-excel-sheets-in-python\u002Fread-all-sheets-from-an-excel-file-into-dataframes\u002F","Read All Sheets from an Excel File into DataFrames"," — the pandas version.",[1867,1986,1987,1991],{},[14,1988,1990],{"href":1989},"\u002Fgetting-started-with-python-excel-automation\u002Fchoosing-a-python-excel-library\u002Fpandas-vs-polars-for-excel-workflows\u002F","Pandas vs Polars for Excel Workflows"," — when Polars is worth it.",[1993,1994,1995],"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 .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s-kum, html code.shiki .s-kum{--shiki-default:#D73A49;--shiki-dark:#FF9492}html pre.shiki code .skGVy, html code.shiki .skGVy{--shiki-default:#24292E;--shiki-dark:#F0F3F6}html pre.shiki code .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}html pre.shiki code .sP0c6, html code.shiki .sP0c6{--shiki-default:#005CC5;--shiki-dark:#91CBFF}html pre.shiki code .s-wDw, html code.shiki .s-wDw{--shiki-default:#6A737D;--shiki-dark:#BDC4CC}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":174,"searchDepth":231,"depth":231,"links":1997},[1998,1999,2000,2001,2002,2003,2004,2005,2006,2007,2008,2009],{"id":166,"depth":231,"text":167},{"id":556,"depth":231,"text":557},{"id":702,"depth":231,"text":703},{"id":1057,"depth":231,"text":1058},{"id":1169,"depth":231,"text":1170},{"id":1393,"depth":231,"text":1394},{"id":1478,"depth":231,"text":1479},{"id":1716,"depth":231,"text":1717},{"id":1861,"depth":231,"text":1862},{"id":1920,"depth":231,"text":1921},{"id":1933,"depth":231,"text":1934},{"id":1961,"depth":231,"text":1962},"2026-09-23","Read the month sheets of a workbook into one Polars DataFrame — list sheet names without loading data, pick sheets by pattern, force consistent column types across sheets, stack them with the right concat mode, turn sheet names into dates and repeat across many workbooks.","md",[2014,2016,2018,2020],{"q":1939,"a":2015},"Pass a list of names to sheet_name, or sheet_id=None for all sheets. Polars returns a dictionary keyed by sheet name with one DataFrame per sheet, which you can tag and concatenate.",{"q":1945,"a":2017},"Open the file with fastexcel, the engine Polars uses, and read its sheet_names attribute. It returns the names in workbook order without loading any rows.",{"q":1951,"a":2019},"The sheets do not have the same columns or the same types. Use how=\"diagonal_relaxed\" to union the columns and widen types, or better, fix the types at read time with schema_overrides so every sheet agrees.",{"q":1957,"a":2021},"Polars infers types per sheet. A sheet whose IDs are all numbers gets Int64, while a sheet containing a value like A-103 gets String. Declare the type with schema_overrides so all sheets match.","read multiple excel sheets with polars",null,{"breadcrumb":2025},[2026,2029,2032],{"name":2027,"item":2028},"Home","\u002F",{"name":2030,"item":2031},"Advanced Data Transformation and Cleaning for Excel Automation","\u002Fadvanced-data-transformation-and-cleaning\u002F",{"name":17,"item":16},"\u002Fadvanced-data-transformation-and-cleaning\u002Freading-excel-with-polars-and-arrow\u002Fread-multiple-excel-sheets-with-polars",{"title":2035,"description":2036},"Read Multiple Excel Sheets with Polars (sheet_name list, concat)","Load several Excel sheets with polars.read_excel — list sheets with fastexcel, select by pattern, schema_overrides for types that differ per sheet, vertical versus diagonal_relaxed concat, sheet names as dates and many workbooks at once.","read-multiple-excel-sheets-with-polars","advanced-data-transformation-and-cleaning\u002Freading-excel-with-polars-and-arrow\u002Fread-multiple-excel-sheets-with-polars\u002Findex",[2040,2041,2042],"polars","sheets-and-files","reading-excel","how-to","X-_uvEfuJZgv2RwrhXWdJ0otzEfhMbFyoOu3iIYLUtA",[2046,2050],{"title":2047,"path":2048,"stem":2049,"children":-1},"Read an Excel File with polars.read_excel","\u002Fadvanced-data-transformation-and-cleaning\u002Freading-excel-with-polars-and-arrow\u002Fread-an-excel-file-with-polars-read-excel","advanced-data-transformation-and-cleaning\u002Freading-excel-with-polars-and-arrow\u002Fread-an-excel-file-with-polars-read-excel\u002Findex",{"title":2051,"path":2052,"stem":2053,"children":-1},"Scan and Filter Many Excel Files Lazily with Polars","\u002Fadvanced-data-transformation-and-cleaning\u002Freading-excel-with-polars-and-arrow\u002Fscan-and-filter-many-excel-files-lazily-with-polars","advanced-data-transformation-and-cleaning\u002Freading-excel-with-polars-and-arrow\u002Fscan-and-filter-many-excel-files-lazily-with-polars\u002Findex",[2055,2057],{"path":2056,"title":2030},"\u002Fadvanced-data-transformation-and-cleaning",{"path":2058,"title":17},"\u002Fadvanced-data-transformation-and-cleaning\u002Freading-excel-with-polars-and-arrow",[],[2061,2063,2066,2069,2073,2076],{"path":2062,"title":1976,"kind":2043},"\u002Fadvanced-data-transformation-and-cleaning\u002Freading-excel-with-polars-and-arrow\u002Fclean-and-aggregate-excel-data-with-polars",{"path":2064,"title":2065,"kind":2043},"\u002Fadvanced-data-transformation-and-cleaning\u002Freading-excel-with-polars-and-arrow\u002Fclean-wide-excel-sheets-with-polars-column-selectors","Clean Wide Excel Sheets with Polars Column Selectors",{"path":2067,"title":2068,"kind":2043},"\u002Fadvanced-data-transformation-and-cleaning\u002Freading-excel-with-polars-and-arrow\u002Fconvert-pandas-excel-code-to-polars","Convert pandas Excel Code to Polars",{"path":2070,"title":2071,"kind":2072},"\u002Fadvanced-data-transformation-and-cleaning\u002Freading-excel-with-polars-and-arrow\u002Ffix-schema-and-type-errors-when-reading-excel-with-polars","Fix Schema and Type Errors When Reading Excel with Polars","fix",{"path":2074,"title":2075,"kind":2043},"\u002Fadvanced-data-transformation-and-cleaning\u002Freading-excel-with-polars-and-arrow\u002Fjoin-and-pivot-excel-data-with-polars","Join and Pivot Excel Data with Polars",{"path":2048,"title":2047,"kind":2043},1790972098292]