[{"data":1,"prerenderedAt":1809},["ShallowReactive",2],{"doc:\u002Fadvanced-data-transformation-and-cleaning\u002Fapplying-conditional-formatting-with-openpyxl\u002Fhighlight-blank-cells-with-openpyxl":3,"surround:\u002Fadvanced-data-transformation-and-cleaning\u002Fapplying-conditional-formatting-with-openpyxl\u002Fhighlight-blank-cells-with-openpyxl":1775,"ancestors:\u002Fadvanced-data-transformation-and-cleaning\u002Fapplying-conditional-formatting-with-openpyxl\u002Fhighlight-blank-cells-with-openpyxl":1784,"children:\u002Fadvanced-data-transformation-and-cleaning\u002Fapplying-conditional-formatting-with-openpyxl\u002Fhighlight-blank-cells-with-openpyxl":1789,"related:\u002Fadvanced-data-transformation-and-cleaning\u002Fapplying-conditional-formatting-with-openpyxl\u002Fhighlight-blank-cells-with-openpyxl":1790},{"id":4,"title":5,"body":6,"dateModified":1740,"datePublished":1740,"description":1741,"extension":1742,"faq":1743,"keyword":1752,"kind":1753,"meta":1754,"navigation":347,"path":1763,"seo":1764,"slug":1767,"stem":1768,"topics":1769,"type":1773,"__hash__":1774},"docs\u002Fadvanced-data-transformation-and-cleaning\u002Fapplying-conditional-formatting-with-openpyxl\u002Fhighlight-blank-cells-with-openpyxl\u002Findex.md","Highlight Blank Cells in Excel with openpyxl",{"type":7,"value":8,"toc":1725},"minimark",[9,19,239,244,270,280,381,385,388,547,560,564,571,672,675,679,682,753,763,767,774,854,860,988,992,995,1183,1191,1195,1211,1403,1406,1410,1413,1573,1581,1585,1624,1628,1634,1638,1660,1664,1670,1676,1682,1688,1692,1721],[10,11,12,13,18],"p",{},"A contact list, an order import or a timesheet is only as good as its required fields, and a missing email in row 412 is easy to miss in a sheet of a thousand rows. Highlighting blanks makes the gaps visible to whoever fills the sheet in, and it keeps working as they fix them. The rule looks trivial, but \"blank\" has three meanings in a spreadsheet — no content, only spaces, and a formula that returns nothing — and the obvious rule also lights up every empty row below the data. This page, part of ",[14,15,17],"a",{"href":16},"\u002Fadvanced-data-transformation-and-cleaning\u002Fapplying-conditional-formatting-with-openpyxl\u002F","Applying Conditional Formatting With openpyxl",", writes rules that catch the right cells; each result was checked by rendering the workbook with LibreOffice.",[20,21,29,30,29,34,29,38,29,46,29,53,29,62,29,66,29,72,29,76,29,80,29,84,29,88,29,92,29,96,29,100,29,105,29,107,29,111,29,113,29,116,29,118,29,121,29,123,29,126,29,129,29,133,29,135,29,138,29,140,29,143,29,145,29,148,29,150,29,153,29,156,29,160,29,162,29,164,29,169,29,171,29,174,29,176,29,178,29,181,29,185,29,187,29,189,29,191,29,195,29,197,29,199,29,202,29,205,29,209,29,211,29,213,29,215,29,218,29,220,29,222,29,224,29,227,29,231,29,233,29,235,29,237],"svg",{"viewBox":22,"role":23,"ariaLabelledBy":24,"xmlns":27,"style":28},"162 0 436 256","img",[25,26],"blk-result-t","blk-result-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:436px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[31,32,33],"title",{"id":25},"Missing contact details highlighted only inside the data",[35,36,37],"desc",{"id":26},"A contacts sheet with four records. The email is missing for id 2 and contains only spaces for id 3, the phone is missing for ids 3 and 4, and the region code formula returns an empty string for those rows. All of these are highlighted. The empty rows below the data are not.",[39,40],"rect",{"x":41,"y":42,"width":43,"height":44,"fill":45},"162","0","436","256","#ffffff",[47,48,52],"text",{"x":49,"y":50,"style":51},"380.0","31.0","font-size:14px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","AND(LEN(TRIM(B2))=0, $A2\u003C>\"\") on B2:D200",[39,54],{"x":55,"y":56,"width":57,"height":58,"rx":42,"fill":59,"stroke":60,"style":61},"178.5","48.0","32.0","24.0","#e7ebef","var(--line,#cdd5e6)","stroke-width:1px",[39,63],{"x":64,"y":56,"width":65,"height":58,"rx":42,"fill":59,"stroke":60,"style":61},"210.5","44.0",[47,67,71],{"x":68,"y":69,"style":70},"232.5","65.0","font-size:11px;font-weight:600;fill:var(--muted,#5b6780);text-anchor:middle","A",[39,73],{"x":74,"y":56,"width":75,"height":58,"rx":42,"fill":59,"stroke":60,"style":61},"254.5","143.2",[47,77,79],{"x":78,"y":69,"style":70},"326.0","B",[39,81],{"x":82,"y":56,"width":83,"height":58,"rx":42,"fill":59,"stroke":60,"style":61},"397.6","83.8",[47,85,87],{"x":86,"y":69,"style":70},"439.5","C",[39,89],{"x":90,"y":56,"width":91,"height":58,"rx":42,"fill":59,"stroke":60,"style":61},"481.4","100.2",[47,93,95],{"x":94,"y":69,"style":70},"531.5","D",[39,97],{"x":55,"y":98,"width":57,"height":99,"rx":42,"fill":59,"stroke":60,"style":61},"72.0","28.0",[47,101,104],{"x":102,"y":103,"style":70},"194.5","91.0","1",[39,106],{"x":64,"y":98,"width":65,"height":99,"rx":42,"fill":45,"stroke":60,"style":61},[47,108,110],{"x":68,"y":103,"style":109},"font-size:12px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","id",[39,112],{"x":74,"y":98,"width":75,"height":99,"rx":42,"fill":45,"stroke":60,"style":61},[47,114,115],{"x":78,"y":103,"style":109},"email",[39,117],{"x":82,"y":98,"width":83,"height":99,"rx":42,"fill":45,"stroke":60,"style":61},[47,119,120],{"x":86,"y":103,"style":109},"phone",[39,122],{"x":90,"y":98,"width":91,"height":99,"rx":42,"fill":45,"stroke":60,"style":61},[47,124,125],{"x":94,"y":103,"style":109},"region_code",[39,127],{"x":55,"y":128,"width":57,"height":99,"rx":42,"fill":59,"stroke":60,"style":61},"100.0",[47,130,132],{"x":102,"y":131,"style":70},"119.0","2",[39,134],{"x":64,"y":128,"width":65,"height":99,"rx":42,"fill":45,"stroke":60,"style":61},[47,136,104],{"x":68,"y":131,"style":137},"font-size:12px;font-weight:400;fill:var(--text,#172033);text-anchor:middle",[39,139],{"x":74,"y":128,"width":75,"height":99,"rx":42,"fill":45,"stroke":60,"style":61},[47,141,142],{"x":78,"y":131,"style":137},"ada@example.com",[39,144],{"x":82,"y":128,"width":83,"height":99,"rx":42,"fill":45,"stroke":60,"style":61},[47,146,147],{"x":86,"y":131,"style":137},"555-0101",[39,149],{"x":90,"y":128,"width":91,"height":99,"rx":42,"fill":45,"stroke":60,"style":61},[47,151,152],{"x":94,"y":131,"style":137},"555",[39,154],{"x":55,"y":155,"width":57,"height":99,"rx":42,"fill":59,"stroke":60,"style":61},"128.0",[47,157,159],{"x":102,"y":158,"style":70},"147.0","3",[39,161],{"x":64,"y":155,"width":65,"height":99,"rx":42,"fill":45,"stroke":60,"style":61},[47,163,132],{"x":68,"y":158,"style":137},[39,165],{"x":74,"y":155,"width":75,"height":99,"rx":42,"fill":166,"stroke":167,"style":168},"#fdefd8","var(--gold,#b4740a)","stroke-width:1.5px",[39,170],{"x":82,"y":155,"width":83,"height":99,"rx":42,"fill":45,"stroke":60,"style":61},[47,172,173],{"x":86,"y":158,"style":137},"555-0102",[39,175],{"x":90,"y":155,"width":91,"height":99,"rx":42,"fill":45,"stroke":60,"style":61},[47,177,152],{"x":94,"y":158,"style":137},[39,179],{"x":55,"y":180,"width":57,"height":99,"rx":42,"fill":59,"stroke":60,"style":61},"156.0",[47,182,184],{"x":102,"y":183,"style":70},"175.0","4",[39,186],{"x":64,"y":180,"width":65,"height":99,"rx":42,"fill":45,"stroke":60,"style":61},[47,188,159],{"x":68,"y":183,"style":137},[39,190],{"x":74,"y":180,"width":75,"height":99,"rx":42,"fill":166,"stroke":167,"style":168},[47,192,194],{"x":78,"y":183,"style":193},"font-size:12px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","(spaces)",[39,196],{"x":82,"y":180,"width":83,"height":99,"rx":42,"fill":166,"stroke":167,"style":168},[39,198],{"x":90,"y":180,"width":91,"height":99,"rx":42,"fill":166,"stroke":167,"style":168},[47,200,201],{"x":94,"y":183,"style":193},"=\"\"",[39,203],{"x":55,"y":204,"width":57,"height":99,"rx":42,"fill":59,"stroke":60,"style":61},"184.0",[47,206,208],{"x":102,"y":207,"style":70},"203.0","5",[39,210],{"x":64,"y":204,"width":65,"height":99,"rx":42,"fill":45,"stroke":60,"style":61},[47,212,184],{"x":68,"y":207,"style":137},[39,214],{"x":74,"y":204,"width":75,"height":99,"rx":42,"fill":45,"stroke":60,"style":61},[47,216,217],{"x":78,"y":207,"style":137},"dev@example.com",[39,219],{"x":82,"y":204,"width":83,"height":99,"rx":42,"fill":166,"stroke":167,"style":168},[39,221],{"x":90,"y":204,"width":91,"height":99,"rx":42,"fill":166,"stroke":167,"style":168},[47,223,201],{"x":94,"y":207,"style":193},[39,225],{"x":55,"y":226,"width":57,"height":99,"rx":42,"fill":59,"stroke":60,"style":61},"212.0",[47,228,230],{"x":102,"y":229,"style":70},"231.0","6",[39,232],{"x":64,"y":226,"width":65,"height":99,"rx":42,"fill":45,"stroke":60,"style":61},[39,234],{"x":74,"y":226,"width":75,"height":99,"rx":42,"fill":45,"stroke":60,"style":61},[39,236],{"x":82,"y":226,"width":83,"height":99,"rx":42,"fill":45,"stroke":60,"style":61},[39,238],{"x":90,"y":226,"width":91,"height":99,"rx":42,"fill":45,"stroke":60,"style":61},[240,241,243],"h2",{"id":242},"prerequisites","Prerequisites",[245,246,251],"pre",{"className":247,"code":248,"language":249,"meta":250,"style":250},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install openpyxl\n","bash","",[252,253,254],"code",{"__ignoreMap":250},[255,256,259,263,267],"span",{"class":257,"line":258},"line",1,[255,260,262],{"class":261},"sMTad","pip",[255,264,266],{"class":265},"srMev"," install",[255,268,269],{"class":265}," openpyxl\n",[10,271,272,275,276,279],{},[252,273,274],{},"contacts.xlsx"," has four contacts: one complete, one with no email, one whose email is three spaces and whose phone is missing, and one with an empty-string phone. Column D is a formula that returns ",[252,277,278],{},"\"\""," when there is no phone. A note in F7 marks the end of the used area so there are empty rows below the data.",[245,281,285],{"className":282,"code":283,"language":284,"meta":250,"style":250},"language-python shiki shiki-themes github-light github-dark-high-contrast","from openpyxl import load_workbook\nfrom openpyxl.formatting.rule import Rule, FormulaRule\nfrom openpyxl.styles import PatternFill\nfrom openpyxl.styles.differential import DifferentialStyle\n\nAMBER = PatternFill(\"solid\", bgColor=\"FFEB9C\")\n","python",[252,286,287,303,316,329,342,349],{"__ignoreMap":250},[255,288,289,293,297,300],{"class":257,"line":258},[255,290,292],{"class":291},"s-kum","from",[255,294,296],{"class":295},"skGVy"," openpyxl ",[255,298,299],{"class":291},"import",[255,301,302],{"class":295}," load_workbook\n",[255,304,306,308,311,313],{"class":257,"line":305},2,[255,307,292],{"class":291},[255,309,310],{"class":295}," openpyxl.formatting.rule ",[255,312,299],{"class":291},[255,314,315],{"class":295}," Rule, FormulaRule\n",[255,317,319,321,324,326],{"class":257,"line":318},3,[255,320,292],{"class":291},[255,322,323],{"class":295}," openpyxl.styles ",[255,325,299],{"class":291},[255,327,328],{"class":295}," PatternFill\n",[255,330,332,334,337,339],{"class":257,"line":331},4,[255,333,292],{"class":291},[255,335,336],{"class":295}," openpyxl.styles.differential ",[255,338,299],{"class":291},[255,340,341],{"class":295}," DifferentialStyle\n",[255,343,345],{"class":257,"line":344},5,[255,346,348],{"emptyLinePlaceholder":347},true,"\n",[255,350,352,356,359,362,365,368,372,375,378],{"class":257,"line":351},6,[255,353,355],{"class":354},"sP0c6","AMBER",[255,357,358],{"class":291}," =",[255,360,361],{"class":295}," PatternFill(",[255,363,364],{"class":265},"\"solid\"",[255,366,367],{"class":295},", ",[255,369,371],{"class":370},"sa561","bgColor",[255,373,374],{"class":291},"=",[255,376,377],{"class":265},"\"FFEB9C\"",[255,379,380],{"class":295},")\n",[240,382,384],{"id":383},"step-1-know-which-cells-count-as-blank","Step 1 — Know which cells count as blank",[10,386,387],{},"The two common tests give different answers on the same cells. Rendering the sheet with each rule applied to B2:D7 showed:",[20,389,29,395,29,398,29,401,29,405,29,411,29,418,29,424,29,427,29,431,29,435,29,440,29,442,29,453,29,455,29,460,29,463,29,467,29,469,29,477,29,479,29,484,29,487,29,491,29,493,29,498,29,500,29,505,29,508,29,512,29,514,29,519,29,521,29,526,29,529,29,533,29,535,29,540,29,542],{"viewBox":390,"role":23,"ariaLabelledBy":391,"xmlns":27,"style":394},"4 0 752 242",[392,393],"blk-tests-t","blk-tests-d","width:100%;max-width:752px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[31,396,397],{"id":392},"What ISBLANK and LEN(TRIM()) treat as blank",[35,399,400],{"id":393},"A comparison of two tests. A cell with no content is blank to both. A cell holding only spaces is blank to LEN(TRIM()) but not to ISBLANK. A formula returning an empty string is blank to LEN(TRIM()) but not to ISBLANK. A cell holding real text is blank to neither. Empty rows below the data are blank to both.",[39,402],{"x":184,"y":42,"width":403,"height":404,"fill":45},"752","242",[39,406],{"x":407,"y":408,"width":409,"height":410,"rx":42,"fill":45,"stroke":60,"style":61},"20.0","16.0","201.2","40.0",[39,412],{"x":413,"y":408,"width":414,"height":410,"rx":42,"fill":415,"stroke":416,"style":417},"221.2","259.4","#f0f4ff","var(--brand,#5b5cf0)","stroke-width:1.2px",[47,419,423],{"x":420,"y":421,"style":422},"350.9","41.0","font-size:13px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","ISBLANK(B2)",[39,425],{"x":426,"y":408,"width":414,"height":410,"rx":42,"fill":415,"stroke":416,"style":417},"480.6",[47,428,430],{"x":429,"y":421,"style":422},"610.3","LEN(TRIM(B2))=0",[39,432],{"x":407,"y":433,"width":409,"height":434,"rx":42,"fill":59,"stroke":60,"style":61},"56.0","34.0",[47,436,439],{"x":57,"y":437,"style":438},"78.0","font-size:12.5px;font-weight:700;fill:var(--text,#172033);text-anchor:start","No content",[39,441],{"x":413,"y":433,"width":414,"height":434,"rx":42,"fill":45,"stroke":60,"style":61},[443,444,446],"g",{"transform":445},"translate(341.9,64.0) scale(0.750)",[447,448],"path",{"d":449,"fill":450,"stroke":451,"style":452},"M5 12.5l4.5 4.5L19 7","none","var(--teal,#0f9488)","stroke-width:2.40px;stroke-linecap:round;stroke-linejoin:round",[39,454],{"x":426,"y":433,"width":414,"height":434,"rx":42,"fill":45,"stroke":60,"style":61},[443,456,458],{"transform":457},"translate(601.3,64.0) scale(0.750)",[447,459],{"d":449,"fill":450,"stroke":451,"style":452},[39,461],{"x":407,"y":462,"width":409,"height":434,"rx":42,"fill":59,"stroke":60,"style":61},"90.0",[47,464,466],{"x":57,"y":465,"style":438},"112.0","Only spaces \"   \"",[39,468],{"x":413,"y":462,"width":414,"height":434,"rx":42,"fill":45,"stroke":60,"style":61},[443,470,472],{"transform":471},"translate(342.9,99.0) scale(0.667)",[447,473],{"d":474,"fill":450,"stroke":475,"style":476},"M6 6l12 12 M18 6L6 18","var(--accent,#f43f8f)","stroke-width:2.70px;stroke-linecap:round;stroke-linejoin:round",[39,478],{"x":426,"y":462,"width":414,"height":434,"rx":42,"fill":45,"stroke":60,"style":61},[443,480,482],{"transform":481},"translate(601.3,98.0) scale(0.750)",[447,483],{"d":449,"fill":450,"stroke":451,"style":452},[39,485],{"x":407,"y":486,"width":409,"height":434,"rx":42,"fill":59,"stroke":60,"style":61},"124.0",[47,488,490],{"x":57,"y":489,"style":438},"146.0","Formula returning \"\"",[39,492],{"x":413,"y":486,"width":414,"height":434,"rx":42,"fill":45,"stroke":60,"style":61},[443,494,496],{"transform":495},"translate(342.9,133.0) scale(0.667)",[447,497],{"d":474,"fill":450,"stroke":475,"style":476},[39,499],{"x":426,"y":486,"width":414,"height":434,"rx":42,"fill":45,"stroke":60,"style":61},[443,501,503],{"transform":502},"translate(601.3,132.0) scale(0.750)",[447,504],{"d":449,"fill":450,"stroke":451,"style":452},[39,506],{"x":407,"y":507,"width":409,"height":434,"rx":42,"fill":59,"stroke":60,"style":61},"158.0",[47,509,511],{"x":57,"y":510,"style":438},"180.0","Real value",[39,513],{"x":413,"y":507,"width":414,"height":434,"rx":42,"fill":45,"stroke":60,"style":61},[443,515,517],{"transform":516},"translate(342.9,167.0) scale(0.667)",[447,518],{"d":474,"fill":450,"stroke":475,"style":476},[39,520],{"x":426,"y":507,"width":414,"height":434,"rx":42,"fill":45,"stroke":60,"style":61},[443,522,524],{"transform":523},"translate(602.3,167.0) scale(0.667)",[447,525],{"d":474,"fill":450,"stroke":475,"style":476},[39,527],{"x":407,"y":528,"width":409,"height":434,"rx":42,"fill":59,"stroke":60,"style":61},"192.0",[47,530,532],{"x":57,"y":531,"style":438},"214.0","Empty rows below the data",[39,534],{"x":413,"y":528,"width":414,"height":434,"rx":42,"fill":45,"stroke":60,"style":61},[443,536,538],{"transform":537},"translate(341.9,200.0) scale(0.750)",[447,539],{"d":449,"fill":450,"stroke":451,"style":452},[39,541],{"x":426,"y":528,"width":414,"height":434,"rx":42,"fill":45,"stroke":60,"style":61},[443,543,545],{"transform":544},"translate(601.3,200.0) scale(0.750)",[447,546],{"d":449,"fill":450,"stroke":451,"style":452},[10,548,549,550,552,553,555,556,559],{},"For required-field checks, ",[252,551,430],{}," is almost always the right test: a person who typed a space into the email cell has not provided an email, and a lookup formula that returns ",[252,554,278],{}," has not found anything. Use ",[252,557,558],{},"ISBLANK"," only when you specifically want cells that have never been touched.",[240,561,563],{"id":562},"step-2-write-a-containsblanks-rule","Step 2 — Write a containsBlanks rule",[10,565,566,567,570],{},"Excel's \"Format only cells that contain Blanks\" is stored as a ",[252,568,569],{},"containsBlanks"," rule. Like the other text-style rules, it needs a formula to evaluate:",[245,572,574],{"className":282,"code":573,"language":284,"meta":250,"style":250},"wb = load_workbook(\"contacts.xlsx\")\nws = wb[\"Contacts\"]\nws.conditional_formatting.add(\"B2:D7\", Rule(\n    type=\"containsBlanks\", formula=[\"LEN(TRIM(B2))=0\"], dxf=DifferentialStyle(fill=AMBER)))\nwb.save(\"blanks_basic.xlsx\")\n",[252,575,576,591,607,618,662],{"__ignoreMap":250},[255,577,578,581,583,586,589],{"class":257,"line":258},[255,579,580],{"class":295},"wb ",[255,582,374],{"class":291},[255,584,585],{"class":295}," load_workbook(",[255,587,588],{"class":265},"\"contacts.xlsx\"",[255,590,380],{"class":295},[255,592,593,596,598,601,604],{"class":257,"line":305},[255,594,595],{"class":295},"ws ",[255,597,374],{"class":291},[255,599,600],{"class":295}," wb[",[255,602,603],{"class":265},"\"Contacts\"",[255,605,606],{"class":295},"]\n",[255,608,609,612,615],{"class":257,"line":318},[255,610,611],{"class":295},"ws.conditional_formatting.add(",[255,613,614],{"class":265},"\"B2:D7\"",[255,616,617],{"class":295},", Rule(\n",[255,619,620,623,625,628,630,633,635,638,641,644,647,649,652,655,657,659],{"class":257,"line":331},[255,621,622],{"class":370},"    type",[255,624,374],{"class":291},[255,626,627],{"class":265},"\"containsBlanks\"",[255,629,367],{"class":295},[255,631,632],{"class":370},"formula",[255,634,374],{"class":291},[255,636,637],{"class":295},"[",[255,639,640],{"class":265},"\"LEN(TRIM(B2))=0\"",[255,642,643],{"class":295},"], ",[255,645,646],{"class":370},"dxf",[255,648,374],{"class":291},[255,650,651],{"class":295},"DifferentialStyle(",[255,653,654],{"class":370},"fill",[255,656,374],{"class":291},[255,658,355],{"class":354},[255,660,661],{"class":295},")))\n",[255,663,664,667,670],{"class":257,"line":344},[255,665,666],{"class":295},"wb.save(",[255,668,669],{"class":265},"\"blanks_basic.xlsx\"",[255,671,380],{"class":295},[10,673,674],{},"The rule marks the missing email, the spaces-only email, the missing and empty-string phones, and the empty region codes. It also marks rows 6 and 7 from B to D, which are empty because there is no data there at all. On a sheet formatted down to row 1,000 for future entries, that is a solid block of amber below the table — technically correct, and useless.",[240,676,678],{"id":677},"step-3-only-check-rows-that-contain-data","Step 3 — Only check rows that contain data",[10,680,681],{},"Tie the test to a column that is always filled in a real record — the ID, a date, a name — so rows without one are skipped:",[245,683,685],{"className":282,"code":684,"language":284,"meta":250,"style":250},"wb = load_workbook(\"contacts.xlsx\")\nws = wb[\"Contacts\"]\nws.conditional_formatting.add(\"B2:D1000\", FormulaRule(\n    formula=['AND(LEN(TRIM(B2))=0,$A2\u003C>\"\")'], fill=AMBER))\nwb.save(\"blanks_in_data.xlsx\")\n",[252,686,687,699,711,721,744],{"__ignoreMap":250},[255,688,689,691,693,695,697],{"class":257,"line":258},[255,690,580],{"class":295},[255,692,374],{"class":291},[255,694,585],{"class":295},[255,696,588],{"class":265},[255,698,380],{"class":295},[255,700,701,703,705,707,709],{"class":257,"line":305},[255,702,595],{"class":295},[255,704,374],{"class":291},[255,706,600],{"class":295},[255,708,603],{"class":265},[255,710,606],{"class":295},[255,712,713,715,718],{"class":257,"line":318},[255,714,611],{"class":295},[255,716,717],{"class":265},"\"B2:D1000\"",[255,719,720],{"class":295},", FormulaRule(\n",[255,722,723,726,728,730,733,735,737,739,741],{"class":257,"line":331},[255,724,725],{"class":370},"    formula",[255,727,374],{"class":291},[255,729,637],{"class":295},[255,731,732],{"class":265},"'AND(LEN(TRIM(B2))=0,$A2\u003C>\"\")'",[255,734,643],{"class":295},[255,736,654],{"class":370},[255,738,374],{"class":291},[255,740,355],{"class":354},[255,742,743],{"class":295},"))\n",[255,745,746,748,751],{"class":257,"line":344},[255,747,666],{"class":295},[255,749,750],{"class":265},"\"blanks_in_data.xlsx\"",[255,752,380],{"class":295},[10,754,755,756,759,760,762],{},"The ",[252,757,758],{},"$"," before ",[252,761,71],{}," makes every cell in a row look at that row's ID; the rule can then cover a generous range, down to row 1,000, and new records typed at the bottom are checked automatically without colouring the empty rows in between. Rendered, only the six cells inside the four records are highlighted.",[240,764,766],{"id":765},"step-4-highlight-the-whole-row-when-a-required-field-is-missing","Step 4 — Highlight the whole row when a required field is missing",[10,768,769,770,773],{},"For a data-entry sheet, colouring the whole record is often clearer than colouring one cell. Apply the rule to the full row range and test the required columns with ",[252,771,772],{},"COUNTBLANK",":",[245,775,777],{"className":282,"code":776,"language":284,"meta":250,"style":250},"wb = load_workbook(\"contacts.xlsx\")\nws = wb[\"Contacts\"]\nws.conditional_formatting.add(\"A2:D1000\", FormulaRule(\n    formula=['AND($A2\u003C>\"\",COUNTBLANK($B2:$C2)>0)'], fill=PatternFill(\"solid\", bgColor=\"FCE4D6\")))\nwb.save(\"rows_with_gaps.xlsx\")\n",[252,778,779,791,803,812,845],{"__ignoreMap":250},[255,780,781,783,785,787,789],{"class":257,"line":258},[255,782,580],{"class":295},[255,784,374],{"class":291},[255,786,585],{"class":295},[255,788,588],{"class":265},[255,790,380],{"class":295},[255,792,793,795,797,799,801],{"class":257,"line":305},[255,794,595],{"class":295},[255,796,374],{"class":291},[255,798,600],{"class":295},[255,800,603],{"class":265},[255,802,606],{"class":295},[255,804,805,807,810],{"class":257,"line":318},[255,806,611],{"class":295},[255,808,809],{"class":265},"\"A2:D1000\"",[255,811,720],{"class":295},[255,813,814,816,818,820,823,825,827,829,832,834,836,838,840,843],{"class":257,"line":331},[255,815,725],{"class":370},[255,817,374],{"class":291},[255,819,637],{"class":295},[255,821,822],{"class":265},"'AND($A2\u003C>\"\",COUNTBLANK($B2:$C2)>0)'",[255,824,643],{"class":295},[255,826,654],{"class":370},[255,828,374],{"class":291},[255,830,831],{"class":295},"PatternFill(",[255,833,364],{"class":265},[255,835,367],{"class":295},[255,837,371],{"class":370},[255,839,374],{"class":291},[255,841,842],{"class":265},"\"FCE4D6\"",[255,844,661],{"class":295},[255,846,847,849,852],{"class":257,"line":344},[255,848,666],{"class":295},[255,850,851],{"class":265},"\"rows_with_gaps.xlsx\"",[255,853,380],{"class":295},[10,855,856,857,859],{},"Records 2, 3 and 4 are coloured from A to D; record 1 is complete and stays plain. ",[252,858,772],{}," counts empty cells and cells holding an empty string. Which columns count as required is a business decision: here email and phone, but not the derived region code, which is blank whenever the phone is and would only double-count.",[20,861,29,866,29,869,29,872,29,875,29,878,29,880,29,882,29,884,29,886,29,888,29,890,29,892,29,894,29,896,29,898,29,900,29,902,29,904,29,906,29,908,29,910,29,912,29,914,29,916,29,918,29,920,29,922,29,924,29,926,29,928,29,930,29,932,29,934,29,936,29,938,29,940,29,943,29,946,29,948,29,950,29,952,29,954,29,956,29,958,29,960,29,962,29,964,29,966,29,968,29,970,29,972,29,974,29,976,29,978,29,980,29,982,29,984,29,986],{"viewBox":862,"role":23,"ariaLabelledBy":863,"xmlns":27,"style":28},"162 0 436 228",[864,865],"blk-rows-t","blk-rows-d",[31,867,868],{"id":864},"Whole records marked when email or phone is missing",[35,870,871],{"id":865},"The contacts sheet with a row rule. Records 2, 3 and 4 are shaded across all four columns because email or phone is missing. Record 1 has both and is not shaded. The empty rows below are not shaded because they have no id.",[39,873],{"x":41,"y":42,"width":43,"height":874,"fill":45},"228",[47,876,877],{"x":49,"y":50,"style":51},"AND($A2\u003C>\"\", COUNTBLANK($B2:$C2)>0)",[39,879],{"x":55,"y":56,"width":57,"height":58,"rx":42,"fill":59,"stroke":60,"style":61},[39,881],{"x":64,"y":56,"width":65,"height":58,"rx":42,"fill":59,"stroke":60,"style":61},[47,883,71],{"x":68,"y":69,"style":70},[39,885],{"x":74,"y":56,"width":75,"height":58,"rx":42,"fill":59,"stroke":60,"style":61},[47,887,79],{"x":78,"y":69,"style":70},[39,889],{"x":82,"y":56,"width":83,"height":58,"rx":42,"fill":59,"stroke":60,"style":61},[47,891,87],{"x":86,"y":69,"style":70},[39,893],{"x":90,"y":56,"width":91,"height":58,"rx":42,"fill":59,"stroke":60,"style":61},[47,895,95],{"x":94,"y":69,"style":70},[39,897],{"x":55,"y":98,"width":57,"height":99,"rx":42,"fill":59,"stroke":60,"style":61},[47,899,104],{"x":102,"y":103,"style":70},[39,901],{"x":64,"y":98,"width":65,"height":99,"rx":42,"fill":45,"stroke":60,"style":61},[47,903,110],{"x":68,"y":103,"style":109},[39,905],{"x":74,"y":98,"width":75,"height":99,"rx":42,"fill":45,"stroke":60,"style":61},[47,907,115],{"x":78,"y":103,"style":109},[39,909],{"x":82,"y":98,"width":83,"height":99,"rx":42,"fill":45,"stroke":60,"style":61},[47,911,120],{"x":86,"y":103,"style":109},[39,913],{"x":90,"y":98,"width":91,"height":99,"rx":42,"fill":45,"stroke":60,"style":61},[47,915,125],{"x":94,"y":103,"style":109},[39,917],{"x":55,"y":128,"width":57,"height":99,"rx":42,"fill":59,"stroke":60,"style":61},[47,919,132],{"x":102,"y":131,"style":70},[39,921],{"x":64,"y":128,"width":65,"height":99,"rx":42,"fill":45,"stroke":60,"style":61},[47,923,104],{"x":68,"y":131,"style":137},[39,925],{"x":74,"y":128,"width":75,"height":99,"rx":42,"fill":45,"stroke":60,"style":61},[47,927,142],{"x":78,"y":131,"style":137},[39,929],{"x":82,"y":128,"width":83,"height":99,"rx":42,"fill":45,"stroke":60,"style":61},[47,931,147],{"x":86,"y":131,"style":137},[39,933],{"x":90,"y":128,"width":91,"height":99,"rx":42,"fill":45,"stroke":60,"style":61},[47,935,152],{"x":94,"y":131,"style":137},[39,937],{"x":55,"y":155,"width":57,"height":99,"rx":42,"fill":59,"stroke":60,"style":61},[47,939,159],{"x":102,"y":158,"style":70},[39,941],{"x":64,"y":155,"width":65,"height":99,"rx":42,"fill":942,"stroke":475,"style":168},"#fee8f2",[47,944,132],{"x":68,"y":158,"style":945},"font-size:12px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle",[39,947],{"x":74,"y":155,"width":75,"height":99,"rx":42,"fill":942,"stroke":475,"style":168},[39,949],{"x":82,"y":155,"width":83,"height":99,"rx":42,"fill":942,"stroke":475,"style":168},[47,951,173],{"x":86,"y":158,"style":945},[39,953],{"x":90,"y":155,"width":91,"height":99,"rx":42,"fill":942,"stroke":475,"style":168},[47,955,152],{"x":94,"y":158,"style":945},[39,957],{"x":55,"y":180,"width":57,"height":99,"rx":42,"fill":59,"stroke":60,"style":61},[47,959,184],{"x":102,"y":183,"style":70},[39,961],{"x":64,"y":180,"width":65,"height":99,"rx":42,"fill":942,"stroke":475,"style":168},[47,963,159],{"x":68,"y":183,"style":945},[39,965],{"x":74,"y":180,"width":75,"height":99,"rx":42,"fill":942,"stroke":475,"style":168},[47,967,194],{"x":78,"y":183,"style":945},[39,969],{"x":82,"y":180,"width":83,"height":99,"rx":42,"fill":942,"stroke":475,"style":168},[39,971],{"x":90,"y":180,"width":91,"height":99,"rx":42,"fill":942,"stroke":475,"style":168},[39,973],{"x":55,"y":204,"width":57,"height":99,"rx":42,"fill":59,"stroke":60,"style":61},[47,975,208],{"x":102,"y":207,"style":70},[39,977],{"x":64,"y":204,"width":65,"height":99,"rx":42,"fill":942,"stroke":475,"style":168},[47,979,184],{"x":68,"y":207,"style":945},[39,981],{"x":74,"y":204,"width":75,"height":99,"rx":42,"fill":942,"stroke":475,"style":168},[47,983,217],{"x":78,"y":207,"style":945},[39,985],{"x":82,"y":204,"width":83,"height":99,"rx":42,"fill":942,"stroke":475,"style":168},[39,987],{"x":90,"y":204,"width":91,"height":99,"rx":42,"fill":942,"stroke":475,"style":168},[240,989,991],{"id":990},"step-5-show-readers-why-a-row-is-marked","Step 5 — Show readers why a row is marked",[10,993,994],{},"Colour says \"something is wrong here\"; a note says what. Add a status column that spells out the missing fields with a formula, so the sheet explains itself when printed or read by someone who cannot distinguish the highlight colour:",[245,996,998],{"className":282,"code":997,"language":284,"meta":250,"style":250},"wb = load_workbook(\"contacts.xlsx\")\nws = wb[\"Contacts\"]\nws[\"E1\"] = \"missing\"\nfor row in range(2, 6):\n    ws[f\"E{row}\"] = (f'=TRIM(IF(LEN(TRIM(B{row}))=0,\"email \",\"\")'\n                     f'&IF(LEN(TRIM(C{row}))=0,\"phone\",\"\"))')\nws.conditional_formatting.add(\"B2:D1000\", FormulaRule(formula=['AND(LEN(TRIM(B2))=0,$A2\u003C>\"\")'], fill=AMBER))\nwb.save(\"blanks_explained.xlsx\")\nprint(ws[\"E4\"].value)\n",[252,999,1000,1012,1024,1040,1066,1111,1130,1158,1168],{"__ignoreMap":250},[255,1001,1002,1004,1006,1008,1010],{"class":257,"line":258},[255,1003,580],{"class":295},[255,1005,374],{"class":291},[255,1007,585],{"class":295},[255,1009,588],{"class":265},[255,1011,380],{"class":295},[255,1013,1014,1016,1018,1020,1022],{"class":257,"line":305},[255,1015,595],{"class":295},[255,1017,374],{"class":291},[255,1019,600],{"class":295},[255,1021,603],{"class":265},[255,1023,606],{"class":295},[255,1025,1026,1029,1032,1035,1037],{"class":257,"line":318},[255,1027,1028],{"class":295},"ws[",[255,1030,1031],{"class":265},"\"E1\"",[255,1033,1034],{"class":295},"] ",[255,1036,374],{"class":291},[255,1038,1039],{"class":265}," \"missing\"\n",[255,1041,1042,1045,1048,1051,1054,1057,1059,1061,1063],{"class":257,"line":331},[255,1043,1044],{"class":291},"for",[255,1046,1047],{"class":295}," row ",[255,1049,1050],{"class":291},"in",[255,1052,1053],{"class":354}," range",[255,1055,1056],{"class":295},"(",[255,1058,132],{"class":354},[255,1060,367],{"class":295},[255,1062,230],{"class":354},[255,1064,1065],{"class":295},"):\n",[255,1067,1068,1071,1074,1077,1081,1084,1087,1090,1092,1094,1097,1099,1102,1104,1106,1108],{"class":257,"line":344},[255,1069,1070],{"class":295},"    ws[",[255,1072,1073],{"class":291},"f",[255,1075,1076],{"class":265},"\"E",[255,1078,1080],{"class":1079},"sSjpA","{",[255,1082,1083],{"class":295},"row",[255,1085,1086],{"class":1079},"}",[255,1088,1089],{"class":265},"\"",[255,1091,1034],{"class":295},[255,1093,374],{"class":291},[255,1095,1096],{"class":295}," (",[255,1098,1073],{"class":291},[255,1100,1101],{"class":265},"'=TRIM(IF(LEN(TRIM(B",[255,1103,1080],{"class":1079},[255,1105,1083],{"class":295},[255,1107,1086],{"class":1079},[255,1109,1110],{"class":265},"))=0,\"email \",\"\")'\n",[255,1112,1113,1116,1119,1121,1123,1125,1128],{"class":257,"line":351},[255,1114,1115],{"class":291},"                     f",[255,1117,1118],{"class":265},"'&IF(LEN(TRIM(C",[255,1120,1080],{"class":1079},[255,1122,1083],{"class":295},[255,1124,1086],{"class":1079},[255,1126,1127],{"class":265},"))=0,\"phone\",\"\"))'",[255,1129,380],{"class":295},[255,1131,1133,1135,1137,1140,1142,1144,1146,1148,1150,1152,1154,1156],{"class":257,"line":1132},7,[255,1134,611],{"class":295},[255,1136,717],{"class":265},[255,1138,1139],{"class":295},", FormulaRule(",[255,1141,632],{"class":370},[255,1143,374],{"class":291},[255,1145,637],{"class":295},[255,1147,732],{"class":265},[255,1149,643],{"class":295},[255,1151,654],{"class":370},[255,1153,374],{"class":291},[255,1155,355],{"class":354},[255,1157,743],{"class":295},[255,1159,1161,1163,1166],{"class":257,"line":1160},8,[255,1162,666],{"class":295},[255,1164,1165],{"class":265},"\"blanks_explained.xlsx\"",[255,1167,380],{"class":295},[255,1169,1171,1174,1177,1180],{"class":257,"line":1170},9,[255,1172,1173],{"class":354},"print",[255,1175,1176],{"class":295},"(ws[",[255,1178,1179],{"class":265},"\"E4\"",[255,1181,1182],{"class":295},"].value)\n",[10,1184,1185,1186,1190],{},"Once the workbook is calculated, the new column reads \"email\" for record 2, \"email phone\" for record 3 and \"phone\" for record 4. Combining a highlight with a text reason is also the more accessible design — colour alone should never be the only signal. ",[14,1187,1189],{"href":1188},"\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"," builds on the same idea for other kinds of invalid data.",[240,1192,1194],{"id":1193},"add-a-live-count-of-missing-fields","Add a live count of missing fields",[10,1196,1197,1198,1202,1203,1206,1207,1210],{},"A highlight shows ",[1199,1200,1201],"em",{},"where"," the gaps are; a count at the top of the sheet shows how many are left, and it falls to zero as people fill them in. ",[252,1204,1205],{},"SUMPRODUCT"," can apply the same ",[252,1208,1209],{},"LEN(TRIM())"," test to a whole block and count only rows that have an ID:",[245,1212,1214],{"className":282,"code":1213,"language":284,"meta":250,"style":250},"import subprocess\n\nwb = load_workbook(\"contacts.xlsx\")\nws = wb[\"Contacts\"]\nws[\"H1\"] = \"Missing fields\"\nws[\"H2\"] = '=SUMPRODUCT((LEN(TRIM(B2:C1000))=0)*(A2:A1000\u003C>\"\"))'\nwb.save(\"with_counter.xlsx\")\n\nsubprocess.run([\"soffice\", \"--headless\", \"--convert-to\", \"xlsx\", \"--outdir\", \"calc\", \"with_counter.xlsx\"],\n               check=True, capture_output=True, timeout=120)\nprint(load_workbook(\"calc\u002Fwith_counter.xlsx\", data_only=True)[\"Contacts\"][\"H2\"].value)\n# 4\n",[252,1215,1216,1223,1227,1239,1251,1265,1279,1288,1292,1332,1364,1396],{"__ignoreMap":250},[255,1217,1218,1220],{"class":257,"line":258},[255,1219,299],{"class":291},[255,1221,1222],{"class":295}," subprocess\n",[255,1224,1225],{"class":257,"line":305},[255,1226,348],{"emptyLinePlaceholder":347},[255,1228,1229,1231,1233,1235,1237],{"class":257,"line":318},[255,1230,580],{"class":295},[255,1232,374],{"class":291},[255,1234,585],{"class":295},[255,1236,588],{"class":265},[255,1238,380],{"class":295},[255,1240,1241,1243,1245,1247,1249],{"class":257,"line":331},[255,1242,595],{"class":295},[255,1244,374],{"class":291},[255,1246,600],{"class":295},[255,1248,603],{"class":265},[255,1250,606],{"class":295},[255,1252,1253,1255,1258,1260,1262],{"class":257,"line":344},[255,1254,1028],{"class":295},[255,1256,1257],{"class":265},"\"H1\"",[255,1259,1034],{"class":295},[255,1261,374],{"class":291},[255,1263,1264],{"class":265}," \"Missing fields\"\n",[255,1266,1267,1269,1272,1274,1276],{"class":257,"line":351},[255,1268,1028],{"class":295},[255,1270,1271],{"class":265},"\"H2\"",[255,1273,1034],{"class":295},[255,1275,374],{"class":291},[255,1277,1278],{"class":265}," '=SUMPRODUCT((LEN(TRIM(B2:C1000))=0)*(A2:A1000\u003C>\"\"))'\n",[255,1280,1281,1283,1286],{"class":257,"line":1132},[255,1282,666],{"class":295},[255,1284,1285],{"class":265},"\"with_counter.xlsx\"",[255,1287,380],{"class":295},[255,1289,1290],{"class":257,"line":1160},[255,1291,348],{"emptyLinePlaceholder":347},[255,1293,1294,1297,1300,1302,1305,1307,1310,1312,1315,1317,1320,1322,1325,1327,1329],{"class":257,"line":1170},[255,1295,1296],{"class":295},"subprocess.run([",[255,1298,1299],{"class":265},"\"soffice\"",[255,1301,367],{"class":295},[255,1303,1304],{"class":265},"\"--headless\"",[255,1306,367],{"class":295},[255,1308,1309],{"class":265},"\"--convert-to\"",[255,1311,367],{"class":295},[255,1313,1314],{"class":265},"\"xlsx\"",[255,1316,367],{"class":295},[255,1318,1319],{"class":265},"\"--outdir\"",[255,1321,367],{"class":295},[255,1323,1324],{"class":265},"\"calc\"",[255,1326,367],{"class":295},[255,1328,1285],{"class":265},[255,1330,1331],{"class":295},"],\n",[255,1333,1335,1338,1340,1343,1345,1348,1350,1352,1354,1357,1359,1362],{"class":257,"line":1334},10,[255,1336,1337],{"class":370},"               check",[255,1339,374],{"class":291},[255,1341,1342],{"class":354},"True",[255,1344,367],{"class":295},[255,1346,1347],{"class":370},"capture_output",[255,1349,374],{"class":291},[255,1351,1342],{"class":354},[255,1353,367],{"class":295},[255,1355,1356],{"class":370},"timeout",[255,1358,374],{"class":291},[255,1360,1361],{"class":354},"120",[255,1363,380],{"class":295},[255,1365,1367,1369,1372,1375,1377,1380,1382,1384,1387,1389,1392,1394],{"class":257,"line":1366},11,[255,1368,1173],{"class":354},[255,1370,1371],{"class":295},"(load_workbook(",[255,1373,1374],{"class":265},"\"calc\u002Fwith_counter.xlsx\"",[255,1376,367],{"class":295},[255,1378,1379],{"class":370},"data_only",[255,1381,374],{"class":291},[255,1383,1342],{"class":354},[255,1385,1386],{"class":295},")[",[255,1388,603],{"class":265},[255,1390,1391],{"class":295},"][",[255,1393,1271],{"class":265},[255,1395,1182],{"class":295},[255,1397,1399],{"class":257,"line":1398},12,[255,1400,1402],{"class":1401},"s-wDw","# 4\n",[10,1404,1405],{},"Recalculated, the counter reads 4: two emails and two phones. Put the counter next to a short instruction — \"fill the amber cells until this reads 0\" — and a data-entry sheet explains its own job. Multiplying the two conditions acts as an AND across the two ranges: a cell counts only when it is empty and its row has an ID. Keep the counter's ranges the same size as the rule's range, or the number and the highlighting will disagree once the data grows past one of them.",[240,1407,1409],{"id":1408},"blanks-in-pandas-before-they-reach-the-sheet","Blanks in pandas before they reach the sheet",[10,1411,1412],{},"When the workbook is produced from a DataFrame, you can also find the gaps before writing and report them, instead of only colouring them:",[245,1414,1416],{"className":282,"code":1415,"language":284,"meta":250,"style":250},"import pandas as pd\n\ncontacts = pd.read_excel(\"contacts.xlsx\", usecols=\"A:C\")\ncontacts = contacts.dropna(subset=[\"id\"])\ntext = contacts[[\"email\", \"phone\"]].astype(\"string\").apply(lambda s: s.str.strip())\nmissing = text.isna() | (text == \"\")\nprint(missing.sum().to_dict())\nprint(contacts.loc[missing.any(axis=1), \"id\"].astype(int).tolist())\n",[252,1417,1418,1431,1435,1459,1481,1514,1538,1545],{"__ignoreMap":250},[255,1419,1420,1422,1425,1428],{"class":257,"line":258},[255,1421,299],{"class":291},[255,1423,1424],{"class":295}," pandas ",[255,1426,1427],{"class":291},"as",[255,1429,1430],{"class":295}," pd\n",[255,1432,1433],{"class":257,"line":305},[255,1434,348],{"emptyLinePlaceholder":347},[255,1436,1437,1440,1442,1445,1447,1449,1452,1454,1457],{"class":257,"line":318},[255,1438,1439],{"class":295},"contacts ",[255,1441,374],{"class":291},[255,1443,1444],{"class":295}," pd.read_excel(",[255,1446,588],{"class":265},[255,1448,367],{"class":295},[255,1450,1451],{"class":370},"usecols",[255,1453,374],{"class":291},[255,1455,1456],{"class":265},"\"A:C\"",[255,1458,380],{"class":295},[255,1460,1461,1463,1465,1468,1471,1473,1475,1478],{"class":257,"line":331},[255,1462,1439],{"class":295},[255,1464,374],{"class":291},[255,1466,1467],{"class":295}," contacts.dropna(",[255,1469,1470],{"class":370},"subset",[255,1472,374],{"class":291},[255,1474,637],{"class":295},[255,1476,1477],{"class":265},"\"id\"",[255,1479,1480],{"class":295},"])\n",[255,1482,1483,1486,1488,1491,1494,1496,1499,1502,1505,1508,1511],{"class":257,"line":344},[255,1484,1485],{"class":295},"text ",[255,1487,374],{"class":291},[255,1489,1490],{"class":295}," contacts[[",[255,1492,1493],{"class":265},"\"email\"",[255,1495,367],{"class":295},[255,1497,1498],{"class":265},"\"phone\"",[255,1500,1501],{"class":295},"]].astype(",[255,1503,1504],{"class":265},"\"string\"",[255,1506,1507],{"class":295},").apply(",[255,1509,1510],{"class":291},"lambda",[255,1512,1513],{"class":295}," s: s.str.strip())\n",[255,1515,1516,1519,1521,1524,1527,1530,1533,1536],{"class":257,"line":351},[255,1517,1518],{"class":295},"missing ",[255,1520,374],{"class":291},[255,1522,1523],{"class":295}," text.isna() ",[255,1525,1526],{"class":291},"|",[255,1528,1529],{"class":295}," (text ",[255,1531,1532],{"class":291},"==",[255,1534,1535],{"class":265}," \"\"",[255,1537,380],{"class":295},[255,1539,1540,1542],{"class":257,"line":1132},[255,1541,1173],{"class":354},[255,1543,1544],{"class":295},"(missing.sum().to_dict())\n",[255,1546,1547,1549,1552,1555,1557,1559,1562,1564,1567,1570],{"class":257,"line":1160},[255,1548,1173],{"class":354},[255,1550,1551],{"class":295},"(contacts.loc[missing.any(",[255,1553,1554],{"class":370},"axis",[255,1556,374],{"class":291},[255,1558,104],{"class":354},[255,1560,1561],{"class":295},"), ",[255,1563,1477],{"class":265},[255,1565,1566],{"class":295},"].astype(",[255,1568,1569],{"class":354},"int",[255,1571,1572],{"class":295},").tolist())\n",[10,1574,1575,1576,1580],{},"The counts — two missing emails and two missing phones — match the highlighted cells. Using both is common: pandas for a summary in the job log or a quality sheet, conditional formatting for the people fixing records in the workbook. ",[14,1577,1579],{"href":1578},"\u002Fadvanced-data-transformation-and-cleaning\u002Fhandling-missing-data-in-excel-reports\u002Ffind-and-report-missing-values-in-an-excel-file\u002F","Find and Report Missing Values in an Excel File"," turns the pandas side into a full report.",[240,1582,1584],{"id":1583},"common-pitfalls","Common pitfalls",[1586,1587,1588,1599,1605,1618],"ul",{},[1589,1590,1591,1595,1596,1598],"li",{},[1592,1593,1594],"strong",{},"Using ISBLANK for required fields."," Spaces and ",[252,1597,278],{}," formulas slip through.",[1589,1600,1601,1604],{},[1592,1602,1603],{},"Rules over empty rows."," Add a condition on an always-filled column.",[1589,1606,1607,1610,1611,1614,1615,1617],{},[1592,1608,1609],{},"Unlocked column references in row rules."," ",[252,1612,1613],{},"COUNTBLANK(B2:C2)"," without ",[252,1616,758],{}," shifts across the row and tests the wrong cells.",[1589,1619,1620,1623],{},[1592,1621,1622],{},"Treating derived columns as required."," A formula column that is blank because its input is blank double-counts the same gap.",[240,1625,1627],{"id":1626},"performance-notes","Performance notes",[10,1629,1630,1631,1633],{},"Blank-checking rules are cheap per cell, and covering a range of a thousand rows ahead of the data costs nothing noticeable. For sheets of hundreds of thousands of rows, prefer a single status column computed once and a simple rule on it, rather than several ",[252,1632,772],{}," rules recalculating across the whole sheet.",[240,1635,1637],{"id":1636},"conclusion","Conclusion",[10,1639,1640,1641,1643,1644,1646,1647,1649,1650,1652,1653,1656,1657,1659],{},"Use ",[252,1642,430],{}," rather than ",[252,1645,558],{}," so that spaces and formulas returning ",[252,1648,278],{}," count as missing, write it as a ",[252,1651,569],{}," rule or a formula rule, and add ",[252,1654,1655],{},"$A2\u003C>\"\""," so only rows with data are checked. Colour whole records with ",[252,1658,772],{}," over the required columns, add a text column that names what is missing, and use pandas to count the same gaps for the job's own report.",[240,1661,1663],{"id":1662},"frequently-asked-questions","Frequently asked questions",[10,1665,1666,1669],{},[1592,1667,1668],{},"Why does ISBLANK not highlight a cell that looks empty?","\nISBLANK is true only for a cell with no content at all. A cell holding spaces, or a formula that returns an empty string, is not blank to ISBLANK. Use LEN(TRIM(B2))=0 to treat all of those as empty.",[10,1671,1672,1675],{},[1592,1673,1674],{},"Why is every empty row under my table highlighted?","\nThe rule covers a range that extends past the data, and those cells are genuinely empty. Add a condition on a column that is always filled in real rows, such as AND(LEN(TRIM(B2))=0,$A2\u003C>\"\"), so only rows with an ID are checked.",[10,1677,1678,1681],{},[1592,1679,1680],{},"How do I highlight the whole row when any required field is missing?","\nApply a formula rule to the full row range and test the required columns with COUNTBLANK, locking the column letters with dollar signs so every cell in the row checks the same columns, as shown in Step 4.",[10,1683,1684,1687],{},[1592,1685,1686],{},"Should I use the containsBlanks rule type or a formula rule?","\nEither works, and both need a formula. containsBlanks shows up as \"Format only cells that contain Blanks\" in Excel's rule manager, which some users find clearer; a plain formula rule is more flexible when you add conditions.",[240,1689,1691],{"id":1690},"related","Related",[1586,1693,1694,1700,1707,1714],{},[1589,1695,1696,1697,1699],{},"Up one level: ",[14,1698,17],{"href":16}," — every rule type.",[1589,1701,1702,1706],{},[14,1703,1705],{"href":1704},"\u002Fadvanced-data-transformation-and-cleaning\u002Fhandling-missing-data-in-excel-reports\u002Fdrop-rows-with-missing-required-fields-in-pandas\u002F","Drop Rows with Missing Required Fields in pandas"," — removing incomplete records instead of flagging them.",[1589,1708,1709,1713],{},[14,1710,1712],{"href":1711},"\u002Fadvanced-data-transformation-and-cleaning\u002Fapplying-conditional-formatting-with-openpyxl\u002Fhighlight-cells-containing-text-with-openpyxl\u002F","Highlight Cells Containing Specific Text with openpyxl"," — the text-rule counterpart.",[1589,1715,1716,1720],{},[14,1717,1719],{"href":1718},"\u002Fgetting-started-with-python-excel-automation\u002Fwriting-dataframes-to-excel-with-pandas\u002Fcontrol-how-nan-and-infinity-appear-in-excel-output\u002F","Control How NaN, None and Infinity Appear in Excel Output"," — how gaps are written in the first place.",[1722,1723,1724],"style",{},"html pre.shiki code .sMTad, html code.shiki .sMTad{--shiki-default:#6F42C1;--shiki-dark:#FFB757}html pre.shiki code .srMev, html code.shiki .srMev{--shiki-default:#032F62;--shiki-dark:#ADDCFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s-kum, html code.shiki .s-kum{--shiki-default:#D73A49;--shiki-dark:#FF9492}html pre.shiki code .skGVy, html code.shiki .skGVy{--shiki-default:#24292E;--shiki-dark:#F0F3F6}html pre.shiki code .sP0c6, html code.shiki .sP0c6{--shiki-default:#005CC5;--shiki-dark:#91CBFF}html pre.shiki code .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}html pre.shiki code .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}html pre.shiki code .s-wDw, html code.shiki .s-wDw{--shiki-default:#6A737D;--shiki-dark:#BDC4CC}",{"title":250,"searchDepth":305,"depth":305,"links":1726},[1727,1728,1729,1730,1731,1732,1733,1734,1735,1736,1737,1738,1739],{"id":242,"depth":305,"text":243},{"id":383,"depth":305,"text":384},{"id":562,"depth":305,"text":563},{"id":677,"depth":305,"text":678},{"id":765,"depth":305,"text":766},{"id":990,"depth":305,"text":991},{"id":1193,"depth":305,"text":1194},{"id":1408,"depth":305,"text":1409},{"id":1583,"depth":305,"text":1584},{"id":1626,"depth":305,"text":1627},{"id":1636,"depth":305,"text":1637},{"id":1662,"depth":305,"text":1663},{"id":1690,"depth":305,"text":1691},"2026-09-25","Make missing values impossible to overlook: highlight empty required fields, whole rows with a gap, or blanks only inside the data. See why ISBLANK misses cells that look empty, how LEN(TRIM()) catches spaces and formulas returning \"\", and how to stop the rule from colouring every empty row below the table.","md",[1744,1746,1748,1750],{"q":1668,"a":1745},"ISBLANK is true only for a cell with no content at all. A cell holding spaces, or a formula that returns an empty string, is not blank to ISBLANK. Use LEN(TRIM(B2))=0 to treat all of those as empty.",{"q":1674,"a":1747},"The rule covers a range that extends past the data, and those cells are genuinely empty. Add a condition on a column that is always filled in real rows, such as AND(LEN(TRIM(B2))=0,$A2\u003C>\"\"), so only rows with an ID are checked.",{"q":1680,"a":1749},"Apply a formula rule to the full row range and test the required columns with COUNTBLANK, locking the column letters with dollar signs so every cell in the row checks the same columns, as shown in Step 4.",{"q":1686,"a":1751},"Either works, and both need a formula. containsBlanks shows up as \"Format only cells that contain Blanks\" in Excel's rule manager, which some users find clearer; a plain formula rule is more flexible when you add conditions.","highlight blank cells in excel with openpyxl",null,{"breadcrumb":1755},[1756,1759,1762],{"name":1757,"item":1758},"Home","\u002F",{"name":1760,"item":1761},"Advanced Data Transformation and Cleaning for Excel Automation","\u002Fadvanced-data-transformation-and-cleaning\u002F",{"name":17,"item":16},"\u002Fadvanced-data-transformation-and-cleaning\u002Fapplying-conditional-formatting-with-openpyxl\u002Fhighlight-blank-cells-with-openpyxl",{"title":1765,"description":1766},"Highlight Blank Cells with openpyxl Conditional Formatting","Highlight empty cells in Excel with openpyxl: containsBlanks rules, ISBLANK versus LEN(TRIM()), cells holding spaces or formulas that return \"\", rows with a missing required field, and limiting the rule to rows that have data.","highlight-blank-cells-with-openpyxl","advanced-data-transformation-and-cleaning\u002Fapplying-conditional-formatting-with-openpyxl\u002Fhighlight-blank-cells-with-openpyxl\u002Findex",[1770,1771,1772],"conditional-formatting","missing-data","openpyxl","how-to","TXexyvO6xochOQ5EwHTx_97N11ZldWlV2uz-oZ3pYms",[1776,1780],{"title":1777,"path":1778,"stem":1779,"children":-1},"Highlight an Entire Row Based on a Cell Value with openpyxl","\u002Fadvanced-data-transformation-and-cleaning\u002Fapplying-conditional-formatting-with-openpyxl\u002Fhighlight-an-entire-row-based-on-a-cell-value-with-openpyxl","advanced-data-transformation-and-cleaning\u002Fapplying-conditional-formatting-with-openpyxl\u002Fhighlight-an-entire-row-based-on-a-cell-value-with-openpyxl\u002Findex",{"title":1781,"path":1782,"stem":1783,"children":-1},"Highlight Cells Above a Threshold with openpyxl","\u002Fadvanced-data-transformation-and-cleaning\u002Fapplying-conditional-formatting-with-openpyxl\u002Fhighlight-cells-above-a-threshold-with-openpyxl","advanced-data-transformation-and-cleaning\u002Fapplying-conditional-formatting-with-openpyxl\u002Fhighlight-cells-above-a-threshold-with-openpyxl\u002Findex",[1785,1787],{"path":1786,"title":17},"\u002Fadvanced-data-transformation-and-cleaning\u002Fapplying-conditional-formatting-with-openpyxl",{"path":1788,"title":1760},"\u002Fadvanced-data-transformation-and-cleaning",[],[1791,1794,1797,1800,1803,1806],{"path":1792,"title":1793,"kind":1773},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fbuilding-excel-reports-with-xlsxwriter\u002Fapply-conditional-formatting-with-xlsxwriter","Apply Conditional Formatting with xlsxwriter",{"path":1795,"title":1796,"kind":1773},"\u002Fadvanced-data-transformation-and-cleaning\u002Fapplying-conditional-formatting-with-openpyxl\u002Fadd-data-bars-and-colour-scales-with-openpyxl","Add Data Bars and Colour Scales with openpyxl",{"path":1798,"title":1799,"kind":1773},"\u002Fadvanced-data-transformation-and-cleaning\u002Fapplying-conditional-formatting-with-openpyxl\u002Fadd-icon-sets-with-openpyxl","Add Icon Sets to Excel Cells with openpyxl",{"path":1801,"title":1802,"kind":1773},"\u002Fadvanced-data-transformation-and-cleaning\u002Fapplying-conditional-formatting-with-openpyxl\u002Fapply-a-colour-scale-per-row-or-per-column-with-openpyxl","Apply a Colour Scale per Row or per Column with openpyxl",{"path":1804,"title":1805,"kind":1773},"\u002Fadvanced-data-transformation-and-cleaning\u002Fapplying-conditional-formatting-with-openpyxl\u002Fapply-conditional-formatting-to-an-excel-table-that-grows","Apply Conditional Formatting to an Excel Table That Grows",{"path":1807,"title":1808,"kind":1773},"\u002Fadvanced-data-transformation-and-cleaning\u002Fapplying-conditional-formatting-with-openpyxl\u002Fcontrol-conditional-formatting-rule-order-and-stop-if-true","Control Conditional Formatting Rule Order and Stop If True",1790972078159]