Highlight Weekends and Holidays in a Date Column
Daily logs, timesheets, delivery schedules and project timelines all have a date column, and the first thing a reader looks for is which days were not normal working days. Shading weekends and public holidays makes gaps and spikes explain themselves — no orders on 31 August, because it was a bank holiday. Doing it with conditional formatting rather than fixed fills means the shading stays correct when rows are added, dates are edited or the sheet is re-sorted. This page writes the rules with openpyxl: a WEEKDAY test for weekends, a holiday list generated with the holidays package and kept on its own sheet, an ordering that lets holidays take precedence, the same rules applied across a row of dates for planner layouts, and a check of which dates each rule actually catches. It belongs to Applying Conditional Formatting with openpyxl.
Prerequisites
pip install openpyxl holidays
The holidays package knows the public holidays of most countries and their regions. The examples use England's bank holidays for 2026 and a daily orders log from 24 August to 13 September 2026, which spans the late-summer bank holiday. LibreOffice (soffice) recalculates helper formulas in Step 5; it is optional.
Step 1 — Write the data and the holiday list
Generate the holidays for the years the sheet covers and put them on their own sheet, with names, so readers can see why a date is shaded:
from datetime import date, timedelta
import holidays
from openpyxl import Workbook
from openpyxl.styles import Font
wb = Workbook()
log = wb.active
log.title = "Orders"
log.append(["Date", "Orders", "Revenue"])
start = date(2026, 8, 24)
for i in range(21):
d = start + timedelta(days=i)
busy = d.weekday() < 5 and d != date(2026, 8, 31)
log.append([d, 40 + (i * 7) % 25 if busy else 3, (40 + (i * 7) % 25) * 52.5 if busy else 120.0])
for row in log.iter_rows(min_row=2, max_col=1):
row[0].number_format = "ddd dd mmm yyyy"
log.column_dimensions["A"].width = 18
hol = wb.create_sheet("Holidays")
hol.append(["Date", "Holiday"])
for d, name in sorted(holidays.UK(subdiv="ENG", years=[2026, 2027]).items()):
hol.append([d, name])
hol.cell(hol.max_row, 1).number_format = "yyyy-mm-dd"
for ws in (log, hol):
for c in ws[1]:
c.font = Font(bold=True)
print(hol.max_row - 1, "holidays listed; first:", hol["B2"].value)
holidays.UK(subdiv="ENG", years=[...]) returns England's bank holidays — including substitute days such as "Boxing Day (observed)" when a holiday falls on a weekend — as a dictionary of dates and names. Writing them as real dates on a Holidays sheet, two years at a time, keeps the rule simple and lets someone add a company shutdown day by hand. The number format ddd dd mmm yyyy shows the weekday next to each date in the log, which helps readers check the shading. Use your own country and region codes; the package covers well over a hundred countries.
Step 2 — A weekend rule
WEEKDAY(date, 2) numbers Monday as 1 and Sunday as 7, so a weekend is any value above 5. Apply the rule to the whole data range so entire rows are shaded:
from openpyxl.formatting.rule import FormulaRule
from openpyxl.styles import PatternFill
last = log.max_row
weekend_fill = PatternFill("solid", fgColor="E7E6E6")
holiday_fill = PatternFill("solid", fgColor="FCE4D6")
weekend_rule = FormulaRule(formula=["WEEKDAY($A2,2)>5"], fill=weekend_fill, font=Font(color="7F7F7F"))
print("weekend rule:", weekend_rule.formula)
The second argument of WEEKDAY matters: with the default (1), Sunday is 1 and Saturday is 7, so >5 would wrongly shade Thursdays and Fridays. Return type 2 makes the test read naturally. $A2 anchors the column, so every cell in the row looks at the row's date, and leaves the row relative, so each row tests its own date. The rule is created here and added in the next step, together with the holiday rule, because their order matters.
Step 3 — A holiday rule that takes precedence
A holiday that falls on a weekday should look different from an ordinary weekend, and a holiday on a weekend should still look like a holiday. Add the holiday rule first and stop evaluation there:
holiday_rule = FormulaRule(formula=["COUNTIF(Holidays!$A$2:$A$200,$A2)>0"], fill=holiday_fill,
font=Font(color="C65911", bold=True), stopIfTrue=True)
log.conditional_formatting.add(f"A2:C{last}", holiday_rule)
log.conditional_formatting.add(f"A2:C{last}", weekend_rule)
wb.save("orders_shaded.xlsx")
print([r.formula for rules in log.conditional_formatting._cf_rules.values() for r in rules])
Rules added first get the higher priority. With stopIfTrue, a date that is a holiday gets the holiday format and the weekend rule is not applied, so a bank holiday Monday is orange and bold rather than grey. The COUNTIF compares each date with the holiday list; because both are real dates, the comparison is exact — if the dates in either place were text, nothing would match. The list range is larger than the list, so holidays for later years can be appended without editing the rule. Rule order is covered in more detail in Control Conditional Formatting Rule Order and Stop If True.
Step 4 — The same rules across a row of dates
Planners and Gantt-style sheets put dates across the top and people or tasks down the side. The rules are the same, with the anchoring turned round: fix the row of the date header and let the column move:
plan = wb.create_sheet("Plan")
plan["A1"] = "Person"
for i in range(21):
plan.cell(1, 2 + i, start + timedelta(days=i)).number_format = "dd"
for r, person in enumerate(["Ana", "Ben", "Chloé"], start=2):
plan.cell(r, 1, person)
last_col = plan.cell(1, 22).column_letter
plan.conditional_formatting.add(f"B1:{last_col}4", FormulaRule(
formula=["COUNTIF(Holidays!$A$2:$A$200,B$1)>0"], fill=holiday_fill, stopIfTrue=True))
plan.conditional_formatting.add(f"B1:{last_col}4", FormulaRule(formula=["WEEKDAY(B$1,2)>5"], fill=weekend_fill))
for col in range(2, 23):
plan.column_dimensions[plan.cell(1, col).column_letter].width = 4
wb.save("orders_shaded.xlsx")
print("plan rules on", f"B1:{last_col}4")
B$1 fixes row 1 and leaves the column relative, so every cell in a column tests that column's date — whole columns are shaded for weekends and the holiday, from the header down through every person's row. It is the same idea as Step 2, transposed. The narrow columns with a dd format make a compact timeline; the Create a Gantt Chart in Excel with Python page builds a fuller version.
Step 5 — Check which dates each rule catches
Python cannot see conditional formatting, but it can recalculate the rules' formulas in helper cells and compare the result with the calendar and the holiday list:
import subprocess
from openpyxl import load_workbook
chk = load_workbook("orders_shaded.xlsx")
ws = chk["Orders"]
ws["E1"], ws["F1"] = "Holiday?", "Weekend?"
for r in range(2, last + 1):
ws[f"E{r}"] = f"=COUNTIF(Holidays!$A$2:$A$200,$A{r})>0"
ws[f"F{r}"] = f"=WEEKDAY($A{r},2)>5"
chk.save("orders_check.xlsx")
subprocess.run(["soffice", "--headless", "--convert-to", "xlsx", "--outdir", "calc", "orders_check.xlsx"],
check=True, capture_output=True)
calc = load_workbook("calc/orders_check.xlsx", data_only=True)["Orders"]
uk = holidays.UK(subdiv="ENG", years=2026)
for r in range(2, last + 1):
d = calc[f"A{r}"].value.date()
shown = "holiday" if calc[f"E{r}"].value else "weekend" if calc[f"F{r}"].value else ""
expected = "holiday" if d in uk else "weekend" if d.weekday() >= 5 else ""
if shown or expected:
print(d.strftime("%a %d %b"), shown, "✓" if shown == expected else f"✗ expected {expected}")
Every Saturday and Sunday in the three weeks is caught by the weekend rule, and Monday 31 August by the holiday rule, matching Python's own calendar and the holidays package. Note that d.weekday() >= 5 is Python's numbering (Monday = 0), while Excel's WEEKDAY(..., 2) > 5 uses Monday = 1 — two different conventions that agree on which days are weekends, which is exactly what this check confirms. Running it on a copy keeps the delivered workbook free of helper columns.
Common pitfalls
WEEKDAYwithout the second argument. The default numbering starts on Sunday; use return type 2.- Dates stored as text. Neither
WEEKDAYnorCOUNTIFagainst real dates will work. - Weekend rule before the holiday rule. A weekday holiday then looks ordinary; put holidays first with stop-if-true.
- Holiday lists for one year only. Generate several years and leave room in the rule's range.
- Wrong anchoring for date headers. Use
B$1when dates run across a row.
Conclusion
To shade weekends and holidays, add two formula rules to the date range: a holiday rule first — COUNTIF against a holiday sheet generated with the holidays package, with stop-if-true — and a weekend rule WEEKDAY($A2,2)>5 after it. For dates across a header row, the same rules use B$1 instead. Keep the dates real dates on both sheets, leave room in the holiday range for future years, and check the rules by recalculating their formulas in helper cells against Python's own calendar.
Frequently asked questions
How do I highlight weekends in Excel with openpyxl? Add a FormulaRule with WEEKDAY($A2,2)>5 to the data range. Return type 2 numbers Monday as 1 and Sunday as 7, so values above 5 are Saturday and Sunday.
How do I highlight public holidays in Excel with Python? Write the holiday dates to a sheet, for example from the holidays package, and add a FormulaRule with COUNTIF of that range against the row's date greater than 0.
Why does my weekend rule highlight Thursdays and Fridays? WEEKDAY without a second argument numbers Sunday as 1 and Saturday as 7, so the test for values above 5 picks Thursday and Friday. Use WEEKDAY(date,2).
How do I make holidays take priority over weekends? Add the holiday rule before the weekend rule and set stopIfTrue on it, so dates matching the holiday rule do not get the weekend format.
Related
- Up one level: Applying Conditional Formatting with openpyxl — rule-based formatting.
- Highlight Overdue Dates with openpyxl — another date rule.
- Flag Public Holidays in Excel Data with Python — holidays as data in pandas.
- Build a Calendar Sheet in Excel with Python — a full calendar layout.
More on conditional formatting
- How-toApply Conditional Formatting with xlsxwriter
- How-toAdd Data Bars and Colour Scales with openpyxl
- How-toAdd Icon Sets to Excel Cells with openpyxl
- How-toApply a Colour Scale per Row or per Column with openpyxl
- How-toApply Conditional Formatting to an Excel Table That Grows
- How-toControl Conditional Formatting Rule Order and Stop If True