Apply Conditional Formatting to a Range in openpyxl
You have a block of cells — a column of totals, a table of metrics — and you want Excel to colour the ones that cross a threshold, without touching each cell by hand. The answer is a single call: build a rule object (CellIsRule, FormulaRule, or ColorScaleRule), give it a PatternFill and/or Font, and register it against an A1-style range with ws.conditional_formatting.add(range_string, rule). openpyxl writes the rule into the worksheet XML; Excel evaluates it against every cell in the range when the file opens.
This page focuses narrowly on the range-binding mechanics — how the range string works, when CellIsRule gives way to FormulaRule, and why a rule sometimes attaches but renders nothing. For the wider tour of every rule type, see the parent guide, Applying Conditional Formatting with openpyxl.
Prerequisites
- Python 3.8 or newer.
- openpyxl installed (
pip install openpyxl). Everything here uses the current 3.1 API. - A basic grasp of A1 cell references (
A1,A1:C10) — the same notation you type in Excel's formula bar. - The colour objects come from
openpyxl.styles; if you have not met them, Styling Excel Cells with openpyxl coversPatternFillandFontin full.
No spreadsheet file is required up front — the first snippet builds one from scratch.
Step-by-step: bind a rule to a range
The pattern is always three steps: create the fill/font, build the rule, then add() it to a range string. Here is the complete minimal example.
pip install openpyxl
from openpyxl import Workbook
from openpyxl.styles import PatternFill, Font
from openpyxl.formatting.rule import CellIsRule
wb = Workbook()
ws = wb.active
# 1. Populate a column with sample values: 10, 20, ... 100
for row in range(1, 11):
ws.cell(row=row, column=1, value=row * 10)
# 2. Style to apply when the condition matches
warn_fill = PatternFill("solid", fgColor="FFC7CE")
bold_red = Font(color="9C0006", bold=True)
# 3. Build the rule and bind it to the range
rule = CellIsRule(operator="greaterThan", formula=["50"], fill=warn_fill, font=bold_red)
ws.conditional_formatting.add("A1:A10", rule)
wb.save("conditional_range.xlsx")
print("Rule applied to A1:A10")
Cells over 50 (60–100) get a red fill and bold red text when the file is opened in Excel. The rule lives in the file, not in the numbers — Excel re-evaluates it every time a value in A1:A10 changes.
Range syntax
add() binds a rule to a range expressed as an A1-style string:
- Contiguous block:
"A1:C10" - A single column:
"A1:A10" - Non-contiguous:
"A1:A10 C1:C10"— separate ranges with a space (this is what Excel stores in thesqrefattribute)
multi_rule = CellIsRule(operator="greaterThan", formula=["50"], fill=warn_fill)
ws.conditional_formatting.add("A1:A10 C1:C10", multi_rule)
wb.save("conditional_range.xlsx")
print("Rule applied to two ranges at once")
Named ranges are not accepted by add() — pass explicit cell references only.
CellIsRule vs FormulaRule
CellIsRule compares each cell against one or two literals: formula=["50"] for a single threshold, formula=["50", "100"] with operator="between". The values stay constant across the whole range.
For thresholds that reference other cells, switch to FormulaRule. Its formula is a relative Excel expression with no leading = — openpyxl writes the string verbatim, and Excel rejects a formula beginning with =. The reference uses the first row of the target range and shifts down automatically:
from openpyxl.formatting.rule import FormulaRule
# Highlight values strictly between 50 and 100
between_rule = FormulaRule(
formula=["AND(A1>50, A1<100)"], # note: no leading "="
fill=warn_fill,
font=bold_red,
stopIfTrue=True,
)
ws.conditional_formatting.add("A1:A10", between_rule)
wb.save("conditional_range.xlsx")
print("FormulaRule applied")
When building rules from an Advanced Data Transformation and Cleaning pipeline, generate the threshold strings from your validation limits rather than hardcoding them. If the source data still has gaps or stray text, run it through Cleaning Excel Data with Pandas first — a FormulaRule comparing against a blank cell evaluates unpredictably.
Rule priority
Rules apply in insertion order, and openpyxl numbers their priority accordingly. Constructors do not take a priority keyword — set the attribute afterward when ordering matters:
rule.priority = 1
between_rule.priority = 2
print("Priorities:", rule.priority, between_rule.priority)
For overlapping ranges, set stopIfTrue=True on the higher-priority rule to stop Excel evaluating the rest for a matched cell.
Inspecting attached rules
Iterate the collection before saving to confirm the rule landed. No output means nothing was attached:
for cf in ws.conditional_formatting:
print(cf.sqref, "->", len(cf.rules), "rule(s)")
Common pitfalls and gotchas
formulamust be a list of strings. Use["50"], not50. ForFormulaRule, omit the leading=:["A1>100"], not["=A1>100"].- Invisible fills.
PatternFillneeds an explicit color andfill_type="solid"(passed positionally above as"solid"); without it the fill defaults toNoneand renders nothing. - Stale rendering. Excel may cache calculation state. Reopen the file or trigger Formulas → Calculate Now to force re-evaluation.
- Re-running stacks duplicates.
ConditionalFormattingListhas noclear(); reassign a freshConditionalFormattingList()to wipe a sheet's rules before re-adding. - Wrong anchor row in
FormulaRule. The formula is relative to the first cell of the range, not the cell it happens to sit in. Anchor your reference to that first row (A1, not the mid-range cell) and let Excel shift it down.
Performance and scale
Each rule is a discrete XML block, and overlapping ranges multiply serialization work. Across ranges over ~100,000 cells, file size and Excel's initial load time grow noticeably. Restrict formatting to summary tables, or apply a native Excel table style with ws.add_table() instead of per-cell rules. One rule over A2:A100000 is far cheaper than 100,000 single-cell rules, so always bind the widest range a rule can legitimately cover rather than looping cell by cell.
Anchoring inside a range rule
from openpyxl.formatting.rule import FormulaRule
from openpyxl.styles import PatternFill
ws.conditional_formatting.add(
f"A2:F{ws.max_row}",
FormulaRule(formula=["$D2<$E2"], fill=PatternFill("solid", start_color="FFC7CE")),
)
The formula is written as though for the top-left cell of the range, and Excel offsets it for every
other cell. That is why $D2 — column pinned, row free — highlights each row against its own target,
while $D$2 compares all of them against row two and produces a sheet that is either entirely
coloured or entirely plain.
Applying one rule to the whole rectangle rather than one per row keeps the workbook fast and the rule list readable in Excel's own manager, where a hundred near-identical rules is unmaintainable.
Rebuild the range on every run
A rule written once against D2:D500 stops covering the sheet the month it grows past five hundred rows, and nothing about the file looks wrong — the extra rows simply never colour. Deriving the range from ws.max_row inside the job removes the whole failure mode, and clearing the sheet's existing rules first stops last month's ranges accumulating alongside this month's. Two lines at the top of the formatting step, and the rules always match the data they describe.
One rule, one range
Applying a rule once to the whole rectangle is both faster and easier to maintain than applying it per row — and it produces identical output.
Ranges come from the data
The habit that keeps a monthly report correct is deriving every range, row count and column position
from what was just written rather than from a number typed once. ws.max_row after the write, a
header map built from row one, a table reference rebuilt on each run — each of those removes a class
of failure that is silent rather than loud. A hardcoded range does not raise when the data outgrows
it; it simply stops covering the rows nobody looks at, and the report is wrong in a way that takes
months to notice. Deriving costs a line and removes the whole category.
Conclusion
Applying conditional formatting to a range comes down to one call — ws.conditional_formatting.add(range_string, rule) — with the subtlety living in the range string (space-separated for non-contiguous blocks, A1-only, no named ranges) and in the choice between CellIsRule for fixed literals and FormulaRule for cell-relative thresholds. Keep the fill visible with fill_type="solid", set priority/stopIfTrue when rules overlap, and iterate ws.conditional_formatting to confirm the rule actually attached before you save.
Frequently asked questions
How do I apply one rule to several non-contiguous ranges at once?
Pass them in a single A1 string separated by spaces: add("A1:A10 C1:C10", rule). That space-separated form is exactly what Excel stores in the sqref attribute.
Can I use a named range instead of cell references?
No. add() only accepts explicit A1-style coordinates; named ranges are rejected. Pass the literal range string.
When should I use FormulaRule instead of CellIsRule?
Use CellIsRule when comparing each cell against fixed literals. Switch to FormulaRule when the threshold references other cells — its formula is relative to the first row of the range and shifts down automatically, with no leading =.
Why does formula=50 raise an error?formula must be a list of strings, even for a single value: use ["50"], not 50. For between, pass two: ["50", "100"].
My formatting doesn't update after reopening — is the rule wrong?
Not necessarily; Excel can cache calculation state. Reopen the file or trigger Formulas → Calculate Now to force re-evaluation, and confirm the rule actually attached by iterating ws.conditional_formatting.
Related
- Up to the parent guide: Applying Conditional Formatting with openpyxl — the full rule catalog (
ColorScaleRule,IconSetRule) and API quirks. - Styling Excel Cells with openpyxl — the
PatternFill,Font, and border objects your rules hand to Excel. - Applying Number and Date Formats in Excel — pair a highlight with the right display format so a flagged currency cell still reads correctly.
- Cleaning Excel Data with Pandas — remove blanks and stray text before a
FormulaRuleevaluates against them.