[{"data":1,"prerenderedAt":3030},["ShallowReactive",2],{"doc:\u002Fautomating-reporting-workflows\u002Fserving-excel-files-from-python-web-apps\u002Fvalidate-an-uploaded-excel-file-in-a-web-app":3,"surround:\u002Fautomating-reporting-workflows\u002Fserving-excel-files-from-python-web-apps\u002Fvalidate-an-uploaded-excel-file-in-a-web-app":2997,"ancestors:\u002Fautomating-reporting-workflows\u002Fserving-excel-files-from-python-web-apps\u002Fvalidate-an-uploaded-excel-file-in-a-web-app":3006,"children:\u002Fautomating-reporting-workflows\u002Fserving-excel-files-from-python-web-apps\u002Fvalidate-an-uploaded-excel-file-in-a-web-app":3011,"related:\u002Fautomating-reporting-workflows\u002Fserving-excel-files-from-python-web-apps\u002Fvalidate-an-uploaded-excel-file-in-a-web-app":3012},{"id":4,"title":5,"body":6,"dateModified":2964,"datePublished":2964,"description":2965,"extension":2966,"faq":2967,"keyword":2976,"kind":2977,"meta":2978,"navigation":240,"path":2986,"seo":2987,"slug":2990,"stem":2991,"topics":2992,"type":2995,"__hash__":2996},"docs\u002Fautomating-reporting-workflows\u002Fserving-excel-files-from-python-web-apps\u002Fvalidate-an-uploaded-excel-file-in-a-web-app\u002Findex.md","Validate an Uploaded Excel File Before Processing It in a Web App",{"type":7,"value":8,"toc":2953},"minimark",[9,28,175,180,207,210,462,466,472,936,939,1088,1104,1108,1111,1114,1255,1259,1262,2329,2345,2594,2598,2601,2808,2828,2832,2869,2873,2881,2885,2895,2901,2907,2916,2920,2949],[10,11,12,13,17,18,21,22,27],"p",{},"A web app that accepts spreadsheets receives whatever people have: CSVs renamed to ",[14,15,16],"code",{},".xlsx",", workbooks from 2003, password-protected files, macro-enabled templates, and occasionally something built to hurt. Passing all of it straight to ",[14,19,20],{},"pd.read_excel"," produces either a stack trace or a confusing message — or, for a compression bomb, a worker stuck for half a minute. This guide, part of ",[23,24,26],"a",{"href":25},"\u002Fautomating-reporting-workflows\u002Fserving-excel-files-from-python-web-apps\u002F","Serving Excel Files from Python Web Apps",", validates uploads in layers, from the cheapest checks to the most thorough, and returns problems the user can act on. Every check was run against real examples of each bad file.",[29,30,38,39,38,43,38,47,38,55,38,65,38,76,38,83,38,88,38,94,38,99,38,104,38,111,38,116,38,119,38,123,38,126,38,131,38,138,38,143,38,146,38,150,38,153,38,156,38,162,38,167,38,171],"svg",{"viewBox":31,"role":32,"ariaLabelledBy":33,"xmlns":36,"style":37},"4 0 752 142","img",[34,35],"vu-layers-t","vu-layers-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:752px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[40,41,42],"title",{"id":34},"Three layers of checks, cheapest first",[44,45,46],"desc",{"id":35},"The first layer looks at the bytes to recognise a zip-based xlsx, an OLE2 file or something else. The second opens the zip directory without extracting anything and checks for the workbook parts, macros, external links and the expanded size. Only files that pass reach the third layer, which reads the sheet and checks every column against a schema, reporting problems by cell address.",[48,49],"rect",{"x":50,"y":51,"width":52,"height":53,"fill":54},"4","0","752","142","#ffffff",[48,56],{"x":57,"y":58,"width":59,"height":60,"rx":61,"fill":62,"stroke":63,"style":64},"20.0","16.0","153.0","110.5","12","#f0f4ff","var(--brand,#5b5cf0)","stroke-width:2px",[66,67,69],"g",{"transform":68},"translate(84.5,37.8) scale(1.000)",[70,71],"path",{"d":72,"fill":73,"stroke":74,"style":75},"M5 9h14 M4 15h14 M10 4l-2 16 M16 4l-2 16","none","var(--brand-strong,#4338ca)","stroke-width:1.80px;stroke-linecap:round;stroke-linejoin:round",[77,78,82],"text",{"x":79,"y":80,"style":81},"96.5","84.2","font-size:13.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Bytes",[77,84,87],{"x":79,"y":85,"style":86},"103.8","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","zip, OLE2 or neither",[89,90],"line",{"x1":91,"y1":92,"x2":93,"y2":92,"stroke":63,"style":64},"178.0","71.2","200.0",[95,96],"polygon",{"points":97,"fill":98},"204.0,71.2 195.0,76.2 195.0,66.2","#5b5cf0",[48,100],{"x":101,"y":58,"width":59,"height":60,"rx":61,"fill":102,"stroke":103,"style":64},"209.0","#fdefd8","var(--gold,#b4740a)",[66,105,107],{"transform":106},"translate(273.5,37.8) scale(1.000)",[70,108],{"d":109,"fill":73,"stroke":110,"style":75},"M12 2.5l8.5 4.7v9.6L12 21.5l-8.5-4.7V7.2z M3.5 7.2l8.5 4.7 8.5-4.7 M12 11.9v9.6","var(--gold-ink,#7a4e06)",[77,112,115],{"x":113,"y":80,"style":114},"285.5","font-size:13.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","Container",[77,117,118],{"x":113,"y":85,"style":86},"macros, links, size",[89,120],{"x1":121,"y1":92,"x2":122,"y2":92,"stroke":63,"style":64},"367.0","389.0",[95,124],{"points":125,"fill":98},"393.0,71.2 384.0,76.2 384.0,66.2",[48,127],{"x":128,"y":58,"width":59,"height":60,"rx":61,"fill":129,"stroke":130,"style":64},"398.0","#d9f4f1","var(--teal,#0f9488)",[66,132,134],{"transform":133},"translate(462.5,37.8) scale(1.000)",[70,135],{"d":136,"fill":73,"stroke":137,"style":75},"M3 5h18v14H3z M3 10h18 M3 15h18 M9 5v14","var(--teal-ink,#0b6157)",[77,139,142],{"x":140,"y":80,"style":141},"474.5","font-size:13.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","Rows",[77,144,145],{"x":140,"y":85,"style":86},"schema, cell by cell",[89,147],{"x1":148,"y1":92,"x2":149,"y2":92,"stroke":63,"style":64},"556.0","578.0",[95,151],{"points":152,"fill":98},"582.0,71.2 573.0,76.2 573.0,66.2",[48,154],{"x":155,"y":58,"width":59,"height":60,"rx":61,"fill":62,"stroke":63,"style":64},"587.0",[66,157,159],{"transform":158},"translate(651.5,29.0) scale(1.000)",[70,160],{"d":161,"fill":73,"stroke":74,"style":75},"M5 12.5l4.5 4.5L19 7",[77,163,166],{"x":164,"y":165,"style":81},"663.5","75.5","Result",[77,168,170],{"x":164,"y":169,"style":86},"95.0","accepted, or a list of",[77,172,174],{"x":164,"y":173,"style":86},"112.5","cells",[176,177,179],"h2",{"id":178},"prerequisites","Prerequisites",[181,182,187],"pre",{"className":183,"code":184,"language":185,"meta":186,"style":186},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install pandas openpyxl\n","bash","",[14,188,189],{"__ignoreMap":186},[190,191,193,197,201,204],"span",{"class":89,"line":192},1,[190,194,196],{"class":195},"sMTad","pip",[190,198,200],{"class":199},"srMev"," install",[190,202,203],{"class":199}," pandas",[190,205,206],{"class":199}," openpyxl\n",[10,208,209],{},"A sample upload with the kinds of mistakes people make — a mistyped region, \"n\u002Fa\" in a number column, a negative amount and a duplicated order number:",[181,211,215],{"className":212,"code":213,"language":214,"meta":186,"style":186},"language-python shiki shiki-themes github-light github-dark-high-contrast","import io\nimport zipfile\n\nimport pandas as pd\nfrom openpyxl.utils import get_column_letter\n\norders = pd.DataFrame({\n    \"Order ID\": [1001, 1002, 1003, 1002],\n    \"Region\": [\"North\", \"South\", \"Nrth\", \"East\"],\n    \"Order date\": pd.to_datetime([\"2026-09-01\", \"2026-09-02\", \"2026-09-02\", \"2026-09-03\"]),\n    \"Amount\": [120.5, \"n\u002Fa\", 80.0, -5.0],\n})\nbuffer = io.BytesIO()\norders.to_excel(buffer, index=False, sheet_name=\"Orders\")\nupload = buffer.getvalue()\n","python",[14,216,217,227,235,242,256,270,275,287,318,346,375,406,412,423,451],{"__ignoreMap":186},[190,218,219,223],{"class":89,"line":192},[190,220,222],{"class":221},"s-kum","import",[190,224,226],{"class":225},"skGVy"," io\n",[190,228,230,232],{"class":89,"line":229},2,[190,231,222],{"class":221},[190,233,234],{"class":225}," zipfile\n",[190,236,238],{"class":89,"line":237},3,[190,239,241],{"emptyLinePlaceholder":240},true,"\n",[190,243,245,247,250,253],{"class":89,"line":244},4,[190,246,222],{"class":221},[190,248,249],{"class":225}," pandas ",[190,251,252],{"class":221},"as",[190,254,255],{"class":225}," pd\n",[190,257,259,262,265,267],{"class":89,"line":258},5,[190,260,261],{"class":221},"from",[190,263,264],{"class":225}," openpyxl.utils ",[190,266,222],{"class":221},[190,268,269],{"class":225}," get_column_letter\n",[190,271,273],{"class":89,"line":272},6,[190,274,241],{"emptyLinePlaceholder":240},[190,276,278,281,284],{"class":89,"line":277},7,[190,279,280],{"class":225},"orders ",[190,282,283],{"class":221},"=",[190,285,286],{"class":225}," pd.DataFrame({\n",[190,288,290,293,296,300,303,306,308,311,313,315],{"class":89,"line":289},8,[190,291,292],{"class":199},"    \"Order ID\"",[190,294,295],{"class":225},": [",[190,297,299],{"class":298},"sP0c6","1001",[190,301,302],{"class":225},", ",[190,304,305],{"class":298},"1002",[190,307,302],{"class":225},[190,309,310],{"class":298},"1003",[190,312,302],{"class":225},[190,314,305],{"class":298},[190,316,317],{"class":225},"],\n",[190,319,321,324,326,329,331,334,336,339,341,344],{"class":89,"line":320},9,[190,322,323],{"class":199},"    \"Region\"",[190,325,295],{"class":225},[190,327,328],{"class":199},"\"North\"",[190,330,302],{"class":225},[190,332,333],{"class":199},"\"South\"",[190,335,302],{"class":225},[190,337,338],{"class":199},"\"Nrth\"",[190,340,302],{"class":225},[190,342,343],{"class":199},"\"East\"",[190,345,317],{"class":225},[190,347,349,352,355,358,360,363,365,367,369,372],{"class":89,"line":348},10,[190,350,351],{"class":199},"    \"Order date\"",[190,353,354],{"class":225},": pd.to_datetime([",[190,356,357],{"class":199},"\"2026-09-01\"",[190,359,302],{"class":225},[190,361,362],{"class":199},"\"2026-09-02\"",[190,364,302],{"class":225},[190,366,362],{"class":199},[190,368,302],{"class":225},[190,370,371],{"class":199},"\"2026-09-03\"",[190,373,374],{"class":225},"]),\n",[190,376,378,381,383,386,388,391,393,396,398,401,404],{"class":89,"line":377},11,[190,379,380],{"class":199},"    \"Amount\"",[190,382,295],{"class":225},[190,384,385],{"class":298},"120.5",[190,387,302],{"class":225},[190,389,390],{"class":199},"\"n\u002Fa\"",[190,392,302],{"class":225},[190,394,395],{"class":298},"80.0",[190,397,302],{"class":225},[190,399,400],{"class":221},"-",[190,402,403],{"class":298},"5.0",[190,405,317],{"class":225},[190,407,409],{"class":89,"line":408},12,[190,410,411],{"class":225},"})\n",[190,413,415,418,420],{"class":89,"line":414},13,[190,416,417],{"class":225},"buffer ",[190,419,283],{"class":221},[190,421,422],{"class":225}," io.BytesIO()\n",[190,424,426,429,433,435,438,440,443,445,448],{"class":89,"line":425},14,[190,427,428],{"class":225},"orders.to_excel(buffer, ",[190,430,432],{"class":431},"sa561","index",[190,434,283],{"class":221},[190,436,437],{"class":298},"False",[190,439,302],{"class":225},[190,441,442],{"class":431},"sheet_name",[190,444,283],{"class":221},[190,446,447],{"class":199},"\"Orders\"",[190,449,450],{"class":225},")\n",[190,452,454,457,459],{"class":89,"line":453},15,[190,455,456],{"class":225},"upload ",[190,458,283],{"class":221},[190,460,461],{"class":225}," buffer.getvalue()\n",[176,463,465],{"id":464},"step-1-recognise-the-file-from-its-bytes-and-zip-directory","Step 1 — Recognise the file from its bytes and zip directory",[10,467,468,469,471],{},"An ",[14,470,16],{}," is a zip archive, and a zip archive lists every part with its compressed and uncompressed size before anything is extracted. That makes a whole class of checks nearly free:",[181,473,475],{"className":212,"code":474,"language":214,"meta":186,"style":186},"OLE_MAGIC = b\"\\xd0\\xcf\\x11\\xe0\\xa1\\xb1\\x1a\\xe1\"\nZIP_MAGIC = b\"PK\\x03\\x04\"\n\ndef inspect_container(data, max_unpacked=100_000_000, max_ratio=100, max_entries=2_000):\n    \"\"\"Return a list of problems found without parsing any sheet.\"\"\"\n    if data.startswith(OLE_MAGIC):\n        if \"EncryptionInfo\".encode(\"utf-16-le\") in data:\n            return [\"the workbook is password-protected; remove the password and upload it again\"]\n        return [\"this is an old .xls workbook; save it as .xlsx and upload it again\"]\n    if not data.startswith(ZIP_MAGIC):\n        return [\"this is not an Excel workbook (.xlsx); is it a CSV or a renamed file?\"]\n    try:\n        archive = zipfile.ZipFile(io.BytesIO(data))\n    except zipfile.BadZipFile:\n        return [\"the file is damaged or incomplete; try uploading it again\"]\n    entries = archive.infolist()\n    names = {e.filename for e in entries}\n    problems = []\n    if \"[Content_Types].xml\" not in names or \"xl\u002Fworkbook.xml\" not in names:\n        return [\"the file is a zip archive but not an Excel workbook\"]\n    unpacked = sum(e.file_size for e in entries)\n    if len(entries) > max_entries or unpacked > max_unpacked or unpacked > max_ratio * len(data):\n        problems.append(f\"the workbook expands to {unpacked \u002F 1e6:.0f} MB, too large to process\")\n    if \"xl\u002FvbaProject.bin\" in names:\n        problems.append(\"the workbook contains macros; save it as a macro-free .xlsx\")\n    if any(n.startswith(\"xl\u002FexternalLinks\u002F\") for n in names):\n        problems.append(\"the workbook links to other files; paste the linked values in and upload again\")\n    return problems\n",[14,476,477,498,515,519,555,560,572,595,609,621,634,645,653,663,671,682,693,715,726,755,767,790,834,869,881,891,917,927],{"__ignoreMap":186},[190,478,479,482,485,488,491,495],{"class":89,"line":192},[190,480,481],{"class":298},"OLE_MAGIC",[190,483,484],{"class":221}," =",[190,486,487],{"class":221}," b",[190,489,490],{"class":199},"\"",[190,492,494],{"class":493},"sSjpA","\\xd0\\xcf\\x11\\xe0\\xa1\\xb1\\x1a\\xe1",[190,496,497],{"class":199},"\"\n",[190,499,500,503,505,507,510,513],{"class":89,"line":229},[190,501,502],{"class":298},"ZIP_MAGIC",[190,504,484],{"class":221},[190,506,487],{"class":221},[190,508,509],{"class":199},"\"PK",[190,511,512],{"class":493},"\\x03\\x04",[190,514,497],{"class":199},[190,516,517],{"class":89,"line":237},[190,518,241],{"emptyLinePlaceholder":240},[190,520,521,524,528,531,533,536,539,541,544,547,549,552],{"class":89,"line":244},[190,522,523],{"class":221},"def",[190,525,527],{"class":526},"s_Opv"," inspect_container",[190,529,530],{"class":225},"(data, max_unpacked",[190,532,283],{"class":221},[190,534,535],{"class":298},"100_000_000",[190,537,538],{"class":225},", max_ratio",[190,540,283],{"class":221},[190,542,543],{"class":298},"100",[190,545,546],{"class":225},", max_entries",[190,548,283],{"class":221},[190,550,551],{"class":298},"2_000",[190,553,554],{"class":225},"):\n",[190,556,557],{"class":89,"line":258},[190,558,559],{"class":199},"    \"\"\"Return a list of problems found without parsing any sheet.\"\"\"\n",[190,561,562,565,568,570],{"class":89,"line":272},[190,563,564],{"class":221},"    if",[190,566,567],{"class":225}," data.startswith(",[190,569,481],{"class":298},[190,571,554],{"class":225},[190,573,574,577,580,583,586,589,592],{"class":89,"line":277},[190,575,576],{"class":221},"        if",[190,578,579],{"class":199}," \"EncryptionInfo\"",[190,581,582],{"class":225},".encode(",[190,584,585],{"class":199},"\"utf-16-le\"",[190,587,588],{"class":225},") ",[190,590,591],{"class":221},"in",[190,593,594],{"class":225}," data:\n",[190,596,597,600,603,606],{"class":89,"line":289},[190,598,599],{"class":221},"            return",[190,601,602],{"class":225}," [",[190,604,605],{"class":199},"\"the workbook is password-protected; remove the password and upload it again\"",[190,607,608],{"class":225},"]\n",[190,610,611,614,616,619],{"class":89,"line":320},[190,612,613],{"class":221},"        return",[190,615,602],{"class":225},[190,617,618],{"class":199},"\"this is an old .xls workbook; save it as .xlsx and upload it again\"",[190,620,608],{"class":225},[190,622,623,625,628,630,632],{"class":89,"line":348},[190,624,564],{"class":221},[190,626,627],{"class":221}," not",[190,629,567],{"class":225},[190,631,502],{"class":298},[190,633,554],{"class":225},[190,635,636,638,640,643],{"class":89,"line":377},[190,637,613],{"class":221},[190,639,602],{"class":225},[190,641,642],{"class":199},"\"this is not an Excel workbook (.xlsx); is it a CSV or a renamed file?\"",[190,644,608],{"class":225},[190,646,647,650],{"class":89,"line":408},[190,648,649],{"class":221},"    try",[190,651,652],{"class":225},":\n",[190,654,655,658,660],{"class":89,"line":414},[190,656,657],{"class":225},"        archive ",[190,659,283],{"class":221},[190,661,662],{"class":225}," zipfile.ZipFile(io.BytesIO(data))\n",[190,664,665,668],{"class":89,"line":425},[190,666,667],{"class":221},"    except",[190,669,670],{"class":225}," zipfile.BadZipFile:\n",[190,672,673,675,677,680],{"class":89,"line":453},[190,674,613],{"class":221},[190,676,602],{"class":225},[190,678,679],{"class":199},"\"the file is damaged or incomplete; try uploading it again\"",[190,681,608],{"class":225},[190,683,685,688,690],{"class":89,"line":684},16,[190,686,687],{"class":225},"    entries ",[190,689,283],{"class":221},[190,691,692],{"class":225}," archive.infolist()\n",[190,694,696,699,701,704,707,710,712],{"class":89,"line":695},17,[190,697,698],{"class":225},"    names ",[190,700,283],{"class":221},[190,702,703],{"class":225}," {e.filename ",[190,705,706],{"class":221},"for",[190,708,709],{"class":225}," e ",[190,711,591],{"class":221},[190,713,714],{"class":225}," entries}\n",[190,716,718,721,723],{"class":89,"line":717},18,[190,719,720],{"class":225},"    problems ",[190,722,283],{"class":221},[190,724,725],{"class":225}," []\n",[190,727,729,731,734,736,739,742,745,748,750,752],{"class":89,"line":728},19,[190,730,564],{"class":221},[190,732,733],{"class":199}," \"[Content_Types].xml\"",[190,735,627],{"class":221},[190,737,738],{"class":221}," in",[190,740,741],{"class":225}," names ",[190,743,744],{"class":221},"or",[190,746,747],{"class":199}," \"xl\u002Fworkbook.xml\"",[190,749,627],{"class":221},[190,751,738],{"class":221},[190,753,754],{"class":225}," names:\n",[190,756,758,760,762,765],{"class":89,"line":757},20,[190,759,613],{"class":221},[190,761,602],{"class":225},[190,763,764],{"class":199},"\"the file is a zip archive but not an Excel workbook\"",[190,766,608],{"class":225},[190,768,770,773,775,778,781,783,785,787],{"class":89,"line":769},21,[190,771,772],{"class":225},"    unpacked ",[190,774,283],{"class":221},[190,776,777],{"class":298}," sum",[190,779,780],{"class":225},"(e.file_size ",[190,782,706],{"class":221},[190,784,709],{"class":225},[190,786,591],{"class":221},[190,788,789],{"class":225}," entries)\n",[190,791,793,795,798,801,804,807,809,812,814,817,819,821,823,826,829,831],{"class":89,"line":792},22,[190,794,564],{"class":221},[190,796,797],{"class":298}," len",[190,799,800],{"class":225},"(entries) ",[190,802,803],{"class":221},">",[190,805,806],{"class":225}," max_entries ",[190,808,744],{"class":221},[190,810,811],{"class":225}," unpacked ",[190,813,803],{"class":221},[190,815,816],{"class":225}," max_unpacked ",[190,818,744],{"class":221},[190,820,811],{"class":225},[190,822,803],{"class":221},[190,824,825],{"class":225}," max_ratio ",[190,827,828],{"class":221},"*",[190,830,797],{"class":298},[190,832,833],{"class":225},"(data):\n",[190,835,837,840,843,846,849,852,855,858,861,864,867],{"class":89,"line":836},23,[190,838,839],{"class":225},"        problems.append(",[190,841,842],{"class":221},"f",[190,844,845],{"class":199},"\"the workbook expands to ",[190,847,848],{"class":493},"{",[190,850,851],{"class":225},"unpacked ",[190,853,854],{"class":221},"\u002F",[190,856,857],{"class":298}," 1e6",[190,859,860],{"class":221},":.0f",[190,862,863],{"class":493},"}",[190,865,866],{"class":199}," MB, too large to process\"",[190,868,450],{"class":225},[190,870,872,874,877,879],{"class":89,"line":871},24,[190,873,564],{"class":221},[190,875,876],{"class":199}," \"xl\u002FvbaProject.bin\"",[190,878,738],{"class":221},[190,880,754],{"class":225},[190,882,884,886,889],{"class":89,"line":883},25,[190,885,839],{"class":225},[190,887,888],{"class":199},"\"the workbook contains macros; save it as a macro-free .xlsx\"",[190,890,450],{"class":225},[190,892,894,896,899,902,905,907,909,912,914],{"class":89,"line":893},26,[190,895,564],{"class":221},[190,897,898],{"class":298}," any",[190,900,901],{"class":225},"(n.startswith(",[190,903,904],{"class":199},"\"xl\u002FexternalLinks\u002F\"",[190,906,588],{"class":225},[190,908,706],{"class":221},[190,910,911],{"class":225}," n ",[190,913,591],{"class":221},[190,915,916],{"class":225}," names):\n",[190,918,920,922,925],{"class":89,"line":919},27,[190,921,839],{"class":225},[190,923,924],{"class":199},"\"the workbook links to other files; paste the linked values in and upload again\"",[190,926,450],{"class":225},[190,928,930,933],{"class":89,"line":929},28,[190,931,932],{"class":221},"    return",[190,934,935],{"class":225}," problems\n",[10,937,938],{},"Each problem is phrased as something the user can do. Run against real files, the checks took well under a millisecond each and returned:",[29,940,38,945,38,948,38,951,38,954,38,960,38,965,38,971,38,974,38,978,38,983,38,989,38,991,38,995,38,997,38,1000,38,1003,38,1007,38,1009,38,1013,38,1015,38,1018,38,1021,38,1025,38,1027,38,1031,38,1033,38,1035,38,1038,38,1042,38,1044,38,1048,38,1050,38,1053,38,1056,38,1060,38,1062,38,1065,38,1067,38,1070,38,1073,38,1077,38,1079,38,1083,38,1085],{"viewBox":941,"role":32,"ariaLabelledBy":942,"xmlns":36,"style":37},"4 0 752 276",[943,944],"vu-bad-t","vu-bad-d",[40,946,947],{"id":943},"What pandas does with each bad upload, and what the check says",[44,949,950],{"id":944},"A CSV renamed to xlsx makes pandas say the format cannot be determined; the check says it is not an Excel workbook. An old xls file is read by pandas only if xlrd is installed; the check asks for xlsx. A password-protected workbook makes pandas report it cannot find the workbook in an OLE2 document; the check says it is password-protected. A macro-enabled file and a file with external links are read silently by pandas; the check refuses both. A compression bomb took pandas 26 seconds and 570 MB; the check refuses it at once.",[48,952],{"x":50,"y":51,"width":52,"height":953,"fill":54},"276",[48,955],{"x":57,"y":58,"width":956,"height":957,"rx":51,"fill":54,"stroke":958,"style":959},"160.4","40.0","var(--line,#cdd5e6)","stroke-width:1px",[48,961],{"x":962,"y":58,"width":963,"height":957,"rx":51,"fill":62,"stroke":63,"style":964},"180.4","279.8","stroke-width:1.2px",[77,966,970],{"x":967,"y":968,"style":969},"320.3","41.0","font-size:13px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","pd.read_excel alone",[48,972],{"x":973,"y":58,"width":963,"height":957,"rx":51,"fill":62,"stroke":63,"style":964},"460.2",[77,975,977],{"x":976,"y":968,"style":969},"600.1","inspect_container",[48,979],{"x":57,"y":980,"width":956,"height":981,"rx":51,"fill":982,"stroke":958,"style":959},"56.0","34.0","#e7ebef",[77,984,988],{"x":985,"y":986,"style":987},"32.0","78.0","font-size:12.5px;font-weight:700;fill:var(--text,#172033);text-anchor:start","CSV renamed .xlsx",[48,990],{"x":962,"y":980,"width":963,"height":981,"rx":51,"fill":54,"stroke":958,"style":959},[77,992,994],{"x":967,"y":993,"style":86},"77.0","ValueError: format cannot be determined",[48,996],{"x":973,"y":980,"width":963,"height":981,"rx":51,"fill":54,"stroke":958,"style":959},[77,998,999],{"x":976,"y":993,"style":86},"not an Excel workbook",[48,1001],{"x":57,"y":1002,"width":956,"height":981,"rx":51,"fill":982,"stroke":958,"style":959},"90.0",[77,1004,1006],{"x":985,"y":1005,"style":987},"112.0","old .xls",[48,1008],{"x":962,"y":1002,"width":963,"height":981,"rx":51,"fill":54,"stroke":958,"style":959},[77,1010,1012],{"x":967,"y":1011,"style":86},"111.0","read only if xlrd is installed",[48,1014],{"x":973,"y":1002,"width":963,"height":981,"rx":51,"fill":54,"stroke":958,"style":959},[77,1016,1017],{"x":976,"y":1011,"style":86},"save it as .xlsx",[48,1019],{"x":57,"y":1020,"width":956,"height":981,"rx":51,"fill":982,"stroke":958,"style":959},"124.0",[77,1022,1024],{"x":985,"y":1023,"style":987},"146.0","password-protected",[48,1026],{"x":962,"y":1020,"width":963,"height":981,"rx":51,"fill":54,"stroke":958,"style":959},[77,1028,1030],{"x":967,"y":1029,"style":86},"145.0","XLRDError: can't find workbook in OLE2",[48,1032],{"x":973,"y":1020,"width":963,"height":981,"rx":51,"fill":54,"stroke":958,"style":959},[77,1034,1024],{"x":976,"y":1029,"style":86},[48,1036],{"x":57,"y":1037,"width":956,"height":981,"rx":51,"fill":982,"stroke":958,"style":959},"158.0",[77,1039,1041],{"x":985,"y":1040,"style":987},"180.0","macros inside",[48,1043],{"x":962,"y":1037,"width":963,"height":981,"rx":51,"fill":54,"stroke":958,"style":959},[77,1045,1047],{"x":967,"y":1046,"style":86},"179.0","read silently",[48,1049],{"x":973,"y":1037,"width":963,"height":981,"rx":51,"fill":54,"stroke":958,"style":959},[77,1051,1052],{"x":976,"y":1046,"style":86},"contains macros",[48,1054],{"x":57,"y":1055,"width":956,"height":981,"rx":51,"fill":982,"stroke":958,"style":959},"192.0",[77,1057,1059],{"x":985,"y":1058,"style":987},"214.0","external links",[48,1061],{"x":962,"y":1055,"width":963,"height":981,"rx":51,"fill":54,"stroke":958,"style":959},[77,1063,1047],{"x":967,"y":1064,"style":86},"213.0",[48,1066],{"x":973,"y":1055,"width":963,"height":981,"rx":51,"fill":54,"stroke":958,"style":959},[77,1068,1069],{"x":976,"y":1064,"style":86},"links to other files",[48,1071],{"x":57,"y":1072,"width":956,"height":981,"rx":51,"fill":982,"stroke":958,"style":959},"226.0",[77,1074,1076],{"x":985,"y":1075,"style":987},"248.0","406 KB bomb",[48,1078],{"x":962,"y":1072,"width":963,"height":981,"rx":51,"fill":54,"stroke":958,"style":959},[77,1080,1082],{"x":967,"y":1081,"style":86},"247.0","26 s, 570 MB peak",[48,1084],{"x":973,"y":1072,"width":963,"height":981,"rx":51,"fill":54,"stroke":958,"style":959},[77,1086,1087],{"x":976,"y":1081,"style":86},"expands to 118 MB",[10,1089,1090,1091,1094,1095,1098,1099,1103],{},"Password-protected workbooks deserve their own message: they are stored in the same OLE2 container format as old ",[14,1092,1093],{},".xls"," files, so without the ",[14,1096,1097],{},"EncryptionInfo"," check a user who protected their file is told to \"save it as .xlsx\" — which it already is. The renamed-CSV case is the one behind most reports of pandas' \"Excel file format cannot be determined\", explained in ",[23,1100,1102],{"href":1101},"\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-excel-file-format-cannot-be-determined-in-pandas\u002F","Fix \"Excel File Format Cannot Be Determined\" in pandas",". Macros and external links are not dangerous to a parser that never runs them, but a service that stores and redistributes uploads should not pass them on.",[176,1105,1107],{"id":1106},"step-2-refuse-compression-bombs-before-parsing","Step 2 — Refuse compression bombs before parsing",[10,1109,1110],{},"The size check in Step 1 exists because of files like this one: a 406 KB upload whose sheet XML repeats one row a million times. Its zip directory says it expands to 118 MB, a ratio of nearly 300 to 1. Given to pandas, it took 26 seconds and peaked at 570 MB of memory before returning a million rows — per request, on a web worker. Legitimate workbooks compress too, typically around 5 to 10 to 1 for data sheets, so a ratio limit of 100 leaves plenty of room while catching deliberate abuse. Set the absolute limit from the largest real file you expect.",[10,1112,1113],{},"Test the check against the sample upload and two quick fakes:",[181,1115,1117],{"className":212,"code":1116,"language":214,"meta":186,"style":186},"print(inspect_container(upload))\nprint(inspect_container(b\"Order ID,Region\\n1001,North\\n\"))\n\nwith zipfile.ZipFile(io.BytesIO(upload)) as src:\n    macro_buffer = io.BytesIO()\n    with zipfile.ZipFile(macro_buffer, \"w\") as dst:\n        for item in src.infolist():\n            dst.writestr(item, src.read(item.filename))\n        dst.writestr(\"xl\u002FvbaProject.bin\", b\"macro code\")\nprint(inspect_container(macro_buffer.getvalue()))\n# []\n# ['this is not an Excel workbook (.xlsx); is it a CSV or a renamed file?']\n# ['the workbook contains macros; save it as a macro-free .xlsx']\n",[14,1118,1119,1127,1153,1157,1170,1179,1197,1210,1215,1232,1239,1245,1250],{"__ignoreMap":186},[190,1120,1121,1124],{"class":89,"line":192},[190,1122,1123],{"class":298},"print",[190,1125,1126],{"class":225},"(inspect_container(upload))\n",[190,1128,1129,1131,1134,1137,1140,1143,1146,1148,1150],{"class":89,"line":229},[190,1130,1123],{"class":298},[190,1132,1133],{"class":225},"(inspect_container(",[190,1135,1136],{"class":221},"b",[190,1138,1139],{"class":199},"\"Order ID,Region",[190,1141,1142],{"class":493},"\\n",[190,1144,1145],{"class":199},"1001,North",[190,1147,1142],{"class":493},[190,1149,490],{"class":199},[190,1151,1152],{"class":225},"))\n",[190,1154,1155],{"class":89,"line":237},[190,1156,241],{"emptyLinePlaceholder":240},[190,1158,1159,1162,1165,1167],{"class":89,"line":244},[190,1160,1161],{"class":221},"with",[190,1163,1164],{"class":225}," zipfile.ZipFile(io.BytesIO(upload)) ",[190,1166,252],{"class":221},[190,1168,1169],{"class":225}," src:\n",[190,1171,1172,1175,1177],{"class":89,"line":258},[190,1173,1174],{"class":225},"    macro_buffer ",[190,1176,283],{"class":221},[190,1178,422],{"class":225},[190,1180,1181,1184,1187,1190,1192,1194],{"class":89,"line":272},[190,1182,1183],{"class":221},"    with",[190,1185,1186],{"class":225}," zipfile.ZipFile(macro_buffer, ",[190,1188,1189],{"class":199},"\"w\"",[190,1191,588],{"class":225},[190,1193,252],{"class":221},[190,1195,1196],{"class":225}," dst:\n",[190,1198,1199,1202,1205,1207],{"class":89,"line":277},[190,1200,1201],{"class":221},"        for",[190,1203,1204],{"class":225}," item ",[190,1206,591],{"class":221},[190,1208,1209],{"class":225}," src.infolist():\n",[190,1211,1212],{"class":89,"line":289},[190,1213,1214],{"class":225},"            dst.writestr(item, src.read(item.filename))\n",[190,1216,1217,1220,1223,1225,1227,1230],{"class":89,"line":320},[190,1218,1219],{"class":225},"        dst.writestr(",[190,1221,1222],{"class":199},"\"xl\u002FvbaProject.bin\"",[190,1224,302],{"class":225},[190,1226,1136],{"class":221},[190,1228,1229],{"class":199},"\"macro code\"",[190,1231,450],{"class":225},[190,1233,1234,1236],{"class":89,"line":348},[190,1235,1123],{"class":298},[190,1237,1238],{"class":225},"(inspect_container(macro_buffer.getvalue()))\n",[190,1240,1241],{"class":89,"line":377},[190,1242,1244],{"class":1243},"s-wDw","# []\n",[190,1246,1247],{"class":89,"line":408},[190,1248,1249],{"class":1243},"# ['this is not an Excel workbook (.xlsx); is it a CSV or a renamed file?']\n",[190,1251,1252],{"class":89,"line":414},[190,1253,1254],{"class":1243},"# ['the workbook contains macros; save it as a macro-free .xlsx']\n",[176,1256,1258],{"id":1257},"step-3-check-the-rows-against-a-schema-cell-by-cell","Step 3 — Check the rows against a schema, cell by cell",[10,1260,1261],{},"A file that passes the container checks can still be wrong in the cells. Describe what each column must contain, check every column, and report each problem with the cell address the user will see in Excel:",[181,1263,1265],{"className":212,"code":1264,"language":214,"meta":186,"style":186},"SCHEMA = {\n    \"order id\": {\"type\": \"integer\", \"unique\": True},\n    \"region\": {\"type\": \"text\", \"allowed\": {\"North\", \"South\", \"East\", \"West\"}},\n    \"order date\": {\"type\": \"date\"},\n    \"amount\": {\"type\": \"number\", \"min\": 0},\n}\n\ndef validate_sheet(data, sheet=\"Orders\", schema=SCHEMA, max_errors=50):\n    try:\n        frame = pd.read_excel(io.BytesIO(data), sheet_name=sheet, dtype=object,\n                              keep_default_na=False, na_values=[\"\"])      # keep \"n\u002Fa\" as text\n    except ValueError as error:                              # e.g. missing sheet\n        return [{\"cell\": None, \"problem\": str(error)}]\n    columns = {str(c).strip().casefold(): i for i, c in enumerate(frame.columns)}\n    missing = [name for name in schema if name not in columns]\n    if missing:\n        return [{\"cell\": None, \"problem\": f\"missing column(s): {', '.join(missing)}\"}]\n\n    errors = []\n    def add(name, index, problem):\n        cell = f\"{get_column_letter(columns[name] + 1)}{index + 2}\"   # header is row 1\n        errors.append({\"cell\": cell, \"column\": name, \"problem\": problem})\n\n    for name, rule in schema.items():\n        values = frame.iloc[:, columns[name]]\n        for i in values.index[values.isna()]:\n            add(name, i, \"is empty\")\n        values = values.dropna()\n        if rule[\"type\"] in (\"integer\", \"number\"):\n            numbers = pd.to_numeric(values, errors=\"coerce\")\n            for i in values.index[numbers.isna()]:\n                add(name, i, f\"expected a number, found {values[i]!r}\")\n            if rule[\"type\"] == \"integer\":\n                for i in values.index[numbers.notna() & (numbers % 1 != 0)]:\n                    add(name, i, f\"expected a whole number, found {values[i]!r}\")\n            if \"min\" in rule:\n                for i in values.index[numbers \u003C rule[\"min\"]]:\n                    add(name, i, f\"must be at least {rule['min']}, found {values[i]!r}\")\n        elif rule[\"type\"] == \"date\":\n            dates = pd.to_datetime(values, errors=\"coerce\")\n            for i in values.index[dates.isna()]:\n                add(name, i, f\"expected a date, found {values[i]!r}\")\n        if \"allowed\" in rule:\n            for i in values.index[~values.isin(rule[\"allowed\"])]:\n                add(name, i, f\"{values[i]!r} is not one of {sorted(rule['allowed'])}\")\n        if rule.get(\"unique\"):\n            for i in values.index[values.duplicated(keep=\"first\")]:\n                add(name, i, f\"duplicate value {values[i]!r}\")\n    errors.sort(key=lambda e: (int(e[\"cell\"].lstrip(\"ABCDEFGHIJKLMNOPQRSTUVWXYZ\")), e[\"cell\"]))\n    return errors[:max_errors]\n\nfor error in validate_sheet(upload):\n    print(error[\"cell\"], error[\"problem\"])\n# D3 expected a number, found 'n\u002Fa'\n# B4 'Nrth' is not one of ['East', 'North', 'South', 'West']\n# A5 duplicate value 1002\n# D5 must be at least 0, found -5\n",[14,1266,1267,1277,1307,1346,1362,1387,1392,1396,1427,1433,1461,1489,1505,1533,1561,1594,1601,1639,1643,1652,1663,1707,1728,1732,1745,1755,1767,1777,1786,1812,1833,1846,1871,1891,1924,1947,1960,1982,2020,2039,2058,2070,2092,2104,2127,2167,2179,2201,2223,2260,2268,2273,2286,2305,2311,2317,2323],{"__ignoreMap":186},[190,1268,1269,1272,1274],{"class":89,"line":192},[190,1270,1271],{"class":298},"SCHEMA",[190,1273,484],{"class":221},[190,1275,1276],{"class":225}," {\n",[190,1278,1279,1282,1285,1288,1291,1294,1296,1299,1301,1304],{"class":89,"line":229},[190,1280,1281],{"class":199},"    \"order id\"",[190,1283,1284],{"class":225},": {",[190,1286,1287],{"class":199},"\"type\"",[190,1289,1290],{"class":225},": ",[190,1292,1293],{"class":199},"\"integer\"",[190,1295,302],{"class":225},[190,1297,1298],{"class":199},"\"unique\"",[190,1300,1290],{"class":225},[190,1302,1303],{"class":298},"True",[190,1305,1306],{"class":225},"},\n",[190,1308,1309,1312,1314,1316,1318,1321,1323,1326,1328,1330,1332,1334,1336,1338,1340,1343],{"class":89,"line":237},[190,1310,1311],{"class":199},"    \"region\"",[190,1313,1284],{"class":225},[190,1315,1287],{"class":199},[190,1317,1290],{"class":225},[190,1319,1320],{"class":199},"\"text\"",[190,1322,302],{"class":225},[190,1324,1325],{"class":199},"\"allowed\"",[190,1327,1284],{"class":225},[190,1329,328],{"class":199},[190,1331,302],{"class":225},[190,1333,333],{"class":199},[190,1335,302],{"class":225},[190,1337,343],{"class":199},[190,1339,302],{"class":225},[190,1341,1342],{"class":199},"\"West\"",[190,1344,1345],{"class":225},"}},\n",[190,1347,1348,1351,1353,1355,1357,1360],{"class":89,"line":244},[190,1349,1350],{"class":199},"    \"order date\"",[190,1352,1284],{"class":225},[190,1354,1287],{"class":199},[190,1356,1290],{"class":225},[190,1358,1359],{"class":199},"\"date\"",[190,1361,1306],{"class":225},[190,1363,1364,1367,1369,1371,1373,1376,1378,1381,1383,1385],{"class":89,"line":258},[190,1365,1366],{"class":199},"    \"amount\"",[190,1368,1284],{"class":225},[190,1370,1287],{"class":199},[190,1372,1290],{"class":225},[190,1374,1375],{"class":199},"\"number\"",[190,1377,302],{"class":225},[190,1379,1380],{"class":199},"\"min\"",[190,1382,1290],{"class":225},[190,1384,51],{"class":298},[190,1386,1306],{"class":225},[190,1388,1389],{"class":89,"line":272},[190,1390,1391],{"class":225},"}\n",[190,1393,1394],{"class":89,"line":277},[190,1395,241],{"emptyLinePlaceholder":240},[190,1397,1398,1400,1403,1406,1408,1410,1413,1415,1417,1420,1422,1425],{"class":89,"line":289},[190,1399,523],{"class":221},[190,1401,1402],{"class":526}," validate_sheet",[190,1404,1405],{"class":225},"(data, sheet",[190,1407,283],{"class":221},[190,1409,447],{"class":199},[190,1411,1412],{"class":225},", schema",[190,1414,283],{"class":221},[190,1416,1271],{"class":298},[190,1418,1419],{"class":225},", max_errors",[190,1421,283],{"class":221},[190,1423,1424],{"class":298},"50",[190,1426,554],{"class":225},[190,1428,1429,1431],{"class":89,"line":320},[190,1430,649],{"class":221},[190,1432,652],{"class":225},[190,1434,1435,1438,1440,1443,1445,1447,1450,1453,1455,1458],{"class":89,"line":348},[190,1436,1437],{"class":225},"        frame ",[190,1439,283],{"class":221},[190,1441,1442],{"class":225}," pd.read_excel(io.BytesIO(data), ",[190,1444,442],{"class":431},[190,1446,283],{"class":221},[190,1448,1449],{"class":225},"sheet, ",[190,1451,1452],{"class":431},"dtype",[190,1454,283],{"class":221},[190,1456,1457],{"class":298},"object",[190,1459,1460],{"class":225},",\n",[190,1462,1463,1466,1468,1470,1472,1475,1477,1480,1483,1486],{"class":89,"line":377},[190,1464,1465],{"class":431},"                              keep_default_na",[190,1467,283],{"class":221},[190,1469,437],{"class":298},[190,1471,302],{"class":225},[190,1473,1474],{"class":431},"na_values",[190,1476,283],{"class":221},[190,1478,1479],{"class":225},"[",[190,1481,1482],{"class":199},"\"\"",[190,1484,1485],{"class":225},"])      ",[190,1487,1488],{"class":1243},"# keep \"n\u002Fa\" as text\n",[190,1490,1491,1493,1496,1499,1502],{"class":89,"line":408},[190,1492,667],{"class":221},[190,1494,1495],{"class":298}," ValueError",[190,1497,1498],{"class":221}," as",[190,1500,1501],{"class":225}," error:                              ",[190,1503,1504],{"class":1243},"# e.g. missing sheet\n",[190,1506,1507,1509,1512,1515,1517,1520,1522,1525,1527,1530],{"class":89,"line":414},[190,1508,613],{"class":221},[190,1510,1511],{"class":225}," [{",[190,1513,1514],{"class":199},"\"cell\"",[190,1516,1290],{"class":225},[190,1518,1519],{"class":298},"None",[190,1521,302],{"class":225},[190,1523,1524],{"class":199},"\"problem\"",[190,1526,1290],{"class":225},[190,1528,1529],{"class":298},"str",[190,1531,1532],{"class":225},"(error)}]\n",[190,1534,1535,1538,1540,1543,1545,1548,1550,1553,1555,1558],{"class":89,"line":425},[190,1536,1537],{"class":225},"    columns ",[190,1539,283],{"class":221},[190,1541,1542],{"class":225}," {",[190,1544,1529],{"class":298},[190,1546,1547],{"class":225},"(c).strip().casefold(): i ",[190,1549,706],{"class":221},[190,1551,1552],{"class":225}," i, c ",[190,1554,591],{"class":221},[190,1556,1557],{"class":298}," enumerate",[190,1559,1560],{"class":225},"(frame.columns)}\n",[190,1562,1563,1566,1568,1571,1573,1576,1578,1581,1584,1586,1589,1591],{"class":89,"line":453},[190,1564,1565],{"class":225},"    missing ",[190,1567,283],{"class":221},[190,1569,1570],{"class":225}," [name ",[190,1572,706],{"class":221},[190,1574,1575],{"class":225}," name ",[190,1577,591],{"class":221},[190,1579,1580],{"class":225}," schema ",[190,1582,1583],{"class":221},"if",[190,1585,1575],{"class":225},[190,1587,1588],{"class":221},"not",[190,1590,738],{"class":221},[190,1592,1593],{"class":225}," columns]\n",[190,1595,1596,1598],{"class":89,"line":684},[190,1597,564],{"class":221},[190,1599,1600],{"class":225}," missing:\n",[190,1602,1603,1605,1607,1609,1611,1613,1615,1617,1619,1621,1624,1626,1629,1632,1634,1636],{"class":89,"line":695},[190,1604,613],{"class":221},[190,1606,1511],{"class":225},[190,1608,1514],{"class":199},[190,1610,1290],{"class":225},[190,1612,1519],{"class":298},[190,1614,302],{"class":225},[190,1616,1524],{"class":199},[190,1618,1290],{"class":225},[190,1620,842],{"class":221},[190,1622,1623],{"class":199},"\"missing column(s): ",[190,1625,848],{"class":493},[190,1627,1628],{"class":199},"', '",[190,1630,1631],{"class":225},".join(missing)",[190,1633,863],{"class":493},[190,1635,490],{"class":199},[190,1637,1638],{"class":225},"}]\n",[190,1640,1641],{"class":89,"line":717},[190,1642,241],{"emptyLinePlaceholder":240},[190,1644,1645,1648,1650],{"class":89,"line":728},[190,1646,1647],{"class":225},"    errors ",[190,1649,283],{"class":221},[190,1651,725],{"class":225},[190,1653,1654,1657,1660],{"class":89,"line":757},[190,1655,1656],{"class":221},"    def",[190,1658,1659],{"class":526}," add",[190,1661,1662],{"class":225},"(name, index, problem):\n",[190,1664,1665,1668,1670,1673,1675,1677,1680,1683,1686,1689,1692,1695,1697,1700,1702,1704],{"class":89,"line":769},[190,1666,1667],{"class":225},"        cell ",[190,1669,283],{"class":221},[190,1671,1672],{"class":221}," f",[190,1674,490],{"class":199},[190,1676,848],{"class":493},[190,1678,1679],{"class":225},"get_column_letter(columns[name] ",[190,1681,1682],{"class":221},"+",[190,1684,1685],{"class":298}," 1",[190,1687,1688],{"class":225},")",[190,1690,1691],{"class":493},"}{",[190,1693,1694],{"class":225},"index ",[190,1696,1682],{"class":221},[190,1698,1699],{"class":298}," 2",[190,1701,863],{"class":493},[190,1703,490],{"class":199},[190,1705,1706],{"class":1243},"   # header is row 1\n",[190,1708,1709,1712,1714,1717,1720,1723,1725],{"class":89,"line":792},[190,1710,1711],{"class":225},"        errors.append({",[190,1713,1514],{"class":199},[190,1715,1716],{"class":225},": cell, ",[190,1718,1719],{"class":199},"\"column\"",[190,1721,1722],{"class":225},": name, ",[190,1724,1524],{"class":199},[190,1726,1727],{"class":225},": problem})\n",[190,1729,1730],{"class":89,"line":836},[190,1731,241],{"emptyLinePlaceholder":240},[190,1733,1734,1737,1740,1742],{"class":89,"line":871},[190,1735,1736],{"class":221},"    for",[190,1738,1739],{"class":225}," name, rule ",[190,1741,591],{"class":221},[190,1743,1744],{"class":225}," schema.items():\n",[190,1746,1747,1750,1752],{"class":89,"line":883},[190,1748,1749],{"class":225},"        values ",[190,1751,283],{"class":221},[190,1753,1754],{"class":225}," frame.iloc[:, columns[name]]\n",[190,1756,1757,1759,1762,1764],{"class":89,"line":893},[190,1758,1201],{"class":221},[190,1760,1761],{"class":225}," i ",[190,1763,591],{"class":221},[190,1765,1766],{"class":225}," values.index[values.isna()]:\n",[190,1768,1769,1772,1775],{"class":89,"line":919},[190,1770,1771],{"class":225},"            add(name, i, ",[190,1773,1774],{"class":199},"\"is empty\"",[190,1776,450],{"class":225},[190,1778,1779,1781,1783],{"class":89,"line":929},[190,1780,1749],{"class":225},[190,1782,283],{"class":221},[190,1784,1785],{"class":225}," values.dropna()\n",[190,1787,1789,1791,1794,1796,1799,1801,1804,1806,1808,1810],{"class":89,"line":1788},29,[190,1790,576],{"class":221},[190,1792,1793],{"class":225}," rule[",[190,1795,1287],{"class":199},[190,1797,1798],{"class":225},"] ",[190,1800,591],{"class":221},[190,1802,1803],{"class":225}," (",[190,1805,1293],{"class":199},[190,1807,302],{"class":225},[190,1809,1375],{"class":199},[190,1811,554],{"class":225},[190,1813,1815,1818,1820,1823,1826,1828,1831],{"class":89,"line":1814},30,[190,1816,1817],{"class":225},"            numbers ",[190,1819,283],{"class":221},[190,1821,1822],{"class":225}," pd.to_numeric(values, ",[190,1824,1825],{"class":431},"errors",[190,1827,283],{"class":221},[190,1829,1830],{"class":199},"\"coerce\"",[190,1832,450],{"class":225},[190,1834,1836,1839,1841,1843],{"class":89,"line":1835},31,[190,1837,1838],{"class":221},"            for",[190,1840,1761],{"class":225},[190,1842,591],{"class":221},[190,1844,1845],{"class":225}," values.index[numbers.isna()]:\n",[190,1847,1849,1852,1854,1857,1859,1862,1865,1867,1869],{"class":89,"line":1848},32,[190,1850,1851],{"class":225},"                add(name, i, ",[190,1853,842],{"class":221},[190,1855,1856],{"class":199},"\"expected a number, found ",[190,1858,848],{"class":493},[190,1860,1861],{"class":225},"values[i]",[190,1863,1864],{"class":221},"!r",[190,1866,863],{"class":493},[190,1868,490],{"class":199},[190,1870,450],{"class":225},[190,1872,1874,1877,1879,1881,1883,1886,1889],{"class":89,"line":1873},33,[190,1875,1876],{"class":221},"            if",[190,1878,1793],{"class":225},[190,1880,1287],{"class":199},[190,1882,1798],{"class":225},[190,1884,1885],{"class":221},"==",[190,1887,1888],{"class":199}," \"integer\"",[190,1890,652],{"class":225},[190,1892,1894,1897,1899,1901,1904,1907,1910,1913,1915,1918,1921],{"class":89,"line":1893},34,[190,1895,1896],{"class":221},"                for",[190,1898,1761],{"class":225},[190,1900,591],{"class":221},[190,1902,1903],{"class":225}," values.index[numbers.notna() ",[190,1905,1906],{"class":221},"&",[190,1908,1909],{"class":225}," (numbers ",[190,1911,1912],{"class":221},"%",[190,1914,1685],{"class":298},[190,1916,1917],{"class":221}," !=",[190,1919,1920],{"class":298}," 0",[190,1922,1923],{"class":225},")]:\n",[190,1925,1927,1930,1932,1935,1937,1939,1941,1943,1945],{"class":89,"line":1926},35,[190,1928,1929],{"class":225},"                    add(name, i, ",[190,1931,842],{"class":221},[190,1933,1934],{"class":199},"\"expected a whole number, found ",[190,1936,848],{"class":493},[190,1938,1861],{"class":225},[190,1940,1864],{"class":221},[190,1942,863],{"class":493},[190,1944,490],{"class":199},[190,1946,450],{"class":225},[190,1948,1950,1952,1955,1957],{"class":89,"line":1949},36,[190,1951,1876],{"class":221},[190,1953,1954],{"class":199}," \"min\"",[190,1956,738],{"class":221},[190,1958,1959],{"class":225}," rule:\n",[190,1961,1963,1965,1967,1969,1972,1975,1977,1979],{"class":89,"line":1962},37,[190,1964,1896],{"class":221},[190,1966,1761],{"class":225},[190,1968,591],{"class":221},[190,1970,1971],{"class":225}," values.index[numbers ",[190,1973,1974],{"class":221},"\u003C",[190,1976,1793],{"class":225},[190,1978,1380],{"class":199},[190,1980,1981],{"class":225},"]]:\n",[190,1983,1985,1987,1989,1992,1994,1997,2000,2003,2005,2008,2010,2012,2014,2016,2018],{"class":89,"line":1984},38,[190,1986,1929],{"class":225},[190,1988,842],{"class":221},[190,1990,1991],{"class":199},"\"must be at least ",[190,1993,848],{"class":493},[190,1995,1996],{"class":225},"rule[",[190,1998,1999],{"class":199},"'min'",[190,2001,2002],{"class":225},"]",[190,2004,863],{"class":493},[190,2006,2007],{"class":199},", found ",[190,2009,848],{"class":493},[190,2011,1861],{"class":225},[190,2013,1864],{"class":221},[190,2015,863],{"class":493},[190,2017,490],{"class":199},[190,2019,450],{"class":225},[190,2021,2023,2026,2028,2030,2032,2034,2037],{"class":89,"line":2022},39,[190,2024,2025],{"class":221},"        elif",[190,2027,1793],{"class":225},[190,2029,1287],{"class":199},[190,2031,1798],{"class":225},[190,2033,1885],{"class":221},[190,2035,2036],{"class":199}," \"date\"",[190,2038,652],{"class":225},[190,2040,2042,2045,2047,2050,2052,2054,2056],{"class":89,"line":2041},40,[190,2043,2044],{"class":225},"            dates ",[190,2046,283],{"class":221},[190,2048,2049],{"class":225}," pd.to_datetime(values, ",[190,2051,1825],{"class":431},[190,2053,283],{"class":221},[190,2055,1830],{"class":199},[190,2057,450],{"class":225},[190,2059,2061,2063,2065,2067],{"class":89,"line":2060},41,[190,2062,1838],{"class":221},[190,2064,1761],{"class":225},[190,2066,591],{"class":221},[190,2068,2069],{"class":225}," values.index[dates.isna()]:\n",[190,2071,2073,2075,2077,2080,2082,2084,2086,2088,2090],{"class":89,"line":2072},42,[190,2074,1851],{"class":225},[190,2076,842],{"class":221},[190,2078,2079],{"class":199},"\"expected a date, found ",[190,2081,848],{"class":493},[190,2083,1861],{"class":225},[190,2085,1864],{"class":221},[190,2087,863],{"class":493},[190,2089,490],{"class":199},[190,2091,450],{"class":225},[190,2093,2095,2097,2100,2102],{"class":89,"line":2094},43,[190,2096,576],{"class":221},[190,2098,2099],{"class":199}," \"allowed\"",[190,2101,738],{"class":221},[190,2103,1959],{"class":225},[190,2105,2107,2109,2111,2113,2116,2119,2122,2124],{"class":89,"line":2106},44,[190,2108,1838],{"class":221},[190,2110,1761],{"class":225},[190,2112,591],{"class":221},[190,2114,2115],{"class":225}," values.index[",[190,2117,2118],{"class":221},"~",[190,2120,2121],{"class":225},"values.isin(rule[",[190,2123,1325],{"class":199},[190,2125,2126],{"class":225},"])]:\n",[190,2128,2130,2132,2134,2136,2138,2140,2142,2144,2147,2149,2152,2155,2158,2161,2163,2165],{"class":89,"line":2129},45,[190,2131,1851],{"class":225},[190,2133,842],{"class":221},[190,2135,490],{"class":199},[190,2137,848],{"class":493},[190,2139,1861],{"class":225},[190,2141,1864],{"class":221},[190,2143,863],{"class":493},[190,2145,2146],{"class":199}," is not one of ",[190,2148,848],{"class":493},[190,2150,2151],{"class":298},"sorted",[190,2153,2154],{"class":225},"(rule[",[190,2156,2157],{"class":199},"'allowed'",[190,2159,2160],{"class":225},"])",[190,2162,863],{"class":493},[190,2164,490],{"class":199},[190,2166,450],{"class":225},[190,2168,2170,2172,2175,2177],{"class":89,"line":2169},46,[190,2171,576],{"class":221},[190,2173,2174],{"class":225}," rule.get(",[190,2176,1298],{"class":199},[190,2178,554],{"class":225},[190,2180,2182,2184,2186,2188,2191,2194,2196,2199],{"class":89,"line":2181},47,[190,2183,1838],{"class":221},[190,2185,1761],{"class":225},[190,2187,591],{"class":221},[190,2189,2190],{"class":225}," values.index[values.duplicated(",[190,2192,2193],{"class":431},"keep",[190,2195,283],{"class":221},[190,2197,2198],{"class":199},"\"first\"",[190,2200,1923],{"class":225},[190,2202,2204,2206,2208,2211,2213,2215,2217,2219,2221],{"class":89,"line":2203},48,[190,2205,1851],{"class":225},[190,2207,842],{"class":221},[190,2209,2210],{"class":199},"\"duplicate value ",[190,2212,848],{"class":493},[190,2214,1861],{"class":225},[190,2216,1864],{"class":221},[190,2218,863],{"class":493},[190,2220,490],{"class":199},[190,2222,450],{"class":225},[190,2224,2226,2229,2232,2235,2238,2241,2244,2246,2249,2252,2255,2257],{"class":89,"line":2225},49,[190,2227,2228],{"class":225},"    errors.sort(",[190,2230,2231],{"class":431},"key",[190,2233,2234],{"class":221},"=lambda",[190,2236,2237],{"class":225}," e: (",[190,2239,2240],{"class":298},"int",[190,2242,2243],{"class":225},"(e[",[190,2245,1514],{"class":199},[190,2247,2248],{"class":225},"].lstrip(",[190,2250,2251],{"class":199},"\"ABCDEFGHIJKLMNOPQRSTUVWXYZ\"",[190,2253,2254],{"class":225},")), e[",[190,2256,1514],{"class":199},[190,2258,2259],{"class":225},"]))\n",[190,2261,2263,2265],{"class":89,"line":2262},50,[190,2264,932],{"class":221},[190,2266,2267],{"class":225}," errors[:max_errors]\n",[190,2269,2271],{"class":89,"line":2270},51,[190,2272,241],{"emptyLinePlaceholder":240},[190,2274,2276,2278,2281,2283],{"class":89,"line":2275},52,[190,2277,706],{"class":221},[190,2279,2280],{"class":225}," error ",[190,2282,591],{"class":221},[190,2284,2285],{"class":225}," validate_sheet(upload):\n",[190,2287,2289,2292,2295,2297,2300,2302],{"class":89,"line":2288},53,[190,2290,2291],{"class":298},"    print",[190,2293,2294],{"class":225},"(error[",[190,2296,1514],{"class":199},[190,2298,2299],{"class":225},"], error[",[190,2301,1524],{"class":199},[190,2303,2304],{"class":225},"])\n",[190,2306,2308],{"class":89,"line":2307},54,[190,2309,2310],{"class":1243},"# D3 expected a number, found 'n\u002Fa'\n",[190,2312,2314],{"class":89,"line":2313},55,[190,2315,2316],{"class":1243},"# B4 'Nrth' is not one of ['East', 'North', 'South', 'West']\n",[190,2318,2320],{"class":89,"line":2319},56,[190,2321,2322],{"class":1243},"# A5 duplicate value 1002\n",[190,2324,2326],{"class":89,"line":2325},57,[190,2327,2328],{"class":1243},"# D5 must be at least 0, found -5\n",[10,2330,2331,2332,2335,2336,2339,2340,2344],{},"Three details make the messages trustworthy. Headers are matched after trimming and case-folding, so \"Order ID \" and \"order id\" both work. Every value is read as-is with ",[14,2333,2334],{},"dtype=object",", so type checks see what the user typed. And ",[14,2337,2338],{},"keep_default_na=False"," stops pandas turning the text \"n\u002Fa\" into a missing value: with the default, the first message read \"expected a number, found nan\", which quotes nothing the user can find. Reading every column as text-or-number and checking it yourself is the same approach as ",[23,2341,2343],{"href":2342},"\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fcheck-excel-data-types-with-pandas\u002F","Check Excel Data Types with pandas",".",[29,2346,38,2352,38,2355,38,2358,38,2363,38,2367,38,2371,38,2377,38,2381,38,2385,38,2389,38,2393,38,2397,38,2401,38,2405,38,2409,38,2412,38,2417,38,2419,38,2421,38,2424,38,2426,38,2428,38,2430,38,2432,38,2434,38,2436,38,2439,38,2443,38,2446,38,2449,38,2451,38,2454,38,2456,38,2459,38,2461,38,2464,38,2466,38,2469,38,2472,38,2476,38,2478,38,2481,38,2483,38,2485,38,2487,38,2490,38,2492,38,2495,38,2497,38,2499,38,2501,38,2504,38,2506,38,2508,38,2510,38,2512,38,2514,38,2517,38,2519,38,2522,38,2526,38,2530,38,2533,38,2537,38,2539,38,2541,38,2543,38,2545,38,2547,38,2550,38,2552,38,2554,38,2556,38,2559,38,2561,38,2565,38,2567,38,2569,38,2571,38,2573,38,2575,38,2578,38,2580,38,2583,38,2585,38,2588],{"viewBox":2347,"role":32,"ariaLabelledBy":2348,"xmlns":36,"style":2351},"167 0 425 250",[2349,2350],"vu-cells-t","vu-cells-d","width:100%;max-width:425px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[40,2353,2354],{"id":2349},"The four cells the validator reports",[44,2356,2357],{"id":2350},"The Orders sheet from the sample upload. B4 contains the misspelled region Nrth, D3 contains the text n\u002Fa in the Amount column, A5 repeats order number 1002 from row 3, and D5 contains a negative amount of minus 5.",[48,2359],{"x":2360,"y":51,"width":2361,"height":2362,"fill":54},"167","425","250",[48,2364],{"x":2365,"y":58,"width":985,"height":2366,"rx":51,"fill":982,"stroke":958,"style":959},"183.8","24.0",[48,2368],{"x":2369,"y":58,"width":2370,"height":2366,"rx":51,"fill":982,"stroke":958,"style":959},"215.8","44.0",[77,2372,2376],{"x":2373,"y":2374,"style":2375},"237.8","33.0","font-size:11px;font-weight:600;fill:var(--muted,#5b6780);text-anchor:middle","A",[48,2378],{"x":2379,"y":58,"width":2380,"height":2366,"rx":51,"fill":982,"stroke":958,"style":959},"259.8","75.0",[77,2382,2384],{"x":2383,"y":2374,"style":2375},"297.3","B",[48,2386],{"x":2387,"y":58,"width":2388,"height":2366,"rx":51,"fill":982,"stroke":958,"style":959},"334.9","66.7",[77,2390,2392],{"x":2391,"y":2374,"style":2375},"368.2","C",[48,2394],{"x":2395,"y":58,"width":2396,"height":2366,"rx":51,"fill":982,"stroke":958,"style":959},"401.6","101.6",[77,2398,2400],{"x":2399,"y":2374,"style":2375},"452.4","D",[48,2402],{"x":2403,"y":58,"width":2404,"height":2366,"rx":51,"fill":982,"stroke":958,"style":959},"503.2","73.0",[77,2406,2408],{"x":2407,"y":2374,"style":2375},"539.7","E",[48,2410],{"x":2365,"y":957,"width":985,"height":2411,"rx":51,"fill":982,"stroke":958,"style":959},"28.0",[77,2413,2416],{"x":2414,"y":2415,"style":2375},"199.8","59.0","1",[48,2418],{"x":2369,"y":957,"width":2370,"height":2411,"rx":51,"fill":54,"stroke":958,"style":959},[48,2420],{"x":2379,"y":957,"width":2380,"height":2411,"rx":51,"fill":54,"stroke":958,"style":959},[77,2422,2376],{"x":2383,"y":2415,"style":2423},"font-size:12px;font-weight:700;fill:var(--text,#172033);text-anchor:middle",[48,2425],{"x":2387,"y":957,"width":2388,"height":2411,"rx":51,"fill":54,"stroke":958,"style":959},[77,2427,2384],{"x":2391,"y":2415,"style":2423},[48,2429],{"x":2395,"y":957,"width":2396,"height":2411,"rx":51,"fill":54,"stroke":958,"style":959},[77,2431,2392],{"x":2399,"y":2415,"style":2423},[48,2433],{"x":2403,"y":957,"width":2404,"height":2411,"rx":51,"fill":54,"stroke":958,"style":959},[77,2435,2400],{"x":2407,"y":2415,"style":2423},[48,2437],{"x":2365,"y":2438,"width":985,"height":2411,"rx":51,"fill":982,"stroke":958,"style":959},"68.0",[77,2440,2442],{"x":2414,"y":2441,"style":2375},"87.0","2",[48,2444],{"x":2369,"y":2438,"width":2370,"height":2411,"rx":51,"fill":62,"stroke":63,"style":2445},"stroke-width:1.5px",[77,2447,2416],{"x":2373,"y":2441,"style":2448},"font-size:12px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle",[48,2450],{"x":2379,"y":2438,"width":2380,"height":2411,"rx":51,"fill":62,"stroke":63,"style":2445},[77,2452,2453],{"x":2383,"y":2441,"style":2448},"Order ID",[48,2455],{"x":2387,"y":2438,"width":2388,"height":2411,"rx":51,"fill":62,"stroke":63,"style":2445},[77,2457,2458],{"x":2391,"y":2441,"style":2448},"Region",[48,2460],{"x":2395,"y":2438,"width":2396,"height":2411,"rx":51,"fill":62,"stroke":63,"style":2445},[77,2462,2463],{"x":2399,"y":2441,"style":2448},"Order date",[48,2465],{"x":2403,"y":2438,"width":2404,"height":2411,"rx":51,"fill":62,"stroke":63,"style":2445},[77,2467,2468],{"x":2407,"y":2441,"style":2448},"Amount",[48,2470],{"x":2365,"y":2471,"width":985,"height":2411,"rx":51,"fill":982,"stroke":958,"style":959},"96.0",[77,2473,2475],{"x":2414,"y":2474,"style":2375},"115.0","3",[48,2477],{"x":2369,"y":2471,"width":2370,"height":2411,"rx":51,"fill":54,"stroke":958,"style":959},[77,2479,2442],{"x":2373,"y":2474,"style":2480},"font-size:12px;font-weight:400;fill:var(--text,#172033);text-anchor:middle",[48,2482],{"x":2379,"y":2471,"width":2380,"height":2411,"rx":51,"fill":54,"stroke":958,"style":959},[77,2484,299],{"x":2383,"y":2474,"style":2480},[48,2486],{"x":2387,"y":2471,"width":2388,"height":2411,"rx":51,"fill":54,"stroke":958,"style":959},[77,2488,2489],{"x":2391,"y":2474,"style":2480},"North",[48,2491],{"x":2395,"y":2471,"width":2396,"height":2411,"rx":51,"fill":54,"stroke":958,"style":959},[77,2493,2494],{"x":2399,"y":2474,"style":2480},"2026-09-01",[48,2496],{"x":2403,"y":2471,"width":2404,"height":2411,"rx":51,"fill":54,"stroke":958,"style":959},[77,2498,385],{"x":2407,"y":2474,"style":2480},[48,2500],{"x":2365,"y":1020,"width":985,"height":2411,"rx":51,"fill":982,"stroke":958,"style":959},[77,2502,50],{"x":2414,"y":2503,"style":2375},"143.0",[48,2505],{"x":2369,"y":1020,"width":2370,"height":2411,"rx":51,"fill":54,"stroke":958,"style":959},[77,2507,2475],{"x":2373,"y":2503,"style":2480},[48,2509],{"x":2379,"y":1020,"width":2380,"height":2411,"rx":51,"fill":54,"stroke":958,"style":959},[77,2511,305],{"x":2383,"y":2503,"style":2480},[48,2513],{"x":2387,"y":1020,"width":2388,"height":2411,"rx":51,"fill":54,"stroke":958,"style":959},[77,2515,2516],{"x":2391,"y":2503,"style":2480},"South",[48,2518],{"x":2395,"y":1020,"width":2396,"height":2411,"rx":51,"fill":54,"stroke":958,"style":959},[77,2520,2521],{"x":2399,"y":2503,"style":2480},"2026-09-02",[48,2523],{"x":2403,"y":1020,"width":2404,"height":2411,"rx":51,"fill":2524,"stroke":2525,"style":2445},"#fce9e9","var(--danger,#dc2626)",[77,2527,2529],{"x":2407,"y":2503,"style":2528},"font-size:12px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","n\u002Fa",[48,2531],{"x":2365,"y":2532,"width":985,"height":2411,"rx":51,"fill":982,"stroke":958,"style":959},"152.0",[77,2534,2536],{"x":2414,"y":2535,"style":2375},"171.0","5",[48,2538],{"x":2369,"y":2532,"width":2370,"height":2411,"rx":51,"fill":54,"stroke":958,"style":959},[77,2540,50],{"x":2373,"y":2535,"style":2480},[48,2542],{"x":2379,"y":2532,"width":2380,"height":2411,"rx":51,"fill":54,"stroke":958,"style":959},[77,2544,310],{"x":2383,"y":2535,"style":2480},[48,2546],{"x":2387,"y":2532,"width":2388,"height":2411,"rx":51,"fill":2524,"stroke":2525,"style":2445},[77,2548,2549],{"x":2391,"y":2535,"style":2528},"Nrth",[48,2551],{"x":2395,"y":2532,"width":2396,"height":2411,"rx":51,"fill":54,"stroke":958,"style":959},[77,2553,2521],{"x":2399,"y":2535,"style":2480},[48,2555],{"x":2403,"y":2532,"width":2404,"height":2411,"rx":51,"fill":54,"stroke":958,"style":959},[77,2557,2558],{"x":2407,"y":2535,"style":2480},"80",[48,2560],{"x":2365,"y":1040,"width":985,"height":2411,"rx":51,"fill":982,"stroke":958,"style":959},[77,2562,2564],{"x":2414,"y":2563,"style":2375},"199.0","6",[48,2566],{"x":2369,"y":1040,"width":2370,"height":2411,"rx":51,"fill":54,"stroke":958,"style":959},[77,2568,2536],{"x":2373,"y":2563,"style":2480},[48,2570],{"x":2379,"y":1040,"width":2380,"height":2411,"rx":51,"fill":2524,"stroke":2525,"style":2445},[77,2572,305],{"x":2383,"y":2563,"style":2528},[48,2574],{"x":2387,"y":1040,"width":2388,"height":2411,"rx":51,"fill":54,"stroke":958,"style":959},[77,2576,2577],{"x":2391,"y":2563,"style":2480},"East",[48,2579],{"x":2395,"y":1040,"width":2396,"height":2411,"rx":51,"fill":54,"stroke":958,"style":959},[77,2581,2582],{"x":2399,"y":2563,"style":2480},"2026-09-03",[48,2584],{"x":2403,"y":1040,"width":2404,"height":2411,"rx":51,"fill":2524,"stroke":2525,"style":2445},[77,2586,2587],{"x":2407,"y":2563,"style":2528},"-5",[77,2589,2593],{"x":2590,"y":2591,"style":2592},"380.0","230.0","font-size:12.5px;font-weight:400;fill:var(--muted,#5b6780);text-anchor:middle","each error names a cell the user can click on: D3, B4, A5, D5",[176,2595,2597],{"id":2596},"step-4-one-function-for-the-web-layer","Step 4 — One function for the web layer",[10,2599,2600],{},"Combine the layers into a single call that returns a result the front end can render, and stop at the first layer that fails — there is no point checking rows in a file that is not a workbook:",[181,2602,2604],{"className":212,"code":2603,"language":214,"meta":186,"style":186},"def check_upload(filename, data, max_bytes=20_000_000):\n    if len(data) > max_bytes:\n        return {\"accepted\": False, \"problems\": [{\"cell\": None, \"problem\": \"the file is larger than 20 MB\"}]}\n    problems = inspect_container(data)\n    if problems:\n        return {\"accepted\": False, \"problems\": [{\"cell\": None, \"problem\": p} for p in problems]}\n    errors = validate_sheet(data)\n    return {\"accepted\": not errors, \"problems\": errors}\n\nresult = check_upload(\"orders.xlsx\", upload)\nprint(result[\"accepted\"], len(result[\"problems\"]))\n# False 4\n",[14,2605,2606,2623,2637,2676,2685,2692,2733,2742,2762,2766,2782,2803],{"__ignoreMap":186},[190,2607,2608,2610,2613,2616,2618,2621],{"class":89,"line":192},[190,2609,523],{"class":221},[190,2611,2612],{"class":526}," check_upload",[190,2614,2615],{"class":225},"(filename, data, max_bytes",[190,2617,283],{"class":221},[190,2619,2620],{"class":298},"20_000_000",[190,2622,554],{"class":225},[190,2624,2625,2627,2629,2632,2634],{"class":89,"line":229},[190,2626,564],{"class":221},[190,2628,797],{"class":298},[190,2630,2631],{"class":225},"(data) ",[190,2633,803],{"class":221},[190,2635,2636],{"class":225}," max_bytes:\n",[190,2638,2639,2641,2643,2646,2648,2650,2652,2655,2658,2660,2662,2664,2666,2668,2670,2673],{"class":89,"line":237},[190,2640,613],{"class":221},[190,2642,1542],{"class":225},[190,2644,2645],{"class":199},"\"accepted\"",[190,2647,1290],{"class":225},[190,2649,437],{"class":298},[190,2651,302],{"class":225},[190,2653,2654],{"class":199},"\"problems\"",[190,2656,2657],{"class":225},": [{",[190,2659,1514],{"class":199},[190,2661,1290],{"class":225},[190,2663,1519],{"class":298},[190,2665,302],{"class":225},[190,2667,1524],{"class":199},[190,2669,1290],{"class":225},[190,2671,2672],{"class":199},"\"the file is larger than 20 MB\"",[190,2674,2675],{"class":225},"}]}\n",[190,2677,2678,2680,2682],{"class":89,"line":244},[190,2679,720],{"class":225},[190,2681,283],{"class":221},[190,2683,2684],{"class":225}," inspect_container(data)\n",[190,2686,2687,2689],{"class":89,"line":258},[190,2688,564],{"class":221},[190,2690,2691],{"class":225}," problems:\n",[190,2693,2694,2696,2698,2700,2702,2704,2706,2708,2710,2712,2714,2716,2718,2720,2723,2725,2728,2730],{"class":89,"line":272},[190,2695,613],{"class":221},[190,2697,1542],{"class":225},[190,2699,2645],{"class":199},[190,2701,1290],{"class":225},[190,2703,437],{"class":298},[190,2705,302],{"class":225},[190,2707,2654],{"class":199},[190,2709,2657],{"class":225},[190,2711,1514],{"class":199},[190,2713,1290],{"class":225},[190,2715,1519],{"class":298},[190,2717,302],{"class":225},[190,2719,1524],{"class":199},[190,2721,2722],{"class":225},": p} ",[190,2724,706],{"class":221},[190,2726,2727],{"class":225}," p ",[190,2729,591],{"class":221},[190,2731,2732],{"class":225}," problems]}\n",[190,2734,2735,2737,2739],{"class":89,"line":277},[190,2736,1647],{"class":225},[190,2738,283],{"class":221},[190,2740,2741],{"class":225}," validate_sheet(data)\n",[190,2743,2744,2746,2748,2750,2752,2754,2757,2759],{"class":89,"line":289},[190,2745,932],{"class":221},[190,2747,1542],{"class":225},[190,2749,2645],{"class":199},[190,2751,1290],{"class":225},[190,2753,1588],{"class":221},[190,2755,2756],{"class":225}," errors, ",[190,2758,2654],{"class":199},[190,2760,2761],{"class":225},": errors}\n",[190,2763,2764],{"class":89,"line":320},[190,2765,241],{"emptyLinePlaceholder":240},[190,2767,2768,2771,2773,2776,2779],{"class":89,"line":348},[190,2769,2770],{"class":225},"result ",[190,2772,283],{"class":221},[190,2774,2775],{"class":225}," check_upload(",[190,2777,2778],{"class":199},"\"orders.xlsx\"",[190,2780,2781],{"class":225},", upload)\n",[190,2783,2784,2786,2789,2791,2794,2797,2799,2801],{"class":89,"line":377},[190,2785,1123],{"class":298},[190,2787,2788],{"class":225},"(result[",[190,2790,2645],{"class":199},[190,2792,2793],{"class":225},"], ",[190,2795,2796],{"class":298},"len",[190,2798,2788],{"class":225},[190,2800,2654],{"class":199},[190,2802,2259],{"class":225},[190,2804,2805],{"class":89,"line":408},[190,2806,2807],{"class":1243},"# False 4\n",[10,2809,2810,2811,2814,2815,2818,2819,2823,2824,2344],{},"In Flask or FastAPI, return this with status 422 when it is not accepted; in Streamlit, show the problems with ",[14,2812,2813],{},"st.error"," and ",[14,2816,2817],{},"st.stop()",", as in ",[23,2820,2822],{"href":2821},"\u002Fautomating-reporting-workflows\u002Fserving-excel-files-from-python-web-apps\u002Fbuild-a-streamlit-app-to-upload-and-download-excel\u002F","Build a Streamlit App to Upload and Download Excel Files",". Receiving the upload itself — body limits, framework specifics — is covered in ",[23,2825,2827],{"href":2826},"\u002Fautomating-reporting-workflows\u002Fserving-excel-files-from-python-web-apps\u002Fhandle-an-uploaded-excel-file-in-flask-and-fastapi\u002F","Handle an Uploaded Excel File in Flask and FastAPI",[176,2829,2831],{"id":2830},"common-pitfalls","Common pitfalls",[2833,2834,2835,2843,2849,2863],"ul",{},[2836,2837,2838,2842],"li",{},[2839,2840,2841],"strong",{},"Trusting the extension or the browser's MIME type."," Both come from the client. Check the bytes.",[2836,2844,2845,2848],{},[2839,2846,2847],{},"Stopping at the first data error."," Users fix one cell, upload again and get the next error. Report all of them, capped at a sensible number such as 50.",[2836,2850,2851,2854,2855,2858,2859,2862],{},[2839,2852,2853],{},"Row numbers off by two."," pandas rows start at 0 and the header occupies row 1, so data row ",[14,2856,2857],{},"i"," is sheet row ",[14,2860,2861],{},"i + 2",". Reporting the pandas index sends users to the wrong row.",[2836,2864,2865,2868],{},[2839,2866,2867],{},"Error messages that echo whole cells."," A cell can hold thousands of characters. Truncate quoted values before putting them in a response.",[176,2870,2872],{"id":2871},"conclusion","Conclusion",[10,2874,2875,2876,2814,2878,2880],{},"Validate uploads in layers: recognise the file from its first bytes, inspect the zip directory for the workbook parts, macros, external links and the expanded size, and only then read the sheet with ",[14,2877,2334],{},[14,2879,2338],{}," and check each column against a schema. Return every problem with a cell address and an instruction the user can follow.",[176,2882,2884],{"id":2883},"frequently-asked-questions","Frequently asked questions",[10,2886,2887,2890,2891,2894],{},[2839,2888,2889],{},"How do I check that an upload really is an .xlsx file?","\nLook at the bytes, not the extension. An .xlsx starts with the zip signature PK\\x03\\x04 and contains ",[190,2892,2893],{},"Content_Types",".xml and xl\u002Fworkbook.xml. Old .xls files and password-protected workbooks start with the OLE2 signature D0 CF 11 E0 instead.",[10,2896,2897,2900],{},[2839,2898,2899],{},"How can I detect a password-protected Excel upload?","\nAn encrypted .xlsx is stored inside an OLE2 container with an EncryptionInfo stream. Check for the OLE2 signature and the UTF-16 name EncryptionInfo. pandas otherwise fails with a confusing \"Can't find workbook in OLE2 compound document\".",[10,2902,2903,2906],{},[2839,2904,2905],{},"What is an Excel zip bomb and how do I stop it?","\nA small upload whose compressed sheet XML expands enormously. In a test, a 406 KB file expanded to 118 MB and took pandas 26 seconds and 570 MB of memory to read. Sum the uncompressed sizes in the zip directory before parsing and refuse files above a limit or ratio.",[10,2908,2909,2912,2913,2915],{},[2839,2910,2911],{},"Why does my validator say a cell is empty when it contains \"n\u002Fa\"?","\npandas treats strings such as \"n\u002Fa\", \"NA\" and \"null\" as missing by default. Read with keep_default_na=False and na_values=",[190,2914,1482],{}," so those strings stay text and the error message can quote what the user actually typed.",[176,2917,2919],{"id":2918},"related","Related",[2833,2921,2922,2928,2935,2942],{},[2836,2923,2924,2925,2927],{},"Up one level: ",[23,2926,26],{"href":25}," — Excel in and out of web applications.",[2836,2929,2930,2934],{},[23,2931,2933],{"href":2932},"\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fhighlight-invalid-cells-in-excel-with-python\u002F","Highlight Invalid Cells in Excel with Python"," — sending the errors back as a marked-up workbook.",[2836,2936,2937,2941],{},[23,2938,2940],{"href":2939},"\u002Fadvanced-data-transformation-and-cleaning\u002Fhandling-missing-data-in-excel-reports\u002Freplace-placeholder-text-with-nan-in-pandas\u002F","Replace Placeholder Text with NaN in pandas"," — the other side of the \"n\u002Fa\" problem.",[2836,2943,2944,2948],{},[23,2945,2947],{"href":2946},"\u002Fgetting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions\u002Fwork-with-macro-enabled-xlsm-files-in-openpyxl\u002F","Work with Macro-Enabled .xlsm Files in openpyxl"," — when macros are part of the requirement.",[2950,2951,2952],"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}html pre.shiki code .s-wDw, html code.shiki .s-wDw{--shiki-default:#6A737D;--shiki-dark:#BDC4CC}",{"title":186,"searchDepth":229,"depth":229,"links":2954},[2955,2956,2957,2958,2959,2960,2961,2962,2963],{"id":178,"depth":229,"text":179},{"id":464,"depth":229,"text":465},{"id":1106,"depth":229,"text":1107},{"id":1257,"depth":229,"text":1258},{"id":2596,"depth":229,"text":2597},{"id":2830,"depth":229,"text":2831},{"id":2871,"depth":229,"text":2872},{"id":2883,"depth":229,"text":2884},{"id":2918,"depth":229,"text":2919},"2026-09-23","Check an uploaded workbook in layers, cheapest first — the bytes, the zip container, then the rows — so renamed CSVs, old .xls files, password-protected workbooks, macros, external links and compression bombs are refused in under a millisecond, and data errors come back as a list of exact cells the user can fix.","md",[2968,2970,2972,2974],{"q":2889,"a":2969},"Look at the bytes, not the extension. An .xlsx starts with the zip signature PK\\x03\\x04 and contains [Content_Types].xml and xl\u002Fworkbook.xml. Old .xls files and password-protected workbooks start with the OLE2 signature D0 CF 11 E0 instead.",{"q":2899,"a":2971},"An encrypted .xlsx is stored inside an OLE2 container with an EncryptionInfo stream. Check for the OLE2 signature and the UTF-16 name EncryptionInfo. pandas otherwise fails with a confusing \"Can't find workbook in OLE2 compound document\".",{"q":2905,"a":2973},"A small upload whose compressed sheet XML expands enormously. In a test, a 406 KB file expanded to 118 MB and took pandas 26 seconds and 570 MB of memory to read. Sum the uncompressed sizes in the zip directory before parsing and refuse files above a limit or ratio.",{"q":2911,"a":2975},"pandas treats strings such as \"n\u002Fa\", \"NA\" and \"null\" as missing by default. Read with keep_default_na=False and na_values=[\"\"] so those strings stay text and the error message can quote what the user actually typed.","validate an uploaded excel file before processing it in a web app",null,{"breadcrumb":2979},[2980,2982,2985],{"name":2981,"item":854},"Home",{"name":2983,"item":2984},"Automating Reporting Workflows with Python and Excel","\u002Fautomating-reporting-workflows\u002F",{"name":26,"item":25},"\u002Fautomating-reporting-workflows\u002Fserving-excel-files-from-python-web-apps\u002Fvalidate-an-uploaded-excel-file-in-a-web-app",{"title":2988,"description":2989},"Validate an Uploaded Excel File in a Python Web App","Validate xlsx uploads in Flask, FastAPI or Streamlit before parsing — magic bytes, OLE2 detection for .xls and encrypted files, zip inspection for vbaProject.bin, externalLinks and expansion ratio, then schema checks with cell addresses like D3, keep_default_na pitfalls and one JSON result for the front end.","validate-an-uploaded-excel-file-in-a-web-app","automating-reporting-workflows\u002Fserving-excel-files-from-python-web-apps\u002Fvalidate-an-uploaded-excel-file-in-a-web-app\u002Findex",[2993,2994],"web-apps","data-validation","how-to","JIwY7g6AsuAY_lOnpLMb-8wXJ87xQkYn8asz1cIWzjY",[2998,3002],{"title":2999,"path":3000,"stem":3001,"children":-1},"Stream an Excel File from a FastAPI Endpoint","\u002Fautomating-reporting-workflows\u002Fserving-excel-files-from-python-web-apps\u002Fstream-an-excel-file-from-a-fastapi-endpoint","automating-reporting-workflows\u002Fserving-excel-files-from-python-web-apps\u002Fstream-an-excel-file-from-a-fastapi-endpoint\u002Findex",{"title":3003,"path":3004,"stem":3005,"children":-1},"Testing and Packaging Excel Automation Scripts","\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts","automating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Findex",[3007,3009],{"path":3008,"title":2983},"\u002Fautomating-reporting-workflows",{"path":3010,"title":26},"\u002Fautomating-reporting-workflows\u002Fserving-excel-files-from-python-web-apps",[],[3013,3016,3019,3022,3025,3027],{"path":3014,"title":3015,"kind":2995},"\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Fschedule-excel-reports-with-celery-beat","Schedule Excel Reports with Celery Beat",{"path":3017,"title":3018,"kind":2995},"\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Ftrigger-an-excel-report-from-a-webhook","Trigger an Excel Report from a Webhook",{"path":3020,"title":3021,"kind":2995},"\u002Fautomating-reporting-workflows\u002Fserving-excel-files-from-python-web-apps\u002Fadd-an-export-to-excel-action-in-django-admin","Add an Export to Excel Action in Django Admin",{"path":3023,"title":3024,"kind":2995},"\u002Fautomating-reporting-workflows\u002Fserving-excel-files-from-python-web-apps\u002Fbuild-a-gradio-app-that-cleans-an-uploaded-excel-file","Build a Gradio App That Cleans an Uploaded Excel File",{"path":3026,"title":2822,"kind":2995},"\u002Fautomating-reporting-workflows\u002Fserving-excel-files-from-python-web-apps\u002Fbuild-a-streamlit-app-to-upload-and-download-excel",{"path":3028,"title":3029,"kind":2995},"\u002Fautomating-reporting-workflows\u002Fserving-excel-files-from-python-web-apps\u002Fbuild-an-excel-workbook-in-memory-with-bytesio","Build an Excel Workbook in Memory with BytesIO",1790972140584]