Read Dropdown Lists and Validation Rules from an Existing Workbook
A template's dropdowns encode business rules: the regions an order can belong to, the statuses a ticket may have, the priority codes finance recognises. When a script reads the filled-in template, those rules are useful twice over. They document what the valid values are — without hard-coding a copy that goes stale when someone edits the list — and they let the script check what people actually entered, because Excel only enforces a dropdown at the moment a value is typed; anything pasted in, imported or entered before the rule existed can violate it freely. This page, part of Extracting Data from Messy Excel Sheets, reads the rules with openpyxl, resolves each to its allowed values and reports every cell that breaks one.
Prerequisites
pip install openpyxl pandas
orders_template.xlsx has an Orders sheet with four validated columns — region from a range on a Lists sheet, status from an inline list, priority from a named range, and quantity limited to whole numbers from 1 to 500 — and four rows of entered data, some of which break the rules.
Step 1 — List the validation rules
Every rule on a sheet lives in the worksheet's data_validations.dataValidation list — one entry per rule. Print each rule's type, source and the cells it covers:
from openpyxl import load_workbook
wb = load_workbook("orders_template.xlsx")
ws = wb["Orders"]
for dv in ws.data_validations.dataValidation:
print(f"{dv.type:6} cells={str(dv.sqref):9} formula1={dv.formula1!r:24} formula2={dv.formula2!r} blank_ok={dv.allow_blank}")
Four rules, one per column. sqref is the range the rule applies to — often far longer than the data, because templates validate empty rows ahead of time. formula1 holds the list source or the lower limit, formula2 the upper limit for "between" rules, and allow_blank whether an empty cell is acceptable.
Step 2 — Resolve each list to its values
A list rule's formula1 comes in three shapes, and each needs resolving differently. A quoted, comma-separated string is the list. A reference such as Lists!$A$1:$A$3 must be read from that sheet. A bare name refers to a defined name, whose destinations give the range:
from openpyxl.utils.cell import range_boundaries
def read_range(wb, sheet, ref):
min_col, min_row, max_col, max_row = range_boundaries(ref.replace("$", ""))
cells = wb[sheet].iter_rows(min_row=min_row, max_row=max_row, min_col=min_col, max_col=max_col, values_only=True)
return [v for row in cells for v in row if v is not None]
def list_values(wb, formula):
f = formula.lstrip("=")
if f.startswith('"') and f.endswith('"'):
return [v.strip() for v in f.strip('"').split(",")]
if "!" in f:
sheet, ref = f.rsplit("!", 1)
return read_range(wb, sheet.strip("'"), ref)
if f in wb.defined_names:
return [v for sheet, ref in wb.defined_names[f].destinations for v in read_range(wb, sheet, ref)]
raise ValueError(f"cannot resolve list source {formula!r}")
for dv in ws.data_validations.dataValidation:
if dv.type == "list":
print(str(dv.sqref), "->", list_values(wb, dv.formula1))
Each column now has its real allowed values, read from wherever the template keeps them. When someone adds "West" to the Lists sheet, the script picks it up on the next run without a code change. Formulas saved by Excel usually omit the leading =, while some tools include it; lstrip("=") handles both. Rules whose source is a formula — INDIRECT for dependent dropdowns, OFFSET for dynamic ranges — cannot be resolved without calculating the workbook, and the resolver raises rather than guessing.
Step 3 — Check the entered data against every rule
With each rule resolved, walk the cells each rule covers and compare the value with what the rule allows. Number rules compare against their limits; list rules against their values; blanks are allowed only where the rule says so:
def check_rules(wb, ws):
problems = []
for dv in ws.data_validations.dataValidation:
allowed = list_values(wb, dv.formula1) if dv.type == "list" else None
for cell_range in dv.sqref.ranges:
for row in ws.iter_rows(min_row=cell_range.min_row, max_row=min(cell_range.max_row, ws.max_row),
min_col=cell_range.min_col, max_col=cell_range.max_col):
for cell in row:
v = cell.value
if v is None:
if not dv.allow_blank:
problems.append((cell.coordinate, v, "blank not allowed"))
continue
if dv.type == "list" and v not in allowed:
problems.append((cell.coordinate, v, f"not in {allowed}"))
if dv.type == "whole" and dv.operator == "between":
low, high = int(dv.formula1), int(dv.formula2)
if not (isinstance(v, int) and low <= v <= high):
problems.append((cell.coordinate, v, f"not a whole number {low}-{high}"))
return problems
for coord, value, reason in check_rules(wb, ws):
print(f"{coord:5} {value!r:10} {reason}")
The second data row breaks every rule — a region, status and priority that are not in their lists and a quantity of 900 — and the fourth has a quantity of 0. The blank region in row 5 is fine, because that rule allows blanks. Capping the loop at ws.max_row stops it from walking the hundred empty rows the template validates ahead of time. Extend the number check to the other operators (greaterThan, lessThanOrEqual and so on) and to decimal and date rules as your templates need them.
Step 4 — Report the problems where people will fix them
A list of coordinates in a log is easy to ignore. Highlight the offending cells in a copy of the workbook, with the reason as a comment, and send that back to whoever filled it in:
from openpyxl.comments import Comment
from openpyxl.styles import PatternFill
flag = PatternFill("solid", fgColor="FFC7CE")
for coord, value, reason in check_rules(wb, ws):
ws[coord].fill = flag
ws[coord].comment = Comment(reason, "validation check")
wb.save("orders_checked.xlsx")
print(sum(1 for c in load_workbook("orders_checked.xlsx")["Orders"]["A2:D5"] for cell in c if cell.comment), "cells flagged")
Each flagged cell carries its own explanation, so the person fixing it does not need the script's output. Highlight Invalid Cells in Excel with Python covers the highlighting options in more detail.
Step 5 — Export the rules as documentation
The same resolution gives a compact description of a template's rules — useful for handing to a new team member or checking a template before it is distributed:
import pandas as pd
doc = []
for dv in ws.data_validations.dataValidation:
header = ws.cell(row=1, column=min(r.min_col for r in dv.sqref.ranges)).value
rule = ", ".join(map(str, list_values(wb, dv.formula1))) if dv.type == "list" else f"{dv.type} {dv.operator} {dv.formula1}-{dv.formula2}"
doc.append({"column": header, "cells": str(dv.sqref), "rule": rule, "blank allowed": bool(dv.allow_blank)})
print(pd.DataFrame(doc).to_string(index=False))
Rules openpyxl cannot see
Newer Excel versions store some validation rules — notably lists whose source is on another sheet, in files saved by certain versions — in an extension list that openpyxl does not read. It warns Data Validation extension is not supported and will be removed and presents the sheet without those rules; saving the workbook with openpyxl then deletes them. If a template's dropdowns are missing from data_validations, that warning is the reason. Fix the "Data Validation extension is not supported" Warning in openpyxl explains how to read those rules another way and how to avoid losing them.
Common pitfalls
- Hard-coding the allowed values. Resolve them from the template so edits to the lists take effect.
- Walking the whole
sqref. Templates validate far more rows than contain data; stop atmax_row. - Formula sources.
INDIRECTandOFFSETlists need calculation; report them instead of guessing. - Assuming Excel enforced the rule. Pasted and imported values bypass dropdowns; check them.
- Saving the template with openpyxl. Extension-list rules are dropped; write results to a copy.
Performance notes
Reading rules is instant; checking visits each validated cell once. For large sheets, load the data with pandas and compare whole columns against the allowed lists with isin, using openpyxl only to read and resolve the rules.
Conclusion
Read a workbook's dropdowns from the worksheet's data_validations.dataValidation, resolve each list rule's formula1 — inline list, range reference or defined name — into its real values, and compare the entered data against every rule, including number limits and whether blanks are allowed. Flag violations in a copy of the workbook with the reason attached, export the rules as documentation, and remember that openpyxl cannot see rules stored in Excel's extension lists.
Frequently asked questions
Where does openpyxl keep a sheet's dropdowns? In the worksheet's data_validations.dataValidation, a list of rules. Each rule has a type ("list", "whole", "decimal", "date" and others), formula1 and formula2 holding its source or limits, and sqref, the cells it applies to.
How do I get the actual values of a dropdown? Look at formula1. A quoted comma-separated string is the list itself. A range such as Lists!$A$1:$A$3 must be read from that sheet. A single name refers to a defined name, whose destinations give the range to read.
Why are some of the workbook's dropdowns missing? openpyxl does not read validation rules stored in Excel's extension lists, which newer Excel versions use for some rules, and warns "Data Validation extension is not supported and will be removed". Those rules are invisible to openpyxl and lost if the file is saved.
Can this tell me which cells break their rule? Yes. Once each rule is resolved to its allowed values or limits, iterate over the cells in its sqref and compare. Excel only validates values as they are typed, so pasted or imported data often breaks the rules without anyone noticing.
Related
- Up one level: Extracting Data from Messy Excel Sheets — reading sheets built for people.
- Add Dropdown Data Validation to Excel with openpyxl — creating the rules in the first place.
- Validate Excel Columns Before Import with pandas — column checks in pandas.
- Create a Named Range in Excel with openpyxl — the defined names list rules often use.
More on messy spreadsheets
- How-toFreeze the Header Row in Excel with openpyxl
- How-toHandle Merged Cells When Reading Excel with pandas
- How-toUnmerge Cells and Fill the Values with openpyxl
- How-toCombine Sheets with Different Layouts into One Table
- How-toExtract Hyperlink URLs from Excel Cells with openpyxl
- How-toExtract Report Header Fields from an Excel Sheet with Python