[{"data":1,"prerenderedAt":2446},["ShallowReactive",2],{"doc:\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-illegalcharactererror-when-writing-excel-with-openpyxl":3,"surround:\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-illegalcharactererror-when-writing-excel-with-openpyxl":2410,"ancestors:\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-illegalcharactererror-when-writing-excel-with-openpyxl":2419,"children:\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-illegalcharactererror-when-writing-excel-with-openpyxl":2424,"related:\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-illegalcharactererror-when-writing-excel-with-openpyxl":2425},{"id":4,"title":5,"body":6,"dateModified":2375,"datePublished":2375,"description":2376,"extension":2377,"faq":2378,"keyword":2387,"kind":2388,"meta":2389,"navigation":195,"path":2398,"seo":2399,"slug":2402,"stem":2403,"topics":2404,"type":2408,"__hash__":2409},"docs\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-illegalcharactererror-when-writing-excel-with-openpyxl\u002Findex.md","Fix IllegalCharacterError When Writing Excel with openpyxl",{"type":7,"value":8,"toc":2360},"minimark",[9,33,128,133,164,167,307,311,317,422,431,435,442,675,691,856,860,871,1106,1120,1124,1135,1251,1270,1281,1370,1374,1381,1456,1467,1471,1474,1937,1940,1944,1947,2121,2128,2132,2250,2254,2263,2267,2281,2285,2292,2298,2308,2314,2318,2356],[10,11,12,13,17,18,22,23,26,27,32],"p",{},"A report job that ran for months fails one morning with ",[14,15,16],"code",{},"openpyxl.utils.exceptions.IllegalCharacterError",",\nfollowed by a customer name and the words ",[19,20,21],"em",{},"cannot be used in worksheets",". The name looks perfectly ordinary in\nthe log. The problem is a character you cannot see: a vertical tab copied out of a Word document, a null byte from a\nlegacy database field, an escape code from a terminal. The ",[14,24,25],{},".xlsx"," format is XML, and XML forbids most control\ncharacters, so openpyxl refuses to write them rather than produce a file Excel would reject. This guide, part of\n",[28,29,31],"a",{"href":30},"\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002F","Troubleshooting Common Python Excel Errors",",\nfinds and fixes them.",[34,35,43,44,43,48,43,52,43,59,43,68,43,75,43,80,43,83,43,87,43,90,43,95,43,100,43,103,43,106,43,110,43,113,43,116,43,120,43,123],"svg",{"viewBox":36,"role":37,"ariaLabelledBy":38,"xmlns":41,"style":42},"0 0 720 380","img",[39,40],"ill-chars-t","ill-chars-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:720px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[45,46,47],"title",{"id":39},"Which characters an xlsx cell can hold",[49,50,51],"desc",{"id":40},"Printable text and tab, line feed and carriage return are allowed. Other ASCII control characters from 0 to 8, 11 and 12, and 14 to 31 are forbidden by XML, which is exactly the set openpyxl rejects.",[53,54],"rect",{"x":55,"y":55,"width":56,"height":57,"fill":58},"0","720","380","#ffffff",[53,60],{"x":61,"y":61,"width":62,"height":63,"rx":64,"fill":65,"stroke":66,"style":67},"20","680","52","12","#d9f4f1","var(--teal,#0f9488)","stroke-width:2px",[69,70,74],"text",{"x":71,"y":72,"style":73},"40","51","font-size:14px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:start","printable text",[69,76,79],{"x":77,"y":72,"style":78},"270","font-size:12px;font-weight:400;fill:var(--text,#172033);text-anchor:start","letters, digits, punctuation, accents, emoji",[53,81],{"x":61,"y":82,"width":62,"height":63,"rx":64,"fill":65,"stroke":66,"style":67},"84",[69,84,86],{"x":71,"y":85,"style":73},"115","\\t \\n \\r",[69,88,89],{"x":77,"y":85,"style":78},"tab, line feed, carriage return: allowed",[53,91],{"x":61,"y":92,"width":62,"height":63,"rx":64,"fill":93,"stroke":94,"style":67},"148","#fee8f2","var(--accent,#f43f8f)",[69,96,99],{"x":71,"y":97,"style":98},"179","font-size:14px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:start","\\x00 – \\x08",[69,101,102],{"x":77,"y":97,"style":78},"null and other control codes: rejected",[53,104],{"x":61,"y":105,"width":62,"height":63,"rx":64,"fill":93,"stroke":94,"style":67},"212",[69,107,109],{"x":71,"y":108,"style":98},"243","\\x0b \\x0c",[69,111,112],{"x":77,"y":108,"style":78},"vertical tab, form feed: rejected",[53,114],{"x":61,"y":115,"width":62,"height":63,"rx":64,"fill":93,"stroke":94,"style":67},"276",[69,117,119],{"x":71,"y":118,"style":98},"307","\\x0e – \\x1f",[69,121,122],{"x":77,"y":118,"style":78},"shift, escape and separators: rejected",[69,124,127],{"x":125,"y":125,"style":126},"360","font-size:12.5px;font-weight:400;fill:var(--muted,#5b6780);text-anchor:middle","the error is XML rules, not an openpyxl limitation",[129,130,132],"h2",{"id":131},"prerequisites","Prerequisites",[134,135,140],"pre",{"className":136,"code":137,"language":138,"meta":139,"style":139},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install pandas openpyxl xlsxwriter\n","bash","",[14,141,142],{"__ignoreMap":139},[143,144,147,151,155,158,161],"span",{"class":145,"line":146},"line",1,[143,148,150],{"class":149},"sMTad","pip",[143,152,154],{"class":153},"srMev"," install",[143,156,157],{"class":153}," pandas",[143,159,160],{"class":153}," openpyxl",[143,162,163],{"class":153}," xlsxwriter\n",[10,165,166],{},"A frame with the two most common offenders — a vertical tab, which Word and some PDF extractors use as a line break, and a null byte from a fixed-width database export:",[134,168,172],{"className":169,"code":170,"language":171,"meta":139,"style":139},"language-python shiki shiki-themes github-light github-dark-high-contrast","import pandas as pd\n\ncustomers = pd.DataFrame({\n    \"Customer\": [\"Acme\\x0bLtd\", \"Globex\", \"Initech\"],\n    \"Note\": [\"priority\", \"line one\\x00\", \"call\\x1bback\"],\n    \"Amount\": [1250.0, 980.0, 430.5],\n})\n","python",[14,173,174,190,197,209,242,277,301],{"__ignoreMap":139},[143,175,176,180,184,187],{"class":145,"line":146},[143,177,179],{"class":178},"s-kum","import",[143,181,183],{"class":182},"skGVy"," pandas ",[143,185,186],{"class":178},"as",[143,188,189],{"class":182}," pd\n",[143,191,193],{"class":145,"line":192},2,[143,194,196],{"emptyLinePlaceholder":195},true,"\n",[143,198,200,203,206],{"class":145,"line":199},3,[143,201,202],{"class":182},"customers ",[143,204,205],{"class":178},"=",[143,207,208],{"class":182}," pd.DataFrame({\n",[143,210,212,215,218,221,225,228,231,234,236,239],{"class":145,"line":211},4,[143,213,214],{"class":153},"    \"Customer\"",[143,216,217],{"class":182},": [",[143,219,220],{"class":153},"\"Acme",[143,222,224],{"class":223},"sSjpA","\\x0b",[143,226,227],{"class":153},"Ltd\"",[143,229,230],{"class":182},", ",[143,232,233],{"class":153},"\"Globex\"",[143,235,230],{"class":182},[143,237,238],{"class":153},"\"Initech\"",[143,240,241],{"class":182},"],\n",[143,243,245,248,250,253,255,258,261,264,266,269,272,275],{"class":145,"line":244},5,[143,246,247],{"class":153},"    \"Note\"",[143,249,217],{"class":182},[143,251,252],{"class":153},"\"priority\"",[143,254,230],{"class":182},[143,256,257],{"class":153},"\"line one",[143,259,260],{"class":223},"\\x00",[143,262,263],{"class":153},"\"",[143,265,230],{"class":182},[143,267,268],{"class":153},"\"call",[143,270,271],{"class":223},"\\x1b",[143,273,274],{"class":153},"back\"",[143,276,241],{"class":182},[143,278,280,283,285,289,291,294,296,299],{"class":145,"line":279},6,[143,281,282],{"class":153},"    \"Amount\"",[143,284,217],{"class":182},[143,286,288],{"class":287},"sP0c6","1250.0",[143,290,230],{"class":182},[143,292,293],{"class":287},"980.0",[143,295,230],{"class":182},[143,297,298],{"class":287},"430.5",[143,300,241],{"class":182},[143,302,304],{"class":145,"line":303},7,[143,305,306],{"class":182},"})\n",[129,308,310],{"id":309},"step-1-reproduce-the-error","Step 1 — Reproduce the error",[10,312,313,316],{},[14,314,315],{},"to_excel"," with the openpyxl engine raises on the first bad cell it meets. The message includes the offending value, but because the character is invisible, it prints as if nothing\nwere wrong.",[134,318,320],{"className":169,"code":319,"language":171,"meta":139,"style":139},"from openpyxl.utils.exceptions import IllegalCharacterError\n\ntry:\n    customers.to_excel(\"customers.xlsx\", index=False, engine=\"openpyxl\")\nexcept IllegalCharacterError as error:\n    print(\"IllegalCharacterError:\", repr(str(error)))\n# IllegalCharacterError: 'Acme\\x0bLtd cannot be used in worksheets.'\n",[14,321,322,335,339,347,379,392,416],{"__ignoreMap":139},[143,323,324,327,330,332],{"class":145,"line":146},[143,325,326],{"class":178},"from",[143,328,329],{"class":182}," openpyxl.utils.exceptions ",[143,331,179],{"class":178},[143,333,334],{"class":182}," IllegalCharacterError\n",[143,336,337],{"class":145,"line":192},[143,338,196],{"emptyLinePlaceholder":195},[143,340,341,344],{"class":145,"line":199},[143,342,343],{"class":178},"try",[143,345,346],{"class":182},":\n",[143,348,349,352,355,357,361,363,366,368,371,373,376],{"class":145,"line":211},[143,350,351],{"class":182},"    customers.to_excel(",[143,353,354],{"class":153},"\"customers.xlsx\"",[143,356,230],{"class":182},[143,358,360],{"class":359},"sa561","index",[143,362,205],{"class":178},[143,364,365],{"class":287},"False",[143,367,230],{"class":182},[143,369,370],{"class":359},"engine",[143,372,205],{"class":178},[143,374,375],{"class":153},"\"openpyxl\"",[143,377,378],{"class":182},")\n",[143,380,381,384,387,389],{"class":145,"line":244},[143,382,383],{"class":178},"except",[143,385,386],{"class":182}," IllegalCharacterError ",[143,388,186],{"class":178},[143,390,391],{"class":182}," error:\n",[143,393,394,397,400,403,405,408,410,413],{"class":145,"line":279},[143,395,396],{"class":287},"    print",[143,398,399],{"class":182},"(",[143,401,402],{"class":153},"\"IllegalCharacterError:\"",[143,404,230],{"class":182},[143,406,407],{"class":287},"repr",[143,409,399],{"class":182},[143,411,412],{"class":287},"str",[143,414,415],{"class":182},"(error)))\n",[143,417,418],{"class":145,"line":303},[143,419,421],{"class":420},"s-wDw","# IllegalCharacterError: 'Acme\\x0bLtd cannot be used in worksheets.'\n",[10,423,424,425,427,428,430],{},"Using ",[14,426,407],{}," is the trick worth remembering: it shows ",[14,429,224],{}," where a normal print shows nothing. The error only reports the first failing cell, so fixing that one value and retrying\ntends to turn into a loop — find them all at once instead.",[129,432,434],{"id":433},"step-2-find-every-offending-cell","Step 2 — Find every offending cell",[10,436,437,438,441],{},"openpyxl exposes the exact pattern it checks as ",[14,439,440],{},"ILLEGAL_CHARACTERS_RE",". Apply it to the text columns of the frame and list each row, column and the characters found, so the report is\ncomplete in one pass.",[134,443,445],{"className":169,"code":444,"language":171,"meta":139,"style":139},"from openpyxl.cell.cell import ILLEGAL_CHARACTERS_RE\n\ndef find_illegal(frame: pd.DataFrame) -> pd.DataFrame:\n    found = []\n    for column in frame.select_dtypes(include=[\"object\", \"string\"]).columns:\n        for index, value in frame[column].items():\n            if isinstance(value, str):\n                characters = sorted({f\"\\\\x{ord(ch):02x}\" for ch in ILLEGAL_CHARACTERS_RE.findall(value)})\n                if characters:\n                    found.append({\"row\": index, \"column\": column, \"characters\": \" \".join(characters), \"value\": repr(value)})\n    return pd.DataFrame(found)\n\nprint(find_illegal(customers))\n",[14,446,447,459,463,475,485,518,531,547,603,612,652,661,666],{"__ignoreMap":139},[143,448,449,451,454,456],{"class":145,"line":146},[143,450,326],{"class":178},[143,452,453],{"class":182}," openpyxl.cell.cell ",[143,455,179],{"class":178},[143,457,458],{"class":287}," ILLEGAL_CHARACTERS_RE\n",[143,460,461],{"class":145,"line":192},[143,462,196],{"emptyLinePlaceholder":195},[143,464,465,468,472],{"class":145,"line":199},[143,466,467],{"class":178},"def",[143,469,471],{"class":470},"s_Opv"," find_illegal",[143,473,474],{"class":182},"(frame: pd.DataFrame) -> pd.DataFrame:\n",[143,476,477,480,482],{"class":145,"line":211},[143,478,479],{"class":182},"    found ",[143,481,205],{"class":178},[143,483,484],{"class":182}," []\n",[143,486,487,490,493,496,499,502,504,507,510,512,515],{"class":145,"line":244},[143,488,489],{"class":178},"    for",[143,491,492],{"class":182}," column ",[143,494,495],{"class":178},"in",[143,497,498],{"class":182}," frame.select_dtypes(",[143,500,501],{"class":359},"include",[143,503,205],{"class":178},[143,505,506],{"class":182},"[",[143,508,509],{"class":153},"\"object\"",[143,511,230],{"class":182},[143,513,514],{"class":153},"\"string\"",[143,516,517],{"class":182},"]).columns:\n",[143,519,520,523,526,528],{"class":145,"line":279},[143,521,522],{"class":178},"        for",[143,524,525],{"class":182}," index, value ",[143,527,495],{"class":178},[143,529,530],{"class":182}," frame[column].items():\n",[143,532,533,536,539,542,544],{"class":145,"line":303},[143,534,535],{"class":178},"            if",[143,537,538],{"class":287}," isinstance",[143,540,541],{"class":182},"(value, ",[143,543,412],{"class":287},[143,545,546],{"class":182},"):\n",[143,548,550,553,555,558,561,564,566,569,572,575,578,581,584,587,589,592,595,597,600],{"class":145,"line":549},8,[143,551,552],{"class":182},"                characters ",[143,554,205],{"class":178},[143,556,557],{"class":287}," sorted",[143,559,560],{"class":182},"({",[143,562,563],{"class":178},"f",[143,565,263],{"class":153},[143,567,568],{"class":223},"\\\\",[143,570,571],{"class":153},"x",[143,573,574],{"class":223},"{",[143,576,577],{"class":287},"ord",[143,579,580],{"class":182},"(ch)",[143,582,583],{"class":178},":02x",[143,585,586],{"class":223},"}",[143,588,263],{"class":153},[143,590,591],{"class":178}," for",[143,593,594],{"class":182}," ch ",[143,596,495],{"class":178},[143,598,599],{"class":287}," ILLEGAL_CHARACTERS_RE",[143,601,602],{"class":182},".findall(value)})\n",[143,604,606,609],{"class":145,"line":605},9,[143,607,608],{"class":178},"                if",[143,610,611],{"class":182}," characters:\n",[143,613,615,618,621,624,627,630,633,636,639,642,645,647,649],{"class":145,"line":614},10,[143,616,617],{"class":182},"                    found.append({",[143,619,620],{"class":153},"\"row\"",[143,622,623],{"class":182},": index, ",[143,625,626],{"class":153},"\"column\"",[143,628,629],{"class":182},": column, ",[143,631,632],{"class":153},"\"characters\"",[143,634,635],{"class":182},": ",[143,637,638],{"class":153},"\" \"",[143,640,641],{"class":182},".join(characters), ",[143,643,644],{"class":153},"\"value\"",[143,646,635],{"class":182},[143,648,407],{"class":287},[143,650,651],{"class":182},"(value)})\n",[143,653,655,658],{"class":145,"line":654},11,[143,656,657],{"class":178},"    return",[143,659,660],{"class":182}," pd.DataFrame(found)\n",[143,662,664],{"class":145,"line":663},12,[143,665,196],{"emptyLinePlaceholder":195},[143,667,669,672],{"class":145,"line":668},13,[143,670,671],{"class":287},"print",[143,673,674],{"class":182},"(find_illegal(customers))\n",[10,676,677,678,681,682,684,685,687,688,690],{},"The ",[14,679,680],{},"characters"," column is what drives the decision in the next step. A ",[14,683,224],{}," means the source intended a line break; a ",[14,686,260],{}," is padding or corruption; ",[14,689,271],{}," is usually the remains\nof terminal colour codes. Each deserves a different replacement.",[34,692,43,698,43,701,43,704,43,708,43,715,43,719,43,724,43,728,43,732,43,735,43,739,43,743,43,747,43,749,43,754,43,756,43,760,43,762,43,765,43,767,43,770,43,772,43,775,43,778,43,782,43,784,43,787,43,789,43,792,43,794,43,797,43,799,43,802,43,805,43,809,43,811,43,813,43,815,43,818,43,820,43,822,43,824,43,827,43,830,43,834,43,836,43,838,43,840,43,842,43,844,43,846,43,848,43,851],{"viewBox":693,"role":37,"ariaLabelledBy":694,"xmlns":41,"style":697},"0 0 456 224",[695,696],"ill-found-t","ill-found-d","width:100%;max-width:456px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[45,699,700],{"id":695},"Offending cells found in one pass",[49,702,703],{"id":696},"Row 0 Customer contains a vertical tab, row 1 Note contains a null byte, and row 2 Note contains an escape character. Amount is numeric and never checked.",[53,705],{"x":55,"y":55,"width":706,"height":707,"fill":58},"456","224",[53,709],{"x":61,"y":61,"width":710,"height":711,"rx":55,"fill":712,"stroke":713,"style":714},"36","30","#e7ebef","var(--line,#cdd5e6)","stroke-width:1px",[53,716],{"x":717,"y":61,"width":718,"height":711,"rx":55,"fill":712,"stroke":713,"style":714},"56","50",[69,720,723],{"x":721,"y":71,"style":722},"81","font-size:11.5px;font-weight:600;fill:var(--muted,#5b6780);text-anchor:middle","A",[53,725],{"x":726,"y":61,"width":727,"height":711,"rx":55,"fill":712,"stroke":713,"style":714},"106","90",[69,729,731],{"x":730,"y":71,"style":722},"151","B",[53,733],{"x":734,"y":61,"width":727,"height":711,"rx":55,"fill":712,"stroke":713,"style":714},"196",[69,736,738],{"x":737,"y":71,"style":722},"241","C",[53,740],{"x":741,"y":61,"width":742,"height":711,"rx":55,"fill":712,"stroke":713,"style":714},"286","150",[69,744,746],{"x":745,"y":71,"style":722},"361","D",[53,748],{"x":61,"y":718,"width":710,"height":711,"rx":55,"fill":712,"stroke":713,"style":714},[69,750,753],{"x":751,"y":752,"style":722},"38","70","1",[53,755],{"x":717,"y":718,"width":718,"height":711,"rx":55,"fill":712,"stroke":713,"style":714},[69,757,759],{"x":721,"y":752,"style":758},"font-size:11.5px;font-weight:400;fill:var(--muted,#5b6780);text-anchor:middle","row",[53,761],{"x":726,"y":718,"width":727,"height":711,"rx":55,"fill":712,"stroke":713,"style":714},[69,763,764],{"x":730,"y":752,"style":758},"column",[53,766],{"x":734,"y":718,"width":727,"height":711,"rx":55,"fill":712,"stroke":713,"style":714},[69,768,769],{"x":737,"y":752,"style":758},"found",[53,771],{"x":741,"y":718,"width":742,"height":711,"rx":55,"fill":712,"stroke":713,"style":714},[69,773,774],{"x":745,"y":752,"style":758},"likely origin",[53,776],{"x":61,"y":777,"width":710,"height":711,"rx":55,"fill":712,"stroke":713,"style":714},"80",[69,779,781],{"x":751,"y":780,"style":722},"100","2",[53,783],{"x":717,"y":777,"width":718,"height":711,"rx":55,"fill":58,"stroke":713,"style":714},[69,785,55],{"x":721,"y":780,"style":786},"font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle",[53,788],{"x":726,"y":777,"width":727,"height":711,"rx":55,"fill":58,"stroke":713,"style":714},[69,790,791],{"x":730,"y":780,"style":786},"Customer",[53,793],{"x":734,"y":777,"width":727,"height":711,"rx":55,"fill":93,"stroke":94,"style":714},[69,795,224],{"x":737,"y":780,"style":796},"font-size:11.5px;font-weight:600;fill:var(--accent-ink,#be185d);text-anchor:middle",[53,798],{"x":741,"y":777,"width":742,"height":711,"rx":55,"fill":58,"stroke":713,"style":714},[69,800,801],{"x":745,"y":780,"style":786},"Word line break",[53,803],{"x":61,"y":804,"width":710,"height":711,"rx":55,"fill":712,"stroke":713,"style":714},"110",[69,806,808],{"x":751,"y":807,"style":722},"130","3",[53,810],{"x":717,"y":804,"width":718,"height":711,"rx":55,"fill":58,"stroke":713,"style":714},[69,812,753],{"x":721,"y":807,"style":786},[53,814],{"x":726,"y":804,"width":727,"height":711,"rx":55,"fill":58,"stroke":713,"style":714},[69,816,817],{"x":730,"y":807,"style":786},"Note",[53,819],{"x":734,"y":804,"width":727,"height":711,"rx":55,"fill":93,"stroke":94,"style":714},[69,821,260],{"x":737,"y":807,"style":796},[53,823],{"x":741,"y":804,"width":742,"height":711,"rx":55,"fill":58,"stroke":713,"style":714},[69,825,826],{"x":745,"y":807,"style":786},"database padding",[53,828],{"x":61,"y":829,"width":710,"height":711,"rx":55,"fill":712,"stroke":713,"style":714},"140",[69,831,833],{"x":751,"y":832,"style":722},"160","4",[53,835],{"x":717,"y":829,"width":718,"height":711,"rx":55,"fill":58,"stroke":713,"style":714},[69,837,781],{"x":721,"y":832,"style":786},[53,839],{"x":726,"y":829,"width":727,"height":711,"rx":55,"fill":58,"stroke":713,"style":714},[69,841,817],{"x":730,"y":832,"style":786},[53,843],{"x":734,"y":829,"width":727,"height":711,"rx":55,"fill":93,"stroke":94,"style":714},[69,845,271],{"x":737,"y":832,"style":796},[53,847],{"x":741,"y":829,"width":742,"height":711,"rx":55,"fill":58,"stroke":713,"style":714},[69,849,850],{"x":745,"y":832,"style":786},"terminal colour code",[69,852,855],{"x":853,"y":854,"style":126},"228","202","repr() shows what print() hides",[129,857,859],{"id":858},"step-3-replace-with-meaning-then-remove-the-rest","Step 3 — Replace with meaning, then remove the rest",[10,861,862,863,866,867,870],{},"Deleting every illegal character works, but it glues words together: ",[14,864,865],{},"Acme\\x0bLtd"," becomes ",[14,868,869],{},"AcmeLtd",". Translate the characters that carry meaning first — vertical tab and form feed to a\nline break, which Excel displays in a wrapped cell — and only then strip whatever remains.",[134,872,874],{"className":169,"code":873,"language":171,"meta":139,"style":139},"MEANINGFUL = {\"\\x0b\": \"\\n\", \"\\x0c\": \"\\n\"}\n\ndef clean_text(value):\n    if not isinstance(value, str):\n        return value\n    for bad, good in MEANINGFUL.items():\n        value = value.replace(bad, good)\n    return ILLEGAL_CHARACTERS_RE.sub(\"\", value)\n\ncleaned = customers.copy()\nfor column in cleaned.select_dtypes(include=[\"object\", \"string\"]).columns:\n    cleaned[column] = cleaned[column].map(clean_text)\n\nprint(cleaned.to_dict(\"list\"))\ncleaned.to_excel(\"customers.xlsx\", index=False, engine=\"openpyxl\")\nprint(\"written\")\n",[14,875,876,922,926,936,952,960,975,985,1000,1004,1014,1040,1050,1054,1068,1094],{"__ignoreMap":139},[143,877,878,881,884,887,889,891,893,895,897,900,902,904,906,909,911,913,915,917,919],{"class":145,"line":146},[143,879,880],{"class":287},"MEANINGFUL",[143,882,883],{"class":178}," =",[143,885,886],{"class":182}," {",[143,888,263],{"class":153},[143,890,224],{"class":223},[143,892,263],{"class":153},[143,894,635],{"class":182},[143,896,263],{"class":153},[143,898,899],{"class":223},"\\n",[143,901,263],{"class":153},[143,903,230],{"class":182},[143,905,263],{"class":153},[143,907,908],{"class":223},"\\x0c",[143,910,263],{"class":153},[143,912,635],{"class":182},[143,914,263],{"class":153},[143,916,899],{"class":223},[143,918,263],{"class":153},[143,920,921],{"class":182},"}\n",[143,923,924],{"class":145,"line":192},[143,925,196],{"emptyLinePlaceholder":195},[143,927,928,930,933],{"class":145,"line":199},[143,929,467],{"class":178},[143,931,932],{"class":470}," clean_text",[143,934,935],{"class":182},"(value):\n",[143,937,938,941,944,946,948,950],{"class":145,"line":211},[143,939,940],{"class":178},"    if",[143,942,943],{"class":178}," not",[143,945,538],{"class":287},[143,947,541],{"class":182},[143,949,412],{"class":287},[143,951,546],{"class":182},[143,953,954,957],{"class":145,"line":244},[143,955,956],{"class":178},"        return",[143,958,959],{"class":182}," value\n",[143,961,962,964,967,969,972],{"class":145,"line":279},[143,963,489],{"class":178},[143,965,966],{"class":182}," bad, good ",[143,968,495],{"class":178},[143,970,971],{"class":287}," MEANINGFUL",[143,973,974],{"class":182},".items():\n",[143,976,977,980,982],{"class":145,"line":303},[143,978,979],{"class":182},"        value ",[143,981,205],{"class":178},[143,983,984],{"class":182}," value.replace(bad, good)\n",[143,986,987,989,991,994,997],{"class":145,"line":549},[143,988,657],{"class":178},[143,990,599],{"class":287},[143,992,993],{"class":182},".sub(",[143,995,996],{"class":153},"\"\"",[143,998,999],{"class":182},", value)\n",[143,1001,1002],{"class":145,"line":605},[143,1003,196],{"emptyLinePlaceholder":195},[143,1005,1006,1009,1011],{"class":145,"line":614},[143,1007,1008],{"class":182},"cleaned ",[143,1010,205],{"class":178},[143,1012,1013],{"class":182}," customers.copy()\n",[143,1015,1016,1019,1021,1023,1026,1028,1030,1032,1034,1036,1038],{"class":145,"line":654},[143,1017,1018],{"class":178},"for",[143,1020,492],{"class":182},[143,1022,495],{"class":178},[143,1024,1025],{"class":182}," cleaned.select_dtypes(",[143,1027,501],{"class":359},[143,1029,205],{"class":178},[143,1031,506],{"class":182},[143,1033,509],{"class":153},[143,1035,230],{"class":182},[143,1037,514],{"class":153},[143,1039,517],{"class":182},[143,1041,1042,1045,1047],{"class":145,"line":663},[143,1043,1044],{"class":182},"    cleaned[column] ",[143,1046,205],{"class":178},[143,1048,1049],{"class":182}," cleaned[column].map(clean_text)\n",[143,1051,1052],{"class":145,"line":668},[143,1053,196],{"emptyLinePlaceholder":195},[143,1055,1057,1059,1062,1065],{"class":145,"line":1056},14,[143,1058,671],{"class":287},[143,1060,1061],{"class":182},"(cleaned.to_dict(",[143,1063,1064],{"class":153},"\"list\"",[143,1066,1067],{"class":182},"))\n",[143,1069,1071,1074,1076,1078,1080,1082,1084,1086,1088,1090,1092],{"class":145,"line":1070},15,[143,1072,1073],{"class":182},"cleaned.to_excel(",[143,1075,354],{"class":153},[143,1077,230],{"class":182},[143,1079,360],{"class":359},[143,1081,205],{"class":178},[143,1083,365],{"class":287},[143,1085,230],{"class":182},[143,1087,370],{"class":359},[143,1089,205],{"class":178},[143,1091,375],{"class":153},[143,1093,378],{"class":182},[143,1095,1097,1099,1101,1104],{"class":145,"line":1096},16,[143,1098,671],{"class":287},[143,1100,399],{"class":182},[143,1102,1103],{"class":153},"\"written\"",[143,1105,378],{"class":182},[10,1107,1108,1109,866,1112,1115,1116,1119],{},"Terminal escape sequences leave debris after the escape character is removed — ",[14,1110,1111],{},"\\x1b[31m",[14,1113,1114],{},"[31m",". If your data comes from captured console output, strip whole ANSI sequences with\n",[14,1117,1118],{},"re.sub(r\"\\x1b\\[[0-9;]*m\", \"\", value)"," before the general cleanup.",[129,1121,1123],{"id":1122},"step-4-clean-a-whole-dataframe-in-one-call","Step 4 — Clean a whole DataFrame in one call",[10,1125,1126,1127,1130,1131,1134],{},"For large frames, a regex replacement across all columns is shorter and faster than mapping a Python function over every cell. ",[14,1128,1129],{},"DataFrame.replace"," with ",[14,1132,1133],{},"regex=True"," touches only string\nvalues, leaving numbers and dates alone.",[134,1136,1138],{"className":169,"code":1137,"language":171,"meta":139,"style":139},"fast = (\n    customers.replace({\"\\x0b\": \"\\n\", \"\\x0c\": \"\\n\"}, regex=True)\n             .replace(ILLEGAL_CHARACTERS_RE, \"\", regex=True)\n)\nprint(fast.equals(cleaned))                 # True\nfast.to_excel(\"customers_fast.xlsx\", index=False)\n",[14,1139,1140,1150,1198,1219,1223,1233],{"__ignoreMap":139},[143,1141,1142,1145,1147],{"class":145,"line":146},[143,1143,1144],{"class":182},"fast ",[143,1146,205],{"class":178},[143,1148,1149],{"class":182}," (\n",[143,1151,1152,1155,1157,1159,1161,1163,1165,1167,1169,1171,1173,1175,1177,1179,1181,1183,1185,1188,1191,1193,1196],{"class":145,"line":192},[143,1153,1154],{"class":182},"    customers.replace({",[143,1156,263],{"class":153},[143,1158,224],{"class":223},[143,1160,263],{"class":153},[143,1162,635],{"class":182},[143,1164,263],{"class":153},[143,1166,899],{"class":223},[143,1168,263],{"class":153},[143,1170,230],{"class":182},[143,1172,263],{"class":153},[143,1174,908],{"class":223},[143,1176,263],{"class":153},[143,1178,635],{"class":182},[143,1180,263],{"class":153},[143,1182,899],{"class":223},[143,1184,263],{"class":153},[143,1186,1187],{"class":182},"}, ",[143,1189,1190],{"class":359},"regex",[143,1192,205],{"class":178},[143,1194,1195],{"class":287},"True",[143,1197,378],{"class":182},[143,1199,1200,1203,1205,1207,1209,1211,1213,1215,1217],{"class":145,"line":199},[143,1201,1202],{"class":182},"             .replace(",[143,1204,440],{"class":287},[143,1206,230],{"class":182},[143,1208,996],{"class":153},[143,1210,230],{"class":182},[143,1212,1190],{"class":359},[143,1214,205],{"class":178},[143,1216,1195],{"class":287},[143,1218,378],{"class":182},[143,1220,1221],{"class":145,"line":211},[143,1222,378],{"class":182},[143,1224,1225,1227,1230],{"class":145,"line":244},[143,1226,671],{"class":287},[143,1228,1229],{"class":182},"(fast.equals(cleaned))                 ",[143,1231,1232],{"class":420},"# True\n",[143,1234,1235,1238,1241,1243,1245,1247,1249],{"class":145,"line":279},[143,1236,1237],{"class":182},"fast.to_excel(",[143,1239,1240],{"class":153},"\"customers_fast.xlsx\"",[143,1242,230],{"class":182},[143,1244,360],{"class":359},[143,1246,205],{"class":178},[143,1248,365],{"class":287},[143,1250,378],{"class":182},[10,1252,1253,1254,1256,1257,230,1260,1262,1263,1266,1267,1269],{},"Both calls need ",[14,1255,1133],{},". With ",[14,1258,1259],{},"regex=False",[14,1261,1129],{}," only swaps values that are ",[19,1264,1265],{},"entirely"," equal to the key, so a cell containing\n",[14,1268,865],{}," would be left untouched — the kind of no-op that makes a fix look applied when it is not.",[10,1271,1272,1273,1275,1276,1280],{},"Put this line immediately before every ",[14,1274,315],{}," in jobs that handle free text from outside sources — CRM notes, scraped pages, form submissions. It costs little and makes the write immune\nto a whole category of failures. The broader text clean-up, including non-breaking spaces and inconsistent whitespace, is covered in\n",[28,1277,1279],{"href":1278},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcleaning-excel-data-with-pandas\u002Fstrip-whitespace-and-normalise-text-columns-with-pandas\u002F","Strip Whitespace and Normalise Text Columns with pandas",".",[34,1282,43,1288,43,1291,43,1294,43,1298,43,1301,43,1306,43,1309,43,1315,43,1320,43,1324,43,1329,43,1332,43,1336,43,1339,43,1342,43,1346,43,1349,43,1353,43,1356,43,1359,43,1363,43,1366],{"viewBox":1283,"role":37,"ariaLabelledBy":1284,"xmlns":41,"style":1287},"0 0 760 136",[1285,1286],"ill-clean-t","ill-clean-d","width:100%;max-width:760px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[45,1289,1290],{"id":1285},"Cleaning text before an Excel write",[49,1292,1293],{"id":1286},"Map meaningful control characters such as vertical tab and form feed to line breaks, remove the remaining illegal characters with openpyxl pattern, then write the workbook.",[53,1295],{"x":55,"y":55,"width":1296,"height":1297,"fill":58},"760","136",[53,1299],{"x":61,"y":61,"width":742,"height":1300,"rx":64,"fill":712,"stroke":713,"style":67},"66",[69,1302,1305],{"x":1303,"y":718,"style":1304},"95","font-size:14px;font-weight:700;fill:var(--muted,#5b6780);text-anchor:middle","find",[69,1307,440],{"x":1303,"y":1308,"style":758},"72",[145,1310],{"x1":1311,"y1":1312,"x2":1313,"y2":1312,"stroke":1314,"style":67},"175","53","204","var(--brand,#5b5cf0)",[1316,1317],"polygon",{"points":1318,"fill":1319},"207,53 198,48 198,58","#5b5cf0",[53,1321],{"x":1322,"y":61,"width":742,"height":1300,"rx":64,"fill":1323,"stroke":1314,"style":67},"210","#f0f4ff",[69,1325,1328],{"x":1326,"y":718,"style":1327},"285","font-size:14px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","translate",[69,1330,1331],{"x":1326,"y":1308,"style":786},"\\x0b, \\x0c → \\n",[145,1333],{"x1":1334,"y1":1312,"x2":1335,"y2":1312,"stroke":1314,"style":67},"365","394",[1316,1337],{"points":1338,"fill":1319},"397,53 388,48 388,58",[53,1340],{"x":1341,"y":61,"width":742,"height":1300,"rx":64,"fill":1323,"stroke":1314,"style":67},"400",[69,1343,1345],{"x":1344,"y":718,"style":1327},"475","strip",[69,1347,1348],{"x":1344,"y":1308,"style":786},"everything else",[145,1350],{"x1":1351,"y1":1312,"x2":1352,"y2":1312,"stroke":1314,"style":67},"555","584",[1316,1354],{"points":1355,"fill":1319},"587,53 578,48 578,58",[53,1357],{"x":1358,"y":61,"width":742,"height":1300,"rx":64,"fill":65,"stroke":66,"style":67},"590",[69,1360,315],{"x":1361,"y":718,"style":1362},"665","font-size:14px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle",[69,1364,1365],{"x":1361,"y":1308,"style":786},"write succeeds",[69,1367,1369],{"x":57,"y":1368,"style":126},"120","translate what carries meaning; delete what does not",[129,1371,1373],{"id":1372},"step-5-know-what-xlsxwriter-does-instead","Step 5 — Know what xlsxwriter does instead",[10,1375,1376,1377,1380],{},"The xlsxwriter engine does not raise. It escapes forbidden characters using the ",[14,1378,1379],{},"_xHHHH_"," notation defined by the file format, so the write succeeds — but the character is still in the data.",[134,1382,1384],{"className":169,"code":1383,"language":171,"meta":139,"style":139},"from openpyxl import load_workbook\n\ncustomers.to_excel(\"customers_xlsxwriter.xlsx\", index=False, engine=\"xlsxwriter\")\nprint(repr(load_workbook(\"customers_xlsxwriter.xlsx\").active[\"A2\"].value))\n# 'Acme_x000B_Ltd'\n",[14,1385,1386,1398,1402,1429,1451],{"__ignoreMap":139},[143,1387,1388,1390,1393,1395],{"class":145,"line":146},[143,1389,326],{"class":178},[143,1391,1392],{"class":182}," openpyxl ",[143,1394,179],{"class":178},[143,1396,1397],{"class":182}," load_workbook\n",[143,1399,1400],{"class":145,"line":192},[143,1401,196],{"emptyLinePlaceholder":195},[143,1403,1404,1407,1410,1412,1414,1416,1418,1420,1422,1424,1427],{"class":145,"line":199},[143,1405,1406],{"class":182},"customers.to_excel(",[143,1408,1409],{"class":153},"\"customers_xlsxwriter.xlsx\"",[143,1411,230],{"class":182},[143,1413,360],{"class":359},[143,1415,205],{"class":178},[143,1417,365],{"class":287},[143,1419,230],{"class":182},[143,1421,370],{"class":359},[143,1423,205],{"class":178},[143,1425,1426],{"class":153},"\"xlsxwriter\"",[143,1428,378],{"class":182},[143,1430,1431,1433,1435,1437,1440,1442,1445,1448],{"class":145,"line":211},[143,1432,671],{"class":287},[143,1434,399],{"class":182},[143,1436,407],{"class":287},[143,1438,1439],{"class":182},"(load_workbook(",[143,1441,1409],{"class":153},[143,1443,1444],{"class":182},").active[",[143,1446,1447],{"class":153},"\"A2\"",[143,1449,1450],{"class":182},"].value))\n",[143,1452,1453],{"class":145,"line":244},[143,1454,1455],{"class":420},"# 'Acme_x000B_Ltd'\n",[10,1457,1458,1459,1462,1463,1466],{},"Excel interprets the escape when it opens the file, but other tools that read the workbook, openpyxl included, return the literal ",[14,1460,1461],{},"_x000B_"," text. A job that switches engines to make the error go\naway has moved the problem downstream, where a lookup on ",[14,1464,1465],{},"Acme Ltd"," silently fails. Clean the data regardless of which engine writes it.",[129,1468,1470],{"id":1469},"step-6-wrap-the-write-in-a-safe-helper","Step 6 — Wrap the write in a safe helper",[10,1472,1473],{},"In a reporting codebase the cleanup should not depend on every author remembering it. A small helper that cleans, counts what it changed, writes, and wraps line-broken\ncells in the output keeps the behaviour consistent across jobs and leaves a trace in the log when a source starts sending control characters.",[134,1475,1477],{"className":169,"code":1476,"language":171,"meta":139,"style":139},"import logging\nfrom openpyxl.styles import Alignment\n\nlogging.basicConfig(level=logging.INFO, format=\"%(levelname)s %(message)s\")\nlog = logging.getLogger(\"excel-writer\")\n\ndef safe_to_excel(frame: pd.DataFrame, path: str, sheet_name: str = \"Sheet1\") -> None:\n    text_columns = frame.select_dtypes(include=[\"object\", \"string\"]).columns\n    cleaned_frame = frame.copy()\n    changed = 0\n    for column in text_columns:\n        before = cleaned_frame[column]\n        after = before.map(clean_text)\n        changed += int((before.fillna(\"\") != after.fillna(\"\")).sum())\n        cleaned_frame[column] = after\n    if changed:\n        log.warning(\"removed or translated control characters in %d cell(s) before writing %s\", changed, path)\n\n    with pd.ExcelWriter(path, engine=\"openpyxl\") as writer:\n        cleaned_frame.to_excel(writer, sheet_name=sheet_name, index=False)\n        sheet = writer.sheets[sheet_name]\n        for row in sheet.iter_rows(min_row=2):\n            for cell in row:\n                if isinstance(cell.value, str) and \"\\n\" in cell.value:\n                    cell.alignment = Alignment(wrap_text=True, vertical=\"top\")\n\nsafe_to_excel(customers, \"customers_safe.xlsx\", sheet_name=\"Customers\")\nprint(repr(load_workbook(\"customers_safe.xlsx\")[\"Customers\"][\"A2\"].value))\n",[14,1478,1479,1486,1498,1502,1537,1552,1556,1586,1610,1620,1630,1641,1651,1661,1691,1701,1708,1731,1736,1758,1780,1791,1813,1827,1857,1887,1892,1912],{"__ignoreMap":139},[143,1480,1481,1483],{"class":145,"line":146},[143,1482,179],{"class":178},[143,1484,1485],{"class":182}," logging\n",[143,1487,1488,1490,1493,1495],{"class":145,"line":192},[143,1489,326],{"class":178},[143,1491,1492],{"class":182}," openpyxl.styles ",[143,1494,179],{"class":178},[143,1496,1497],{"class":182}," Alignment\n",[143,1499,1500],{"class":145,"line":199},[143,1501,196],{"emptyLinePlaceholder":195},[143,1503,1504,1507,1510,1512,1515,1518,1520,1523,1525,1527,1530,1533,1535],{"class":145,"line":211},[143,1505,1506],{"class":182},"logging.basicConfig(",[143,1508,1509],{"class":359},"level",[143,1511,205],{"class":178},[143,1513,1514],{"class":182},"logging.",[143,1516,1517],{"class":287},"INFO",[143,1519,230],{"class":182},[143,1521,1522],{"class":359},"format",[143,1524,205],{"class":178},[143,1526,263],{"class":153},[143,1528,1529],{"class":223},"%(levelname)s",[143,1531,1532],{"class":223}," %(message)s",[143,1534,263],{"class":153},[143,1536,378],{"class":182},[143,1538,1539,1542,1544,1547,1550],{"class":145,"line":244},[143,1540,1541],{"class":182},"log ",[143,1543,205],{"class":178},[143,1545,1546],{"class":182}," logging.getLogger(",[143,1548,1549],{"class":153},"\"excel-writer\"",[143,1551,378],{"class":182},[143,1553,1554],{"class":145,"line":279},[143,1555,196],{"emptyLinePlaceholder":195},[143,1557,1558,1560,1563,1566,1568,1571,1573,1575,1578,1581,1584],{"class":145,"line":303},[143,1559,467],{"class":178},[143,1561,1562],{"class":470}," safe_to_excel",[143,1564,1565],{"class":182},"(frame: pd.DataFrame, path: ",[143,1567,412],{"class":287},[143,1569,1570],{"class":182},", sheet_name: ",[143,1572,412],{"class":287},[143,1574,883],{"class":178},[143,1576,1577],{"class":153}," \"Sheet1\"",[143,1579,1580],{"class":182},") -> ",[143,1582,1583],{"class":287},"None",[143,1585,346],{"class":182},[143,1587,1588,1591,1593,1595,1597,1599,1601,1603,1605,1607],{"class":145,"line":549},[143,1589,1590],{"class":182},"    text_columns ",[143,1592,205],{"class":178},[143,1594,498],{"class":182},[143,1596,501],{"class":359},[143,1598,205],{"class":178},[143,1600,506],{"class":182},[143,1602,509],{"class":153},[143,1604,230],{"class":182},[143,1606,514],{"class":153},[143,1608,1609],{"class":182},"]).columns\n",[143,1611,1612,1615,1617],{"class":145,"line":605},[143,1613,1614],{"class":182},"    cleaned_frame ",[143,1616,205],{"class":178},[143,1618,1619],{"class":182}," frame.copy()\n",[143,1621,1622,1625,1627],{"class":145,"line":614},[143,1623,1624],{"class":182},"    changed ",[143,1626,205],{"class":178},[143,1628,1629],{"class":287}," 0\n",[143,1631,1632,1634,1636,1638],{"class":145,"line":654},[143,1633,489],{"class":178},[143,1635,492],{"class":182},[143,1637,495],{"class":178},[143,1639,1640],{"class":182}," text_columns:\n",[143,1642,1643,1646,1648],{"class":145,"line":663},[143,1644,1645],{"class":182},"        before ",[143,1647,205],{"class":178},[143,1649,1650],{"class":182}," cleaned_frame[column]\n",[143,1652,1653,1656,1658],{"class":145,"line":668},[143,1654,1655],{"class":182},"        after ",[143,1657,205],{"class":178},[143,1659,1660],{"class":182}," before.map(clean_text)\n",[143,1662,1663,1666,1669,1672,1675,1677,1680,1683,1686,1688],{"class":145,"line":1056},[143,1664,1665],{"class":182},"        changed ",[143,1667,1668],{"class":178},"+=",[143,1670,1671],{"class":287}," int",[143,1673,1674],{"class":182},"((before.fillna(",[143,1676,996],{"class":153},[143,1678,1679],{"class":182},") ",[143,1681,1682],{"class":178},"!=",[143,1684,1685],{"class":182}," after.fillna(",[143,1687,996],{"class":153},[143,1689,1690],{"class":182},")).sum())\n",[143,1692,1693,1696,1698],{"class":145,"line":1070},[143,1694,1695],{"class":182},"        cleaned_frame[column] ",[143,1697,205],{"class":178},[143,1699,1700],{"class":182}," after\n",[143,1702,1703,1705],{"class":145,"line":1096},[143,1704,940],{"class":178},[143,1706,1707],{"class":182}," changed:\n",[143,1709,1711,1714,1717,1720,1723,1726,1728],{"class":145,"line":1710},17,[143,1712,1713],{"class":182},"        log.warning(",[143,1715,1716],{"class":153},"\"removed or translated control characters in ",[143,1718,1719],{"class":223},"%d",[143,1721,1722],{"class":153}," cell(s) before writing ",[143,1724,1725],{"class":223},"%s",[143,1727,263],{"class":153},[143,1729,1730],{"class":182},", changed, path)\n",[143,1732,1734],{"class":145,"line":1733},18,[143,1735,196],{"emptyLinePlaceholder":195},[143,1737,1739,1742,1745,1747,1749,1751,1753,1755],{"class":145,"line":1738},19,[143,1740,1741],{"class":178},"    with",[143,1743,1744],{"class":182}," pd.ExcelWriter(path, ",[143,1746,370],{"class":359},[143,1748,205],{"class":178},[143,1750,375],{"class":153},[143,1752,1679],{"class":182},[143,1754,186],{"class":178},[143,1756,1757],{"class":182}," writer:\n",[143,1759,1761,1764,1767,1769,1772,1774,1776,1778],{"class":145,"line":1760},20,[143,1762,1763],{"class":182},"        cleaned_frame.to_excel(writer, ",[143,1765,1766],{"class":359},"sheet_name",[143,1768,205],{"class":178},[143,1770,1771],{"class":182},"sheet_name, ",[143,1773,360],{"class":359},[143,1775,205],{"class":178},[143,1777,365],{"class":287},[143,1779,378],{"class":182},[143,1781,1783,1786,1788],{"class":145,"line":1782},21,[143,1784,1785],{"class":182},"        sheet ",[143,1787,205],{"class":178},[143,1789,1790],{"class":182}," writer.sheets[sheet_name]\n",[143,1792,1794,1796,1799,1801,1804,1807,1809,1811],{"class":145,"line":1793},22,[143,1795,522],{"class":178},[143,1797,1798],{"class":182}," row ",[143,1800,495],{"class":178},[143,1802,1803],{"class":182}," sheet.iter_rows(",[143,1805,1806],{"class":359},"min_row",[143,1808,205],{"class":178},[143,1810,781],{"class":287},[143,1812,546],{"class":182},[143,1814,1816,1819,1822,1824],{"class":145,"line":1815},23,[143,1817,1818],{"class":178},"            for",[143,1820,1821],{"class":182}," cell ",[143,1823,495],{"class":178},[143,1825,1826],{"class":182}," row:\n",[143,1828,1830,1832,1834,1837,1839,1841,1844,1847,1849,1851,1854],{"class":145,"line":1829},24,[143,1831,608],{"class":178},[143,1833,538],{"class":287},[143,1835,1836],{"class":182},"(cell.value, ",[143,1838,412],{"class":287},[143,1840,1679],{"class":182},[143,1842,1843],{"class":178},"and",[143,1845,1846],{"class":153}," \"",[143,1848,899],{"class":223},[143,1850,263],{"class":153},[143,1852,1853],{"class":178}," in",[143,1855,1856],{"class":182}," cell.value:\n",[143,1858,1860,1863,1865,1868,1871,1873,1875,1877,1880,1882,1885],{"class":145,"line":1859},25,[143,1861,1862],{"class":182},"                    cell.alignment ",[143,1864,205],{"class":178},[143,1866,1867],{"class":182}," Alignment(",[143,1869,1870],{"class":359},"wrap_text",[143,1872,205],{"class":178},[143,1874,1195],{"class":287},[143,1876,230],{"class":182},[143,1878,1879],{"class":359},"vertical",[143,1881,205],{"class":178},[143,1883,1884],{"class":153},"\"top\"",[143,1886,378],{"class":182},[143,1888,1890],{"class":145,"line":1889},26,[143,1891,196],{"emptyLinePlaceholder":195},[143,1893,1895,1898,1901,1903,1905,1907,1910],{"class":145,"line":1894},27,[143,1896,1897],{"class":182},"safe_to_excel(customers, ",[143,1899,1900],{"class":153},"\"customers_safe.xlsx\"",[143,1902,230],{"class":182},[143,1904,1766],{"class":359},[143,1906,205],{"class":178},[143,1908,1909],{"class":153},"\"Customers\"",[143,1911,378],{"class":182},[143,1913,1915,1917,1919,1921,1923,1925,1928,1930,1933,1935],{"class":145,"line":1914},28,[143,1916,671],{"class":287},[143,1918,399],{"class":182},[143,1920,407],{"class":287},[143,1922,1439],{"class":182},[143,1924,1900],{"class":153},[143,1926,1927],{"class":182},")[",[143,1929,1909],{"class":153},[143,1931,1932],{"class":182},"][",[143,1934,1447],{"class":153},[143,1936,1450],{"class":182},[10,1938,1939],{},"The warning is deliberately a log line rather than an error. The report still goes out, correctly, and whoever watches the logs learns that a source system has started including\ncontrol characters — often the first sign of a change upstream, such as a new export format or a different text field being copied in.",[129,1941,1943],{"id":1942},"step-7-keep-a-control-character-in-your-test-data","Step 7 — Keep a control character in your test data",[10,1945,1946],{},"This error tends to appear first in production because test fixtures are typed by hand and never contain invisible characters. Add one deliberately, so a refactor that drops the\ncleanup fails a test instead of a scheduled job.",[134,1948,1950],{"className":169,"code":1949,"language":171,"meta":139,"style":139},"def test_writer_handles_control_characters(tmp_path=\".\"):\n    frame = pd.DataFrame({\"Name\": [\"Acme\\x0bLtd\", \"Null\\x00Byte\"], \"Value\": [1, 2]})\n    target = f\"{tmp_path}\u002Fcontrol_chars.xlsx\"\n    safe_to_excel(frame, target)\n    values = [cell.value for cell in load_workbook(target).active[\"A\"][1:]]\n    assert values == [\"Acme\\nLtd\", \"NullByte\"]\n\ntest_writer_handles_control_characters()\nprint(\"control character test passed\")\n",[14,1951,1952,1969,2017,2039,2044,2073,2101,2105,2110],{"__ignoreMap":139},[143,1953,1954,1956,1959,1962,1964,1967],{"class":145,"line":146},[143,1955,467],{"class":178},[143,1957,1958],{"class":470}," test_writer_handles_control_characters",[143,1960,1961],{"class":182},"(tmp_path",[143,1963,205],{"class":178},[143,1965,1966],{"class":153},"\".\"",[143,1968,546],{"class":182},[143,1970,1971,1974,1976,1979,1982,1984,1986,1988,1990,1992,1995,1997,2000,2003,2006,2008,2010,2012,2014],{"class":145,"line":192},[143,1972,1973],{"class":182},"    frame ",[143,1975,205],{"class":178},[143,1977,1978],{"class":182}," pd.DataFrame({",[143,1980,1981],{"class":153},"\"Name\"",[143,1983,217],{"class":182},[143,1985,220],{"class":153},[143,1987,224],{"class":223},[143,1989,227],{"class":153},[143,1991,230],{"class":182},[143,1993,1994],{"class":153},"\"Null",[143,1996,260],{"class":223},[143,1998,1999],{"class":153},"Byte\"",[143,2001,2002],{"class":182},"], ",[143,2004,2005],{"class":153},"\"Value\"",[143,2007,217],{"class":182},[143,2009,753],{"class":287},[143,2011,230],{"class":182},[143,2013,781],{"class":287},[143,2015,2016],{"class":182},"]})\n",[143,2018,2019,2022,2024,2027,2029,2031,2034,2036],{"class":145,"line":199},[143,2020,2021],{"class":182},"    target ",[143,2023,205],{"class":178},[143,2025,2026],{"class":178}," f",[143,2028,263],{"class":153},[143,2030,574],{"class":223},[143,2032,2033],{"class":182},"tmp_path",[143,2035,586],{"class":223},[143,2037,2038],{"class":153},"\u002Fcontrol_chars.xlsx\"\n",[143,2040,2041],{"class":145,"line":211},[143,2042,2043],{"class":182},"    safe_to_excel(frame, target)\n",[143,2045,2046,2049,2051,2054,2056,2058,2060,2063,2066,2068,2070],{"class":145,"line":244},[143,2047,2048],{"class":182},"    values ",[143,2050,205],{"class":178},[143,2052,2053],{"class":182}," [cell.value ",[143,2055,1018],{"class":178},[143,2057,1821],{"class":182},[143,2059,495],{"class":178},[143,2061,2062],{"class":182}," load_workbook(target).active[",[143,2064,2065],{"class":153},"\"A\"",[143,2067,1932],{"class":182},[143,2069,753],{"class":287},[143,2071,2072],{"class":182},":]]\n",[143,2074,2075,2078,2081,2084,2087,2089,2091,2093,2095,2098],{"class":145,"line":279},[143,2076,2077],{"class":178},"    assert",[143,2079,2080],{"class":182}," values ",[143,2082,2083],{"class":178},"==",[143,2085,2086],{"class":182}," [",[143,2088,220],{"class":153},[143,2090,899],{"class":223},[143,2092,227],{"class":153},[143,2094,230],{"class":182},[143,2096,2097],{"class":153},"\"NullByte\"",[143,2099,2100],{"class":182},"]\n",[143,2102,2103],{"class":145,"line":303},[143,2104,196],{"emptyLinePlaceholder":195},[143,2106,2107],{"class":145,"line":549},[143,2108,2109],{"class":182},"test_writer_handles_control_characters()\n",[143,2111,2112,2114,2116,2119],{"class":145,"line":605},[143,2113,671],{"class":287},[143,2115,399],{"class":182},[143,2117,2118],{"class":153},"\"control character test passed\"",[143,2120,378],{"class":182},[10,2122,2123,2124,1280],{},"The pattern generalises to other invisible-input bugs — non-breaking spaces, zero-width joiners, byte-order marks — and belongs alongside the checks in\n",[28,2125,2127],{"href":2126},"\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Ftest-excel-output-with-pytest\u002F","Test Excel Output with pytest",[129,2129,2131],{"id":2130},"common-pitfalls","Common pitfalls",[2133,2134,2135,2151],"table",{},[2136,2137,2138],"thead",{},[2139,2140,2141,2145,2148],"tr",{},[2142,2143,2144],"th",{},"Symptom",[2142,2146,2147],{},"Cause",[2142,2149,2150],{},"Fix",[2152,2153,2154,2175,2194,2209,2222,2239],"tbody",{},[2139,2155,2156,2163,2166],{},[2157,2158,2159,2162],"td",{},[14,2160,2161],{},"IllegalCharacterError"," naming a normal-looking value",[2157,2164,2165],{},"Invisible control character",[2157,2167,2168,2169,2172,2173],{},"Inspect with ",[14,2170,2171],{},"repr()","; clean with ",[14,2174,440],{},[2139,2176,2177,2180,2183],{},[2157,2178,2179],{},"Words run together after cleaning",[2157,2181,2182],{},"Vertical tab deleted instead of translated",[2157,2184,2185,2186,2188,2189,1130,2191,2193],{},"Replace ",[14,2187,224],{}," and ",[14,2190,908],{},[14,2192,899],{}," first",[2139,2195,2196,2199,2202],{},[2157,2197,2198],{},"Line breaks not visible in Excel",[2157,2200,2201],{},"Cell not set to wrap text",[2157,2203,2204,2205,2208],{},"Apply ",[14,2206,2207],{},"Alignment(wrap_text=True)"," to those cells",[2139,2210,2211,2216,2219],{},[2157,2212,2213,2215],{},[14,2214,1461],{}," appears in text read back",[2157,2217,2218],{},"File written by xlsxwriter with dirty data",[2157,2220,2221],{},"Clean before writing with any engine",[2139,2223,2224,2229,2232],{},[2157,2225,2226,2228],{},[14,2227,1114],{}," fragments remain",[2157,2230,2231],{},"ANSI escape sequences partly removed",[2157,2233,2234,2235,2238],{},"Strip whole ",[14,2236,2237],{},"\\x1b[...m"," sequences first",[2139,2240,2241,2244,2247],{},[2157,2242,2243],{},"Error appears only in production",[2157,2245,2246],{},"Test data never contained control characters",[2157,2248,2249],{},"Add a control character to a test fixture",[129,2251,2253],{"id":2252},"performance-and-scale-notes","Performance and scale notes",[10,2255,2256,2258,2259,2262],{},[14,2257,1129],{}," with a compiled pattern is vectorised per column and handles hundreds of thousands of text cells in well under a second. The per-cell ",[14,2260,2261],{},"map"," in Step 3 is easier to extend with\nspecial cases and fine for report-sized data. Scanning for offenders (Step 2) is a diagnostic; in a production job, clean unconditionally and log a count of changed cells, which avoids scanning twice.",[129,2264,2266],{"id":2265},"conclusion","Conclusion",[10,2268,2269,2271,2272,2274,2275,2277,2278,2280],{},[14,2270,2161],{}," means a string contains a control character that XML, and therefore the ",[14,2273,25],{}," format, does not allow. Use ",[14,2276,2171],{}," to see it, ",[14,2279,440],{}," to find every\naffected cell at once, translate vertical tabs and form feeds into line breaks, and strip what remains before writing. Do this whatever the engine — xlsxwriter's escaping lets the write succeed but\ncarries the problem into the file — and include a control character in your test data so the protection stays in place.",[129,2282,2284],{"id":2283},"frequently-asked-questions","Frequently asked questions",[10,2286,2287,2291],{},[2288,2289,2290],"strong",{},"Which characters does openpyxl reject?","\nASCII control characters 0–8, 11–12 and 14–31. Tab, line feed and carriage return are allowed, as is all normal text including accented letters and emoji.",[10,2293,2294,2297],{},[2288,2295,2296],{},"Is IllegalCharacterError a bug in openpyxl?","\nNo. The xlsx format is XML, and XML 1.0 does not permit those control characters in text. openpyxl raises instead of writing a file Excel would report as corrupt.",[10,2299,2300,2303,2304,2307],{},[2288,2301,2302],{},"Can I keep the characters by encoding them?","\nxlsxwriter escapes them as ",[19,2305,2306],{},"xHHHH"," sequences, which Excel understands. Most other readers show the escape literally, so cleaning is usually the better choice.",[10,2309,2310,2313],{},[2288,2311,2312],{},"Where do these characters usually come from?","\nCopying from Word or PDFs (vertical tabs), fixed-width or binary database fields (null bytes), and captured console output (escape codes).",[129,2315,2317],{"id":2316},"related","Related",[2319,2320,2321,2328,2335,2342,2349],"ul",{},[2322,2323,2324,2325,2327],"li",{},"Up one level: ",[28,2326,31],{"href":30}," — the most common failures and their fixes.",[2322,2329,2330,2334],{},[28,2331,2333],{"href":2332},"\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-excel-repaired-content-after-writing-with-python\u002F","Fix \"Excel Found Unreadable Content\" After Writing with Python"," — when a file is written but Excel repairs it.",[2322,2336,2337,2341],{},[28,2338,2340],{"href":2339},"\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-number-stored-as-text-warnings-in-excel-output\u002F","Fix \"Number Stored as Text\" Warnings in Excel Output"," — another data-type problem visible only after writing.",[2322,2343,2344,2348],{},[28,2345,2347],{"href":2346},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fvalidate-an-excel-report-before-sending-it\u002F","Validate an Excel Report Before Sending It"," — catching defects before a report goes out.",[2322,2350,2351,2355],{},[28,2352,2354],{"href":2353},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fstyling-excel-cells-with-openpyxl\u002F","Styling Excel Cells with openpyxl"," — wrap text so translated line breaks display.",[2357,2358,2359],"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 .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}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 .s-wDw, html code.shiki .s-wDw{--shiki-default:#6A737D;--shiki-dark:#BDC4CC}html pre.shiki code .s_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}",{"title":139,"searchDepth":192,"depth":192,"links":2361},[2362,2363,2364,2365,2366,2367,2368,2369,2370,2371,2372,2373,2374],{"id":131,"depth":192,"text":132},{"id":309,"depth":192,"text":310},{"id":433,"depth":192,"text":434},{"id":858,"depth":192,"text":859},{"id":1122,"depth":192,"text":1123},{"id":1372,"depth":192,"text":1373},{"id":1469,"depth":192,"text":1470},{"id":1942,"depth":192,"text":1943},{"id":2130,"depth":192,"text":2131},{"id":2252,"depth":192,"text":2253},{"id":2265,"depth":192,"text":2266},{"id":2283,"depth":192,"text":2284},{"id":2316,"depth":192,"text":2317},"2026-09-17","openpyxl refuses to write text containing control characters such as \\x00 or a vertical tab. Find which cells hold them, decide what each character should become, and clean the data so the write succeeds.","md",[2379,2381,2383,2385],{"q":2290,"a":2380},"ASCII control characters 0–8, 11–12 and 14–31. Tab, line feed and carriage return are allowed, as is all normal text including accented letters and emoji.",{"q":2296,"a":2382},"No. The xlsx format is XML, and XML 1.0 does not permit those control characters in text. openpyxl raises instead of writing a file Excel would report as corrupt.",{"q":2302,"a":2384},"xlsxwriter escapes them as _xHHHH_ sequences, which Excel understands. Most other readers show the escape literally, so cleaning is usually the better choice.",{"q":2312,"a":2386},"Copying from Word or PDFs (vertical tabs), fixed-width or binary database fields (null bytes), and captured console output (escape codes).","fix illegalcharactererror when writing excel with openpyxl",null,{"breadcrumb":2390},[2391,2394,2397],{"name":2392,"item":2393},"Home","\u002F",{"name":2395,"item":2396},"Getting Started with Python Excel Automation","\u002Fgetting-started-with-python-excel-automation\u002F",{"name":31,"item":30},"\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-illegalcharactererror-when-writing-excel-with-openpyxl",{"title":2400,"description":2401},"Fix openpyxl IllegalCharacterError When Writing Excel","Resolve IllegalCharacterError ... cannot be used in worksheets: which control characters XML forbids, finding offending cells in pandas, replacing vertical tabs with line breaks, and xlsxwriter's escaping.","fix-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",[2405,2406,2407],"troubleshooting","writing-excel","openpyxl","how-to","pSAE6_9f0UtO1iffi5mr7Ki5DcxlQ5ebLw5gfJhb_dk",[2411,2415],{"title":2412,"path":2413,"stem":2414,"children":-1},"Fix #### Showing Instead of Values in Excel Output","\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-hash-marks-showing-instead-of-values-in-excel","getting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-hash-marks-showing-instead-of-values-in-excel\u002Findex",{"title":2416,"path":2417,"stem":2418,"children":-1},"Fix Invalid Worksheet Name Errors When Writing Excel","\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-invalid-worksheet-name-errors","getting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-invalid-worksheet-name-errors\u002Findex",[2420,2422],{"path":2421,"title":2395},"\u002Fgetting-started-with-python-excel-automation",{"path":2423,"title":31},"\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors",[],[2426,2430,2433,2436,2439,2442],{"path":2427,"title":2428,"kind":2429},"\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":2431,"title":2432,"kind":2408},"\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":2434,"title":2435,"kind":2429},"\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":2437,"title":2438,"kind":2408},"\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":2440,"title":2441,"kind":2408},"\u002Fautomating-reporting-workflows\u002Fautomating-excel-online-with-microsoft-graph\u002Fhandle-microsoft-graph-throttling-for-excel","Handle Microsoft Graph Throttling in Excel Automation",{"path":2443,"title":2444,"kind":2445},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation","Error Handling and Logging in Excel Automation","guide",1790972065827]