[{"data":1,"prerenderedAt":2557},["ShallowReactive",2],{"doc:\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fvalidate-excel-rows-with-pydantic":3,"surround:\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fvalidate-excel-rows-with-pydantic":2524,"ancestors:\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fvalidate-excel-rows-with-pydantic":2532,"children:\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fvalidate-excel-rows-with-pydantic":2537,"related:\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fvalidate-excel-rows-with-pydantic":2538},{"id":4,"title":5,"body":6,"dateModified":2490,"datePublished":2490,"description":2491,"extension":2492,"faq":2493,"keyword":2502,"kind":2503,"meta":2504,"navigation":304,"path":2513,"seo":2514,"slug":2517,"stem":2518,"topics":2519,"type":2522,"__hash__":2523},"docs\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fvalidate-excel-rows-with-pydantic\u002Findex.md","Validate Excel Rows with pydantic Models",{"type":7,"value":8,"toc":2475},"minimark",[9,29,194,199,229,235,239,250,674,702,706,716,1091,1117,1121,1132,1432,1449,1581,1585,1588,1819,1841,2111,2115,2129,2174,2185,2189,2215,2299,2309,2313,2327,2331,2376,2380,2392,2396,2411,2415,2421,2427,2433,2439,2443,2471],[10,11,12,13,17,18,22,23,28],"p",{},"Checking a sheet cell by cell with ",[14,15,16],"code",{},"if"," statements works for three rules and becomes unreadable at thirty. A pydantic model turns the rules into a declaration — this column is a date, that one a positive whole number, this one one of three statuses — and validating a row against it reports ",[19,20,21],"em",{},"every"," broken rule for that row, with the field name and the value that failed. Because each row is checked separately, you can map each error back to its Excel row and column and hand the person who filled in the sheet a precise list to fix. This page is part of ",[24,25,27],"a",{"href":26},"\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002F","Validating Excel Data with Python",".",[30,31,39,40,39,44,39,48,39,56,39,66,39,77,39,84,39,89,39,95,39,100,39,103,39,109,39,114,39,118,39,122,39,126,39,129,39,132,39,138,39,142,39,145,39,149,39,152,39,155,39,161,39,165,39,168,39,172,39,175,39,178,39,184,39,188,39,191],"svg",{"viewBox":32,"role":33,"ariaLabelledBy":34,"xmlns":37,"style":38},"4 0 752 142","img",[35,36],"pyd-flow-t","pyd-flow-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  ",[41,42,43],"title",{"id":35},"Validating a sheet row by row",[45,46,47],"desc",{"id":36},"Five steps. Read the header row and each data row with openpyxl. Build a dict for each row keyed by header. Validate it with the pydantic model. Collect valid rows as model objects and invalid rows as errors with their Excel row number. Write an error report and load only the valid rows.",[49,50],"rect",{"x":51,"y":52,"width":53,"height":54,"fill":55},"4","0","752","142","#ffffff",[49,57],{"x":58,"y":59,"width":60,"height":61,"rx":62,"fill":63,"stroke":64,"style":65},"20.0","16.0","115.2","110.5","12","#f0f4ff","var(--brand,#5b5cf0)","stroke-width:2px",[67,68,70],"g",{"transform":69},"translate(65.6,37.8) scale(1.000)",[71,72],"path",{"d":73,"fill":74,"stroke":75,"style":76},"M4 4h16v16H4z M4 9h16 M4 14h16 M10 4v16","none","var(--brand-strong,#4338ca)","stroke-width:1.80px;stroke-linecap:round;stroke-linejoin:round",[78,79,83],"text",{"x":80,"y":81,"style":82},"77.6","84.2","font-size:13.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Read rows",[78,85,88],{"x":80,"y":86,"style":87},"103.8","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","openpyxl values",[90,91],"line",{"x1":92,"y1":93,"x2":94,"y2":93,"stroke":64,"style":65},"140.2","71.2","162.2",[96,97],"polygon",{"points":98,"fill":99},"166.2,71.2 157.2,76.2 157.2,66.2","#5b5cf0",[49,101],{"x":102,"y":59,"width":60,"height":61,"rx":62,"fill":63,"stroke":64,"style":65},"171.2",[67,104,106],{"transform":105},"translate(216.8,29.0) scale(1.000)",[71,107],{"d":108,"fill":74,"stroke":75,"style":76},"M3 5h18v14H3z M3 10h18 M3 15h18 M9 5v14",[78,110,113],{"x":111,"y":112,"style":82},"228.8","75.5","Row to dict",[78,115,117],{"x":111,"y":116,"style":87},"95.0","keyed by",[78,119,121],{"x":111,"y":120,"style":87},"112.5","header",[90,123],{"x1":124,"y1":93,"x2":125,"y2":93,"stroke":64,"style":65},"291.4","313.4",[96,127],{"points":128,"fill":99},"317.4,71.2 308.4,76.2 308.4,66.2",[49,130],{"x":131,"y":59,"width":60,"height":61,"rx":62,"fill":63,"stroke":64,"style":65},"322.4",[67,133,135],{"transform":134},"translate(368.0,37.8) scale(1.000)",[71,136],{"d":137,"fill":74,"stroke":75,"style":76},"M5 12.5l4.5 4.5L19 7",[78,139,141],{"x":140,"y":81,"style":82},"380.0","Validate",[78,143,144],{"x":140,"y":86,"style":87},"model_validate",[90,146],{"x1":147,"y1":93,"x2":148,"y2":93,"stroke":64,"style":65},"442.6","464.6",[96,150],{"points":151,"fill":99},"468.6,71.2 459.6,76.2 459.6,66.2",[49,153],{"x":154,"y":59,"width":60,"height":61,"rx":62,"fill":63,"stroke":64,"style":65},"473.6",[67,156,158],{"transform":157},"translate(519.2,37.8) scale(1.000)",[71,159],{"d":160,"fill":74,"stroke":75,"style":76},"M4 12h6 M10 12l6-6h4 M10 12l6 6h4 M17 3l3 3-3 3 M17 15l3 3-3 3",[78,162,164],{"x":163,"y":81,"style":82},"531.2","Split",[78,166,167],{"x":163,"y":86,"style":87},"valid or errors",[90,169],{"x1":170,"y1":93,"x2":171,"y2":93,"stroke":64,"style":65},"593.8","615.8",[96,173],{"points":174,"fill":99},"619.8,71.2 610.8,76.2 610.8,66.2",[49,176],{"x":177,"y":59,"width":60,"height":61,"rx":62,"fill":63,"stroke":64,"style":65},"624.8",[67,179,181],{"transform":180},"translate(670.4,29.0) scale(1.000)",[71,182],{"d":183,"fill":74,"stroke":75,"style":76},"M12 3.5l9.5 17h-19z M12 10v5 M12 17.8v.4",[78,185,187],{"x":186,"y":112,"style":82},"682.4","Report",[78,189,190],{"x":186,"y":116,"style":87},"row, column,",[78,192,193],{"x":186,"y":120,"style":87},"value",[195,196,198],"h2",{"id":197},"prerequisites","Prerequisites",[200,201,206],"pre",{"className":202,"code":203,"language":204,"meta":205,"style":205},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install \"pydantic>=2\" openpyxl pandas\n","bash","",[14,207,208],{"__ignoreMap":205},[209,210,212,216,220,223,226],"span",{"class":90,"line":211},1,[209,213,215],{"class":214},"sMTad","pip",[209,217,219],{"class":218},"srMev"," install",[209,221,222],{"class":218}," \"pydantic>=2\"",[209,224,225],{"class":218}," openpyxl",[209,227,228],{"class":218}," pandas\n",[10,230,231,234],{},[14,232,233],{},"orders.xlsx"," has six orders. The first is clean; the rest between them have a bad email address, a quantity of zero, a status in the wrong case, a date with a time, a price of \"n\u002Fa\", an unknown status, a discount on a small order and a missing order ID.",[195,236,238],{"id":237},"step-1-describe-a-valid-row-as-a-model","Step 1 — Describe a valid row as a model",[10,240,241,242,245,246,249],{},"Each field is a column. The type says what the value must be, ",[14,243,244],{},"Field"," adds constraints, and ",[14,247,248],{},"alias"," maps the field to the Excel header, which usually contains spaces and capitals:",[200,251,255],{"className":252,"code":253,"language":254,"meta":205,"style":205},"language-python shiki shiki-themes github-light github-dark-high-contrast","from datetime import date\nfrom typing import Literal, Optional\nfrom pydantic import BaseModel, ConfigDict, Field\n\nclass Order(BaseModel):\n    model_config = ConfigDict(str_strip_whitespace=True)\n\n    order_id: str = Field(alias=\"Order ID\", pattern=r\"^SO-\\d{4}$\")\n    email: str = Field(alias=\"Customer email\", pattern=r\"^[^@\\s]+@[^@\\s]+\\.[a-z]{2,}$\")\n    order_date: date = Field(alias=\"Order date\")\n    qty: int = Field(alias=\"Qty\", gt=0)\n    unit_price: float = Field(alias=\"Unit price\", gt=0)\n    status: Literal[\"open\", \"paid\", \"void\"] = Field(alias=\"Status\")\n    discount_pct: Optional[float] = Field(default=None, alias=\"Discount %\", ge=0, le=50)\n\nprint(list(Order.model_fields))\n","python",[14,256,257,273,286,299,306,325,350,355,410,488,507,538,568,603,655,660],{"__ignoreMap":205},[209,258,259,263,267,270],{"class":90,"line":211},[209,260,262],{"class":261},"s-kum","from",[209,264,266],{"class":265},"skGVy"," datetime ",[209,268,269],{"class":261},"import",[209,271,272],{"class":265}," date\n",[209,274,276,278,281,283],{"class":90,"line":275},2,[209,277,262],{"class":261},[209,279,280],{"class":265}," typing ",[209,282,269],{"class":261},[209,284,285],{"class":265}," Literal, Optional\n",[209,287,289,291,294,296],{"class":90,"line":288},3,[209,290,262],{"class":261},[209,292,293],{"class":265}," pydantic ",[209,295,269],{"class":261},[209,297,298],{"class":265}," BaseModel, ConfigDict, Field\n",[209,300,302],{"class":90,"line":301},4,[209,303,305],{"emptyLinePlaceholder":304},true,"\n",[209,307,309,312,315,318,322],{"class":90,"line":308},5,[209,310,311],{"class":261},"class",[209,313,314],{"class":214}," Order",[209,316,317],{"class":265},"(",[209,319,321],{"class":320},"sHg5J","BaseModel",[209,323,324],{"class":265},"):\n",[209,326,328,331,334,337,341,343,347],{"class":90,"line":327},6,[209,329,330],{"class":265},"    model_config ",[209,332,333],{"class":261},"=",[209,335,336],{"class":265}," ConfigDict(",[209,338,340],{"class":339},"sa561","str_strip_whitespace",[209,342,333],{"class":261},[209,344,346],{"class":345},"sP0c6","True",[209,348,349],{"class":265},")\n",[209,351,353],{"class":90,"line":352},7,[209,354,305],{"emptyLinePlaceholder":304},[209,356,358,361,364,367,370,372,374,377,380,383,385,388,391,394,397,400,403,406,408],{"class":90,"line":357},8,[209,359,360],{"class":265},"    order_id: ",[209,362,363],{"class":345},"str",[209,365,366],{"class":261}," =",[209,368,369],{"class":265}," Field(",[209,371,248],{"class":339},[209,373,333],{"class":261},[209,375,376],{"class":218},"\"Order ID\"",[209,378,379],{"class":265},", ",[209,381,382],{"class":339},"pattern",[209,384,333],{"class":261},[209,386,387],{"class":261},"r",[209,389,390],{"class":218},"\"",[209,392,393],{"class":345},"^",[209,395,396],{"class":218},"SO-",[209,398,399],{"class":345},"\\d",[209,401,402],{"class":261},"{4}",[209,404,405],{"class":345},"$",[209,407,390],{"class":218},[209,409,349],{"class":265},[209,411,413,416,418,420,422,424,426,429,431,433,435,437,439,442,444,448,451,454,456,459,461,463,465,467,471,473,476,479,482,484,486],{"class":90,"line":412},9,[209,414,415],{"class":265},"    email: ",[209,417,363],{"class":345},[209,419,366],{"class":261},[209,421,369],{"class":265},[209,423,248],{"class":339},[209,425,333],{"class":261},[209,427,428],{"class":218},"\"Customer email\"",[209,430,379],{"class":265},[209,432,382],{"class":339},[209,434,333],{"class":261},[209,436,387],{"class":261},[209,438,390],{"class":218},[209,440,441],{"class":345},"^[",[209,443,393],{"class":261},[209,445,447],{"class":446},"sSjpA","@",[209,449,450],{"class":345},"\\s]",[209,452,453],{"class":261},"+",[209,455,447],{"class":218},[209,457,458],{"class":345},"[",[209,460,393],{"class":261},[209,462,447],{"class":446},[209,464,450],{"class":345},[209,466,453],{"class":261},[209,468,470],{"class":469},"s_b0D","\\.",[209,472,458],{"class":345},[209,474,475],{"class":446},"a-z",[209,477,478],{"class":345},"]",[209,480,481],{"class":261},"{2,}",[209,483,405],{"class":345},[209,485,390],{"class":218},[209,487,349],{"class":265},[209,489,491,494,496,498,500,502,505],{"class":90,"line":490},10,[209,492,493],{"class":265},"    order_date: date ",[209,495,333],{"class":261},[209,497,369],{"class":265},[209,499,248],{"class":339},[209,501,333],{"class":261},[209,503,504],{"class":218},"\"Order date\"",[209,506,349],{"class":265},[209,508,510,513,516,518,520,522,524,527,529,532,534,536],{"class":90,"line":509},11,[209,511,512],{"class":265},"    qty: ",[209,514,515],{"class":345},"int",[209,517,366],{"class":261},[209,519,369],{"class":265},[209,521,248],{"class":339},[209,523,333],{"class":261},[209,525,526],{"class":218},"\"Qty\"",[209,528,379],{"class":265},[209,530,531],{"class":339},"gt",[209,533,333],{"class":261},[209,535,52],{"class":345},[209,537,349],{"class":265},[209,539,541,544,547,549,551,553,555,558,560,562,564,566],{"class":90,"line":540},12,[209,542,543],{"class":265},"    unit_price: ",[209,545,546],{"class":345},"float",[209,548,366],{"class":261},[209,550,369],{"class":265},[209,552,248],{"class":339},[209,554,333],{"class":261},[209,556,557],{"class":218},"\"Unit price\"",[209,559,379],{"class":265},[209,561,531],{"class":339},[209,563,333],{"class":261},[209,565,52],{"class":345},[209,567,349],{"class":265},[209,569,571,574,577,579,582,584,587,590,592,594,596,598,601],{"class":90,"line":570},13,[209,572,573],{"class":265},"    status: Literal[",[209,575,576],{"class":218},"\"open\"",[209,578,379],{"class":265},[209,580,581],{"class":218},"\"paid\"",[209,583,379],{"class":265},[209,585,586],{"class":218},"\"void\"",[209,588,589],{"class":265},"] ",[209,591,333],{"class":261},[209,593,369],{"class":265},[209,595,248],{"class":339},[209,597,333],{"class":261},[209,599,600],{"class":218},"\"Status\"",[209,602,349],{"class":265},[209,604,606,609,611,613,615,617,620,622,625,627,629,631,634,636,639,641,643,645,648,650,653],{"class":90,"line":605},14,[209,607,608],{"class":265},"    discount_pct: Optional[",[209,610,546],{"class":345},[209,612,589],{"class":265},[209,614,333],{"class":261},[209,616,369],{"class":265},[209,618,619],{"class":339},"default",[209,621,333],{"class":261},[209,623,624],{"class":345},"None",[209,626,379],{"class":265},[209,628,248],{"class":339},[209,630,333],{"class":261},[209,632,633],{"class":218},"\"Discount %\"",[209,635,379],{"class":265},[209,637,638],{"class":339},"ge",[209,640,333],{"class":261},[209,642,52],{"class":345},[209,644,379],{"class":265},[209,646,647],{"class":339},"le",[209,649,333],{"class":261},[209,651,652],{"class":345},"50",[209,654,349],{"class":265},[209,656,658],{"class":90,"line":657},15,[209,659,305],{"emptyLinePlaceholder":304},[209,661,663,666,668,671],{"class":90,"line":662},16,[209,664,665],{"class":345},"print",[209,667,317],{"class":265},[209,669,670],{"class":345},"list",[209,672,673],{"class":265},"(Order.model_fields))\n",[10,675,676,678,679,682,683,686,687,689,690,693,694,697,698,701],{},[14,677,340],{}," removes stray spaces before any check runs, so ",[14,680,681],{},"\" cai@example.com \""," passes. ",[14,684,685],{},"Literal"," restricts the status to three values, ",[14,688,382],{}," checks the ID and email format, and ",[14,691,692],{},"Optional[...] = None"," makes the discount column optional while still limiting its range when filled. For proper email validation, install ",[14,695,696],{},"email-validator"," and use pydantic's ",[14,699,700],{},"EmailStr"," instead of the pattern.",[195,703,705],{"id":704},"step-2-validate-each-row-and-keep-the-errors","Step 2 — Validate each row and keep the errors",[10,707,708,709,711,712,715],{},"Read the header and rows with openpyxl — empty cells arrive as ",[14,710,624],{},", which is what pydantic expects for a missing value — and validate one row at a time. ",[14,713,714],{},"ValidationError.errors()"," lists every failure for the row:",[200,717,719],{"className":252,"code":718,"language":254,"meta":205,"style":205},"from openpyxl import load_workbook\nfrom pydantic import ValidationError\n\nws = load_workbook(\"orders.xlsx\", read_only=True)[\"Orders\"]\nrows = ws.iter_rows(values_only=True)\nheader = next(rows)\n\ndef validate_sheet(model, header, rows):\n    valid, errors = [], []\n    for excel_row, values in enumerate(rows, start=2):\n        record = dict(zip(header, values))\n        try:\n            valid.append(model.model_validate(record))\n        except ValidationError as exc:\n            for err in exc.errors():\n                errors.append({\"row\": excel_row, \"column\": err[\"loc\"][0] if err[\"loc\"] else None,\n                               \"value\": err.get(\"input\"), \"problem\": err[\"msg\"]})\n    return valid, errors\n\nvalid, errors = validate_sheet(Order, header, rows)\nprint(len(valid), \"valid rows\")\nfor e in errors:\n    print(e[\"row\"], e[\"column\"], repr(e[\"value\"]), \"-\", e[\"problem\"])\n",[14,720,721,733,744,748,779,798,811,815,827,837,864,882,890,895,909,922,967,993,1002,1007,1018,1036,1050],{"__ignoreMap":205},[209,722,723,725,728,730],{"class":90,"line":211},[209,724,262],{"class":261},[209,726,727],{"class":265}," openpyxl ",[209,729,269],{"class":261},[209,731,732],{"class":265}," load_workbook\n",[209,734,735,737,739,741],{"class":90,"line":275},[209,736,262],{"class":261},[209,738,293],{"class":265},[209,740,269],{"class":261},[209,742,743],{"class":265}," ValidationError\n",[209,745,746],{"class":90,"line":288},[209,747,305],{"emptyLinePlaceholder":304},[209,749,750,753,755,758,761,763,766,768,770,773,776],{"class":90,"line":301},[209,751,752],{"class":265},"ws ",[209,754,333],{"class":261},[209,756,757],{"class":265}," load_workbook(",[209,759,760],{"class":218},"\"orders.xlsx\"",[209,762,379],{"class":265},[209,764,765],{"class":339},"read_only",[209,767,333],{"class":261},[209,769,346],{"class":345},[209,771,772],{"class":265},")[",[209,774,775],{"class":218},"\"Orders\"",[209,777,778],{"class":265},"]\n",[209,780,781,784,786,789,792,794,796],{"class":90,"line":308},[209,782,783],{"class":265},"rows ",[209,785,333],{"class":261},[209,787,788],{"class":265}," ws.iter_rows(",[209,790,791],{"class":339},"values_only",[209,793,333],{"class":261},[209,795,346],{"class":345},[209,797,349],{"class":265},[209,799,800,803,805,808],{"class":90,"line":327},[209,801,802],{"class":265},"header ",[209,804,333],{"class":261},[209,806,807],{"class":345}," next",[209,809,810],{"class":265},"(rows)\n",[209,812,813],{"class":90,"line":352},[209,814,305],{"emptyLinePlaceholder":304},[209,816,817,820,824],{"class":90,"line":357},[209,818,819],{"class":261},"def",[209,821,823],{"class":822},"s_Opv"," validate_sheet",[209,825,826],{"class":265},"(model, header, rows):\n",[209,828,829,832,834],{"class":90,"line":412},[209,830,831],{"class":265},"    valid, errors ",[209,833,333],{"class":261},[209,835,836],{"class":265}," [], []\n",[209,838,839,842,845,848,851,854,857,859,862],{"class":90,"line":490},[209,840,841],{"class":261},"    for",[209,843,844],{"class":265}," excel_row, values ",[209,846,847],{"class":261},"in",[209,849,850],{"class":345}," enumerate",[209,852,853],{"class":265},"(rows, ",[209,855,856],{"class":339},"start",[209,858,333],{"class":261},[209,860,861],{"class":345},"2",[209,863,324],{"class":265},[209,865,866,869,871,874,876,879],{"class":90,"line":509},[209,867,868],{"class":265},"        record ",[209,870,333],{"class":261},[209,872,873],{"class":345}," dict",[209,875,317],{"class":265},[209,877,878],{"class":345},"zip",[209,880,881],{"class":265},"(header, values))\n",[209,883,884,887],{"class":90,"line":540},[209,885,886],{"class":261},"        try",[209,888,889],{"class":265},":\n",[209,891,892],{"class":90,"line":570},[209,893,894],{"class":265},"            valid.append(model.model_validate(record))\n",[209,896,897,900,903,906],{"class":90,"line":605},[209,898,899],{"class":261},"        except",[209,901,902],{"class":265}," ValidationError ",[209,904,905],{"class":261},"as",[209,907,908],{"class":265}," exc:\n",[209,910,911,914,917,919],{"class":90,"line":657},[209,912,913],{"class":261},"            for",[209,915,916],{"class":265}," err ",[209,918,847],{"class":261},[209,920,921],{"class":265}," exc.errors():\n",[209,923,924,927,930,933,936,939,942,945,947,949,951,954,956,958,961,964],{"class":90,"line":662},[209,925,926],{"class":265},"                errors.append({",[209,928,929],{"class":218},"\"row\"",[209,931,932],{"class":265},": excel_row, ",[209,934,935],{"class":218},"\"column\"",[209,937,938],{"class":265},": err[",[209,940,941],{"class":218},"\"loc\"",[209,943,944],{"class":265},"][",[209,946,52],{"class":345},[209,948,589],{"class":265},[209,950,16],{"class":261},[209,952,953],{"class":265}," err[",[209,955,941],{"class":218},[209,957,589],{"class":265},[209,959,960],{"class":261},"else",[209,962,963],{"class":345}," None",[209,965,966],{"class":265},",\n",[209,968,970,973,976,979,982,985,987,990],{"class":90,"line":969},17,[209,971,972],{"class":218},"                               \"value\"",[209,974,975],{"class":265},": err.get(",[209,977,978],{"class":218},"\"input\"",[209,980,981],{"class":265},"), ",[209,983,984],{"class":218},"\"problem\"",[209,986,938],{"class":265},[209,988,989],{"class":218},"\"msg\"",[209,991,992],{"class":265},"]})\n",[209,994,996,999],{"class":90,"line":995},18,[209,997,998],{"class":261},"    return",[209,1000,1001],{"class":265}," valid, errors\n",[209,1003,1005],{"class":90,"line":1004},19,[209,1006,305],{"emptyLinePlaceholder":304},[209,1008,1010,1013,1015],{"class":90,"line":1009},20,[209,1011,1012],{"class":265},"valid, errors ",[209,1014,333],{"class":261},[209,1016,1017],{"class":265}," validate_sheet(Order, header, rows)\n",[209,1019,1021,1023,1025,1028,1031,1034],{"class":90,"line":1020},21,[209,1022,665],{"class":345},[209,1024,317],{"class":265},[209,1026,1027],{"class":345},"len",[209,1029,1030],{"class":265},"(valid), ",[209,1032,1033],{"class":218},"\"valid rows\"",[209,1035,349],{"class":265},[209,1037,1039,1042,1045,1047],{"class":90,"line":1038},22,[209,1040,1041],{"class":261},"for",[209,1043,1044],{"class":265}," e ",[209,1046,847],{"class":261},[209,1048,1049],{"class":265}," errors:\n",[209,1051,1053,1056,1059,1061,1064,1066,1069,1072,1074,1077,1080,1083,1086,1088],{"class":90,"line":1052},23,[209,1054,1055],{"class":345},"    print",[209,1057,1058],{"class":265},"(e[",[209,1060,929],{"class":218},[209,1062,1063],{"class":265},"], e[",[209,1065,935],{"class":218},[209,1067,1068],{"class":265},"], ",[209,1070,1071],{"class":345},"repr",[209,1073,1058],{"class":265},[209,1075,1076],{"class":218},"\"value\"",[209,1078,1079],{"class":265},"]), ",[209,1081,1082],{"class":218},"\"-\"",[209,1084,1085],{"class":265},", e[",[209,1087,984],{"class":218},[209,1089,1090],{"class":265},"])\n",[10,1092,1093,1094,1097,1098,1101,1102,1105,1106,1109,1110,1112,1113,1116],{},"Two rows pass. Row 3 fails on two fields at once — both reported, not only the first. Row 4's only error is its status, ",[14,1095,1096],{},"\"Paid\"",", which is not one of the lower-case choices; its quantity, the text ",[14,1099,1100],{},"\"12\"",", and its date, the text ",[14,1103,1104],{},"\"2026-09-03\"",", were accepted, because pydantic's default ",[19,1107,1108],{},"lax"," mode converts compatible values. Row 5 shows an Excel subtlety: a date field rejects a datetime with a time part, so a timestamp typed into a date column is caught. Row 7 has no order ID, and ",[14,1111,624],{}," is not a string. Numbering from 2 makes ",[14,1114,1115],{},"row"," the real Excel row, because row 1 is the header.",[195,1118,1120],{"id":1119},"step-3-normalise-values-before-checking","Step 3 — Normalise values before checking",[10,1122,1123,1124,1127,1128,1131],{},"Some values are acceptable once tidied: a status in the wrong case, a code with lower-case letters. A ",[14,1125,1126],{},"field_validator"," with ",[14,1129,1130],{},"mode=\"before\""," runs on the raw value before type checks, so it can clean up what the model should accept:",[200,1133,1135],{"className":252,"code":1134,"language":254,"meta":205,"style":205},"from pydantic import field_validator, model_validator\n\nclass Order(Order):\n    @field_validator(\"status\", mode=\"before\")\n    @classmethod\n    def lower_status(cls, v):\n        return v.lower() if isinstance(v, str) else v\n\n    @model_validator(mode=\"after\")\n    def discount_needs_volume(self):\n        if self.discount_pct and self.qty \u003C 10:\n            raise ValueError(\"a discount needs a quantity of 10 or more\")\n        return self\n\nrows = load_workbook(\"orders.xlsx\", read_only=True)[\"Orders\"].iter_rows(values_only=True)\nheader = next(rows)\nvalid, errors = validate_sheet(Order, header, rows)\nprint(len(valid), \"valid rows:\", [o.order_id for o in valid])\nfor e in errors:\n    print(e[\"row\"], e[\"column\"], repr(e[\"value\"]), \"-\", e[\"problem\"])\n",[14,1136,1137,1148,1152,1165,1187,1195,1206,1232,1236,1252,1262,1289,1304,1311,1315,1348,1358,1366,1392,1402],{"__ignoreMap":205},[209,1138,1139,1141,1143,1145],{"class":90,"line":211},[209,1140,262],{"class":261},[209,1142,293],{"class":265},[209,1144,269],{"class":261},[209,1146,1147],{"class":265}," field_validator, model_validator\n",[209,1149,1150],{"class":90,"line":275},[209,1151,305],{"emptyLinePlaceholder":304},[209,1153,1154,1156,1158,1160,1163],{"class":90,"line":288},[209,1155,311],{"class":261},[209,1157,314],{"class":214},[209,1159,317],{"class":265},[209,1161,1162],{"class":320},"Order",[209,1164,324],{"class":265},[209,1166,1167,1170,1172,1175,1177,1180,1182,1185],{"class":90,"line":301},[209,1168,1169],{"class":822},"    @field_validator",[209,1171,317],{"class":265},[209,1173,1174],{"class":218},"\"status\"",[209,1176,379],{"class":265},[209,1178,1179],{"class":339},"mode",[209,1181,333],{"class":261},[209,1183,1184],{"class":218},"\"before\"",[209,1186,349],{"class":265},[209,1188,1189,1192],{"class":90,"line":308},[209,1190,1191],{"class":822},"    @",[209,1193,1194],{"class":345},"classmethod\n",[209,1196,1197,1200,1203],{"class":90,"line":327},[209,1198,1199],{"class":261},"    def",[209,1201,1202],{"class":822}," lower_status",[209,1204,1205],{"class":265},"(cls, v):\n",[209,1207,1208,1211,1214,1216,1219,1222,1224,1227,1229],{"class":90,"line":352},[209,1209,1210],{"class":261},"        return",[209,1212,1213],{"class":265}," v.lower() ",[209,1215,16],{"class":261},[209,1217,1218],{"class":345}," isinstance",[209,1220,1221],{"class":265},"(v, ",[209,1223,363],{"class":345},[209,1225,1226],{"class":265},") ",[209,1228,960],{"class":261},[209,1230,1231],{"class":265}," v\n",[209,1233,1234],{"class":90,"line":357},[209,1235,305],{"emptyLinePlaceholder":304},[209,1237,1238,1241,1243,1245,1247,1250],{"class":90,"line":412},[209,1239,1240],{"class":822},"    @model_validator",[209,1242,317],{"class":265},[209,1244,1179],{"class":339},[209,1246,333],{"class":261},[209,1248,1249],{"class":218},"\"after\"",[209,1251,349],{"class":265},[209,1253,1254,1256,1259],{"class":90,"line":490},[209,1255,1199],{"class":261},[209,1257,1258],{"class":822}," discount_needs_volume",[209,1260,1261],{"class":265},"(self):\n",[209,1263,1264,1267,1270,1273,1276,1278,1281,1284,1287],{"class":90,"line":509},[209,1265,1266],{"class":261},"        if",[209,1268,1269],{"class":345}," self",[209,1271,1272],{"class":265},".discount_pct ",[209,1274,1275],{"class":261},"and",[209,1277,1269],{"class":345},[209,1279,1280],{"class":265},".qty ",[209,1282,1283],{"class":261},"\u003C",[209,1285,1286],{"class":345}," 10",[209,1288,889],{"class":265},[209,1290,1291,1294,1297,1299,1302],{"class":90,"line":540},[209,1292,1293],{"class":261},"            raise",[209,1295,1296],{"class":345}," ValueError",[209,1298,317],{"class":265},[209,1300,1301],{"class":218},"\"a discount needs a quantity of 10 or more\"",[209,1303,349],{"class":265},[209,1305,1306,1308],{"class":90,"line":570},[209,1307,1210],{"class":261},[209,1309,1310],{"class":345}," self\n",[209,1312,1313],{"class":90,"line":605},[209,1314,305],{"emptyLinePlaceholder":304},[209,1316,1317,1319,1321,1323,1325,1327,1329,1331,1333,1335,1337,1340,1342,1344,1346],{"class":90,"line":657},[209,1318,783],{"class":265},[209,1320,333],{"class":261},[209,1322,757],{"class":265},[209,1324,760],{"class":218},[209,1326,379],{"class":265},[209,1328,765],{"class":339},[209,1330,333],{"class":261},[209,1332,346],{"class":345},[209,1334,772],{"class":265},[209,1336,775],{"class":218},[209,1338,1339],{"class":265},"].iter_rows(",[209,1341,791],{"class":339},[209,1343,333],{"class":261},[209,1345,346],{"class":345},[209,1347,349],{"class":265},[209,1349,1350,1352,1354,1356],{"class":90,"line":662},[209,1351,802],{"class":265},[209,1353,333],{"class":261},[209,1355,807],{"class":345},[209,1357,810],{"class":265},[209,1359,1360,1362,1364],{"class":90,"line":969},[209,1361,1012],{"class":265},[209,1363,333],{"class":261},[209,1365,1017],{"class":265},[209,1367,1368,1370,1372,1374,1376,1379,1382,1384,1387,1389],{"class":90,"line":995},[209,1369,665],{"class":345},[209,1371,317],{"class":265},[209,1373,1027],{"class":345},[209,1375,1030],{"class":265},[209,1377,1378],{"class":218},"\"valid rows:\"",[209,1380,1381],{"class":265},", [o.order_id ",[209,1383,1041],{"class":261},[209,1385,1386],{"class":265}," o ",[209,1388,847],{"class":261},[209,1390,1391],{"class":265}," valid])\n",[209,1393,1394,1396,1398,1400],{"class":90,"line":1004},[209,1395,1041],{"class":261},[209,1397,1044],{"class":265},[209,1399,847],{"class":261},[209,1401,1049],{"class":265},[209,1403,1404,1406,1408,1410,1412,1414,1416,1418,1420,1422,1424,1426,1428,1430],{"class":90,"line":1009},[209,1405,1055],{"class":345},[209,1407,1058],{"class":265},[209,1409,929],{"class":218},[209,1411,1063],{"class":265},[209,1413,935],{"class":218},[209,1415,1068],{"class":265},[209,1417,1071],{"class":345},[209,1419,1058],{"class":265},[209,1421,1076],{"class":218},[209,1423,1079],{"class":265},[209,1425,1082],{"class":218},[209,1427,1085],{"class":265},[209,1429,984],{"class":218},[209,1431,1090],{"class":265},[10,1433,1434,1435,1127,1438,1441,1442,1445,1446,1448],{},"Row 4 now passes. The ",[14,1436,1437],{},"model_validator",[14,1439,1440],{},"mode=\"after\""," sees the whole validated row, so it can express rules that span columns — here, that only orders of 10 or more may carry a discount, which catches row 6. Its error has no field, so ",[14,1443,1444],{},"column"," is ",[14,1447,624],{},"; the report calls it a row-level problem. A model validator only runs when every field is valid, so a row with field errors is reported for those first.",[30,1450,39,1455,39,1458,39,1461,39,1464,39,1470,39,1475,39,1481,39,1484,39,1488,39,1493,39,1499,39,1501,39,1505,39,1507,39,1510,39,1513,39,1517,39,1519,39,1523,39,1525,39,1528,39,1531,39,1535,39,1537,39,1540,39,1542,39,1545,39,1548,39,1552,39,1554,39,1558,39,1560,39,1563,39,1566,39,1570,39,1572,39,1576,39,1578],{"viewBox":1451,"role":33,"ariaLabelledBy":1452,"xmlns":37,"style":38},"4 0 752 242",[1453,1454],"pyd-rules-t","pyd-rules-d",[41,1456,1457],{"id":1453},"Which pydantic feature expresses which rule",[45,1459,1460],{"id":1454},"A table of rule kinds. A type such as int or date checks the kind of value. Field with gt, ge, le or pattern constrains its range or format. Literal restricts it to a fixed set of choices. A before field_validator cleans the raw value first, for example lower-casing. An after model_validator checks rules that span several columns.",[49,1462],{"x":51,"y":52,"width":53,"height":1463,"fill":55},"242",[49,1465],{"x":58,"y":59,"width":1466,"height":1467,"rx":52,"fill":55,"stroke":1468,"style":1469},"131.7","40.0","var(--line,#cdd5e6)","stroke-width:1px",[49,1471],{"x":1472,"y":59,"width":1473,"height":1467,"rx":52,"fill":63,"stroke":64,"style":1474},"151.7","294.1","stroke-width:1.2px",[78,1476,1480],{"x":1477,"y":1478,"style":1479},"298.8","41.0","font-size:13px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","pydantic feature",[49,1482],{"x":1483,"y":59,"width":1473,"height":1467,"rx":52,"fill":63,"stroke":64,"style":1474},"445.9",[78,1485,1487],{"x":1486,"y":1478,"style":1479},"592.9","Example",[49,1489],{"x":58,"y":1490,"width":1466,"height":1491,"rx":52,"fill":1492,"stroke":1468,"style":1469},"56.0","34.0","#e7ebef",[78,1494,1498],{"x":1495,"y":1496,"style":1497},"32.0","78.0","font-size:12.5px;font-weight:700;fill:var(--text,#172033);text-anchor:start","Kind of value",[49,1500],{"x":1472,"y":1490,"width":1473,"height":1491,"rx":52,"fill":55,"stroke":1468,"style":1469},[78,1502,1504],{"x":1477,"y":1503,"style":87},"77.0","type annotation",[49,1506],{"x":1483,"y":1490,"width":1473,"height":1491,"rx":52,"fill":55,"stroke":1468,"style":1469},[78,1508,1509],{"x":1486,"y":1503,"style":87},"qty: int",[49,1511],{"x":58,"y":1512,"width":1466,"height":1491,"rx":52,"fill":1492,"stroke":1468,"style":1469},"90.0",[78,1514,1516],{"x":1495,"y":1515,"style":1497},"112.0","Range or format",[49,1518],{"x":1472,"y":1512,"width":1473,"height":1491,"rx":52,"fill":55,"stroke":1468,"style":1469},[78,1520,1522],{"x":1477,"y":1521,"style":87},"111.0","Field(gt=, pattern=)",[49,1524],{"x":1483,"y":1512,"width":1473,"height":1491,"rx":52,"fill":55,"stroke":1468,"style":1469},[78,1526,1527],{"x":1486,"y":1521,"style":87},"gt=0",[49,1529],{"x":58,"y":1530,"width":1466,"height":1491,"rx":52,"fill":1492,"stroke":1468,"style":1469},"124.0",[78,1532,1534],{"x":1495,"y":1533,"style":1497},"146.0","Fixed choices",[49,1536],{"x":1472,"y":1530,"width":1473,"height":1491,"rx":52,"fill":55,"stroke":1468,"style":1469},[78,1538,685],{"x":1477,"y":1539,"style":87},"145.0",[49,1541],{"x":1483,"y":1530,"width":1473,"height":1491,"rx":52,"fill":55,"stroke":1468,"style":1469},[78,1543,1544],{"x":1486,"y":1539,"style":87},"open, paid, void",[49,1546],{"x":58,"y":1547,"width":1466,"height":1491,"rx":52,"fill":1492,"stroke":1468,"style":1469},"158.0",[78,1549,1551],{"x":1495,"y":1550,"style":1497},"180.0","Tidy first",[49,1553],{"x":1472,"y":1547,"width":1473,"height":1491,"rx":52,"fill":55,"stroke":1468,"style":1469},[78,1555,1557],{"x":1477,"y":1556,"style":87},"179.0","field_validator before",[49,1559],{"x":1483,"y":1547,"width":1473,"height":1491,"rx":52,"fill":55,"stroke":1468,"style":1469},[78,1561,1562],{"x":1486,"y":1556,"style":87},"lower-case status",[49,1564],{"x":58,"y":1565,"width":1466,"height":1491,"rx":52,"fill":1492,"stroke":1468,"style":1469},"192.0",[78,1567,1569],{"x":1495,"y":1568,"style":1497},"214.0","Across columns",[49,1571],{"x":1472,"y":1565,"width":1473,"height":1491,"rx":52,"fill":55,"stroke":1468,"style":1469},[78,1573,1575],{"x":1477,"y":1574,"style":87},"213.0","model_validator after",[49,1577],{"x":1483,"y":1565,"width":1473,"height":1491,"rx":52,"fill":55,"stroke":1468,"style":1469},[78,1579,1580],{"x":1486,"y":1574,"style":87},"discount needs qty ≥ 10",[195,1582,1584],{"id":1583},"step-4-write-an-error-report-people-can-act-on","Step 4 — Write an error report people can act on",[10,1586,1587],{},"Turn the errors into a sheet with the Excel row, the column letter and header, the value and the problem. Mapping the header back to its letter lets the reader jump straight to the cell:",[200,1589,1591],{"className":252,"code":1590,"language":254,"meta":205,"style":205},"import pandas as pd\nfrom openpyxl.utils import get_column_letter\n\nreport = pd.DataFrame([{\n    \"cell\": f\"{get_column_letter(header.index(e['column']) + 1)}{e['row']}\" if e[\"column\"] else f\"row {e['row']}\",\n    \"column\": e[\"column\"] or \"(whole row)\",\n    \"value\": e[\"value\"] if e[\"column\"] else None,\n    \"problem\": e[\"problem\"],\n} for e in errors])\nreport.to_excel(\"orders_errors.xlsx\", index=False)\nprint(report.to_string(index=False))\n",[14,1592,1593,1605,1617,1621,1631,1712,1732,1757,1769,1783,1803],{"__ignoreMap":205},[209,1594,1595,1597,1600,1602],{"class":90,"line":211},[209,1596,269],{"class":261},[209,1598,1599],{"class":265}," pandas ",[209,1601,905],{"class":261},[209,1603,1604],{"class":265}," pd\n",[209,1606,1607,1609,1612,1614],{"class":90,"line":275},[209,1608,262],{"class":261},[209,1610,1611],{"class":265}," openpyxl.utils ",[209,1613,269],{"class":261},[209,1615,1616],{"class":265}," get_column_letter\n",[209,1618,1619],{"class":90,"line":288},[209,1620,305],{"emptyLinePlaceholder":304},[209,1622,1623,1626,1628],{"class":90,"line":301},[209,1624,1625],{"class":265},"report ",[209,1627,333],{"class":261},[209,1629,1630],{"class":265}," pd.DataFrame([{\n",[209,1632,1633,1636,1639,1642,1644,1647,1650,1653,1656,1658,1661,1664,1667,1670,1673,1675,1678,1680,1683,1686,1688,1690,1692,1695,1698,1700,1702,1704,1706,1708,1710],{"class":90,"line":308},[209,1634,1635],{"class":218},"    \"cell\"",[209,1637,1638],{"class":265},": ",[209,1640,1641],{"class":261},"f",[209,1643,390],{"class":218},[209,1645,1646],{"class":446},"{",[209,1648,1649],{"class":265},"get_column_letter(header.index(e[",[209,1651,1652],{"class":218},"'column'",[209,1654,1655],{"class":265},"]) ",[209,1657,453],{"class":261},[209,1659,1660],{"class":345}," 1",[209,1662,1663],{"class":265},")",[209,1665,1666],{"class":446},"}{",[209,1668,1669],{"class":265},"e[",[209,1671,1672],{"class":218},"'row'",[209,1674,478],{"class":265},[209,1676,1677],{"class":446},"}",[209,1679,390],{"class":218},[209,1681,1682],{"class":261}," if",[209,1684,1685],{"class":265}," e[",[209,1687,935],{"class":218},[209,1689,589],{"class":265},[209,1691,960],{"class":261},[209,1693,1694],{"class":261}," f",[209,1696,1697],{"class":218},"\"row ",[209,1699,1646],{"class":446},[209,1701,1669],{"class":265},[209,1703,1672],{"class":218},[209,1705,478],{"class":265},[209,1707,1677],{"class":446},[209,1709,390],{"class":218},[209,1711,966],{"class":265},[209,1713,1714,1717,1720,1722,1724,1727,1730],{"class":90,"line":327},[209,1715,1716],{"class":218},"    \"column\"",[209,1718,1719],{"class":265},": e[",[209,1721,935],{"class":218},[209,1723,589],{"class":265},[209,1725,1726],{"class":261},"or",[209,1728,1729],{"class":218}," \"(whole row)\"",[209,1731,966],{"class":265},[209,1733,1734,1737,1739,1741,1743,1745,1747,1749,1751,1753,1755],{"class":90,"line":352},[209,1735,1736],{"class":218},"    \"value\"",[209,1738,1719],{"class":265},[209,1740,1076],{"class":218},[209,1742,589],{"class":265},[209,1744,16],{"class":261},[209,1746,1685],{"class":265},[209,1748,935],{"class":218},[209,1750,589],{"class":265},[209,1752,960],{"class":261},[209,1754,963],{"class":345},[209,1756,966],{"class":265},[209,1758,1759,1762,1764,1766],{"class":90,"line":357},[209,1760,1761],{"class":218},"    \"problem\"",[209,1763,1719],{"class":265},[209,1765,984],{"class":218},[209,1767,1768],{"class":265},"],\n",[209,1770,1771,1774,1776,1778,1780],{"class":90,"line":412},[209,1772,1773],{"class":265},"} ",[209,1775,1041],{"class":261},[209,1777,1044],{"class":265},[209,1779,847],{"class":261},[209,1781,1782],{"class":265}," errors])\n",[209,1784,1785,1788,1791,1793,1796,1798,1801],{"class":90,"line":490},[209,1786,1787],{"class":265},"report.to_excel(",[209,1789,1790],{"class":218},"\"orders_errors.xlsx\"",[209,1792,379],{"class":265},[209,1794,1795],{"class":339},"index",[209,1797,333],{"class":261},[209,1799,1800],{"class":345},"False",[209,1802,349],{"class":265},[209,1804,1805,1807,1810,1812,1814,1816],{"class":90,"line":509},[209,1806,665],{"class":345},[209,1808,1809],{"class":265},"(report.to_string(",[209,1811,1795],{"class":339},[209,1813,333],{"class":261},[209,1815,1800],{"class":345},[209,1817,1818],{"class":265},"))\n",[10,1820,1821,1822,1825,1826,379,1829,379,1832,1835,1836,1840],{},"Pydantic's messages are clear enough to show as they are; replace them with your own wording by mapping on ",[14,1823,1824],{},"err[\"type\"]"," — ",[14,1827,1828],{},"greater_than",[14,1830,1831],{},"literal_error",[14,1833,1834],{},"string_pattern_mismatch"," and so on — if the audience needs friendlier text. ",[24,1837,1839],{"href":1838},"\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"," marks the same cells in a copy of the workbook.",[30,1842,39,1848,39,1851,39,1854,39,1859,39,1864,39,1869,39,1873,39,1879,39,1883,39,1887,39,1891,39,1895,39,1899,39,1903,39,1907,39,1912,39,1914,39,1918,39,1920,39,1922,39,1924,39,1926,39,1928,39,1931,39,1934,39,1937,39,1939,39,1943,39,1945,39,1948,39,1950,39,1953,39,1955,39,1958,39,1961,39,1965,39,1967,39,1970,39,1972,39,1975,39,1977,39,1979,39,1981,39,1984,39,1987,39,1990,39,1992,39,1995,39,1997,39,2000,39,2002,39,2005,39,2007,39,2010,39,2013,39,2017,39,2019,39,2022,39,2024,39,2027,39,2029,39,2032,39,2034,39,2037,39,2040,39,2044,39,2046,39,2049,39,2051,39,2054,39,2056,39,2059,39,2061,39,2064,39,2067,39,2071,39,2073,39,2076,39,2078,39,2081,39,2083,39,2085,39,2087,39,2090,39,2094,39,2096,39,2099,39,2101,39,2104,39,2106,39,2108],{"viewBox":1843,"role":33,"ariaLabelledBy":1844,"xmlns":37,"style":1847},"103 0 554 312",[1845,1846],"pyd-report-t","pyd-report-d","width:100%;max-width:554px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[41,1849,1850],{"id":1845},"The error report, one line per broken rule",[45,1852,1853],{"id":1846},"The orders_errors.xlsx sheet. B3 Customer email ben@example does not match the pattern. D3 Qty 0 should be greater than 0. C5 Order date has a time part. E5 Unit price n\u002Fa is not a valid number. F5 Status shipped is not open, paid or void. Row 6 as a whole has a discount with a quantity under 10. A7 Order ID is missing.",[49,1855],{"x":1856,"y":52,"width":1857,"height":1858,"fill":55},"103","554","312",[78,1860,1863],{"x":140,"y":1861,"style":1862},"31.0","font-size:14px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","orders_errors.xlsx",[49,1865],{"x":1866,"y":1867,"width":1495,"height":1868,"rx":52,"fill":1492,"stroke":1468,"style":1469},"119.2","48.0","24.0",[49,1870],{"x":1871,"y":1867,"width":1872,"height":1868,"rx":52,"fill":1492,"stroke":1468,"style":1469},"151.2","58.7",[78,1874,1878],{"x":1875,"y":1876,"style":1877},"180.5","65.0","font-size:11px;font-weight:600;fill:var(--muted,#5b6780);text-anchor:middle","A",[49,1880],{"x":1881,"y":1867,"width":1882,"height":1868,"rx":52,"fill":1492,"stroke":1468,"style":1469},"209.9","120.9",[78,1884,1886],{"x":1885,"y":1876,"style":1877},"270.3","B",[49,1888],{"x":1889,"y":1867,"width":1890,"height":1868,"rx":52,"fill":1492,"stroke":1468,"style":1469},"330.8","139.8",[78,1892,1894],{"x":1893,"y":1876,"style":1877},"400.7","C",[49,1896],{"x":1897,"y":1867,"width":1898,"height":1868,"rx":52,"fill":1492,"stroke":1468,"style":1469},"470.6","170.3",[78,1900,1902],{"x":1901,"y":1876,"style":1877},"555.7","D",[49,1904],{"x":1866,"y":1905,"width":1495,"height":1906,"rx":52,"fill":1492,"stroke":1468,"style":1469},"72.0","28.0",[78,1908,1911],{"x":1909,"y":1910,"style":1877},"135.2","91.0","1",[49,1913],{"x":1871,"y":1905,"width":1872,"height":1906,"rx":52,"fill":55,"stroke":1468,"style":1469},[78,1915,1917],{"x":1875,"y":1910,"style":1916},"font-size:12px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","cell",[49,1919],{"x":1881,"y":1905,"width":1882,"height":1906,"rx":52,"fill":55,"stroke":1468,"style":1469},[78,1921,1444],{"x":1885,"y":1910,"style":1916},[49,1923],{"x":1889,"y":1905,"width":1890,"height":1906,"rx":52,"fill":55,"stroke":1468,"style":1469},[78,1925,193],{"x":1893,"y":1910,"style":1916},[49,1927],{"x":1897,"y":1905,"width":1898,"height":1906,"rx":52,"fill":55,"stroke":1468,"style":1469},[78,1929,1930],{"x":1901,"y":1910,"style":1916},"problem",[49,1932],{"x":1866,"y":1933,"width":1495,"height":1906,"rx":52,"fill":1492,"stroke":1468,"style":1469},"100.0",[78,1935,861],{"x":1909,"y":1936,"style":1877},"119.0",[49,1938],{"x":1871,"y":1933,"width":1872,"height":1906,"rx":52,"fill":55,"stroke":1468,"style":1469},[78,1940,1942],{"x":1875,"y":1936,"style":1941},"font-size:12px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","B3",[49,1944],{"x":1881,"y":1933,"width":1882,"height":1906,"rx":52,"fill":55,"stroke":1468,"style":1469},[78,1946,1947],{"x":1885,"y":1936,"style":1941},"Customer email",[49,1949],{"x":1889,"y":1933,"width":1890,"height":1906,"rx":52,"fill":55,"stroke":1468,"style":1469},[78,1951,1952],{"x":1893,"y":1936,"style":1941},"ben@example",[49,1954],{"x":1897,"y":1933,"width":1898,"height":1906,"rx":52,"fill":55,"stroke":1468,"style":1469},[78,1956,1957],{"x":1901,"y":1936,"style":1941},"pattern mismatch",[49,1959],{"x":1866,"y":1960,"width":1495,"height":1906,"rx":52,"fill":1492,"stroke":1468,"style":1469},"128.0",[78,1962,1964],{"x":1909,"y":1963,"style":1877},"147.0","3",[49,1966],{"x":1871,"y":1960,"width":1872,"height":1906,"rx":52,"fill":55,"stroke":1468,"style":1469},[78,1968,1969],{"x":1875,"y":1963,"style":1941},"D3",[49,1971],{"x":1881,"y":1960,"width":1882,"height":1906,"rx":52,"fill":55,"stroke":1468,"style":1469},[78,1973,1974],{"x":1885,"y":1963,"style":1941},"Qty",[49,1976],{"x":1889,"y":1960,"width":1890,"height":1906,"rx":52,"fill":55,"stroke":1468,"style":1469},[78,1978,52],{"x":1893,"y":1963,"style":1941},[49,1980],{"x":1897,"y":1960,"width":1898,"height":1906,"rx":52,"fill":55,"stroke":1468,"style":1469},[78,1982,1983],{"x":1901,"y":1963,"style":1941},"must be > 0",[49,1985],{"x":1866,"y":1986,"width":1495,"height":1906,"rx":52,"fill":1492,"stroke":1468,"style":1469},"156.0",[78,1988,51],{"x":1909,"y":1989,"style":1877},"175.0",[49,1991],{"x":1871,"y":1986,"width":1872,"height":1906,"rx":52,"fill":55,"stroke":1468,"style":1469},[78,1993,1994],{"x":1875,"y":1989,"style":1941},"C5",[49,1996],{"x":1881,"y":1986,"width":1882,"height":1906,"rx":52,"fill":55,"stroke":1468,"style":1469},[78,1998,1999],{"x":1885,"y":1989,"style":1941},"Order date",[49,2001],{"x":1889,"y":1986,"width":1890,"height":1906,"rx":52,"fill":55,"stroke":1468,"style":1469},[78,2003,2004],{"x":1893,"y":1989,"style":1941},"2026-09-04 14:30",[49,2006],{"x":1897,"y":1986,"width":1898,"height":1906,"rx":52,"fill":55,"stroke":1468,"style":1469},[78,2008,2009],{"x":1901,"y":1989,"style":1941},"has a time",[49,2011],{"x":1866,"y":2012,"width":1495,"height":1906,"rx":52,"fill":1492,"stroke":1468,"style":1469},"184.0",[78,2014,2016],{"x":1909,"y":2015,"style":1877},"203.0","5",[49,2018],{"x":1871,"y":2012,"width":1872,"height":1906,"rx":52,"fill":55,"stroke":1468,"style":1469},[78,2020,2021],{"x":1875,"y":2015,"style":1941},"E5",[49,2023],{"x":1881,"y":2012,"width":1882,"height":1906,"rx":52,"fill":55,"stroke":1468,"style":1469},[78,2025,2026],{"x":1885,"y":2015,"style":1941},"Unit price",[49,2028],{"x":1889,"y":2012,"width":1890,"height":1906,"rx":52,"fill":55,"stroke":1468,"style":1469},[78,2030,2031],{"x":1893,"y":2015,"style":1941},"n\u002Fa",[49,2033],{"x":1897,"y":2012,"width":1898,"height":1906,"rx":52,"fill":55,"stroke":1468,"style":1469},[78,2035,2036],{"x":1901,"y":2015,"style":1941},"not a number",[49,2038],{"x":1866,"y":2039,"width":1495,"height":1906,"rx":52,"fill":1492,"stroke":1468,"style":1469},"212.0",[78,2041,2043],{"x":1909,"y":2042,"style":1877},"231.0","6",[49,2045],{"x":1871,"y":2039,"width":1872,"height":1906,"rx":52,"fill":55,"stroke":1468,"style":1469},[78,2047,2048],{"x":1875,"y":2042,"style":1941},"F5",[49,2050],{"x":1881,"y":2039,"width":1882,"height":1906,"rx":52,"fill":55,"stroke":1468,"style":1469},[78,2052,2053],{"x":1885,"y":2042,"style":1941},"Status",[49,2055],{"x":1889,"y":2039,"width":1890,"height":1906,"rx":52,"fill":55,"stroke":1468,"style":1469},[78,2057,2058],{"x":1893,"y":2042,"style":1941},"shipped",[49,2060],{"x":1897,"y":2039,"width":1898,"height":1906,"rx":52,"fill":55,"stroke":1468,"style":1469},[78,2062,2063],{"x":1901,"y":2042,"style":1941},"not a choice",[49,2065],{"x":1866,"y":2066,"width":1495,"height":1906,"rx":52,"fill":1492,"stroke":1468,"style":1469},"240.0",[78,2068,2070],{"x":1909,"y":2069,"style":1877},"259.0","7",[49,2072],{"x":1871,"y":2066,"width":1872,"height":1906,"rx":52,"fill":55,"stroke":1468,"style":1469},[78,2074,2075],{"x":1875,"y":2069,"style":1941},"row 6",[49,2077],{"x":1881,"y":2066,"width":1882,"height":1906,"rx":52,"fill":55,"stroke":1468,"style":1469},[78,2079,2080],{"x":1885,"y":2069,"style":1941},"(whole row)",[49,2082],{"x":1889,"y":2066,"width":1890,"height":1906,"rx":52,"fill":55,"stroke":1468,"style":1469},[49,2084],{"x":1897,"y":2066,"width":1898,"height":1906,"rx":52,"fill":55,"stroke":1468,"style":1469},[78,2086,1580],{"x":1901,"y":2069,"style":1941},[49,2088],{"x":1866,"y":2089,"width":1495,"height":1906,"rx":52,"fill":1492,"stroke":1468,"style":1469},"268.0",[78,2091,2093],{"x":1909,"y":2092,"style":1877},"287.0","8",[49,2095],{"x":1871,"y":2089,"width":1872,"height":1906,"rx":52,"fill":55,"stroke":1468,"style":1469},[78,2097,2098],{"x":1875,"y":2092,"style":1941},"A7",[49,2100],{"x":1881,"y":2089,"width":1882,"height":1906,"rx":52,"fill":55,"stroke":1468,"style":1469},[78,2102,2103],{"x":1885,"y":2092,"style":1941},"Order ID",[49,2105],{"x":1889,"y":2089,"width":1890,"height":1906,"rx":52,"fill":55,"stroke":1468,"style":1469},[49,2107],{"x":1897,"y":2089,"width":1898,"height":1906,"rx":52,"fill":55,"stroke":1468,"style":1469},[78,2109,2110],{"x":1901,"y":2092,"style":1941},"not a string",[195,2112,2114],{"id":2113},"step-5-load-only-the-valid-rows","Step 5 — Load only the valid rows",[10,2116,2117,2118,2121,2122,2124,2125,2128],{},"Valid rows are model objects with clean, typed values: dates are ",[14,2119,2120],{},"date",", quantities ",[14,2123,515],{},", text stripped and statuses lower-case. ",[14,2126,2127],{},"model_dump()"," turns them back into dicts for a DataFrame, a database insert or an API call:",[200,2130,2132],{"className":252,"code":2131,"language":254,"meta":205,"style":205},"clean = pd.DataFrame([o.model_dump() for o in valid])\nprint(clean.to_string(index=False))\nprint(clean.dtypes.to_dict())\n",[14,2133,2134,2152,2167],{"__ignoreMap":205},[209,2135,2136,2139,2141,2144,2146,2148,2150],{"class":90,"line":211},[209,2137,2138],{"class":265},"clean ",[209,2140,333],{"class":261},[209,2142,2143],{"class":265}," pd.DataFrame([o.model_dump() ",[209,2145,1041],{"class":261},[209,2147,1386],{"class":265},[209,2149,847],{"class":261},[209,2151,1391],{"class":265},[209,2153,2154,2156,2159,2161,2163,2165],{"class":90,"line":275},[209,2155,665],{"class":345},[209,2157,2158],{"class":265},"(clean.to_string(",[209,2160,1795],{"class":339},[209,2162,333],{"class":261},[209,2164,1800],{"class":345},[209,2166,1818],{"class":265},[209,2168,2169,2171],{"class":90,"line":288},[209,2170,665],{"class":345},[209,2172,2173],{"class":265},"(clean.dtypes.to_dict())\n",[10,2175,2176,2177,2180,2181,2184],{},"The field names, not the Excel headers, become the columns — convenient for loading into a database. Pass ",[14,2178,2179],{},"by_alias=True"," to ",[14,2182,2183],{},"model_dump"," to get the original headers back.",[195,2186,2188],{"id":2187},"reading-with-pandas-instead","Reading with pandas instead",[10,2190,2191,2192,2195,2196,2199,2200,2202,2203,2205,2206,2209,2210,1127,2212,2214],{},"If the rows come from ",[14,2193,2194],{},"pd.read_excel",", empty cells arrive as ",[14,2197,2198],{},"NaN",", a float — not ",[14,2201,624],{},". An optional field then receives ",[14,2204,2198],{},", and a constraint such as ",[14,2207,2208],{},"le=50"," rejects it with a confusing \"less than or equal to 50\" message; an unconstrained optional float would silently accept it. Replace ",[14,2211,2198],{},[14,2213,624],{}," before validating:",[200,2216,2218],{"className":252,"code":2217,"language":254,"meta":205,"style":205},"df = pd.read_excel(\"orders.xlsx\")\nrecords = df.astype(object).where(df.notna(), None).to_dict(\"records\")\nprint(df.loc[0, \"Discount %\"], \"->\", records[0][\"Discount %\"], \"->\", Order.model_validate(records[0]).discount_pct)\n",[14,2219,2220,2234,2260],{"__ignoreMap":205},[209,2221,2222,2225,2227,2230,2232],{"class":90,"line":211},[209,2223,2224],{"class":265},"df ",[209,2226,333],{"class":261},[209,2228,2229],{"class":265}," pd.read_excel(",[209,2231,760],{"class":218},[209,2233,349],{"class":265},[209,2235,2236,2239,2241,2244,2247,2250,2252,2255,2258],{"class":90,"line":275},[209,2237,2238],{"class":265},"records ",[209,2240,333],{"class":261},[209,2242,2243],{"class":265}," df.astype(",[209,2245,2246],{"class":345},"object",[209,2248,2249],{"class":265},").where(df.notna(), ",[209,2251,624],{"class":345},[209,2253,2254],{"class":265},").to_dict(",[209,2256,2257],{"class":218},"\"records\"",[209,2259,349],{"class":265},[209,2261,2262,2264,2267,2269,2271,2273,2275,2278,2281,2283,2285,2287,2289,2291,2294,2296],{"class":90,"line":288},[209,2263,665],{"class":345},[209,2265,2266],{"class":265},"(df.loc[",[209,2268,52],{"class":345},[209,2270,379],{"class":265},[209,2272,633],{"class":218},[209,2274,1068],{"class":265},[209,2276,2277],{"class":218},"\"->\"",[209,2279,2280],{"class":265},", records[",[209,2282,52],{"class":345},[209,2284,944],{"class":265},[209,2286,633],{"class":218},[209,2288,1068],{"class":265},[209,2290,2277],{"class":218},[209,2292,2293],{"class":265},", Order.model_validate(records[",[209,2295,52],{"class":345},[209,2297,2298],{"class":265},"]).discount_pct)\n",[10,2300,2301,2302,2305,2306,2308],{},"pandas also turns the date column into ",[14,2303,2304],{},"Timestamp","s at midnight, which pydantic accepts for a ",[14,2307,2120],{}," field.",[195,2310,2312],{"id":2311},"strict-or-lax","Strict or lax",[10,2314,2315,2316,2318,2319,2322,2323,2326],{},"Lax mode accepted ",[14,2317,1100],{}," as the quantity 12. That is usually what you want for spreadsheet data, where numbers are often typed as text, but when a text number signals a real problem — a column of IDs imported as text that should be numeric — use ",[14,2320,2321],{},"Field(strict=True)"," on that field, or ",[14,2324,2325],{},"ConfigDict(strict=True)"," for the whole model, and the row is rejected instead.",[195,2328,2330],{"id":2329},"common-pitfalls","Common pitfalls",[2332,2333,2334,2346,2352,2361,2370],"ul",{},[2335,2336,2337,2341,2342,2345],"li",{},[2338,2339,2340],"strong",{},"Stopping at the first error."," Validate row by row and collect ",[14,2343,2344],{},"exc.errors()","; each lists every failed field.",[2335,2347,2348,2351],{},[2338,2349,2350],{},"Losing the Excel row number."," Start the row counter at 2 so it matches the sheet.",[2335,2353,2354,2357,2358,2360],{},[2338,2355,2356],{},"Passing pandas NaN."," Convert to ",[14,2359,624],{}," first, or read with openpyxl.",[2335,2362,2363,2366,2367,2369],{},[2338,2364,2365],{},"Datetimes in date fields."," A time part fails ",[14,2368,2120],{},"; decide whether that is an error or truncate it in a before-validator.",[2335,2371,2372,2375],{},[2338,2373,2374],{},"Cross-column rules hidden by field errors."," Model validators run only when fields pass; fix field errors first.",[195,2377,2379],{"id":2378},"performance-notes","Performance notes",[10,2381,2382,2383,2386,2387,2391],{},"pydantic v2 validates tens of thousands of rows a second. For very large sheets, validate with ",[14,2384,2385],{},"TypeAdapter(list[Order])"," in one call when you only need the valid\u002Finvalid split, or use a column-based tool such as ",[24,2388,2390],{"href":2389},"\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fvalidate-excel-data-with-pandera-schemas\u002F","pandera",", which checks whole columns at once.",[195,2393,2395],{"id":2394},"conclusion","Conclusion",[10,2397,2398,2399,2401,2402,2404,2405,2407,2408,2410],{},"Describe a valid row as a pydantic model: types for kinds of value, ",[14,2400,244],{}," for ranges and patterns, ",[14,2403,685],{}," for choices, aliases for Excel headers, before-validators to tidy raw values and a model validator for rules across columns. Read rows with openpyxl so blanks are ",[14,2406,624],{},", validate each row, and collect every error with its Excel row and column into a report. Load only the valid rows, using ",[14,2409,2127],{}," for clean, typed records.",[195,2412,2414],{"id":2413},"frequently-asked-questions","Frequently asked questions",[10,2416,2417,2420],{},[2338,2418,2419],{},"Why use pydantic to validate Excel data?","\nA pydantic model states the rules for a row in one place — types, ranges, allowed values, patterns and rules across columns — and reports every broken rule for a row at once, with the field and the offending value. That makes a precise error report straightforward.",[10,2422,2423,2426],{},[2338,2424,2425],{},"How do I map Excel headers with spaces to pydantic fields?","\nGive each field an alias matching the header, such as order_id: str = Field(alias=\"Order ID\"), and validate a dict built from the header row and the data row with Model.model_validate(record).",[10,2428,2429,2432],{},[2338,2430,2431],{},"Why does pydantic reject an empty Excel cell read with pandas?","\npandas turns empty cells into NaN, which is a float, not None. An optional field receives NaN instead of None, and constraints such as le=50 fail on it. Read rows with openpyxl, where empty cells are None, or replace NaN with None before validating.",[10,2434,2435,2438],{},[2338,2436,2437],{},"Does pydantic convert \"12\" to 12?","\nYes, by default pydantic coerces compatible values — the text \"12\" becomes the integer 12. Set strict=True on a field or in the model config to reject text where a number is required.",[195,2440,2442],{"id":2441},"related","Related",[2332,2444,2445,2451,2457,2464],{},[2335,2446,2447,2448,2450],{},"Up one level: ",[24,2449,27],{"href":26}," — every way to check a sheet.",[2335,2452,2453,2456],{},[24,2454,2455],{"href":2389},"Validate Excel Data with pandera Schemas"," — column-based validation.",[2335,2458,2459,2463],{},[24,2460,2462],{"href":2461},"\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fvalidate-excel-columns-before-import-with-pandas\u002F","Validate Excel Columns Before Import with pandas"," — header and type checks.",[2335,2465,2466,2470],{},[24,2467,2469],{"href":2468},"\u002Fautomating-reporting-workflows\u002Fserving-excel-files-from-python-web-apps\u002Fvalidate-an-uploaded-excel-file-in-a-web-app\u002F","Validate an Uploaded Excel File in a Web App"," — the same checks behind an upload form.",[2472,2473,2474],"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 .sHg5J, html code.shiki .sHg5J{--shiki-default:#6F42C1;--shiki-dark:#91CBFF}html pre.shiki code .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}html pre.shiki code .sP0c6, html code.shiki .sP0c6{--shiki-default:#005CC5;--shiki-dark:#91CBFF}html pre.shiki code .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}html pre.shiki code .s_b0D, html code.shiki .s_b0D{--shiki-default:#22863A;--shiki-default-font-weight:bold;--shiki-dark:#72F088;--shiki-dark-font-weight:bold}html pre.shiki code .s_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}",{"title":205,"searchDepth":275,"depth":275,"links":2476},[2477,2478,2479,2480,2481,2482,2483,2484,2485,2486,2487,2488,2489],{"id":197,"depth":275,"text":198},{"id":237,"depth":275,"text":238},{"id":704,"depth":275,"text":705},{"id":1119,"depth":275,"text":1120},{"id":1583,"depth":275,"text":1584},{"id":2113,"depth":275,"text":2114},{"id":2187,"depth":275,"text":2188},{"id":2311,"depth":275,"text":2312},{"id":2329,"depth":275,"text":2330},{"id":2378,"depth":275,"text":2379},{"id":2394,"depth":275,"text":2395},{"id":2413,"depth":275,"text":2414},{"id":2441,"depth":275,"text":2442},"2026-09-25","Describe what a valid row looks like once, as a pydantic model, and check every row of a sheet against it. Map Excel headers to fields with aliases, constrain values, normalise text before checking, add rules that span columns, and turn validation errors into a report that names the Excel row, column and value.","md",[2494,2496,2498,2500],{"q":2419,"a":2495},"A pydantic model states the rules for a row in one place — types, ranges, allowed values, patterns and rules across columns — and reports every broken rule for a row at once, with the field and the offending value. That makes a precise error report straightforward.",{"q":2425,"a":2497},"Give each field an alias matching the header, such as order_id: str = Field(alias=\"Order ID\"), and validate a dict built from the header row and the data row with Model.model_validate(record).",{"q":2431,"a":2499},"pandas turns empty cells into NaN, which is a float, not None. An optional field receives NaN instead of None, and constraints such as le=50 fail on it. Read rows with openpyxl, where empty cells are None, or replace NaN with None before validating.",{"q":2437,"a":2501},"Yes, by default pydantic coerces compatible values — the text \"12\" becomes the integer 12. Set strict=True on a field or in the model config to reject text where a number is required.","validate excel rows with pydantic models",null,{"breadcrumb":2505},[2506,2509,2512],{"name":2507,"item":2508},"Home","\u002F",{"name":2510,"item":2511},"Advanced Data Transformation and Cleaning for Excel Automation","\u002Fadvanced-data-transformation-and-cleaning\u002F",{"name":27,"item":26},"\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fvalidate-excel-rows-with-pydantic",{"title":2515,"description":2516},"Validate Excel Data Row by Row with pydantic (Python)","Validate Excel rows with pydantic v2: models with header aliases, Field constraints, Literal choices, before-validators, cross-column model validators, pandas NaN pitfalls, strict mode, and an error report with Excel row numbers and column letters.","validate-excel-rows-with-pydantic","advanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fvalidate-excel-rows-with-pydantic\u002Findex",[2520,2521],"data-validation","openpyxl","how-to","44m96JQYh-nBzOA06k12hFMT2laB1quxFD5-vK0ugRU",[2525,2528],{"title":2455,"path":2526,"stem":2527,"children":-1},"\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fvalidate-excel-data-with-pandera-schemas","advanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fvalidate-excel-data-with-pandera-schemas\u002Findex",{"title":2529,"path":2530,"stem":2531,"children":-1},"Validate Parent-Child Codes in a Hierarchy","\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fvalidate-parent-child-codes-in-a-hierarchy","advanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fvalidate-parent-child-codes-in-a-hierarchy\u002Findex",[2533,2535],{"path":2534,"title":2510},"\u002Fadvanced-data-transformation-and-cleaning",{"path":2536,"title":27},"\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python",[],[2539,2542,2545,2548,2551,2554],{"path":2540,"title":2541,"kind":2522},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcleaning-excel-data-with-pandas\u002Fdetect-outliers-in-excel-data-with-pandas","Detect Outliers in Excel Data with pandas",{"path":2543,"title":2544,"kind":2522},"\u002Fadvanced-data-transformation-and-cleaning\u002Fapplying-conditional-formatting-with-openpyxl\u002Fhighlight-cells-containing-text-with-openpyxl","Highlight Cells Containing Specific Text with openpyxl",{"path":2546,"title":2547,"kind":2522},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcleaning-excel-data-with-pandas\u002Ffind-near-duplicate-names-within-one-excel-column","Find Near-Duplicate Names Within One Excel Column",{"path":2549,"title":2550,"kind":2522},"\u002Fadvanced-data-transformation-and-cleaning\u002Fextracting-data-from-messy-excel-sheets\u002Fread-dropdown-lists-from-an-existing-workbook","Read Dropdown Lists and Validation Rules from an Existing Workbook",{"path":2552,"title":2553,"kind":2522},"\u002Fadvanced-data-transformation-and-cleaning\u002Fhandling-missing-data-in-excel-reports\u002Ffind-and-report-missing-values-in-an-excel-file","Find and Report Missing Values in an Excel File",{"path":2555,"title":2556,"kind":2522},"\u002Fadvanced-data-transformation-and-cleaning\u002Fhandling-missing-data-in-excel-reports\u002Fflag-rows-with-too-many-missing-values","Flag Rows with Too Many Missing Values",1790972100968]