Build a One-Page Executive Summary Sheet
Most people who receive a monthly report read one page of it. An executive summary sheet accepts that and puts everything they need on that page: a handful of headline figures with their comparison to budget and last year, two or three sentences saying what happened, a small chart showing the trend, and the few items that moved the most. Everything else — the detail by region, product and month — lives on the sheets behind it for those who want it. The difference from a dashboard is discipline: the summary is printed, forwarded and read on a phone, so it must fit one page, read top to bottom, and make sense without any interaction. This page builds such a sheet with pandas and openpyxl, writes the commentary from the numbers so it is never out of date, and proves it fits on one page by converting it to PDF. It belongs to Building Multi-Sheet Excel Dashboards.
Prerequisites
pip install pandas openpyxl
The input is a monthly fact table with one row per month and region: revenue, gross margin, number of orders, budgeted revenue and revenue for the same month last year. The same approach works for any report whose readers need the headline before the detail.
Plan the page before writing code
A summary page that fits on one sheet of A4 in portrait has room for roughly 45 rows of default height and seven or eight normal-width columns, so space is the constraint that shapes everything. Decide the blocks and their order first: title and period at the top, then the headline figures, then the commentary, then the chart, then the movers, then a footer saying where the numbers come from. Each block gets a fixed band of rows, which keeps the layout stable from month to month however the numbers change.
Step 1 — Calculate the headline figures
import pandas as pd
df = pd.read_excel("monthly_sales.xlsx")
month = df["Month"].max()
cur = df[df["Month"] == month]
prev = df[df["Month"] == month - pd.DateOffset(months=1)]
def kpis(d):
return {"Revenue": d["Revenue"].sum(), "Gross margin %": d["Gross margin"].sum() / d["Revenue"].sum(),
"Orders": d["Orders"].sum(), "Average order": d["Revenue"].sum() / d["Orders"].sum()}
now, before = kpis(cur), kpis(prev)
budget, last_year = cur["Budget revenue"].sum(), cur["Revenue last year"].sum()
for k, v in now.items():
print(f"{k:15} {v:>12,.3f} previous month {before[k]:>12,.3f}")
print(f"vs budget {now['Revenue'] / budget - 1:+.1%} vs last year {now['Revenue'] / last_year - 1:+.1%}")
Four figures are enough. Revenue on its own, gross margin as a percentage, the number of orders and the average order value tell the story between them: revenue up with orders flat means bigger orders; revenue up with margin down means discounting. Each is compared with the previous month, and revenue is also compared with budget and with the same month last year, because those are the two questions a senior reader asks first.
Step 2 — Write the commentary from the numbers
by_region = df.pivot_table(index="Region", columns="Month", values="Revenue", aggfunc="sum")
change = (by_region[month] - by_region[month - pd.DateOffset(months=1)]).sort_values()
up, down = change.idxmax(), change.idxmin()
def pct(a, b):
return f"{a / b - 1:+.1%}"
def money(x):
return f"{'+' if x >= 0 else '−'}£{abs(x):,.0f}"
lines = [
f"Revenue for {month:%B %Y} was £{now['Revenue']:,.0f}, {pct(now['Revenue'], budget)} against budget "
f"and {pct(now['Revenue'], last_year)} against {month:%B} last year.",
f"The biggest increase came from {up} ({money(change[up])} on the previous month); "
f"the biggest fall was in {down} ({money(change[down])}).",
f"Gross margin was {now['Gross margin %']:.1%} against {before['Gross margin %']:.1%} in the previous month, "
f"on {now['Orders']:,} orders averaging £{now['Average order']:,.0f}.",
]
print("\n".join(lines))
Commentary written by hand is usually the part of a report that is late, and it is sometimes wrong because it was written against an earlier version of the numbers. Generating the sentences from the same data as the figures means they always agree. The templates are deliberately plain — what happened, compared with what, and why in terms of where — and the reader's knowledge supplies the rest. A finance analyst can still add a sentence of judgement on top; the generated lines are the factual baseline, not a replacement for it. The same variance logic, region by region and line by line, is the subject of Build a Budget vs Actual Variance Report in Excel with Python.
Step 3 — Lay out the sheet
from openpyxl import Workbook
from openpyxl.chart import LineChart, Reference
from openpyxl.styles import Alignment, Font, PatternFill
wb = Workbook()
ws = wb.active
ws.title = "Summary"
detail = wb.create_sheet("Detail")
for col in "ABCDEFGH":
ws.column_dimensions[col].width = 11.5
ws.sheet_view.showGridLines = False
ws["A1"] = "Monthly sales summary"
ws["A1"].font = Font(size=18, bold=True, color="1F3864")
ws["A2"] = f"{month:%B %Y} · all regions"
ws["A2"].font = Font(size=11, color="595959")
# Band 1: four tiles across, each two columns wide (label, value, comparison)
tiles = [("Revenue", now["Revenue"], "£#,##0", pct(now["Revenue"], budget) + " vs budget"),
("Gross margin", now["Gross margin %"], "0.0%",
f"{(now['Gross margin %'] - before['Gross margin %']) * 100:+.1f} pts on month"),
("Orders", now["Orders"], "#,##0", pct(now["Orders"], before["Orders"]) + " on month"),
("Average order", now["Average order"], "£#,##0", pct(now["Average order"], before["Average order"]) + " on month")]
tile_fill = PatternFill("solid", fgColor="EEF3FA")
for i, (label, value, fmt, note) in enumerate(tiles):
c = 1 + i * 2
for r, content, font in [(4, label, Font(size=9, color="595959")), (5, value, Font(size=15, bold=True)),
(6, note, Font(size=9, color="1F3864"))]:
ws.merge_cells(start_row=r, start_column=c, end_row=r, end_column=c + 1)
cell = ws.cell(r, c, content)
cell.font, cell.fill = font, tile_fill
ws.cell(r, c + 1).fill = tile_fill
ws.cell(5, c).number_format = fmt
ws.row_dimensions[5].height = 24
# Band 2: commentary, one merged and wrapped row per sentence
for r, text in zip(range(8, 11), lines):
ws.merge_cells(start_row=r, start_column=1, end_row=r, end_column=8)
ws.cell(r, 1, text).alignment = Alignment(wrap_text=True, vertical="top")
ws.row_dimensions[r].height = 30
The sheet is eight equal columns wide, and every block is built from them: four tiles of two columns, commentary merged across all eight, the chart spanning the full width. Gridlines are switched off because the summary should look like a page, not a worksheet. Each commentary sentence gets its own merged row with wrapping and a fixed height of two lines — Excel does not grow the height of merged rows to fit their text, so the height has to be set explicitly, and two lines at this width holds a sentence of up to about 180 characters. The tiles follow the pattern in Add a KPI Summary Block to an Excel Dashboard, reduced to one row of four.
Step 4 — Chart, movers and the detail behind them
monthly = df.groupby("Month")[["Revenue", "Budget revenue", "Revenue last year"]].sum()
detail.append(["Month", "Revenue", "Budget", "Last year"])
for m, row in monthly.iterrows():
detail.append([m.strftime("%b %y"), *row.round(0).tolist()])
chart = LineChart()
chart.title = "Revenue, last 12 months"
chart.height, chart.width = 7.5, 17.5
chart.add_data(Reference(detail, min_col=2, max_col=4, min_row=1, max_row=len(monthly) + 1), titles_from_data=True)
chart.set_categories(Reference(detail, min_col=1, min_row=2, max_row=len(monthly) + 1))
chart.y_axis.numFmt = "£#,##0"
chart.x_axis.delete = chart.y_axis.delete = False
chart.legend.position = "b"
ws.add_chart(chart, "A12")
ws["A29"] = "Biggest movers on the previous month"
ws["A29"].font = Font(bold=True, color="1F3864")
movers = change.reindex(change.abs().sort_values(ascending=False).index).head(5)
for r, (region, delta) in enumerate(movers.items(), start=30):
ws.cell(r, 1, region)
ws.cell(r, 3, by_region.loc[region, month]).number_format = "£#,##0"
d = ws.cell(r, 5, delta)
d.number_format = '+£#,##0;[Red]-£#,##0'
ws["A37"] = f"Source: monthly_sales.xlsx · produced {pd.Timestamp.now():%d %b %Y %H:%M}"
ws["A37"].font = Font(size=8, color="808080")
The chart's data lives on the Detail sheet, which also gives readers who want them the actual numbers behind the lines. Three series — actual, budget and last year — are about as many as a small chart can carry before it becomes hard to read. The axes are set explicitly to stay visible, because openpyxl otherwise writes charts whose axes Excel hides. The movers block ranks regions by the size of their change, up or down, so a large fall is as visible as a large rise; the signed number format adds a plus sign to rises and shows falls in red.
Step 5 — Make it print on one page, and prove it
ws.print_area = "A1:H37"
ws.page_setup.orientation = "portrait"
ws.page_setup.paperSize = ws.PAPERSIZE_A4
ws.page_setup.fitToWidth = ws.page_setup.fitToHeight = 1
ws.sheet_properties.pageSetUpPr.fitToPage = True
ws.page_margins.left = ws.page_margins.right = 0.5
ws.print_options.horizontalCentered = True
wb.save("sales_summary_2026-09.xlsx")
soffice --headless --convert-to pdf sales_summary_2026-09.xlsx > /dev/null 2>&1
pdfinfo sales_summary_2026-09.pdf | grep Pages
A print area limited to the summary block and fit-to-page in both directions guarantee one page whatever the content does: if a sentence runs long, the page shrinks slightly rather than spilling onto a second sheet. Converting the workbook to PDF with LibreOffice and counting the pages turns "it should fit" into a check that can run every month; the Detail sheet adds its own page, so the PDF has two pages in total, and the summary is the first. If the summary ever needs two pages on its own, that is a sign it has stopped being a summary. Printing settings in more depth are in Fit an Excel Sheet to One Page Wide When Exporting to PDF.
What to leave out
The hardest part of a summary page is deciding what is not on it. A useful test is to ask, for each candidate figure, what the reader would do differently if it moved. Figures that would not change anyone's decision belong on the detail sheets. The same goes for precision: revenue to the nearest pound, percentages to one decimal place, never pence on a summary. Limit colour to meaning — red for a fall, the brand colour for headings — and avoid conditional formatting that changes colours from month to month in ways that need a legend. Finally, keep the layout the same every month. Readers learn where to look, and a summary that moves its blocks around makes them search instead of read.
Common pitfalls
- Merged rows that hide text. Set the row height explicitly; merged cells do not auto-fit.
- Hand-written commentary. It goes stale; generate the factual sentences from the data.
- Hidden chart axes. Set
delete = Falseon both axes. - No print settings. Set the print area and fit-to-page, then check the PDF page count.
- Too many figures. Four headline numbers; everything else belongs behind the summary.
Conclusion
A one-page executive summary is the first sheet most readers see and often the only one. Build it from a few headline figures compared with budget, last year and last month, add three sentences of commentary generated from the same numbers, a twelve-month trend chart and the biggest movers, then fix the print area and fit-to-page and confirm with a PDF conversion that it really is one page.
Frequently asked questions
How do I make an Excel sheet print on one page with openpyxl?
Set the print area, set fitToWidth and fitToHeight to 1 and enable sheet_properties.pageSetUpPr.fitToPage.
How can I generate report commentary automatically? Write sentence templates with placeholders and fill them from the same figures as the report, so the words always match the numbers.
Why does my merged commentary cell cut off text? Excel does not auto-fit row height for merged cells; set the row height in openpyxl to fit the longest expected text.
How many figures should an executive summary have? Usually four to six; each one should be something that would change a decision if it moved.
Related
- Up one level: Building Multi-Sheet Excel Dashboards — dashboards over several sheets.
- Add a Summary Sheet to an Excel Report with Python — a summary built from other sheets.
- Lay Out a Dashboard Sheet on a Grid — grid layout for the blocks.
- Add Variance Arrows and Trend Indicators to a Dashboard — arrows next to the figures.
More on report layouts & templates
- GuideBuilding Finance and Operations Reports in Excel with Python
- How-toAllocate Shared Costs Across Departments with Python
- How-toBuild a Bank Reconciliation Report with Python and Excel
- How-toBuild a Bill of Materials Cost Rollup
- How-toBuild a Break-Even Analysis in Excel with Python
- ComparisonBuild a Budget vs Actual Variance Report in Excel with Python