[{"data":1,"prerenderedAt":2331},["ShallowReactive",2],{"doc:\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-invalid-worksheet-name-errors":3,"surround:\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-invalid-worksheet-name-errors":2295,"ancestors:\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-invalid-worksheet-name-errors":2304,"children:\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-invalid-worksheet-name-errors":2309,"related:\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-invalid-worksheet-name-errors":2310},{"id":4,"title":5,"body":6,"dateModified":2260,"datePublished":2260,"description":2261,"extension":2262,"faq":2263,"keyword":2272,"kind":2273,"meta":2274,"navigation":342,"path":2283,"seo":2284,"slug":2287,"stem":2288,"topics":2289,"type":2293,"__hash__":2294},"docs\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-invalid-worksheet-name-errors\u002Findex.md","Fix Invalid Worksheet Name Errors When Writing Excel",{"type":7,"value":8,"toc":2247},"minimark",[9,13,24,37,185,190,219,271,275,278,698,732,865,880,884,887,1129,1140,1144,1147,1426,1429,1634,1638,1645,1856,1864,1868,1871,2088,2100,2104,2156,2160,2168,2172,2179,2183,2192,2198,2204,2210,2214,2243],[10,11,12],"p",{},"Scripts that make one sheet per group — per region, per customer, per month — hand data to Excel as sheet names, and data does not respect Excel's rules. The first customer called \"Smith \u002F Jones Ltd\" stops the job:",[14,15,21],"pre",{"className":16,"code":18,"language":19,"meta":20},[17],"language-text","ValueError: Invalid character \u002F found in sheet title\n","text","",[22,23,18],"code",{"__ignoreMap":20},[10,25,26,27,30,31,36],{},"With xlsxwriter the same name raises ",[22,28,29],{},"InvalidWorksheetName",", and a month label like \"2026: March\" fails on the colon. Other rules fail more quietly: openpyxl accepts a 40-character name with only a warning and writes a file that breaks Excel's length limit, and it renames a case-insensitive duplicate without telling you. This guide, part of ",[32,33,35],"a",{"href":34},"\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002F","Troubleshooting Common Python Excel Errors",", lists the rules, shows how each library reacts to breaking them, and ends with one function that turns any label into a safe, unique sheet name.",[38,39,47,48,47,52,47,56,47,64,47,70,47,80,47,91,47,96,47,101,47,106,47,113,47,118,47,122,47,127,47,134,47,139,47,143,47,148,47,155,47,160,47,164,47,169,47,176,47,181],"svg",{"viewBox":40,"role":41,"ariaLabelledBy":42,"xmlns":45,"style":46},"64 0 632 569","img",[43,44],"wsn-rules-t","wsn-rules-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:632px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[49,50,51],"title",{"id":43},"The rules a worksheet name must pass",[53,54,55],"desc",{"id":44},"Five stacked checks a sheet name must pass in order: not empty; no forbidden characters from the set [ ] : * ? \u002F \\; no apostrophe at the start or end; at most 31 characters; and unique among the workbook's sheets ignoring case. A name that passes all five is safe for Excel.",[57,58],"rect",{"x":59,"y":60,"width":61,"height":62,"fill":63},"64","0","632","569","#ffffff",[19,65,69],{"x":66,"y":67,"style":68},"380.0","31.0","font-size:14px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Every generated sheet name should pass all five",[57,71],{"x":72,"y":73,"width":74,"height":75,"rx":76,"fill":77,"stroke":78,"style":79},"80.0","48.0","600.0","93.0","12","#e7ebef","var(--line,#cdd5e6)","stroke-width:2px",[81,82,84],"g",{"transform":83},"translate(368.0,61.0) scale(1.000)",[85,86],"path",{"d":87,"fill":88,"stroke":89,"style":90},"M5 12.5l4.5 4.5L19 7","none","var(--text,#172033)","stroke-width:1.80px;stroke-linecap:round;stroke-linejoin:round",[19,92,95],{"x":66,"y":93,"style":94},"107.5","font-size:13.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","Not empty",[19,97,100],{"x":66,"y":98,"style":99},"127.0","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","an empty or all-space label needs a fallback",[57,102],{"x":72,"y":103,"width":74,"height":75,"rx":76,"fill":104,"stroke":105,"style":79},"151.0","#fee8f2","var(--accent,#f43f8f)",[81,107,109],{"transform":108},"translate(368.0,164.0) scale(1.000)",[85,110],{"d":111,"fill":88,"stroke":112,"style":90},"M6 6l12 12 M18 6L6 18","var(--accent-ink,#be185d)",[19,114,117],{"x":66,"y":115,"style":116},"210.5","font-size:13.5px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","None of [ ] : * ? \u002F \\",[19,119,121],{"x":66,"y":120,"style":99},"230.0","replace them before anything else",[57,123],{"x":72,"y":124,"width":74,"height":75,"rx":76,"fill":125,"stroke":126,"style":79},"254.0","#fdefd8","var(--gold,#b4740a)",[81,128,130],{"transform":129},"translate(368.0,267.0) scale(1.000)",[85,131],{"d":132,"fill":88,"stroke":133,"style":90},"M12 3.5l9.5 17h-19z M12 10v5 M12 17.8v.4","var(--gold-ink,#7a4e06)",[19,135,138],{"x":66,"y":136,"style":137},"313.5","font-size:13.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","No apostrophe at either end",[19,140,142],{"x":66,"y":141,"style":99},"333.0","apostrophes quote names inside formulas",[57,144],{"x":72,"y":145,"width":74,"height":75,"rx":76,"fill":146,"stroke":147,"style":79},"357.0","#f0f4ff","var(--brand,#5b5cf0)",[81,149,151],{"transform":150},"translate(368.0,370.0) scale(1.000)",[85,152],{"d":153,"fill":88,"stroke":154,"style":90},"M5 9h14 M4 15h14 M10 4l-2 16 M16 4l-2 16","var(--brand-strong,#4338ca)",[19,156,159],{"x":66,"y":157,"style":158},"416.5","font-size:13.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","31 characters at most",[19,161,163],{"x":66,"y":162,"style":99},"436.0","truncate after cleaning",[57,165],{"x":72,"y":166,"width":74,"height":75,"rx":76,"fill":167,"stroke":168,"style":79},"460.0","#d9f4f1","var(--teal,#0f9488)",[81,170,172],{"transform":171},"translate(368.0,473.0) scale(1.000)",[85,173],{"d":174,"fill":88,"stroke":175,"style":90},"M12 3l9 5-9 5-9-5z M3 13l9 5 9-5 M3 17l9 5 9-5","var(--teal-ink,#0b6157)",[19,177,180],{"x":66,"y":178,"style":179},"519.5","font-size:13.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","Unique ignoring case",[19,182,184],{"x":66,"y":183,"style":99},"539.0","\"Sales\" and \"sales\" collide",[186,187,189],"h2",{"id":188},"prerequisites","Prerequisites",[14,191,195],{"className":192,"code":193,"language":194,"meta":20,"style":20},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install pandas openpyxl xlsxwriter\n","bash",[22,196,197],{"__ignoreMap":20},[198,199,202,206,210,213,216],"span",{"class":200,"line":201},"line",1,[198,203,205],{"class":204},"sMTad","pip",[198,207,209],{"class":208},"srMev"," install",[198,211,212],{"class":208}," pandas",[198,214,215],{"class":208}," openpyxl",[198,217,218],{"class":208}," xlsxwriter\n",[14,220,224],{"className":221,"code":222,"language":223,"meta":20,"style":20},"language-python shiki shiki-themes github-light github-dark-high-contrast","import warnings\nimport pandas as pd\nfrom openpyxl import Workbook\nimport xlsxwriter\n","python",[22,225,226,236,250,264],{"__ignoreMap":20},[198,227,228,232],{"class":200,"line":201},[198,229,231],{"class":230},"s-kum","import",[198,233,235],{"class":234},"skGVy"," warnings\n",[198,237,239,241,244,247],{"class":200,"line":238},2,[198,240,231],{"class":230},[198,242,243],{"class":234}," pandas ",[198,245,246],{"class":230},"as",[198,248,249],{"class":234}," pd\n",[198,251,253,256,259,261],{"class":200,"line":252},3,[198,254,255],{"class":230},"from",[198,257,258],{"class":234}," openpyxl ",[198,260,231],{"class":230},[198,262,263],{"class":234}," Workbook\n",[198,265,267,269],{"class":200,"line":266},4,[198,268,231],{"class":230},[198,270,218],{"class":234},[186,272,274],{"id":273},"step-1-see-how-each-library-reacts","Step 1 — See how each library reacts",[10,276,277],{},"The three writers you are likely to use enforce the rules differently. Run the same awkward names through openpyxl and xlsxwriter and record what happens:",[14,279,281],{"className":221,"code":280,"language":223,"meta":20,"style":20},"names = [\"Q1\u002FQ2\", \"Sales: EU\", \"A\" * 32, \"'quoted\", \"Sales\", \"sales\", \"\"]\n\nwb = Workbook()\nfor name in names:\n    try:\n        with warnings.catch_warnings(record=True) as caught:\n            warnings.simplefilter(\"always\")\n            ws = wb.create_sheet(name)\n        note = f\" (warning: {caught[0].message})\" if caught else \"\"\n        print(f\"openpyxl  {name!r:40} -> {ws.title!r}{note}\")\n    except ValueError as exc:\n        print(f\"openpyxl  {name!r:40} -> ValueError: {exc}\")\n\nbook = xlsxwriter.Workbook(\"probe.xlsx\")\nfor name in names:\n    try:\n        print(f\"xlsxwriter {name!r:39} -> {book.add_worksheet(name).name!r}\")\n    except Exception as exc:\n        print(f\"xlsxwriter {name!r:39} -> {type(exc).__name__}\")\nbook.close()\n",[22,282,283,338,344,354,368,377,403,415,426,470,519,534,567,572,588,599,606,642,654,692],{"__ignoreMap":20},[198,284,285,288,291,294,297,300,303,305,308,311,315,317,320,322,325,327,330,332,335],{"class":200,"line":201},[198,286,287],{"class":234},"names ",[198,289,290],{"class":230},"=",[198,292,293],{"class":234}," [",[198,295,296],{"class":208},"\"Q1\u002FQ2\"",[198,298,299],{"class":234},", ",[198,301,302],{"class":208},"\"Sales: EU\"",[198,304,299],{"class":234},[198,306,307],{"class":208},"\"A\"",[198,309,310],{"class":230}," *",[198,312,314],{"class":313},"sP0c6"," 32",[198,316,299],{"class":234},[198,318,319],{"class":208},"\"'quoted\"",[198,321,299],{"class":234},[198,323,324],{"class":208},"\"Sales\"",[198,326,299],{"class":234},[198,328,329],{"class":208},"\"sales\"",[198,331,299],{"class":234},[198,333,334],{"class":208},"\"\"",[198,336,337],{"class":234},"]\n",[198,339,340],{"class":200,"line":238},[198,341,343],{"emptyLinePlaceholder":342},true,"\n",[198,345,346,349,351],{"class":200,"line":252},[198,347,348],{"class":234},"wb ",[198,350,290],{"class":230},[198,352,353],{"class":234}," Workbook()\n",[198,355,356,359,362,365],{"class":200,"line":266},[198,357,358],{"class":230},"for",[198,360,361],{"class":234}," name ",[198,363,364],{"class":230},"in",[198,366,367],{"class":234}," names:\n",[198,369,371,374],{"class":200,"line":370},5,[198,372,373],{"class":230},"    try",[198,375,376],{"class":234},":\n",[198,378,380,383,386,390,392,395,398,400],{"class":200,"line":379},6,[198,381,382],{"class":230},"        with",[198,384,385],{"class":234}," warnings.catch_warnings(",[198,387,389],{"class":388},"sa561","record",[198,391,290],{"class":230},[198,393,394],{"class":313},"True",[198,396,397],{"class":234},") ",[198,399,246],{"class":230},[198,401,402],{"class":234}," caught:\n",[198,404,406,409,412],{"class":200,"line":405},7,[198,407,408],{"class":234},"            warnings.simplefilter(",[198,410,411],{"class":208},"\"always\"",[198,413,414],{"class":234},")\n",[198,416,418,421,423],{"class":200,"line":417},8,[198,419,420],{"class":234},"            ws ",[198,422,290],{"class":230},[198,424,425],{"class":234}," wb.create_sheet(name)\n",[198,427,429,432,434,437,440,444,447,449,452,455,458,461,464,467],{"class":200,"line":428},9,[198,430,431],{"class":234},"        note ",[198,433,290],{"class":230},[198,435,436],{"class":230}," f",[198,438,439],{"class":208},"\" (warning: ",[198,441,443],{"class":442},"sSjpA","{",[198,445,446],{"class":234},"caught[",[198,448,60],{"class":313},[198,450,451],{"class":234},"].message",[198,453,454],{"class":442},"}",[198,456,457],{"class":208},")\"",[198,459,460],{"class":230}," if",[198,462,463],{"class":234}," caught ",[198,465,466],{"class":230},"else",[198,468,469],{"class":208}," \"\"\n",[198,471,473,476,479,482,485,487,490,493,495,498,500,503,506,509,512,514,517],{"class":200,"line":472},10,[198,474,475],{"class":313},"        print",[198,477,478],{"class":234},"(",[198,480,481],{"class":230},"f",[198,483,484],{"class":208},"\"openpyxl  ",[198,486,443],{"class":442},[198,488,489],{"class":234},"name",[198,491,492],{"class":230},"!r:40",[198,494,454],{"class":442},[198,496,497],{"class":208}," -> ",[198,499,443],{"class":442},[198,501,502],{"class":234},"ws.title",[198,504,505],{"class":230},"!r",[198,507,508],{"class":442},"}{",[198,510,511],{"class":234},"note",[198,513,454],{"class":442},[198,515,516],{"class":208},"\"",[198,518,414],{"class":234},[198,520,522,525,528,531],{"class":200,"line":521},11,[198,523,524],{"class":230},"    except",[198,526,527],{"class":313}," ValueError",[198,529,530],{"class":230}," as",[198,532,533],{"class":234}," exc:\n",[198,535,537,539,541,543,545,547,549,551,553,556,558,561,563,565],{"class":200,"line":536},12,[198,538,475],{"class":313},[198,540,478],{"class":234},[198,542,481],{"class":230},[198,544,484],{"class":208},[198,546,443],{"class":442},[198,548,489],{"class":234},[198,550,492],{"class":230},[198,552,454],{"class":442},[198,554,555],{"class":208}," -> ValueError: ",[198,557,443],{"class":442},[198,559,560],{"class":234},"exc",[198,562,454],{"class":442},[198,564,516],{"class":208},[198,566,414],{"class":234},[198,568,570],{"class":200,"line":569},13,[198,571,343],{"emptyLinePlaceholder":342},[198,573,575,578,580,583,586],{"class":200,"line":574},14,[198,576,577],{"class":234},"book ",[198,579,290],{"class":230},[198,581,582],{"class":234}," xlsxwriter.Workbook(",[198,584,585],{"class":208},"\"probe.xlsx\"",[198,587,414],{"class":234},[198,589,591,593,595,597],{"class":200,"line":590},15,[198,592,358],{"class":230},[198,594,361],{"class":234},[198,596,364],{"class":230},[198,598,367],{"class":234},[198,600,602,604],{"class":200,"line":601},16,[198,603,373],{"class":230},[198,605,376],{"class":234},[198,607,609,611,613,615,618,620,622,625,627,629,631,634,636,638,640],{"class":200,"line":608},17,[198,610,475],{"class":313},[198,612,478],{"class":234},[198,614,481],{"class":230},[198,616,617],{"class":208},"\"xlsxwriter ",[198,619,443],{"class":442},[198,621,489],{"class":234},[198,623,624],{"class":230},"!r:39",[198,626,454],{"class":442},[198,628,497],{"class":208},[198,630,443],{"class":442},[198,632,633],{"class":234},"book.add_worksheet(name).name",[198,635,505],{"class":230},[198,637,454],{"class":442},[198,639,516],{"class":208},[198,641,414],{"class":234},[198,643,645,647,650,652],{"class":200,"line":644},18,[198,646,524],{"class":230},[198,648,649],{"class":313}," Exception",[198,651,530],{"class":230},[198,653,533],{"class":234},[198,655,657,659,661,663,665,667,669,671,673,675,677,680,683,686,688,690],{"class":200,"line":656},19,[198,658,475],{"class":313},[198,660,478],{"class":234},[198,662,481],{"class":230},[198,664,617],{"class":208},[198,666,443],{"class":442},[198,668,489],{"class":234},[198,670,624],{"class":230},[198,672,454],{"class":442},[198,674,497],{"class":208},[198,676,443],{"class":442},[198,678,679],{"class":313},"type",[198,681,682],{"class":234},"(exc).",[198,684,685],{"class":313},"__name__",[198,687,454],{"class":442},[198,689,516],{"class":208},[198,691,414],{"class":234},[198,693,695],{"class":200,"line":694},20,[198,696,697],{"class":234},"book.close()\n",[10,699,700,701,704,705,708,709,712,713,716,717,719,720,723,724,727,728,731],{},"The output is the whole story in a few lines. openpyxl raises for forbidden characters, warns but accepts the 32-character name, accepts the leading apostrophe, renames ",[22,702,703],{},"sales"," to ",[22,706,707],{},"sales1"," because ",[22,710,711],{},"Sales"," exists, and turns the empty name into ",[22,714,715],{},"Sheet1","-style default. xlsxwriter is strict: it raises ",[22,718,29],{}," for the characters, the length and the apostrophe, and ",[22,721,722],{},"DuplicateWorksheetName"," for the case-insensitive clash. pandas passes ",[22,725,726],{},"sheet_name"," straight to whichever engine it is using, so ",[22,729,730],{},"to_excel"," inherits that engine's behaviour.",[38,733,47,739,47,742,47,745,47,750,47,757,47,762,47,768,47,771,47,775,47,779,47,785,47,787,47,791,47,793,47,796,47,799,47,803,47,805,47,809,47,811,47,813,47,816,47,820,47,822,47,826,47,828,47,830,47,833,47,837,47,839,47,843,47,845,47,848,47,851,47,855,47,857,47,861,47,863],{"viewBox":734,"role":41,"ariaLabelledBy":735,"xmlns":45,"style":738},"4 0 752 242",[736,737],"wsn-libs-t","wsn-libs-d","width:100%;max-width:752px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[49,740,741],{"id":736},"How openpyxl and xlsxwriter handle each broken rule",[53,743,744],{"id":737},"A comparison grid. For forbidden characters both libraries raise an error. For names over 31 characters openpyxl only warns and writes the name while xlsxwriter raises. For a leading apostrophe openpyxl accepts it and xlsxwriter raises. For a case-insensitive duplicate openpyxl silently renames the sheet and xlsxwriter raises. For an empty name both substitute a default name.",[57,746],{"x":747,"y":60,"width":748,"height":749,"fill":63},"4","752","242",[57,751],{"x":752,"y":753,"width":754,"height":755,"rx":60,"fill":63,"stroke":78,"style":756},"20.0","16.0","165.8","40.0","stroke-width:1px",[57,758],{"x":759,"y":753,"width":760,"height":755,"rx":60,"fill":146,"stroke":147,"style":761},"185.8","277.1","stroke-width:1.2px",[19,763,767],{"x":764,"y":765,"style":766},"324.3","41.0","font-size:13px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","openpyxl",[57,769],{"x":770,"y":753,"width":760,"height":755,"rx":60,"fill":146,"stroke":147,"style":761},"462.9",[19,772,774],{"x":773,"y":765,"style":766},"601.4","xlsxwriter",[57,776],{"x":752,"y":777,"width":754,"height":778,"rx":60,"fill":77,"stroke":78,"style":756},"56.0","34.0",[19,780,784],{"x":781,"y":782,"style":783},"32.0","78.0","font-size:12.5px;font-weight:700;fill:var(--text,#172033);text-anchor:start","Forbidden character",[57,786],{"x":759,"y":777,"width":760,"height":778,"rx":60,"fill":63,"stroke":78,"style":756},[19,788,790],{"x":764,"y":789,"style":99},"77.0","raises ValueError",[57,792],{"x":770,"y":777,"width":760,"height":778,"rx":60,"fill":63,"stroke":78,"style":756},[19,794,795],{"x":773,"y":789,"style":99},"raises InvalidWorksheetName",[57,797],{"x":752,"y":798,"width":754,"height":778,"rx":60,"fill":77,"stroke":78,"style":756},"90.0",[19,800,802],{"x":781,"y":801,"style":783},"112.0","Over 31 characters",[57,804],{"x":759,"y":798,"width":760,"height":778,"rx":60,"fill":63,"stroke":78,"style":756},[19,806,808],{"x":764,"y":807,"style":99},"111.0","warns and writes it",[57,810],{"x":770,"y":798,"width":760,"height":778,"rx":60,"fill":63,"stroke":78,"style":756},[19,812,795],{"x":773,"y":807,"style":99},[57,814],{"x":752,"y":815,"width":754,"height":778,"rx":60,"fill":77,"stroke":78,"style":756},"124.0",[19,817,819],{"x":781,"y":818,"style":783},"146.0","Apostrophe at an end",[57,821],{"x":759,"y":815,"width":760,"height":778,"rx":60,"fill":63,"stroke":78,"style":756},[19,823,825],{"x":764,"y":824,"style":99},"145.0","accepts it",[57,827],{"x":770,"y":815,"width":760,"height":778,"rx":60,"fill":63,"stroke":78,"style":756},[19,829,795],{"x":773,"y":824,"style":99},[57,831],{"x":752,"y":832,"width":754,"height":778,"rx":60,"fill":77,"stroke":78,"style":756},"158.0",[19,834,836],{"x":781,"y":835,"style":783},"180.0","Duplicate, other case",[57,838],{"x":759,"y":832,"width":760,"height":778,"rx":60,"fill":63,"stroke":78,"style":756},[19,840,842],{"x":764,"y":841,"style":99},"179.0","renames to \"sales1\"",[57,844],{"x":770,"y":832,"width":760,"height":778,"rx":60,"fill":63,"stroke":78,"style":756},[19,846,847],{"x":773,"y":841,"style":99},"raises DuplicateWorksheetName",[57,849],{"x":752,"y":850,"width":754,"height":778,"rx":60,"fill":77,"stroke":78,"style":756},"192.0",[19,852,854],{"x":781,"y":853,"style":783},"214.0","Empty name",[57,856],{"x":759,"y":850,"width":760,"height":778,"rx":60,"fill":63,"stroke":78,"style":756},[19,858,860],{"x":764,"y":859,"style":99},"213.0","default name",[57,862],{"x":770,"y":850,"width":760,"height":778,"rx":60,"fill":63,"stroke":78,"style":756},[19,864,860],{"x":773,"y":859,"style":99},[10,866,867,868,871,872,876,877,879],{},"The quiet cases are the dangerous ones. A 32-character name written by openpyxl breaks a limit Excel enforces for every sheet it creates itself, and openpyxl's own warning says as much: some applications may not be able to read the file. The silent rename is worse for automation: code that later does ",[22,869,870],{},"wb[\"sales\"]"," gets the ",[873,874,875],"em",{},"first"," sheet, ",[22,878,711],{},", because lookups ignore case too.",[186,881,883],{"id":882},"step-2-clean-the-characters-first","Step 2 — Clean the characters first",[10,885,886],{},"Replace each forbidden character with something readable, or drop it where it adds nothing. A hyphen works for slashes, which keeps \"Q1\u002FQ2\" and \"North \u002F East\" recognisable; square brackets can become round ones; colons, asterisks and question marks carry little meaning in a label and can simply go:",[14,888,890],{"className":221,"code":889,"language":223,"meta":20,"style":20},"import re\n\nREPLACE = {\"[\": \"(\", \"]\": \")\", \"\u002F\": \"-\", \"\\\\\": \"-\", \":\": \"\", \"*\": \"\", \"?\": \"\"}\n\ndef clean_chars(label):\n    text = \"\".join(REPLACE.get(ch, ch) for ch in str(label))\n    return re.sub(r\"\\s+\", \" \", text).strip(\" '\")\n\nfor raw in [\"Q1\u002FQ2\", \"Sales: EU\", \"[Draft] plan\", \"'quoted'\", \"  spaced   out  \"]:\n    print(repr(raw), \"->\", repr(clean_chars(raw)))\n",[22,891,892,899,903,986,990,1002,1033,1067,1071,1106],{"__ignoreMap":20},[198,893,894,896],{"class":200,"line":201},[198,895,231],{"class":230},[198,897,898],{"class":234}," re\n",[198,900,901],{"class":200,"line":238},[198,902,343],{"emptyLinePlaceholder":342},[198,904,905,908,911,914,917,920,923,925,928,930,933,935,938,940,943,945,947,950,952,954,956,958,961,963,965,967,970,972,974,976,979,981,983],{"class":200,"line":252},[198,906,907],{"class":313},"REPLACE",[198,909,910],{"class":230}," =",[198,912,913],{"class":234}," {",[198,915,916],{"class":208},"\"[\"",[198,918,919],{"class":234},": ",[198,921,922],{"class":208},"\"(\"",[198,924,299],{"class":234},[198,926,927],{"class":208},"\"]\"",[198,929,919],{"class":234},[198,931,932],{"class":208},"\")\"",[198,934,299],{"class":234},[198,936,937],{"class":208},"\"\u002F\"",[198,939,919],{"class":234},[198,941,942],{"class":208},"\"-\"",[198,944,299],{"class":234},[198,946,516],{"class":208},[198,948,949],{"class":442},"\\\\",[198,951,516],{"class":208},[198,953,919],{"class":234},[198,955,942],{"class":208},[198,957,299],{"class":234},[198,959,960],{"class":208},"\":\"",[198,962,919],{"class":234},[198,964,334],{"class":208},[198,966,299],{"class":234},[198,968,969],{"class":208},"\"*\"",[198,971,919],{"class":234},[198,973,334],{"class":208},[198,975,299],{"class":234},[198,977,978],{"class":208},"\"?\"",[198,980,919],{"class":234},[198,982,334],{"class":208},[198,984,985],{"class":234},"}\n",[198,987,988],{"class":200,"line":266},[198,989,343],{"emptyLinePlaceholder":342},[198,991,992,995,999],{"class":200,"line":370},[198,993,994],{"class":230},"def",[198,996,998],{"class":997},"s_Opv"," clean_chars",[198,1000,1001],{"class":234},"(label):\n",[198,1003,1004,1007,1009,1012,1015,1017,1020,1022,1025,1027,1030],{"class":200,"line":379},[198,1005,1006],{"class":234},"    text ",[198,1008,290],{"class":230},[198,1010,1011],{"class":208}," \"\"",[198,1013,1014],{"class":234},".join(",[198,1016,907],{"class":313},[198,1018,1019],{"class":234},".get(ch, ch) ",[198,1021,358],{"class":230},[198,1023,1024],{"class":234}," ch ",[198,1026,364],{"class":230},[198,1028,1029],{"class":313}," str",[198,1031,1032],{"class":234},"(label))\n",[198,1034,1035,1038,1041,1044,1046,1049,1052,1054,1056,1059,1062,1065],{"class":200,"line":405},[198,1036,1037],{"class":230},"    return",[198,1039,1040],{"class":234}," re.sub(",[198,1042,1043],{"class":230},"r",[198,1045,516],{"class":208},[198,1047,1048],{"class":313},"\\s",[198,1050,1051],{"class":230},"+",[198,1053,516],{"class":208},[198,1055,299],{"class":234},[198,1057,1058],{"class":208},"\" \"",[198,1060,1061],{"class":234},", text).strip(",[198,1063,1064],{"class":208},"\" '\"",[198,1066,414],{"class":234},[198,1068,1069],{"class":200,"line":417},[198,1070,343],{"emptyLinePlaceholder":342},[198,1072,1073,1075,1078,1080,1082,1084,1086,1088,1090,1093,1095,1098,1100,1103],{"class":200,"line":428},[198,1074,358],{"class":230},[198,1076,1077],{"class":234}," raw ",[198,1079,364],{"class":230},[198,1081,293],{"class":234},[198,1083,296],{"class":208},[198,1085,299],{"class":234},[198,1087,302],{"class":208},[198,1089,299],{"class":234},[198,1091,1092],{"class":208},"\"[Draft] plan\"",[198,1094,299],{"class":234},[198,1096,1097],{"class":208},"\"'quoted'\"",[198,1099,299],{"class":234},[198,1101,1102],{"class":208},"\"  spaced   out  \"",[198,1104,1105],{"class":234},"]:\n",[198,1107,1108,1111,1113,1116,1119,1122,1124,1126],{"class":200,"line":472},[198,1109,1110],{"class":313},"    print",[198,1112,478],{"class":234},[198,1114,1115],{"class":313},"repr",[198,1117,1118],{"class":234},"(raw), ",[198,1120,1121],{"class":208},"\"->\"",[198,1123,299],{"class":234},[198,1125,1115],{"class":313},[198,1127,1128],{"class":234},"(clean_chars(raw)))\n",[10,1130,1131,1132,1135,1136,1139],{},"Stripping apostrophes from both ends happens here too, because Excel uses them to quote sheet names inside formulas (",[22,1133,1134],{},"='Sales EU'!A1","). An apostrophe ",[873,1137,1138],{},"inside"," a name is legal and kept, so \"O'Brien Ltd\" stays as it is. Collapsing runs of whitespace is not required by Excel, but labels from data often carry double spaces or trailing tabs that would otherwise produce two sheets whose names look identical in the tab bar and differ only in invisible characters — the kind of problem that takes an hour to spot by eye and a second to prevent in code.",[186,1141,1143],{"id":1142},"step-3-truncate-then-make-the-name-unique","Step 3 — Truncate, then make the name unique",[10,1145,1146],{},"Truncating must happen after cleaning — cleaning can change the length — and uniqueness must be checked after truncating, because two long names that differ only in their last characters become identical at 31. Keep a set of lower-cased names already used, and add a counter suffix when a name collides, shortening the base so the suffix still fits:",[14,1148,1150],{"className":221,"code":1149,"language":223,"meta":20,"style":20},"def safe_sheet_name(label, used, fallback=\"Sheet\"):\n    base = clean_chars(label) or fallback\n    if base.lower() == \"history\":\n        base = \"History data\"\n    name = base[:31]\n    n = 2\n    while name.lower() in used:\n        suffix = f\" ({n})\"\n        name = base[: 31 - len(suffix)] + suffix\n        n += 1\n    used.add(name.lower())\n    return name\n\nused = set()\nlabels = [\"North \u002F East\", \"north \u002F east\", \"Customer accounts receivable ageing 2026\",\n          \"Customer accounts receivable ageing 2027\", \"\", \"History\"]\nfor label in labels:\n    print(f\"{label!r:45} -> {safe_sheet_name(label, used)!r}\")\n",[22,1151,1152,1170,1186,1202,1212,1227,1237,1250,1272,1298,1309,1314,1321,1325,1338,1363,1379,1391],{"__ignoreMap":20},[198,1153,1154,1156,1159,1162,1164,1167],{"class":200,"line":201},[198,1155,994],{"class":230},[198,1157,1158],{"class":997}," safe_sheet_name",[198,1160,1161],{"class":234},"(label, used, fallback",[198,1163,290],{"class":230},[198,1165,1166],{"class":208},"\"Sheet\"",[198,1168,1169],{"class":234},"):\n",[198,1171,1172,1175,1177,1180,1183],{"class":200,"line":238},[198,1173,1174],{"class":234},"    base ",[198,1176,290],{"class":230},[198,1178,1179],{"class":234}," clean_chars(label) ",[198,1181,1182],{"class":230},"or",[198,1184,1185],{"class":234}," fallback\n",[198,1187,1188,1191,1194,1197,1200],{"class":200,"line":252},[198,1189,1190],{"class":230},"    if",[198,1192,1193],{"class":234}," base.lower() ",[198,1195,1196],{"class":230},"==",[198,1198,1199],{"class":208}," \"history\"",[198,1201,376],{"class":234},[198,1203,1204,1207,1209],{"class":200,"line":266},[198,1205,1206],{"class":234},"        base ",[198,1208,290],{"class":230},[198,1210,1211],{"class":208}," \"History data\"\n",[198,1213,1214,1217,1219,1222,1225],{"class":200,"line":370},[198,1215,1216],{"class":234},"    name ",[198,1218,290],{"class":230},[198,1220,1221],{"class":234}," base[:",[198,1223,1224],{"class":313},"31",[198,1226,337],{"class":234},[198,1228,1229,1232,1234],{"class":200,"line":379},[198,1230,1231],{"class":234},"    n ",[198,1233,290],{"class":230},[198,1235,1236],{"class":313}," 2\n",[198,1238,1239,1242,1245,1247],{"class":200,"line":405},[198,1240,1241],{"class":230},"    while",[198,1243,1244],{"class":234}," name.lower() ",[198,1246,364],{"class":230},[198,1248,1249],{"class":234}," used:\n",[198,1251,1252,1255,1257,1259,1262,1264,1267,1269],{"class":200,"line":417},[198,1253,1254],{"class":234},"        suffix ",[198,1256,290],{"class":230},[198,1258,436],{"class":230},[198,1260,1261],{"class":208},"\" (",[198,1263,443],{"class":442},[198,1265,1266],{"class":234},"n",[198,1268,454],{"class":442},[198,1270,1271],{"class":208},")\"\n",[198,1273,1274,1277,1279,1282,1284,1287,1290,1293,1295],{"class":200,"line":428},[198,1275,1276],{"class":234},"        name ",[198,1278,290],{"class":230},[198,1280,1281],{"class":234}," base[: ",[198,1283,1224],{"class":313},[198,1285,1286],{"class":230}," -",[198,1288,1289],{"class":313}," len",[198,1291,1292],{"class":234},"(suffix)] ",[198,1294,1051],{"class":230},[198,1296,1297],{"class":234}," suffix\n",[198,1299,1300,1303,1306],{"class":200,"line":472},[198,1301,1302],{"class":234},"        n ",[198,1304,1305],{"class":230},"+=",[198,1307,1308],{"class":313}," 1\n",[198,1310,1311],{"class":200,"line":521},[198,1312,1313],{"class":234},"    used.add(name.lower())\n",[198,1315,1316,1318],{"class":200,"line":536},[198,1317,1037],{"class":230},[198,1319,1320],{"class":234}," name\n",[198,1322,1323],{"class":200,"line":569},[198,1324,343],{"emptyLinePlaceholder":342},[198,1326,1327,1330,1332,1335],{"class":200,"line":574},[198,1328,1329],{"class":234},"used ",[198,1331,290],{"class":230},[198,1333,1334],{"class":313}," set",[198,1336,1337],{"class":234},"()\n",[198,1339,1340,1343,1345,1347,1350,1352,1355,1357,1360],{"class":200,"line":590},[198,1341,1342],{"class":234},"labels ",[198,1344,290],{"class":230},[198,1346,293],{"class":234},[198,1348,1349],{"class":208},"\"North \u002F East\"",[198,1351,299],{"class":234},[198,1353,1354],{"class":208},"\"north \u002F east\"",[198,1356,299],{"class":234},[198,1358,1359],{"class":208},"\"Customer accounts receivable ageing 2026\"",[198,1361,1362],{"class":234},",\n",[198,1364,1365,1368,1370,1372,1374,1377],{"class":200,"line":601},[198,1366,1367],{"class":208},"          \"Customer accounts receivable ageing 2027\"",[198,1369,299],{"class":234},[198,1371,334],{"class":208},[198,1373,299],{"class":234},[198,1375,1376],{"class":208},"\"History\"",[198,1378,337],{"class":234},[198,1380,1381,1383,1386,1388],{"class":200,"line":608},[198,1382,358],{"class":230},[198,1384,1385],{"class":234}," label ",[198,1387,364],{"class":230},[198,1389,1390],{"class":234}," labels:\n",[198,1392,1393,1395,1397,1399,1401,1403,1406,1409,1411,1413,1415,1418,1420,1422,1424],{"class":200,"line":644},[198,1394,1110],{"class":313},[198,1396,478],{"class":234},[198,1398,481],{"class":230},[198,1400,516],{"class":208},[198,1402,443],{"class":442},[198,1404,1405],{"class":234},"label",[198,1407,1408],{"class":230},"!r:45",[198,1410,454],{"class":442},[198,1412,497],{"class":208},[198,1414,443],{"class":442},[198,1416,1417],{"class":234},"safe_sheet_name(label, used)",[198,1419,505],{"class":230},[198,1421,454],{"class":442},[198,1423,516],{"class":208},[198,1425,414],{"class":234},[10,1427,1428],{},"The two long \"ageing\" labels show why the order matters: both truncate to the same 31 characters, and only the uniqueness step tells them apart. When the counter suffix makes a name less informative than you would like, keep a mapping from the original label to the sheet name — Step 5 uses it to build an index sheet.",[38,1430,47,1436,47,1439,47,1442,47,1447,47,1451,47,1455,47,1461,47,1465,47,1470,47,1472,47,1475,47,1478,47,1481,47,1484,47,1488,47,1491,47,1495,47,1497,47,1500,47,1503,47,1506,47,1508,47,1511,47,1514,47,1518,47,1520,47,1523,47,1526,47,1530,47,1532,47,1535,47,1538,47,1542,47,1545,47,1547,47,1550,47,1552,47,1555,47,1557,47,1559,47,1561,47,1565,47,1567,47,1569,47,1571,47,1574,47,1576,47,1578,47,1580,47,1583,47,1585,47,1587,47,1589,47,1592,47,1594,47,1596,47,1598,47,1601,47,1607,47,1611,47,1616,47,1621,47,1628],{"viewBox":1431,"role":41,"ariaLabelledBy":1432,"xmlns":45,"style":1435},"34 0 692 248",[1433,1434],"wsn-safe-t","wsn-safe-d","width:100%;max-width:692px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[49,1437,1438],{"id":1433},"Labels from data and the sheet names they become",[53,1440,1441],{"id":1434},"Before: raw group labels including a slash, a colon, a duplicate in lower case, a 32-character label and an empty label. After safe_sheet_name: slashes become hyphens and the colon is dropped, the duplicate gets a (2) suffix, the long label is cut to 31 characters, and the empty label becomes Sheet.",[57,1443],{"x":1444,"y":60,"width":1445,"height":1446,"fill":63},"34","692","248",[57,1448],{"x":1449,"y":755,"width":781,"height":1450,"rx":60,"fill":77,"stroke":78,"style":756},"50.4","24.0",[57,1452],{"x":1453,"y":755,"width":1454,"height":1450,"rx":60,"fill":77,"stroke":78,"style":756},"82.4","232.9",[19,1456,1460],{"x":1457,"y":1458,"style":1459},"198.8","57.0","font-size:11px;font-weight:600;fill:var(--muted,#5b6780);text-anchor:middle","A",[57,1462],{"x":1449,"y":1463,"width":781,"height":1464,"rx":60,"fill":77,"stroke":78,"style":756},"64.0","28.0",[19,1466,1469],{"x":1467,"y":1468,"style":1459},"66.4","83.0","1",[57,1471],{"x":1453,"y":1463,"width":1454,"height":1464,"rx":60,"fill":63,"stroke":78,"style":756},[19,1473,1405],{"x":1457,"y":1468,"style":1474},"font-size:12px;font-weight:700;fill:var(--text,#172033);text-anchor:middle",[57,1476],{"x":1449,"y":1477,"width":781,"height":1464,"rx":60,"fill":77,"stroke":78,"style":756},"92.0",[19,1479,1480],{"x":1467,"y":807,"style":1459},"2",[57,1482],{"x":1453,"y":1477,"width":1454,"height":1464,"rx":60,"fill":104,"stroke":105,"style":1483},"stroke-width:1.5px",[19,1485,1487],{"x":1457,"y":807,"style":1486},"font-size:12px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","North \u002F East",[57,1489],{"x":1449,"y":1490,"width":781,"height":1464,"rx":60,"fill":77,"stroke":78,"style":756},"120.0",[19,1492,1494],{"x":1467,"y":1493,"style":1459},"139.0","3",[57,1496],{"x":1453,"y":1490,"width":1454,"height":1464,"rx":60,"fill":104,"stroke":105,"style":1483},[19,1498,1499],{"x":1457,"y":1493,"style":1486},"north \u002F east",[57,1501],{"x":1449,"y":1502,"width":781,"height":1464,"rx":60,"fill":77,"stroke":78,"style":756},"148.0",[19,1504,747],{"x":1467,"y":1505,"style":1459},"167.0",[57,1507],{"x":1453,"y":1502,"width":1454,"height":1464,"rx":60,"fill":104,"stroke":105,"style":1483},[19,1509,1510],{"x":1457,"y":1505,"style":1486},"Sales: EU",[57,1512],{"x":1449,"y":1513,"width":781,"height":1464,"rx":60,"fill":77,"stroke":78,"style":756},"176.0",[19,1515,1517],{"x":1467,"y":1516,"style":1459},"195.0","5",[57,1519],{"x":1453,"y":1513,"width":1454,"height":1464,"rx":60,"fill":104,"stroke":105,"style":1483},[19,1521,1522],{"x":1457,"y":1516,"style":1486},"Quarterly revenue by region 2026",[57,1524],{"x":1449,"y":1525,"width":781,"height":1464,"rx":60,"fill":77,"stroke":78,"style":756},"204.0",[19,1527,1529],{"x":1467,"y":1528,"style":1459},"223.0","6",[57,1531],{"x":1453,"y":1525,"width":1454,"height":1464,"rx":60,"fill":104,"stroke":105,"style":1483},[19,1533,1534],{"x":1457,"y":1528,"style":1486},"(empty)",[57,1536],{"x":1537,"y":755,"width":781,"height":1450,"rx":60,"fill":77,"stroke":78,"style":756},"452.9",[57,1539],{"x":1540,"y":755,"width":1541,"height":1450,"rx":60,"fill":77,"stroke":78,"style":756},"484.9","224.7",[19,1543,1460],{"x":1544,"y":1458,"style":1459},"597.3",[57,1546],{"x":1537,"y":1463,"width":781,"height":1464,"rx":60,"fill":77,"stroke":78,"style":756},[19,1548,1469],{"x":1549,"y":1468,"style":1459},"468.9",[57,1551],{"x":1540,"y":1463,"width":1541,"height":1464,"rx":60,"fill":63,"stroke":78,"style":756},[19,1553,1554],{"x":1544,"y":1468,"style":1474},"sheet name",[57,1556],{"x":1537,"y":1477,"width":781,"height":1464,"rx":60,"fill":77,"stroke":78,"style":756},[19,1558,1480],{"x":1549,"y":807,"style":1459},[57,1560],{"x":1540,"y":1477,"width":1541,"height":1464,"rx":60,"fill":167,"stroke":168,"style":1483},[19,1562,1564],{"x":1544,"y":807,"style":1563},"font-size:12px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","North - East",[57,1566],{"x":1537,"y":1490,"width":781,"height":1464,"rx":60,"fill":77,"stroke":78,"style":756},[19,1568,1494],{"x":1549,"y":1493,"style":1459},[57,1570],{"x":1540,"y":1490,"width":1541,"height":1464,"rx":60,"fill":167,"stroke":168,"style":1483},[19,1572,1573],{"x":1544,"y":1493,"style":1563},"north - east (2)",[57,1575],{"x":1537,"y":1502,"width":781,"height":1464,"rx":60,"fill":77,"stroke":78,"style":756},[19,1577,747],{"x":1549,"y":1505,"style":1459},[57,1579],{"x":1540,"y":1502,"width":1541,"height":1464,"rx":60,"fill":167,"stroke":168,"style":1483},[19,1581,1582],{"x":1544,"y":1505,"style":1563},"Sales EU",[57,1584],{"x":1537,"y":1513,"width":781,"height":1464,"rx":60,"fill":77,"stroke":78,"style":756},[19,1586,1517],{"x":1549,"y":1516,"style":1459},[57,1588],{"x":1540,"y":1513,"width":1541,"height":1464,"rx":60,"fill":167,"stroke":168,"style":1483},[19,1590,1591],{"x":1544,"y":1516,"style":1563},"Quarterly revenue by region 202",[57,1593],{"x":1537,"y":1525,"width":781,"height":1464,"rx":60,"fill":77,"stroke":78,"style":756},[19,1595,1529],{"x":1549,"y":1528,"style":1459},[57,1597],{"x":1540,"y":1525,"width":1541,"height":1464,"rx":60,"fill":167,"stroke":168,"style":1483},[19,1599,1600],{"x":1544,"y":1528,"style":1563},"Sheet",[19,1602,1606],{"x":1603,"y":1604,"style":1605},"182.8","29.0","font-size:12.5px;font-weight:700;fill:var(--muted,#5b6780);text-anchor:middle","labels from the data",[19,1608,1610],{"x":1609,"y":1604,"style":1605},"581.3","sheet names written",[200,1612],{"x1":1613,"y1":1614,"x2":1615,"y2":1614,"stroke":147,"style":79},"325.2","136.0","438.9",[1617,1618],"polygon",{"points":1619,"fill":1620},"442.9,136.0 433.9,141.0 433.9,131.0","#5b5cf0",[57,1622],{"x":1623,"y":1624,"width":1625,"height":1626,"rx":1627,"fill":63,"stroke":78,"style":761},"345.0","103.0","78.2","22.0","11",[19,1629,1633],{"x":1630,"y":1631,"style":1632},"384.1","118.0","font-size:11.5px;font-weight:400;fill:var(--brand-strong,#4338ca);text-anchor:middle;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace","sanitise",[186,1635,1637],{"id":1636},"step-4-use-it-when-writing-one-sheet-per-group","Step 4 — Use it when writing one sheet per group",[10,1639,1640,1641,1644],{},"The function slots straight into the usual pandas pattern. The ",[22,1642,1643],{},"used"," set lives for the whole writer, so names are unique across every sheet in the file:",[14,1646,1648],{"className":221,"code":1647,"language":223,"meta":20,"style":20},"orders = pd.DataFrame({\n    \"customer\": [\"Smith \u002F Jones Ltd\", \"smith \u002F jones ltd\", \"A&B: Wholesale\", \"Northwind\"],\n    \"amount\": [120, 80, 45, 300],\n})\n\nused, index = set(), []\nwith pd.ExcelWriter(\"by_customer.xlsx\", engine=\"xlsxwriter\") as writer:\n    for customer, group in orders.groupby(\"customer\", sort=False):\n        name = safe_sheet_name(customer, used)\n        group.to_excel(writer, sheet_name=name, index=False)\n        index.append({\"customer\": customer, \"sheet\": name})\n\nprint(pd.ExcelFile(\"by_customer.xlsx\").sheet_names)\n",[22,1649,1650,1660,1689,1716,1721,1725,1737,1765,1793,1802,1823,1839,1843],{"__ignoreMap":20},[198,1651,1652,1655,1657],{"class":200,"line":201},[198,1653,1654],{"class":234},"orders ",[198,1656,290],{"class":230},[198,1658,1659],{"class":234}," pd.DataFrame({\n",[198,1661,1662,1665,1668,1671,1673,1676,1678,1681,1683,1686],{"class":200,"line":238},[198,1663,1664],{"class":208},"    \"customer\"",[198,1666,1667],{"class":234},": [",[198,1669,1670],{"class":208},"\"Smith \u002F Jones Ltd\"",[198,1672,299],{"class":234},[198,1674,1675],{"class":208},"\"smith \u002F jones ltd\"",[198,1677,299],{"class":234},[198,1679,1680],{"class":208},"\"A&B: Wholesale\"",[198,1682,299],{"class":234},[198,1684,1685],{"class":208},"\"Northwind\"",[198,1687,1688],{"class":234},"],\n",[198,1690,1691,1694,1696,1699,1701,1704,1706,1709,1711,1714],{"class":200,"line":252},[198,1692,1693],{"class":208},"    \"amount\"",[198,1695,1667],{"class":234},[198,1697,1698],{"class":313},"120",[198,1700,299],{"class":234},[198,1702,1703],{"class":313},"80",[198,1705,299],{"class":234},[198,1707,1708],{"class":313},"45",[198,1710,299],{"class":234},[198,1712,1713],{"class":313},"300",[198,1715,1688],{"class":234},[198,1717,1718],{"class":200,"line":266},[198,1719,1720],{"class":234},"})\n",[198,1722,1723],{"class":200,"line":370},[198,1724,343],{"emptyLinePlaceholder":342},[198,1726,1727,1730,1732,1734],{"class":200,"line":379},[198,1728,1729],{"class":234},"used, index ",[198,1731,290],{"class":230},[198,1733,1334],{"class":313},[198,1735,1736],{"class":234},"(), []\n",[198,1738,1739,1742,1745,1748,1750,1753,1755,1758,1760,1762],{"class":200,"line":405},[198,1740,1741],{"class":230},"with",[198,1743,1744],{"class":234}," pd.ExcelWriter(",[198,1746,1747],{"class":208},"\"by_customer.xlsx\"",[198,1749,299],{"class":234},[198,1751,1752],{"class":388},"engine",[198,1754,290],{"class":230},[198,1756,1757],{"class":208},"\"xlsxwriter\"",[198,1759,397],{"class":234},[198,1761,246],{"class":230},[198,1763,1764],{"class":234}," writer:\n",[198,1766,1767,1770,1773,1775,1778,1781,1783,1786,1788,1791],{"class":200,"line":417},[198,1768,1769],{"class":230},"    for",[198,1771,1772],{"class":234}," customer, group ",[198,1774,364],{"class":230},[198,1776,1777],{"class":234}," orders.groupby(",[198,1779,1780],{"class":208},"\"customer\"",[198,1782,299],{"class":234},[198,1784,1785],{"class":388},"sort",[198,1787,290],{"class":230},[198,1789,1790],{"class":313},"False",[198,1792,1169],{"class":234},[198,1794,1795,1797,1799],{"class":200,"line":428},[198,1796,1276],{"class":234},[198,1798,290],{"class":230},[198,1800,1801],{"class":234}," safe_sheet_name(customer, used)\n",[198,1803,1804,1807,1809,1811,1814,1817,1819,1821],{"class":200,"line":472},[198,1805,1806],{"class":234},"        group.to_excel(writer, ",[198,1808,726],{"class":388},[198,1810,290],{"class":230},[198,1812,1813],{"class":234},"name, ",[198,1815,1816],{"class":388},"index",[198,1818,290],{"class":230},[198,1820,1790],{"class":313},[198,1822,414],{"class":234},[198,1824,1825,1828,1830,1833,1836],{"class":200,"line":521},[198,1826,1827],{"class":234},"        index.append({",[198,1829,1780],{"class":208},[198,1831,1832],{"class":234},": customer, ",[198,1834,1835],{"class":208},"\"sheet\"",[198,1837,1838],{"class":234},": name})\n",[198,1840,1841],{"class":200,"line":536},[198,1842,343],{"emptyLinePlaceholder":342},[198,1844,1845,1848,1851,1853],{"class":200,"line":569},[198,1846,1847],{"class":313},"print",[198,1849,1850],{"class":234},"(pd.ExcelFile(",[198,1852,1747],{"class":208},[198,1854,1855],{"class":234},").sheet_names)\n",[10,1857,1858,1859,1863],{},"Using xlsxwriter here is a deliberate choice: because it raises on every broken rule, a bug in the naming function shows up as an error instead of as a file Excel quietly repairs. With openpyxl as the engine, the same code works, but a mistake would slip through. ",[32,1860,1862],{"href":1861},"\u002Fgetting-started-with-python-excel-automation\u002Fworking-with-multiple-excel-sheets-in-python\u002Fcreate-a-sheet-for-each-group-in-a-dataframe\u002F","Create a Sheet for Each Group in a DataFrame"," covers the grouping side in more depth.",[186,1865,1867],{"id":1866},"step-5-keep-the-original-labels-readable","Step 5 — Keep the original labels readable",[10,1869,1870],{},"Once names are cleaned and truncated, the sheet tab no longer tells a reader the full customer name. Put the mapping on a first sheet so nothing is lost, with links to each tab:",[14,1872,1874],{"className":221,"code":1873,"language":223,"meta":20,"style":20},"from openpyxl import load_workbook\n\nwb = load_workbook(\"by_customer.xlsx\")\ntoc = wb.create_sheet(\"Index\", 0)\ntoc.append([\"Customer\", \"Sheet\"])\nfor row, item in enumerate(index, start=2):\n    toc.cell(row=row, column=1, value=item[\"customer\"])\n    link = toc.cell(row=row, column=2, value=item[\"sheet\"])\n    link.hyperlink = f\"#'{item['sheet']}'!A1\"\n    link.style = \"Hyperlink\"\nwb.save(\"by_customer.xlsx\")\nprint(load_workbook(\"by_customer.xlsx\").sheetnames)\n",[22,1875,1876,1887,1891,1904,1923,1938,1962,1996,2030,2057,2067,2076],{"__ignoreMap":20},[198,1877,1878,1880,1882,1884],{"class":200,"line":201},[198,1879,255],{"class":230},[198,1881,258],{"class":234},[198,1883,231],{"class":230},[198,1885,1886],{"class":234}," load_workbook\n",[198,1888,1889],{"class":200,"line":238},[198,1890,343],{"emptyLinePlaceholder":342},[198,1892,1893,1895,1897,1900,1902],{"class":200,"line":252},[198,1894,348],{"class":234},[198,1896,290],{"class":230},[198,1898,1899],{"class":234}," load_workbook(",[198,1901,1747],{"class":208},[198,1903,414],{"class":234},[198,1905,1906,1909,1911,1914,1917,1919,1921],{"class":200,"line":266},[198,1907,1908],{"class":234},"toc ",[198,1910,290],{"class":230},[198,1912,1913],{"class":234}," wb.create_sheet(",[198,1915,1916],{"class":208},"\"Index\"",[198,1918,299],{"class":234},[198,1920,60],{"class":313},[198,1922,414],{"class":234},[198,1924,1925,1928,1931,1933,1935],{"class":200,"line":370},[198,1926,1927],{"class":234},"toc.append([",[198,1929,1930],{"class":208},"\"Customer\"",[198,1932,299],{"class":234},[198,1934,1166],{"class":208},[198,1936,1937],{"class":234},"])\n",[198,1939,1940,1942,1945,1947,1950,1953,1956,1958,1960],{"class":200,"line":379},[198,1941,358],{"class":230},[198,1943,1944],{"class":234}," row, item ",[198,1946,364],{"class":230},[198,1948,1949],{"class":313}," enumerate",[198,1951,1952],{"class":234},"(index, ",[198,1954,1955],{"class":388},"start",[198,1957,290],{"class":230},[198,1959,1480],{"class":313},[198,1961,1169],{"class":234},[198,1963,1964,1967,1970,1972,1975,1978,1980,1982,1984,1987,1989,1992,1994],{"class":200,"line":405},[198,1965,1966],{"class":234},"    toc.cell(",[198,1968,1969],{"class":388},"row",[198,1971,290],{"class":230},[198,1973,1974],{"class":234},"row, ",[198,1976,1977],{"class":388},"column",[198,1979,290],{"class":230},[198,1981,1469],{"class":313},[198,1983,299],{"class":234},[198,1985,1986],{"class":388},"value",[198,1988,290],{"class":230},[198,1990,1991],{"class":234},"item[",[198,1993,1780],{"class":208},[198,1995,1937],{"class":234},[198,1997,1998,2001,2003,2006,2008,2010,2012,2014,2016,2018,2020,2022,2024,2026,2028],{"class":200,"line":417},[198,1999,2000],{"class":234},"    link ",[198,2002,290],{"class":230},[198,2004,2005],{"class":234}," toc.cell(",[198,2007,1969],{"class":388},[198,2009,290],{"class":230},[198,2011,1974],{"class":234},[198,2013,1977],{"class":388},[198,2015,290],{"class":230},[198,2017,1480],{"class":313},[198,2019,299],{"class":234},[198,2021,1986],{"class":388},[198,2023,290],{"class":230},[198,2025,1991],{"class":234},[198,2027,1835],{"class":208},[198,2029,1937],{"class":234},[198,2031,2032,2035,2037,2039,2042,2044,2046,2049,2052,2054],{"class":200,"line":428},[198,2033,2034],{"class":234},"    link.hyperlink ",[198,2036,290],{"class":230},[198,2038,436],{"class":230},[198,2040,2041],{"class":208},"\"#'",[198,2043,443],{"class":442},[198,2045,1991],{"class":234},[198,2047,2048],{"class":208},"'sheet'",[198,2050,2051],{"class":234},"]",[198,2053,454],{"class":442},[198,2055,2056],{"class":208},"'!A1\"\n",[198,2058,2059,2062,2064],{"class":200,"line":472},[198,2060,2061],{"class":234},"    link.style ",[198,2063,290],{"class":230},[198,2065,2066],{"class":208}," \"Hyperlink\"\n",[198,2068,2069,2072,2074],{"class":200,"line":521},[198,2070,2071],{"class":234},"wb.save(",[198,2073,1747],{"class":208},[198,2075,414],{"class":234},[198,2077,2078,2080,2083,2085],{"class":200,"line":536},[198,2079,1847],{"class":313},[198,2081,2082],{"class":234},"(load_workbook(",[198,2084,1747],{"class":208},[198,2086,2087],{"class":234},").sheetnames)\n",[10,2089,2090,2091,2094,2095,2099],{},"Note the quotes in the link target: ",[22,2092,2093],{},"'{sheet}'!A1"," is how Excel refers to a sheet whose name contains spaces or punctuation, the same quoting that makes leading and trailing apostrophes illegal. ",[32,2096,2098],{"href":2097},"\u002Fautomating-reporting-workflows\u002Fbuilding-multi-sheet-excel-dashboards\u002Fadd-a-table-of-contents-sheet-with-hyperlinks-in-excel\u002F","Add a Table of Contents Sheet with Hyperlinks in Excel"," builds a fuller version.",[186,2101,2103],{"id":2102},"common-pitfalls","Common pitfalls",[2105,2106,2107,2115,2125,2137,2150],"ul",{},[2108,2109,2110,2114],"li",{},[2111,2112,2113],"strong",{},"Truncating before cleaning."," Replacing characters can change the length; always clean, then cut to 31.",[2108,2116,2117,2120,2121,2124],{},[2111,2118,2119],{},"Checking uniqueness case-sensitively."," Excel does not, so ",[22,2122,2123],{},"\"Sales\" != \"sales\""," in Python is not a useful test. Compare lower-cased names.",[2108,2126,2127,2130,2131,2133,2134,2136],{},[2111,2128,2129],{},"Relying on openpyxl's automatic rename."," ",[22,2132,707],{}," is unique but unexpected, and a later ",[22,2135,870],{}," returns the other sheet.",[2108,2138,2139,2142,2143,2145,2146,2149],{},[2111,2140,2141],{},"Forgetting existing sheets."," When adding sheets to a workbook that already has some, seed ",[22,2144,1643],{}," with ",[22,2147,2148],{},"{n.lower() for n in wb.sheetnames}"," first.",[2108,2151,2152,2155],{},[2111,2153,2154],{},"Using sheet names as data keys."," The name is a display label that may be shortened; keep the real key in the data or the index sheet.",[186,2157,2159],{"id":2158},"performance-notes","Performance notes",[10,2161,2162,2163,2167],{},"Name handling is negligible next to writing the data. The only scaling concern is the uniqueness loop, which is linear in the number of collisions for one base name — irrelevant for the dozens or hundreds of sheets a workbook can reasonably hold. If you find yourself generating thousands of sheets, the report probably wants one sheet with a filter instead; see ",[32,2164,2166],{"href":2165},"\u002Fgetting-started-with-python-excel-automation\u002Fworking-with-multiple-excel-sheets-in-python\u002Fsplit-one-excel-sheet-into-multiple-files-by-value\u002F","Split One Excel Sheet into Multiple Files by Column Value"," for the separate-files alternative.",[186,2169,2171],{"id":2170},"conclusion","Conclusion",[10,2173,2174,2175,2178],{},"Sheet names must be non-empty, free of ",[22,2176,2177],{},"[ ] : * ? \u002F \\",", without an apostrophe at either end, at most 31 characters and unique ignoring case. openpyxl enforces only the character rule strictly and quietly works around the others; xlsxwriter raises on all of them. Clean characters, truncate, then de-duplicate case-insensitively in one function, write with an engine that fails loudly, and keep an index sheet that maps each tab back to the full label.",[186,2180,2182],{"id":2181},"frequently-asked-questions","Frequently asked questions",[10,2184,2185,2188,2189,2191],{},[2111,2186,2187],{},"What characters are not allowed in an Excel sheet name?","\nThe seven characters ",[198,2190],{}," : * ? \u002F and \\ are forbidden. A name also cannot be empty, cannot be longer than 31 characters, and cannot start or end with an apostrophe.",[10,2193,2194,2197],{},[2111,2195,2196],{},"Why does openpyxl accept a 40-character sheet name?","\nopenpyxl only warns (\"Title is more than 31 characters\") and writes the long name anyway, producing a file that breaks a limit Excel enforces. Truncate names yourself so the file stays within the rules.",[10,2199,2200,2203],{},[2111,2201,2202],{},"Are sheet names case-sensitive?","\nNo. Excel treats \"Sales\" and \"sales\" as the same name. xlsxwriter raises DuplicateWorksheetName, and openpyxl silently renames the second sheet to \"sales1\", which can break code that looks the sheet up later.",[10,2205,2206,2209],{},[2111,2207,2208],{},"Can I call a sheet History?","\nAvoid it. Excel reserves the name History for its change-tracking feature and will not let a user give a sheet that name, so a generated sheet called History is an accident waiting to happen. Rename it, for example to \"History data\".",[186,2211,2213],{"id":2212},"related","Related",[2105,2215,2216,2222,2229,2236],{},[2108,2217,2218,2219,2221],{},"Up one level: ",[32,2220,35],{"href":34}," — other errors raised while writing.",[2108,2223,2224,2228],{},[32,2225,2227],{"href":2226},"\u002Fgetting-started-with-python-excel-automation\u002Fworking-with-multiple-excel-sheets-in-python\u002Frename-reorder-and-delete-excel-sheets-with-openpyxl\u002F","Rename, Reorder and Delete Excel Sheets with openpyxl"," — managing sheets after they exist.",[2108,2230,2231,2235],{},[32,2232,2234],{"href":2233},"\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-worksheet-does-not-exist-keyerror-in-openpyxl\u002F","Fix \"Worksheet Does Not Exist\" KeyError in openpyxl"," — the lookup side of sheet names.",[2108,2237,2238,2242],{},[32,2239,2241],{"href":2240},"\u002Fautomating-reporting-workflows\u002Fgenerating-excel-reports-from-templates\u002Fgenerate-one-excel-report-per-region-in-a-loop\u002F","Generate One Excel Report per Region in a Loop"," — the same naming problem for file names.",[2244,2245,2246],"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 .sP0c6, html code.shiki .sP0c6{--shiki-default:#005CC5;--shiki-dark:#91CBFF}html pre.shiki code .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}html pre.shiki code .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}html pre.shiki code .s_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}",{"title":20,"searchDepth":238,"depth":238,"links":2248},[2249,2250,2251,2252,2253,2254,2255,2256,2257,2258,2259],{"id":188,"depth":238,"text":189},{"id":273,"depth":238,"text":274},{"id":882,"depth":238,"text":883},{"id":1142,"depth":238,"text":1143},{"id":1636,"depth":238,"text":1637},{"id":1866,"depth":238,"text":1867},{"id":2102,"depth":238,"text":2103},{"id":2158,"depth":238,"text":2159},{"id":2170,"depth":238,"text":2171},{"id":2181,"depth":238,"text":2182},{"id":2212,"depth":238,"text":2213},"2026-09-25","Sheet names built from data — regions, customers, dates — break Excel's rules: 31 characters at most, none of [ ] : * ? \u002F \\, no leading or trailing apostrophe, unique regardless of case. See what openpyxl, xlsxwriter and pandas do with each rule and write one function that makes any name safe.","md",[2264,2266,2268,2270],{"q":2187,"a":2265},"The seven characters [ ] : * ? \u002F and \\ are forbidden. A name also cannot be empty, cannot be longer than 31 characters, and cannot start or end with an apostrophe.",{"q":2196,"a":2267},"openpyxl only warns (\"Title is more than 31 characters\") and writes the long name anyway, producing a file that breaks a limit Excel enforces. Truncate names yourself so the file stays within the rules.",{"q":2202,"a":2269},"No. Excel treats \"Sales\" and \"sales\" as the same name. xlsxwriter raises DuplicateWorksheetName, and openpyxl silently renames the second sheet to \"sales1\", which can break code that looks the sheet up later.",{"q":2208,"a":2271},"Avoid it. Excel reserves the name History for its change-tracking feature and will not let a user give a sheet that name, so a generated sheet called History is an accident waiting to happen. Rename it, for example to \"History data\".","fix invalid worksheet name errors when writing excel",null,{"breadcrumb":2275},[2276,2279,2282],{"name":2277,"item":2278},"Home","\u002F",{"name":2280,"item":2281},"Getting Started with Python Excel Automation","\u002Fgetting-started-with-python-excel-automation\u002F",{"name":35,"item":34},"\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-invalid-worksheet-name-errors",{"title":2285,"description":2286},"Fix \"Invalid character \u002F found in sheet title\" and Sheet Name Errors","Fix openpyxl \"Invalid character found in sheet title\", xlsxwriter InvalidWorksheetName and DuplicateWorksheetName, and names over 31 characters with a safe sheet-name function.","fix-invalid-worksheet-name-errors","getting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-invalid-worksheet-name-errors\u002Findex",[2290,2291,2292],"troubleshooting","sheets-and-files","writing-excel","how-to","V2HUKw30PGjbenD_URZDof8cbzkOwF7Vk_0w1X9AkNc",[2296,2300],{"title":2297,"path":2298,"stem":2299,"children":-1},"Fix IllegalCharacterError When Writing Excel with openpyxl","\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-illegalcharactererror-when-writing-excel-with-openpyxl","getting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-illegalcharactererror-when-writing-excel-with-openpyxl\u002Findex",{"title":2301,"path":2302,"stem":2303,"children":-1},"Fix KeyError from Spaces in Excel Column Names","\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-keyerror-from-spaces-in-excel-column-names","getting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-keyerror-from-spaces-in-excel-column-names\u002Findex",[2305,2307],{"path":2306,"title":2280},"\u002Fgetting-started-with-python-excel-automation",{"path":2308,"title":35},"\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors",[],[2311,2315,2318,2321,2324,2327],{"path":2312,"title":2313,"kind":2314},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fhandle-permission-denied-when-writing-excel-in-python","Handle Permission Denied When Writing Excel in Python","fix",{"path":2316,"title":2317,"kind":2293},"\u002Fgetting-started-with-python-excel-automation\u002Fusing-openpyxl-for-excel-file-manipulation\u002Fwhat-openpyxl-loses-when-saving-an-existing-workbook","What openpyxl Loses When You Save an Existing Workbook",{"path":2319,"title":2320,"kind":2314},"\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-dates-and-times-in-excel-data\u002Ffix-excel-serial-numbers-showing-instead-of-dates","Fix Excel Serial Numbers Showing Instead of Dates",{"path":2322,"title":2323,"kind":2293},"\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-dates-and-times-in-excel-data\u002Fhandle-daylight-saving-time-in-excel-timestamps","Handle Daylight Saving Time in Excel Timestamps",{"path":2325,"title":2326,"kind":2293},"\u002Fautomating-reporting-workflows\u002Fautomating-excel-online-with-microsoft-graph\u002Fhandle-microsoft-graph-throttling-for-excel","Handle Microsoft Graph Throttling in Excel Automation",{"path":2328,"title":2329,"kind":2330},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation","Error Handling and Logging in Excel Automation","guide",1790972065850]