[{"data":1,"prerenderedAt":2170},["ShallowReactive",2],{"doc:\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fvalidate-email-and-phone-formats-in-excel-data":3,"surround:\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fvalidate-email-and-phone-formats-in-excel-data":2137,"ancestors:\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fvalidate-email-and-phone-formats-in-excel-data":2145,"children:\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fvalidate-email-and-phone-formats-in-excel-data":2150,"related:\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fvalidate-email-and-phone-formats-in-excel-data":2151},{"id":4,"title":5,"body":6,"dateModified":2103,"datePublished":2103,"description":2104,"extension":2105,"faq":2106,"keyword":2115,"kind":2116,"meta":2117,"navigation":319,"path":2126,"seo":2127,"slug":2129,"stem":2130,"topics":2131,"type":2135,"__hash__":2136},"docs\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fvalidate-email-and-phone-formats-in-excel-data\u002Findex.md","Validate Email and Phone Formats in Excel Data with Python",{"type":7,"value":8,"toc":2090},"minimark",[9,24,235,240,274,277,591,605,609,612,709,722,726,732,939,949,1117,1121,1128,1334,1352,1356,1359,1423,1445,1449,1452,1635,1642,1775,1779,1785,1964,1970,1974,2012,2016,2025,2029,2035,2041,2047,2053,2057,2086],[10,11,12,13,17,18,23],"p",{},"A contact list maintained in Excel is only as good as the data people typed into it: \"cai@example\" with no domain ending, \"dee@@example.com\", a phone number stored as ",[14,15,16],"code",{},"7911123456"," because Excel dropped the leading zero, and the same office number written three different ways. Import that into a CRM or mailing tool and the bad rows bounce, fail silently or create duplicates. This guide, part of ",[19,20,22],"a",{"href":21},"\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002F","Validating Excel Data with Python",", checks both columns properly, normalises the valid values and produces a report of the invalid cells with their row numbers in the sheet.",[25,26,34,35,34,39,34,43,34,51,34,60,34,64,34,71,34,75,34,79,34,83,34,87,34,91,34,96,34,98,34,102,34,104,34,107,34,109,34,112,34,115,34,119,34,121,34,125,34,127,34,130,34,132,34,135,34,138,34,142,34,144,34,147,34,152,34,156,34,158,34,161,34,164,34,168,34,170,34,173,34,177,34,181,34,183,34,186,34,189,34,193,34,195,34,198,34,200,34,203,34,205,34,208,34,211,34,215,34,217,34,220,34,222,34,225,34,227,34,229],"svg",{"viewBox":27,"role":28,"ariaLabelledBy":29,"xmlns":32,"style":33},"157 0 446 250","img",[30,31],"ev-sheet-t","ev-sheet-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:446px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[36,37,38],"title",{"id":30},"A contact sheet with typical format problems",[40,41,42],"desc",{"id":31},"Ann's email and phone are fine. Bob's email has upper-case letters and a trailing space and his phone uses the international format. Cai's email has no dot in the domain, Dee's email has two at signs, and Eve's phone was stored as a number without its leading zero.",[44,45],"rect",{"x":46,"y":47,"width":48,"height":49,"fill":50},"157","0","446","250","#ffffff",[44,52],{"x":53,"y":54,"width":55,"height":56,"rx":47,"fill":57,"stroke":58,"style":59},"173.3","16.0","32.0","24.0","#e7ebef","var(--line,#cdd5e6)","stroke-width:1px",[44,61],{"x":62,"y":54,"width":63,"height":56,"rx":47,"fill":57,"stroke":58,"style":59},"205.3","58.2",[65,66,70],"text",{"x":67,"y":68,"style":69},"234.4","33.0","font-size:11px;font-weight:600;fill:var(--muted,#5b6780);text-anchor:middle","A",[44,72],{"x":73,"y":54,"width":74,"height":56,"rx":47,"fill":57,"stroke":58,"style":59},"263.5","183.4",[65,76,78],{"x":77,"y":68,"style":69},"355.1","B",[44,80],{"x":81,"y":54,"width":82,"height":56,"rx":47,"fill":57,"stroke":58,"style":59},"446.8","139.9",[65,84,86],{"x":85,"y":68,"style":69},"516.8","C",[44,88],{"x":53,"y":89,"width":55,"height":90,"rx":47,"fill":57,"stroke":58,"style":59},"40.0","28.0",[65,92,95],{"x":93,"y":94,"style":69},"189.3","59.0","1",[44,97],{"x":62,"y":89,"width":63,"height":90,"rx":47,"fill":50,"stroke":58,"style":59},[65,99,101],{"x":67,"y":94,"style":100},"font-size:12px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","name",[44,103],{"x":73,"y":89,"width":74,"height":90,"rx":47,"fill":50,"stroke":58,"style":59},[65,105,106],{"x":77,"y":94,"style":100},"email",[44,108],{"x":81,"y":89,"width":82,"height":90,"rx":47,"fill":50,"stroke":58,"style":59},[65,110,111],{"x":85,"y":94,"style":100},"phone",[44,113],{"x":53,"y":114,"width":55,"height":90,"rx":47,"fill":57,"stroke":58,"style":59},"68.0",[65,116,118],{"x":93,"y":117,"style":69},"87.0","2",[44,120],{"x":62,"y":114,"width":63,"height":90,"rx":47,"fill":50,"stroke":58,"style":59},[65,122,124],{"x":67,"y":117,"style":123},"font-size:12px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","Ann",[44,126],{"x":73,"y":114,"width":74,"height":90,"rx":47,"fill":50,"stroke":58,"style":59},[65,128,129],{"x":77,"y":117,"style":123},"ann@example.com",[44,131],{"x":81,"y":114,"width":82,"height":90,"rx":47,"fill":50,"stroke":58,"style":59},[65,133,134],{"x":85,"y":117,"style":123},"020 7946 0958",[44,136],{"x":53,"y":137,"width":55,"height":90,"rx":47,"fill":57,"stroke":58,"style":59},"96.0",[65,139,141],{"x":93,"y":140,"style":69},"115.0","3",[44,143],{"x":62,"y":137,"width":63,"height":90,"rx":47,"fill":50,"stroke":58,"style":59},[65,145,146],{"x":67,"y":140,"style":123},"Bob",[44,148],{"x":73,"y":137,"width":74,"height":90,"rx":47,"fill":149,"stroke":150,"style":151},"#fdefd8","var(--gold,#b4740a)","stroke-width:1.5px",[65,153,155],{"x":77,"y":140,"style":154},"font-size:12px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","Bob.Ray@Example.COM␠",[44,157],{"x":81,"y":137,"width":82,"height":90,"rx":47,"fill":50,"stroke":58,"style":59},[65,159,160],{"x":85,"y":140,"style":123},"+44 20 7946 0958",[44,162],{"x":53,"y":163,"width":55,"height":90,"rx":47,"fill":57,"stroke":58,"style":59},"124.0",[65,165,167],{"x":93,"y":166,"style":69},"143.0","4",[44,169],{"x":62,"y":163,"width":63,"height":90,"rx":47,"fill":50,"stroke":58,"style":59},[65,171,172],{"x":67,"y":166,"style":123},"Cai",[44,174],{"x":73,"y":163,"width":74,"height":90,"rx":47,"fill":175,"stroke":176,"style":151},"#fee8f2","var(--accent,#f43f8f)",[65,178,180],{"x":77,"y":166,"style":179},"font-size:12px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","cai@example",[44,182],{"x":81,"y":163,"width":82,"height":90,"rx":47,"fill":50,"stroke":58,"style":59},[65,184,185],{"x":85,"y":166,"style":123},"07520 123456",[44,187],{"x":53,"y":188,"width":55,"height":90,"rx":47,"fill":57,"stroke":58,"style":59},"152.0",[65,190,192],{"x":93,"y":191,"style":69},"171.0","5",[44,194],{"x":62,"y":188,"width":63,"height":90,"rx":47,"fill":50,"stroke":58,"style":59},[65,196,197],{"x":67,"y":191,"style":123},"Dee",[44,199],{"x":73,"y":188,"width":74,"height":90,"rx":47,"fill":175,"stroke":176,"style":151},[65,201,202],{"x":77,"y":191,"style":179},"dee@@example.com",[44,204],{"x":81,"y":188,"width":82,"height":90,"rx":47,"fill":175,"stroke":176,"style":151},[65,206,207],{"x":85,"y":191,"style":179},"12345",[44,209],{"x":53,"y":210,"width":55,"height":90,"rx":47,"fill":57,"stroke":58,"style":59},"180.0",[65,212,214],{"x":93,"y":213,"style":69},"199.0","6",[44,216],{"x":62,"y":210,"width":63,"height":90,"rx":47,"fill":50,"stroke":58,"style":59},[65,218,219],{"x":67,"y":213,"style":123},"Eve",[44,221],{"x":73,"y":210,"width":74,"height":90,"rx":47,"fill":50,"stroke":58,"style":59},[65,223,224],{"x":77,"y":213,"style":123},"eve@example.co.uk",[44,226],{"x":81,"y":210,"width":82,"height":90,"rx":47,"fill":149,"stroke":150,"style":151},[65,228,16],{"x":85,"y":213,"style":154},[65,230,234],{"x":231,"y":232,"style":233},"380.0","230.0","font-size:12.5px;font-weight:400;fill:var(--muted,#5b6780);text-anchor:middle","pink cells are invalid; gold cells are valid but need normalising",[236,237,239],"h2",{"id":238},"prerequisites","Prerequisites",[241,242,247],"pre",{"className":243,"code":244,"language":245,"meta":246,"style":246},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install pandas openpyxl email-validator phonenumbers\n","bash","",[14,248,249],{"__ignoreMap":246},[250,251,254,258,262,265,268,271],"span",{"class":252,"line":253},"line",1,[250,255,257],{"class":256},"sMTad","pip",[250,259,261],{"class":260},"srMev"," install",[250,263,264],{"class":260}," pandas",[250,266,267],{"class":260}," openpyxl",[250,269,270],{"class":260}," email-validator",[250,272,273],{"class":260}," phonenumbers\n",[10,275,276],{},"The sample sheet, with Eve's phone stored as a number the way Excel stores digits typed without spaces:",[241,278,282],{"className":279,"code":280,"language":281,"meta":246,"style":246},"language-python shiki shiki-themes github-light github-dark-high-contrast","import pandas as pd\nfrom openpyxl import Workbook\n\nwb = Workbook()\nws = wb.active\nws.append([\"name\", \"email\", \"phone\"])\nfor row in [[\"Ann\", \"ann@example.com\", \"020 7946 0958\"],\n            [\"Bob\", \"Bob.Ray@Example.COM \", \"+44 20 7946 0958\"],\n            [\"Cai\", \"cai@example\", \"07520 123456\"],\n            [\"Dee\", \"dee@@example.com\", \"12345\"],\n            [\"Eve\", \"eve@example.co.uk\", 7911123456]]:\n    ws.append(row)\nwb.save(\"contacts.xlsx\")\n\ndf = pd.read_excel(\"contacts.xlsx\", dtype={\"phone\": str, \"email\": str})\ndf.insert(0, \"sheet_row\", df.index + 2)\nprint(df[\"phone\"].tolist())\n# ['020 7946 0958', '+44 20 7946 0958', '07520 123456', '12345', '7911123456']\n","python",[14,283,284,300,314,321,333,344,367,398,419,439,459,480,486,498,503,546,570,584],{"__ignoreMap":246},[250,285,286,290,294,297],{"class":252,"line":253},[250,287,289],{"class":288},"s-kum","import",[250,291,293],{"class":292},"skGVy"," pandas ",[250,295,296],{"class":288},"as",[250,298,299],{"class":292}," pd\n",[250,301,303,306,309,311],{"class":252,"line":302},2,[250,304,305],{"class":288},"from",[250,307,308],{"class":292}," openpyxl ",[250,310,289],{"class":288},[250,312,313],{"class":292}," Workbook\n",[250,315,317],{"class":252,"line":316},3,[250,318,320],{"emptyLinePlaceholder":319},true,"\n",[250,322,324,327,330],{"class":252,"line":323},4,[250,325,326],{"class":292},"wb ",[250,328,329],{"class":288},"=",[250,331,332],{"class":292}," Workbook()\n",[250,334,336,339,341],{"class":252,"line":335},5,[250,337,338],{"class":292},"ws ",[250,340,329],{"class":288},[250,342,343],{"class":292}," wb.active\n",[250,345,347,350,353,356,359,361,364],{"class":252,"line":346},6,[250,348,349],{"class":292},"ws.append([",[250,351,352],{"class":260},"\"name\"",[250,354,355],{"class":292},", ",[250,357,358],{"class":260},"\"email\"",[250,360,355],{"class":292},[250,362,363],{"class":260},"\"phone\"",[250,365,366],{"class":292},"])\n",[250,368,370,373,376,379,382,385,387,390,392,395],{"class":252,"line":369},7,[250,371,372],{"class":288},"for",[250,374,375],{"class":292}," row ",[250,377,378],{"class":288},"in",[250,380,381],{"class":292}," [[",[250,383,384],{"class":260},"\"Ann\"",[250,386,355],{"class":292},[250,388,389],{"class":260},"\"ann@example.com\"",[250,391,355],{"class":292},[250,393,394],{"class":260},"\"020 7946 0958\"",[250,396,397],{"class":292},"],\n",[250,399,401,404,407,409,412,414,417],{"class":252,"line":400},8,[250,402,403],{"class":292},"            [",[250,405,406],{"class":260},"\"Bob\"",[250,408,355],{"class":292},[250,410,411],{"class":260},"\"Bob.Ray@Example.COM \"",[250,413,355],{"class":292},[250,415,416],{"class":260},"\"+44 20 7946 0958\"",[250,418,397],{"class":292},[250,420,422,424,427,429,432,434,437],{"class":252,"line":421},9,[250,423,403],{"class":292},[250,425,426],{"class":260},"\"Cai\"",[250,428,355],{"class":292},[250,430,431],{"class":260},"\"cai@example\"",[250,433,355],{"class":292},[250,435,436],{"class":260},"\"07520 123456\"",[250,438,397],{"class":292},[250,440,442,444,447,449,452,454,457],{"class":252,"line":441},10,[250,443,403],{"class":292},[250,445,446],{"class":260},"\"Dee\"",[250,448,355],{"class":292},[250,450,451],{"class":260},"\"dee@@example.com\"",[250,453,355],{"class":292},[250,455,456],{"class":260},"\"12345\"",[250,458,397],{"class":292},[250,460,462,464,467,469,472,474,477],{"class":252,"line":461},11,[250,463,403],{"class":292},[250,465,466],{"class":260},"\"Eve\"",[250,468,355],{"class":292},[250,470,471],{"class":260},"\"eve@example.co.uk\"",[250,473,355],{"class":292},[250,475,16],{"class":476},"sP0c6",[250,478,479],{"class":292},"]]:\n",[250,481,483],{"class":252,"line":482},12,[250,484,485],{"class":292},"    ws.append(row)\n",[250,487,489,492,495],{"class":252,"line":488},13,[250,490,491],{"class":292},"wb.save(",[250,493,494],{"class":260},"\"contacts.xlsx\"",[250,496,497],{"class":292},")\n",[250,499,501],{"class":252,"line":500},14,[250,502,320],{"emptyLinePlaceholder":319},[250,504,506,509,511,514,516,518,522,524,527,529,532,535,537,539,541,543],{"class":252,"line":505},15,[250,507,508],{"class":292},"df ",[250,510,329],{"class":288},[250,512,513],{"class":292}," pd.read_excel(",[250,515,494],{"class":260},[250,517,355],{"class":292},[250,519,521],{"class":520},"sa561","dtype",[250,523,329],{"class":288},[250,525,526],{"class":292},"{",[250,528,363],{"class":260},[250,530,531],{"class":292},": ",[250,533,534],{"class":476},"str",[250,536,355],{"class":292},[250,538,358],{"class":260},[250,540,531],{"class":292},[250,542,534],{"class":476},[250,544,545],{"class":292},"})\n",[250,547,549,552,554,556,559,562,565,568],{"class":252,"line":548},16,[250,550,551],{"class":292},"df.insert(",[250,553,47],{"class":476},[250,555,355],{"class":292},[250,557,558],{"class":260},"\"sheet_row\"",[250,560,561],{"class":292},", df.index ",[250,563,564],{"class":288},"+",[250,566,567],{"class":476}," 2",[250,569,497],{"class":292},[250,571,573,576,579,581],{"class":252,"line":572},17,[250,574,575],{"class":476},"print",[250,577,578],{"class":292},"(df[",[250,580,363],{"class":260},[250,582,583],{"class":292},"].tolist())\n",[250,585,587],{"class":252,"line":586},18,[250,588,590],{"class":589},"s-wDw","# ['020 7946 0958', '+44 20 7946 0958', '07520 123456', '12345', '7911123456']\n",[10,592,593,594,596,597,600,601,604],{},"Reading ",[14,595,111],{}," as text is the most important line on this page. Without it, a column where every number is stored as digits comes back as integers — or as floats like ",[14,598,599],{},"7911123456.0"," if any cell is blank — and the phone library then sees a different number. The ",[14,602,603],{},"sheet_row"," column records the spreadsheet row for each record, for the report at the end.",[236,606,608],{"id":607},"step-1-a-quick-regex-screen","Step 1 — A quick regex screen",[10,610,611],{},"A regular expression catches the gross errors cheaply and needs no extra library. Keep it deliberately loose; its job is to flag obvious junk, not to judge borderline addresses:",[241,613,615],{"className":279,"code":614,"language":281,"meta":246,"style":246},"SCREEN = r\"^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$\"\nemail = df[\"email\"].str.strip()\nprint(email.str.match(SCREEN).tolist())    # [True, True, False, False, True]\n",[14,616,617,679,694],{"__ignoreMap":246},[250,618,619,622,625,628,631,634,637,641,644,646,648,651,653,655,657,659,663,665,667,669,671,673,676],{"class":252,"line":253},[250,620,621],{"class":476},"SCREEN",[250,623,624],{"class":288}," =",[250,626,627],{"class":288}," r",[250,629,630],{"class":260},"\"",[250,632,633],{"class":476},"^[",[250,635,636],{"class":288},"^",[250,638,640],{"class":639},"sSjpA","@",[250,642,643],{"class":476},"\\s]",[250,645,564],{"class":288},[250,647,640],{"class":260},[250,649,650],{"class":476},"[",[250,652,636],{"class":288},[250,654,640],{"class":639},[250,656,643],{"class":476},[250,658,564],{"class":288},[250,660,662],{"class":661},"s_b0D","\\.",[250,664,650],{"class":476},[250,666,636],{"class":288},[250,668,640],{"class":639},[250,670,643],{"class":476},[250,672,564],{"class":288},[250,674,675],{"class":476},"$",[250,677,678],{"class":260},"\"\n",[250,680,681,684,686,689,691],{"class":252,"line":302},[250,682,683],{"class":292},"email ",[250,685,329],{"class":288},[250,687,688],{"class":292}," df[",[250,690,358],{"class":260},[250,692,693],{"class":292},"].str.strip()\n",[250,695,696,698,701,703,706],{"class":252,"line":316},[250,697,575],{"class":476},[250,699,700],{"class":292},"(email.str.match(",[250,702,621],{"class":476},[250,704,705],{"class":292},").tolist())    ",[250,707,708],{"class":589},"# [True, True, False, False, True]\n",[10,710,711,712,714,715,717,718,721],{},"The pattern requires one ",[14,713,640],{},", no spaces, and a dot somewhere after the ",[14,716,640],{},". That rejects Cai and Dee. It would also accept plenty of invalid addresses — ",[14,719,720],{},"a@b..c",", for instance — which is why the next step uses a real validator.",[236,723,725],{"id":724},"step-2-validate-and-normalise-emails","Step 2 — Validate and normalise emails",[10,727,728,731],{},[14,729,730],{},"email-validator"," implements the address syntax rules, including internationalised domains, and returns a normalised form. Turn off the DNS lookup for bulk checks; it is slow and needs network access:",[241,733,735],{"className":279,"code":734,"language":281,"meta":246,"style":246},"from email_validator import EmailNotValidError, validate_email\n\ndef check_email(value):\n    if pd.isna(value) or not str(value).strip():\n        return None, \"missing\"\n    try:\n        result = validate_email(str(value).strip(), check_deliverability=False)\n        return result.normalized, None\n    except EmailNotValidError as exc:\n        return None, str(exc)\n\nchecked = df[\"email\"].map(check_email)\ndf[\"email_norm\"] = checked.str[0]\ndf[\"email_error\"] = checked.str[1]\nprint(df[[\"email_norm\", \"email_error\"]].values.tolist())\n",[14,736,737,749,753,765,785,798,806,831,841,854,867,871,885,906,923],{"__ignoreMap":246},[250,738,739,741,744,746],{"class":252,"line":253},[250,740,305],{"class":288},[250,742,743],{"class":292}," email_validator ",[250,745,289],{"class":288},[250,747,748],{"class":292}," EmailNotValidError, validate_email\n",[250,750,751],{"class":252,"line":302},[250,752,320],{"emptyLinePlaceholder":319},[250,754,755,758,762],{"class":252,"line":316},[250,756,757],{"class":288},"def",[250,759,761],{"class":760},"s_Opv"," check_email",[250,763,764],{"class":292},"(value):\n",[250,766,767,770,773,776,779,782],{"class":252,"line":323},[250,768,769],{"class":288},"    if",[250,771,772],{"class":292}," pd.isna(value) ",[250,774,775],{"class":288},"or",[250,777,778],{"class":288}," not",[250,780,781],{"class":476}," str",[250,783,784],{"class":292},"(value).strip():\n",[250,786,787,790,793,795],{"class":252,"line":335},[250,788,789],{"class":288},"        return",[250,791,792],{"class":476}," None",[250,794,355],{"class":292},[250,796,797],{"class":260},"\"missing\"\n",[250,799,800,803],{"class":252,"line":346},[250,801,802],{"class":288},"    try",[250,804,805],{"class":292},":\n",[250,807,808,811,813,816,818,821,824,826,829],{"class":252,"line":369},[250,809,810],{"class":292},"        result ",[250,812,329],{"class":288},[250,814,815],{"class":292}," validate_email(",[250,817,534],{"class":476},[250,819,820],{"class":292},"(value).strip(), ",[250,822,823],{"class":520},"check_deliverability",[250,825,329],{"class":288},[250,827,828],{"class":476},"False",[250,830,497],{"class":292},[250,832,833,835,838],{"class":252,"line":400},[250,834,789],{"class":288},[250,836,837],{"class":292}," result.normalized, ",[250,839,840],{"class":476},"None\n",[250,842,843,846,849,851],{"class":252,"line":421},[250,844,845],{"class":288},"    except",[250,847,848],{"class":292}," EmailNotValidError ",[250,850,296],{"class":288},[250,852,853],{"class":292}," exc:\n",[250,855,856,858,860,862,864],{"class":252,"line":441},[250,857,789],{"class":288},[250,859,792],{"class":476},[250,861,355],{"class":292},[250,863,534],{"class":476},[250,865,866],{"class":292},"(exc)\n",[250,868,869],{"class":252,"line":461},[250,870,320],{"emptyLinePlaceholder":319},[250,872,873,876,878,880,882],{"class":252,"line":482},[250,874,875],{"class":292},"checked ",[250,877,329],{"class":288},[250,879,688],{"class":292},[250,881,358],{"class":260},[250,883,884],{"class":292},"].map(check_email)\n",[250,886,887,890,893,896,898,901,903],{"class":252,"line":488},[250,888,889],{"class":292},"df[",[250,891,892],{"class":260},"\"email_norm\"",[250,894,895],{"class":292},"] ",[250,897,329],{"class":288},[250,899,900],{"class":292}," checked.str[",[250,902,47],{"class":476},[250,904,905],{"class":292},"]\n",[250,907,908,910,913,915,917,919,921],{"class":252,"line":500},[250,909,889],{"class":292},[250,911,912],{"class":260},"\"email_error\"",[250,914,895],{"class":292},[250,916,329],{"class":288},[250,918,900],{"class":292},[250,920,95],{"class":476},[250,922,905],{"class":292},[250,924,925,927,930,932,934,936],{"class":252,"line":505},[250,926,575],{"class":476},[250,928,929],{"class":292},"(df[[",[250,931,892],{"class":260},[250,933,355],{"class":292},[250,935,912],{"class":260},[250,937,938],{"class":292},"]].values.tolist())\n",[10,940,941,942,945,946,948],{},"Bob's address comes back as ",[14,943,944],{},"Bob.Ray@example.com",": the domain is lower-cased, while the part before the ",[14,947,640],{}," keeps its case, because in principle a mail server may treat it as case-sensitive. The errors are readable sentences — \"The part after the @-sign is not valid. It should have a period.\" — which can go straight into a report for the person who maintains the sheet. Valid syntax does not prove the mailbox exists; only sending a message does.",[25,950,34,956,34,959,34,962,34,966,34,975,34,986,34,992,34,997,34,1002,34,1007,34,1012,34,1019,34,1025,34,1029,34,1032,34,1036,34,1039,34,1042,34,1048,34,1052,34,1055,34,1058,34,1062,34,1066,34,1069,34,1072,34,1078,34,1082,34,1086,34,1089,34,1093,34,1096,34,1099,34,1106,34,1111,34,1114],{"viewBox":951,"role":28,"ariaLabelledBy":952,"xmlns":32,"style":955},"4 0 752 160",[953,954],"ev-flow-t","ev-flow-d","width:100%;max-width:752px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[36,957,958],{"id":953},"From raw contact columns to a validation report",[40,960,961],{"id":954},"Read email and phone as text, screen emails with a loose regex, validate and normalise each address with email-validator and each phone number with phonenumbers using a default region, detect duplicates on the normalised values, then write a report of invalid cells with their sheet rows.",[44,963],{"x":167,"y":47,"width":964,"height":965,"fill":50},"752","160",[44,967],{"x":968,"y":54,"width":969,"height":970,"rx":971,"fill":972,"stroke":973,"style":974},"20.0","115.2","128.0","12","#f0f4ff","var(--brand,#5b5cf0)","stroke-width:2px",[976,977,979],"g",{"transform":978},"translate(65.6,46.5) scale(1.000)",[980,981],"path",{"d":982,"fill":983,"stroke":984,"style":985},"M4 4h16v16H4z M4 9h16 M4 14h16 M10 4v16","none","var(--brand-strong,#4338ca)","stroke-width:1.80px;stroke-linecap:round;stroke-linejoin:round",[65,987,991],{"x":988,"y":989,"style":990},"77.6","93.0","font-size:13.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Read as text",[65,993,996],{"x":988,"y":994,"style":995},"112.5","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","dtype=str",[252,998],{"x1":999,"y1":1000,"x2":1001,"y2":1000,"stroke":973,"style":974},"140.2","80.0","162.2",[1003,1004],"polygon",{"points":1005,"fill":1006},"166.2,80.0 157.2,85.0 157.2,75.0","#5b5cf0",[44,1008],{"x":1009,"y":54,"width":969,"height":970,"rx":971,"fill":1010,"stroke":1011,"style":974},"171.2","#d9f4f1","var(--teal,#0f9488)",[976,1013,1015],{"transform":1014},"translate(216.8,37.8) scale(1.000)",[980,1016],{"d":1017,"fill":983,"stroke":1018,"style":985},"M3 6h18v12H3z M3 7l9 6 9-6","var(--teal-ink,#0b6157)",[65,1020,1024],{"x":1021,"y":1022,"style":1023},"228.8","84.2","font-size:13.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","Validate",[65,1026,1028],{"x":1021,"y":1027,"style":1023},"101.8","emails",[65,1030,730],{"x":1021,"y":1031,"style":995},"121.2",[252,1033],{"x1":1034,"y1":1000,"x2":1035,"y2":1000,"stroke":973,"style":974},"291.4","313.4",[1003,1037],{"points":1038,"fill":1006},"317.4,80.0 308.4,85.0 308.4,75.0",[44,1040],{"x":1041,"y":54,"width":969,"height":970,"rx":971,"fill":972,"stroke":973,"style":974},"322.4",[976,1043,1045],{"transform":1044},"translate(368.0,29.0) scale(1.000)",[980,1046],{"d":1047,"fill":983,"stroke":984,"style":985},"M5 9h14 M4 15h14 M10 4l-2 16 M16 4l-2 16",[65,1049,1051],{"x":231,"y":1050,"style":990},"75.5","Parse",[65,1053,1054],{"x":231,"y":989,"style":990},"phones",[65,1056,1057],{"x":231,"y":994,"style":995},"phonenumbers,",[65,1059,1061],{"x":231,"y":1060,"style":995},"130.0","region GB",[252,1063],{"x1":1064,"y1":1000,"x2":1065,"y2":1000,"stroke":973,"style":974},"442.6","464.6",[1003,1067],{"points":1068,"fill":1006},"468.6,80.0 459.6,85.0 459.6,75.0",[44,1070],{"x":1071,"y":54,"width":969,"height":970,"rx":971,"fill":972,"stroke":973,"style":974},"473.6",[976,1073,1075],{"transform":1074},"translate(519.2,37.8) scale(1.000)",[980,1076],{"d":1077,"fill":983,"stroke":984,"style":985},"M20 11a8 8 0 1 0-2.3 5.7 M20 4v7h-7",[65,1079,1081],{"x":1080,"y":1022,"style":990},"531.2","Normalise",[65,1083,1085],{"x":1080,"y":1084,"style":995},"103.8","domain case,",[65,1087,1088],{"x":1080,"y":1031,"style":995},"E.164",[252,1090],{"x1":1091,"y1":1000,"x2":1092,"y2":1000,"stroke":973,"style":974},"593.8","615.8",[1003,1094],{"points":1095,"fill":1006},"619.8,80.0 610.8,85.0 610.8,75.0",[44,1097],{"x":1098,"y":54,"width":969,"height":970,"rx":971,"fill":149,"stroke":150,"style":974},"624.8",[976,1100,1102],{"transform":1101},"translate(670.4,37.8) scale(1.000)",[980,1103],{"d":1104,"fill":983,"stroke":1105,"style":985},"M12 3.5l9.5 17h-19z M12 10v5 M12 17.8v.4","var(--gold-ink,#7a4e06)",[65,1107,1110],{"x":1108,"y":1022,"style":1109},"682.4","font-size:13.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","Report",[65,1112,1113],{"x":1108,"y":1084,"style":995},"row, column,",[65,1115,1116],{"x":1108,"y":1031,"style":995},"problem",[236,1118,1120],{"id":1119},"step-3-parse-phone-numbers-with-a-default-region","Step 3 — Parse phone numbers with a default region",[10,1122,1123,1124,1127],{},"Phone numbers need a library: the rules for valid lengths and prefixes differ by country and change over time. ",[14,1125,1126],{},"phonenumbers",", a port of Google's libphonenumber, parses national and international formats. A default region tells it how to read numbers written without a country code:",[241,1129,1131],{"className":279,"code":1130,"language":281,"meta":246,"style":246},"import phonenumbers as pn\n\ndef check_phone(value, region=\"GB\"):\n    if pd.isna(value) or not str(value).strip():\n        return None, \"missing\"\n    try:\n        number = pn.parse(str(value), region)\n    except pn.NumberParseException as exc:\n        return None, str(exc)\n    if not pn.is_valid_number(number):\n        return None, \"not a valid number for its region\"\n    return pn.format_number(number, pn.PhoneNumberFormat.E164), None\n\nchecked = df[\"phone\"].map(check_phone)\ndf[\"phone_norm\"] = checked.str[0]\ndf[\"phone_error\"] = checked.str[1]\nprint(df[[\"name\", \"phone_norm\", \"phone_error\"]].values.tolist())\n",[14,1132,1133,1145,1149,1167,1181,1191,1197,1212,1223,1235,1244,1255,1265,1269,1282,1299,1316],{"__ignoreMap":246},[250,1134,1135,1137,1140,1142],{"class":252,"line":253},[250,1136,289],{"class":288},[250,1138,1139],{"class":292}," phonenumbers ",[250,1141,296],{"class":288},[250,1143,1144],{"class":292}," pn\n",[250,1146,1147],{"class":252,"line":302},[250,1148,320],{"emptyLinePlaceholder":319},[250,1150,1151,1153,1156,1159,1161,1164],{"class":252,"line":316},[250,1152,757],{"class":288},[250,1154,1155],{"class":760}," check_phone",[250,1157,1158],{"class":292},"(value, region",[250,1160,329],{"class":288},[250,1162,1163],{"class":260},"\"GB\"",[250,1165,1166],{"class":292},"):\n",[250,1168,1169,1171,1173,1175,1177,1179],{"class":252,"line":323},[250,1170,769],{"class":288},[250,1172,772],{"class":292},[250,1174,775],{"class":288},[250,1176,778],{"class":288},[250,1178,781],{"class":476},[250,1180,784],{"class":292},[250,1182,1183,1185,1187,1189],{"class":252,"line":335},[250,1184,789],{"class":288},[250,1186,792],{"class":476},[250,1188,355],{"class":292},[250,1190,797],{"class":260},[250,1192,1193,1195],{"class":252,"line":346},[250,1194,802],{"class":288},[250,1196,805],{"class":292},[250,1198,1199,1202,1204,1207,1209],{"class":252,"line":369},[250,1200,1201],{"class":292},"        number ",[250,1203,329],{"class":288},[250,1205,1206],{"class":292}," pn.parse(",[250,1208,534],{"class":476},[250,1210,1211],{"class":292},"(value), region)\n",[250,1213,1214,1216,1219,1221],{"class":252,"line":400},[250,1215,845],{"class":288},[250,1217,1218],{"class":292}," pn.NumberParseException ",[250,1220,296],{"class":288},[250,1222,853],{"class":292},[250,1224,1225,1227,1229,1231,1233],{"class":252,"line":421},[250,1226,789],{"class":288},[250,1228,792],{"class":476},[250,1230,355],{"class":292},[250,1232,534],{"class":476},[250,1234,866],{"class":292},[250,1236,1237,1239,1241],{"class":252,"line":441},[250,1238,769],{"class":288},[250,1240,778],{"class":288},[250,1242,1243],{"class":292}," pn.is_valid_number(number):\n",[250,1245,1246,1248,1250,1252],{"class":252,"line":461},[250,1247,789],{"class":288},[250,1249,792],{"class":476},[250,1251,355],{"class":292},[250,1253,1254],{"class":260},"\"not a valid number for its region\"\n",[250,1256,1257,1260,1263],{"class":252,"line":482},[250,1258,1259],{"class":288},"    return",[250,1261,1262],{"class":292}," pn.format_number(number, pn.PhoneNumberFormat.E164), ",[250,1264,840],{"class":476},[250,1266,1267],{"class":252,"line":488},[250,1268,320],{"emptyLinePlaceholder":319},[250,1270,1271,1273,1275,1277,1279],{"class":252,"line":500},[250,1272,875],{"class":292},[250,1274,329],{"class":288},[250,1276,688],{"class":292},[250,1278,363],{"class":260},[250,1280,1281],{"class":292},"].map(check_phone)\n",[250,1283,1284,1286,1289,1291,1293,1295,1297],{"class":252,"line":505},[250,1285,889],{"class":292},[250,1287,1288],{"class":260},"\"phone_norm\"",[250,1290,895],{"class":292},[250,1292,329],{"class":288},[250,1294,900],{"class":292},[250,1296,47],{"class":476},[250,1298,905],{"class":292},[250,1300,1301,1303,1306,1308,1310,1312,1314],{"class":252,"line":548},[250,1302,889],{"class":292},[250,1304,1305],{"class":260},"\"phone_error\"",[250,1307,895],{"class":292},[250,1309,329],{"class":288},[250,1311,900],{"class":292},[250,1313,95],{"class":476},[250,1315,905],{"class":292},[250,1317,1318,1320,1322,1324,1326,1328,1330,1332],{"class":252,"line":572},[250,1319,575],{"class":476},[250,1321,929],{"class":292},[250,1323,352],{"class":260},[250,1325,355],{"class":292},[250,1327,1288],{"class":260},[250,1329,355],{"class":292},[250,1331,1305],{"class":260},[250,1333,938],{"class":292},[10,1335,1336,1337,1340,1341,1343,1344,1347,1348,1351],{},"Ann's \"020 7946 0958\" and Bob's \"+44 20 7946 0958\" both become ",[14,1338,1339],{},"+442079460958",". Eve's ",[14,1342,16],{}," — the number Excel stripped of its leading zero — is parsed as a UK mobile and restored to ",[14,1345,1346],{},"+447911123456",". Dee's \"12345\" is rejected: it is not a valid UK number. If a contact list spans several countries, store the country in its own column and pass it as the region per row. ",[14,1349,1350],{},"pn.number_type(number)"," also tells you whether a number is a mobile, which matters when the list is used for text messages.",[236,1353,1355],{"id":1354},"step-4-find-duplicates-after-normalising","Step 4 — Find duplicates after normalising",[10,1357,1358],{},"Normalised values expose duplicates that formatting hid. Ann and Bob share an office number written two different ways:",[241,1360,1362],{"className":279,"code":1361,"language":281,"meta":246,"style":246},"dupe_phones = df[df[\"phone_norm\"].notna() & df[\"phone_norm\"].duplicated(keep=False)]\nprint(dupe_phones[[\"sheet_row\", \"name\", \"phone\"]].values.tolist())\n# [[2, 'Ann', '020 7946 0958'], [3, 'Bob', '+44 20 7946 0958']]\n",[14,1363,1364,1399,1418],{"__ignoreMap":246},[250,1365,1366,1369,1371,1374,1376,1379,1382,1384,1386,1389,1392,1394,1396],{"class":252,"line":253},[250,1367,1368],{"class":292},"dupe_phones ",[250,1370,329],{"class":288},[250,1372,1373],{"class":292}," df[df[",[250,1375,1288],{"class":260},[250,1377,1378],{"class":292},"].notna() ",[250,1380,1381],{"class":288},"&",[250,1383,688],{"class":292},[250,1385,1288],{"class":260},[250,1387,1388],{"class":292},"].duplicated(",[250,1390,1391],{"class":520},"keep",[250,1393,329],{"class":288},[250,1395,828],{"class":476},[250,1397,1398],{"class":292},")]\n",[250,1400,1401,1403,1406,1408,1410,1412,1414,1416],{"class":252,"line":302},[250,1402,575],{"class":476},[250,1404,1405],{"class":292},"(dupe_phones[[",[250,1407,558],{"class":260},[250,1409,355],{"class":292},[250,1411,352],{"class":260},[250,1413,355],{"class":292},[250,1415,363],{"class":260},[250,1417,938],{"class":292},[250,1419,1420],{"class":252,"line":316},[250,1421,1422],{"class":589},"# [[2, 'Ann', '020 7946 0958'], [3, 'Bob', '+44 20 7946 0958']]\n",[10,1424,1425,1426,1429,1430,1434,1435,1439,1440,1444],{},"A shared switchboard number is legitimate; a shared mobile or email usually means the same person entered twice. Report duplicates for review rather than dropping them. Run the same check on ",[14,1427,1428],{},"email_norm"," — the domain is already lower-cased there, so \"",[19,1431,1433],{"href":1432},"mailto:Ann@EXAMPLE.com","Ann@EXAMPLE.com","\" and \"",[19,1436,1438],{"href":1437},"mailto:Ann@example.com","Ann@example.com","\" match. Duplicate detection more generally is covered in ",[19,1441,1443],{"href":1442},"\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Ffind-duplicate-rows-in-excel-with-python\u002F","Find Duplicate Rows in Excel with Python",".",[236,1446,1448],{"id":1447},"step-5-report-the-invalid-cells","Step 5 — Report the invalid cells",[10,1450,1451],{},"Collect every problem as one row — sheet row, column, the value as typed and the reason — which is the most useful shape for whoever has to fix the source:",[241,1453,1455],{"className":279,"code":1454,"language":281,"meta":246,"style":246},"problems = pd.concat([\n    df.loc[df[\"email_error\"].notna(), [\"sheet_row\", \"email\", \"email_error\"]]\n      .set_axis([\"sheet_row\", \"value\", \"problem\"], axis=1).assign(column=\"email\"),\n    df.loc[df[\"phone_error\"].notna(), [\"sheet_row\", \"phone\", \"phone_error\"]]\n      .set_axis([\"sheet_row\", \"value\", \"problem\"], axis=1).assign(column=\"phone\"),\n]).sort_values(\"sheet_row\")[[\"sheet_row\", \"column\", \"value\", \"problem\"]]\nprint(problems.values.tolist())\nproblems.to_excel(\"contact_problems.xlsx\", index=False)\n",[14,1456,1457,1467,1490,1530,1550,1582,1609,1616],{"__ignoreMap":246},[250,1458,1459,1462,1464],{"class":252,"line":253},[250,1460,1461],{"class":292},"problems ",[250,1463,329],{"class":288},[250,1465,1466],{"class":292}," pd.concat([\n",[250,1468,1469,1472,1474,1477,1479,1481,1483,1485,1487],{"class":252,"line":302},[250,1470,1471],{"class":292},"    df.loc[df[",[250,1473,912],{"class":260},[250,1475,1476],{"class":292},"].notna(), [",[250,1478,558],{"class":260},[250,1480,355],{"class":292},[250,1482,358],{"class":260},[250,1484,355],{"class":292},[250,1486,912],{"class":260},[250,1488,1489],{"class":292},"]]\n",[250,1491,1492,1495,1497,1499,1502,1504,1507,1510,1513,1515,1517,1520,1523,1525,1527],{"class":252,"line":316},[250,1493,1494],{"class":292},"      .set_axis([",[250,1496,558],{"class":260},[250,1498,355],{"class":292},[250,1500,1501],{"class":260},"\"value\"",[250,1503,355],{"class":292},[250,1505,1506],{"class":260},"\"problem\"",[250,1508,1509],{"class":292},"], ",[250,1511,1512],{"class":520},"axis",[250,1514,329],{"class":288},[250,1516,95],{"class":476},[250,1518,1519],{"class":292},").assign(",[250,1521,1522],{"class":520},"column",[250,1524,329],{"class":288},[250,1526,358],{"class":260},[250,1528,1529],{"class":292},"),\n",[250,1531,1532,1534,1536,1538,1540,1542,1544,1546,1548],{"class":252,"line":323},[250,1533,1471],{"class":292},[250,1535,1305],{"class":260},[250,1537,1476],{"class":292},[250,1539,558],{"class":260},[250,1541,355],{"class":292},[250,1543,363],{"class":260},[250,1545,355],{"class":292},[250,1547,1305],{"class":260},[250,1549,1489],{"class":292},[250,1551,1552,1554,1556,1558,1560,1562,1564,1566,1568,1570,1572,1574,1576,1578,1580],{"class":252,"line":335},[250,1553,1494],{"class":292},[250,1555,558],{"class":260},[250,1557,355],{"class":292},[250,1559,1501],{"class":260},[250,1561,355],{"class":292},[250,1563,1506],{"class":260},[250,1565,1509],{"class":292},[250,1567,1512],{"class":520},[250,1569,329],{"class":288},[250,1571,95],{"class":476},[250,1573,1519],{"class":292},[250,1575,1522],{"class":520},[250,1577,329],{"class":288},[250,1579,363],{"class":260},[250,1581,1529],{"class":292},[250,1583,1584,1587,1589,1592,1594,1596,1599,1601,1603,1605,1607],{"class":252,"line":346},[250,1585,1586],{"class":292},"]).sort_values(",[250,1588,558],{"class":260},[250,1590,1591],{"class":292},")[[",[250,1593,558],{"class":260},[250,1595,355],{"class":292},[250,1597,1598],{"class":260},"\"column\"",[250,1600,355],{"class":292},[250,1602,1501],{"class":260},[250,1604,355],{"class":292},[250,1606,1506],{"class":260},[250,1608,1489],{"class":292},[250,1610,1611,1613],{"class":252,"line":369},[250,1612,575],{"class":476},[250,1614,1615],{"class":292},"(problems.values.tolist())\n",[250,1617,1618,1621,1624,1626,1629,1631,1633],{"class":252,"line":400},[250,1619,1620],{"class":292},"problems.to_excel(",[250,1622,1623],{"class":260},"\"contact_problems.xlsx\"",[250,1625,355],{"class":292},[250,1627,1628],{"class":520},"index",[250,1630,329],{"class":288},[250,1632,828],{"class":476},[250,1634,497],{"class":292},[10,1636,1637,1638,1444],{},"Three problems, each pointing at a cell someone can open and correct: rows 4 and 5 for the two bad emails and row 5 for Dee's phone. Colouring those cells in the original workbook makes the fix even faster; see ",[19,1639,1641],{"href":1640},"\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",[25,1643,34,1648,34,1651,34,1654,34,1657,34,1660,34,1665,34,1670,34,1673,34,1676,34,1680,34,1684,34,1686,34,1693,34,1695,34,1700,34,1703,34,1706,34,1708,34,1712,34,1714,34,1716,34,1718,34,1721,34,1723,34,1730,34,1732,34,1737,34,1740,34,1742,34,1744,34,1749,34,1751,34,1756,34,1759,34,1762,34,1764,34,1769,34,1771],{"viewBox":1644,"role":28,"ariaLabelledBy":1645,"xmlns":32,"style":955},"4 0 752 242",[1646,1647],"ev-results-t","ev-results-d",[36,1649,1650],{"id":1646},"The outcome for each contact",[40,1652,1653],{"id":1647},"Ann is valid on both columns. Bob is valid once his email domain is lower-cased and his phone normalised. Cai has an invalid email. Dee has an invalid email and an invalid phone. Eve's email is valid and her phone is valid once the leading zero Excel removed is restored.",[44,1655],{"x":167,"y":47,"width":964,"height":1656,"fill":50},"242",[44,1658],{"x":968,"y":54,"width":1659,"height":89,"rx":47,"fill":50,"stroke":58,"style":59},"54.2",[44,1661],{"x":1662,"y":54,"width":1663,"height":89,"rx":47,"fill":972,"stroke":973,"style":1664},"74.2","332.9","stroke-width:1.2px",[65,1666,106],{"x":1667,"y":1668,"style":1669},"240.6","41.0","font-size:13px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle",[44,1671],{"x":1672,"y":54,"width":1663,"height":89,"rx":47,"fill":972,"stroke":973,"style":1664},"407.1",[65,1674,111],{"x":1675,"y":1668,"style":1669},"573.5",[44,1677],{"x":968,"y":1678,"width":1659,"height":1679,"rx":47,"fill":57,"stroke":58,"style":59},"56.0","34.0",[65,1681,124],{"x":55,"y":1682,"style":1683},"78.0","font-size:12.5px;font-weight:700;fill:var(--text,#172033);text-anchor:start",[44,1685],{"x":1662,"y":1678,"width":1663,"height":1679,"rx":47,"fill":50,"stroke":58,"style":59},[976,1687,1689],{"transform":1688},"translate(231.6,64.0) scale(0.750)",[980,1690],{"d":1691,"fill":983,"stroke":1011,"style":1692},"M5 12.5l4.5 4.5L19 7","stroke-width:2.40px;stroke-linecap:round;stroke-linejoin:round",[44,1694],{"x":1672,"y":1678,"width":1663,"height":1679,"rx":47,"fill":50,"stroke":58,"style":59},[976,1696,1698],{"transform":1697},"translate(564.5,64.0) scale(0.750)",[980,1699],{"d":1691,"fill":983,"stroke":1011,"style":1692},[44,1701],{"x":968,"y":1702,"width":1659,"height":1679,"rx":47,"fill":57,"stroke":58,"style":59},"90.0",[65,1704,146],{"x":55,"y":1705,"style":1683},"112.0",[44,1707],{"x":1662,"y":1702,"width":1663,"height":1679,"rx":47,"fill":50,"stroke":58,"style":59},[65,1709,1711],{"x":1667,"y":1710,"style":995},"111.0","normalised",[44,1713],{"x":1672,"y":1702,"width":1663,"height":1679,"rx":47,"fill":50,"stroke":58,"style":59},[65,1715,1711],{"x":1675,"y":1710,"style":995},[44,1717],{"x":968,"y":163,"width":1659,"height":1679,"rx":47,"fill":57,"stroke":58,"style":59},[65,1719,172],{"x":55,"y":1720,"style":1683},"146.0",[44,1722],{"x":1662,"y":163,"width":1663,"height":1679,"rx":47,"fill":50,"stroke":58,"style":59},[976,1724,1726],{"transform":1725},"translate(232.6,133.0) scale(0.667)",[980,1727],{"d":1728,"fill":983,"stroke":176,"style":1729},"M6 6l12 12 M18 6L6 18","stroke-width:2.70px;stroke-linecap:round;stroke-linejoin:round",[44,1731],{"x":1672,"y":163,"width":1663,"height":1679,"rx":47,"fill":50,"stroke":58,"style":59},[976,1733,1735],{"transform":1734},"translate(564.5,132.0) scale(0.750)",[980,1736],{"d":1691,"fill":983,"stroke":1011,"style":1692},[44,1738],{"x":968,"y":1739,"width":1659,"height":1679,"rx":47,"fill":57,"stroke":58,"style":59},"158.0",[65,1741,197],{"x":55,"y":210,"style":1683},[44,1743],{"x":1662,"y":1739,"width":1663,"height":1679,"rx":47,"fill":50,"stroke":58,"style":59},[976,1745,1747],{"transform":1746},"translate(232.6,167.0) scale(0.667)",[980,1748],{"d":1728,"fill":983,"stroke":176,"style":1729},[44,1750],{"x":1672,"y":1739,"width":1663,"height":1679,"rx":47,"fill":50,"stroke":58,"style":59},[976,1752,1754],{"transform":1753},"translate(565.5,167.0) scale(0.667)",[980,1755],{"d":1728,"fill":983,"stroke":176,"style":1729},[44,1757],{"x":968,"y":1758,"width":1659,"height":1679,"rx":47,"fill":57,"stroke":58,"style":59},"192.0",[65,1760,219],{"x":55,"y":1761,"style":1683},"214.0",[44,1763],{"x":1662,"y":1758,"width":1663,"height":1679,"rx":47,"fill":50,"stroke":58,"style":59},[976,1765,1767],{"transform":1766},"translate(231.6,200.0) scale(0.750)",[980,1768],{"d":1691,"fill":983,"stroke":1011,"style":1692},[44,1770],{"x":1672,"y":1758,"width":1663,"height":1679,"rx":47,"fill":50,"stroke":58,"style":59},[65,1772,1774],{"x":1675,"y":1773,"style":995},"213.0","restored",[236,1776,1778],{"id":1777},"step-6-write-the-normalised-values-back","Step 6 — Write the normalised values back",[10,1780,1781,1782,1784],{},"Once the problems are fixed at the source, the normalised columns are what should travel on. Write them next to the originals so the change is visible, and use a text format for the phone column so Excel never turns ",[14,1783,1346],{}," into a number again:",[241,1786,1788],{"className":279,"code":1787,"language":281,"meta":246,"style":246},"from openpyxl import load_workbook\n\nout = df[[\"sheet_row\", \"name\", \"email\", \"email_norm\", \"phone\", \"phone_norm\"]]\nout.to_excel(\"contacts_checked.xlsx\", index=False)\nwb = load_workbook(\"contacts_checked.xlsx\")\nws = wb.active\nfor (cell,) in ws.iter_rows(min_row=2, min_col=6, max_col=6):\n    cell.number_format = \"@\"\nwb.save(\"contacts_checked.xlsx\")\nprint(pd.read_excel(\"contacts_checked.xlsx\", dtype=str)[\"phone_norm\"].tolist()[:2])\n",[14,1789,1790,1801,1805,1839,1857,1870,1878,1917,1927,1935],{"__ignoreMap":246},[250,1791,1792,1794,1796,1798],{"class":252,"line":253},[250,1793,305],{"class":288},[250,1795,308],{"class":292},[250,1797,289],{"class":288},[250,1799,1800],{"class":292}," load_workbook\n",[250,1802,1803],{"class":252,"line":302},[250,1804,320],{"emptyLinePlaceholder":319},[250,1806,1807,1810,1812,1815,1817,1819,1821,1823,1825,1827,1829,1831,1833,1835,1837],{"class":252,"line":316},[250,1808,1809],{"class":292},"out ",[250,1811,329],{"class":288},[250,1813,1814],{"class":292}," df[[",[250,1816,558],{"class":260},[250,1818,355],{"class":292},[250,1820,352],{"class":260},[250,1822,355],{"class":292},[250,1824,358],{"class":260},[250,1826,355],{"class":292},[250,1828,892],{"class":260},[250,1830,355],{"class":292},[250,1832,363],{"class":260},[250,1834,355],{"class":292},[250,1836,1288],{"class":260},[250,1838,1489],{"class":292},[250,1840,1841,1844,1847,1849,1851,1853,1855],{"class":252,"line":323},[250,1842,1843],{"class":292},"out.to_excel(",[250,1845,1846],{"class":260},"\"contacts_checked.xlsx\"",[250,1848,355],{"class":292},[250,1850,1628],{"class":520},[250,1852,329],{"class":288},[250,1854,828],{"class":476},[250,1856,497],{"class":292},[250,1858,1859,1861,1863,1866,1868],{"class":252,"line":335},[250,1860,326],{"class":292},[250,1862,329],{"class":288},[250,1864,1865],{"class":292}," load_workbook(",[250,1867,1846],{"class":260},[250,1869,497],{"class":292},[250,1871,1872,1874,1876],{"class":252,"line":346},[250,1873,338],{"class":292},[250,1875,329],{"class":288},[250,1877,343],{"class":292},[250,1879,1880,1882,1885,1887,1890,1893,1895,1897,1899,1902,1904,1906,1908,1911,1913,1915],{"class":252,"line":369},[250,1881,372],{"class":288},[250,1883,1884],{"class":292}," (cell,) ",[250,1886,378],{"class":288},[250,1888,1889],{"class":292}," ws.iter_rows(",[250,1891,1892],{"class":520},"min_row",[250,1894,329],{"class":288},[250,1896,118],{"class":476},[250,1898,355],{"class":292},[250,1900,1901],{"class":520},"min_col",[250,1903,329],{"class":288},[250,1905,214],{"class":476},[250,1907,355],{"class":292},[250,1909,1910],{"class":520},"max_col",[250,1912,329],{"class":288},[250,1914,214],{"class":476},[250,1916,1166],{"class":292},[250,1918,1919,1922,1924],{"class":252,"line":400},[250,1920,1921],{"class":292},"    cell.number_format ",[250,1923,329],{"class":288},[250,1925,1926],{"class":260}," \"@\"\n",[250,1928,1929,1931,1933],{"class":252,"line":421},[250,1930,491],{"class":292},[250,1932,1846],{"class":260},[250,1934,497],{"class":292},[250,1936,1937,1939,1942,1944,1946,1948,1950,1952,1955,1957,1960,1962],{"class":252,"line":441},[250,1938,575],{"class":476},[250,1940,1941],{"class":292},"(pd.read_excel(",[250,1943,1846],{"class":260},[250,1945,355],{"class":292},[250,1947,521],{"class":520},[250,1949,329],{"class":288},[250,1951,534],{"class":476},[250,1953,1954],{"class":292},")[",[250,1956,1288],{"class":260},[250,1958,1959],{"class":292},"].tolist()[:",[250,1961,118],{"class":476},[250,1963,366],{"class":292},[10,1965,1966,1967,1969],{},"The ",[14,1968,640],{}," number format marks the cells as text, so a person who later edits a number in Excel keeps the plus sign and leading digits. Keeping the original columns alongside the normalised ones lets reviewers check that normalisation did what they expected before the old values are dropped.",[236,1971,1973],{"id":1972},"common-pitfalls","Common pitfalls",[1975,1976,1977,1987,1993,2006],"ul",{},[1978,1979,1980,1984,1985,1444],"li",{},[1981,1982,1983],"strong",{},"Reading phone numbers as numbers."," Leading zeros and plus signs disappear, and blanks turn the column into floats. Always use ",[14,1986,996],{},[1978,1988,1989,1992],{},[1981,1990,1991],{},"Rejecting valid addresses with a strict regex."," Plus signs, long domain endings and non-ASCII characters are all legal. Use the regex only as a screen.",[1978,1994,1995,1998,1999,2001,2002,2005],{},[1981,1996,1997],{},"Extensions and notes in phone cells."," ",[14,2000,1126],{}," understands \"020 7946 0958 ext. 12\" and keeps the extension in ",[14,2003,2004],{},"number.extension",", but E.164 has no place for it. Store extensions in their own column, and treat free text such as \"call after 5pm\" as a problem to report.",[1978,2007,2008,2011],{},[1981,2009,2010],{},"Validating once."," Formats rot as people edit the sheet. Run the checks on every import, not just the first.",[236,2013,2015],{"id":2014},"conclusion","Conclusion",[10,2017,2018,2019,2021,2022,2024],{},"Read contact columns as text, screen emails with a loose regex, then validate and normalise them with ",[14,2020,730],{}," and phone numbers with ",[14,2023,1126],{}," using a default region. Store emails with lower-cased domains and phones in E.164 so that duplicates become visible, and report every invalid cell with its sheet row, column and reason so the source can be fixed.",[236,2026,2028],{"id":2027},"frequently-asked-questions","Frequently asked questions",[10,2030,2031,2034],{},[1981,2032,2033],{},"Is a regular expression enough to validate email addresses?","\nIt is enough for a quick screen that catches blanks, missing @ signs and spaces. For real validation, email-validator checks the full syntax, including domains and internationalised addresses, and returns a normalised form. Neither proves the mailbox exists.",[10,2036,2037,2040],{},[1981,2038,2039],{},"Why do phone numbers from Excel lose their leading zero?","\nWhen a phone number is typed without spaces, Excel stores it as a number, and 07911123456 becomes 7911123456. Read the column as text with dtype=str and parse it with a default region, which restores the national format correctly.",[10,2042,2043,2046],{},[1981,2044,2045],{},"What is E.164 format?","\nThe international format with a plus sign, country code and national number without spaces or punctuation, such as +447911123456. It is the best form to store and compare phone numbers, because every way of writing the same number normalises to one string.",[10,2048,2049,2052],{},[1981,2050,2051],{},"How do I find duplicate contacts with different formatting?","\nNormalise first — lower-case email domains and E.164 phone numbers — and then look for duplicates on the normalised columns. \"020 7946 0958\" and \"+44 20 7946 0958\" are the same number once normalised.",[236,2054,2056],{"id":2055},"related","Related",[1975,2058,2059,2065,2072,2079],{},[1978,2060,2061,2062,2064],{},"Up one level: ",[19,2063,22],{"href":21}," — checks before data moves on.",[1978,2066,2067,2071],{},[19,2068,2070],{"href":2069},"\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fvalidate-excel-data-with-pandera-schemas\u002F","Validate Excel Data with Pandera Schemas"," — declaring rules for whole sheets.",[1978,2073,2074,2078],{},[19,2075,2077],{"href":2076},"\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"," — structural checks first.",[1978,2080,2081,2085],{},[19,2082,2084],{"href":2083},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcleaning-excel-data-with-pandas\u002Fremove-non-printable-characters-from-excel-data\u002F","Remove Non-Printable Characters from Excel Data with Python"," — invisible characters that break validation.",[2087,2088,2089],"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 .s-wDw, html code.shiki .s-wDw{--shiki-default:#6A737D;--shiki-dark:#BDC4CC}html pre.shiki code .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}html pre.shiki code .s_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":246,"searchDepth":302,"depth":302,"links":2091},[2092,2093,2094,2095,2096,2097,2098,2099,2100,2101,2102],{"id":238,"depth":302,"text":239},{"id":607,"depth":302,"text":608},{"id":724,"depth":302,"text":725},{"id":1119,"depth":302,"text":1120},{"id":1354,"depth":302,"text":1355},{"id":1447,"depth":302,"text":1448},{"id":1777,"depth":302,"text":1778},{"id":1972,"depth":302,"text":1973},{"id":2014,"depth":302,"text":2015},{"id":2027,"depth":302,"text":2028},{"id":2055,"depth":302,"text":2056},"2026-09-23","Check the email addresses and phone numbers in a contact sheet before importing it — a quick regex screen, email-validator for real address syntax, phonenumbers for national and international formats, normalised values for deduplication, and a report of invalid cells with their spreadsheet row numbers.","md",[2107,2109,2111,2113],{"q":2033,"a":2108},"It is enough for a quick screen that catches blanks, missing @ signs and spaces. For real validation, email-validator checks the full syntax, including domains and internationalised addresses, and returns a normalised form. Neither proves the mailbox exists.",{"q":2039,"a":2110},"When a phone number is typed without spaces, Excel stores it as a number, and 07911123456 becomes 7911123456. Read the column as text with dtype=str and parse it with a default region, which restores the national format correctly.",{"q":2045,"a":2112},"The international format with a plus sign, country code and national number without spaces or punctuation, such as +447911123456. It is the best form to store and compare phone numbers, because every way of writing the same number normalises to one string.",{"q":2051,"a":2114},"Normalise first — lower-case email domains and E.164 phone numbers — and then look for duplicates on the normalised columns. \"020 7946 0958\" and \"+44 20 7946 0958\" are the same number once normalised.","validate email and phone formats in excel data with python",null,{"breadcrumb":2118},[2119,2122,2125],{"name":2120,"item":2121},"Home","\u002F",{"name":2123,"item":2124},"Advanced Data Transformation and Cleaning for Excel Automation","\u002Fadvanced-data-transformation-and-cleaning\u002F",{"name":22,"item":21},"\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fvalidate-email-and-phone-formats-in-excel-data",{"title":5,"description":2128},"Validate emails and phone numbers from Excel with pandas, email-validator and phonenumbers — keep phones as text, parse national and +44 formats, normalise to E.164, detect duplicates after normalisation and report invalid rows.","validate-email-and-phone-formats-in-excel-data","advanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fvalidate-email-and-phone-formats-in-excel-data\u002Findex",[2132,2133,2134],"data-validation","data-cleaning","pandas","how-to","nxqe2UszQwbbfpdwF8Du7LL_cILh0peBF1PiBSp23q4",[2138,2142],{"title":2139,"path":2140,"stem":2141,"children":-1},"Validate Dates Fall Within an Expected Period","\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fvalidate-dates-fall-within-an-expected-period","advanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fvalidate-dates-fall-within-an-expected-period\u002Findex",{"title":2077,"path":2143,"stem":2144,"children":-1},"\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fvalidate-excel-columns-before-import-with-pandas","advanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fvalidate-excel-columns-before-import-with-pandas\u002Findex",[2146,2148],{"path":2147,"title":2123},"\u002Fadvanced-data-transformation-and-cleaning",{"path":2149,"title":22},"\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python",[],[2152,2155,2158,2161,2164,2167],{"path":2153,"title":2154,"kind":2135},"\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":2156,"title":2157,"kind":2135},"\u002Fadvanced-data-transformation-and-cleaning\u002Fapplying-conditional-formatting-with-openpyxl\u002Fhighlight-cells-containing-text-with-openpyxl","Highlight Cells Containing Specific Text with openpyxl",{"path":2159,"title":2160,"kind":2135},"\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":2162,"title":2163,"kind":2135},"\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":2165,"title":2166,"kind":2135},"\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":2168,"title":2169,"kind":2135},"\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",1790972100879]