How-to
Automating Reporting WorkflowsDeep dive

Generate PowerPoint Slides from Excel Data

Build the monthly deck from the spreadsheet instead of by copy and paste: python-pptx slides with a title per region, native editable PowerPoint charts from pandas summaries, a formatted table, a KPI headline computed from the data, a company template's layouts, and a check that renders the deck to PDF.

The monthly review deck is often the most manual part of reporting: the numbers are already in a workbook, but someone spends an afternoon pasting charts into slides, retyping headline figures and fixing fonts. python-pptx writes .pptx files directly, and — importantly — it creates native PowerPoint charts from data, not pictures of charts, so the deck's charts can be restyled, resized and even edited in PowerPoint afterwards. Generating the deck from the same pandas summaries that feed the workbook means the slides and the spreadsheet can never disagree. This page is part of Generating Excel Reports from Templates.

Prerequisites

Bash
pip install python-pptx pandas openpyxl

python-pptx is installed as python-pptx and imported as pptx. regional_sales.xlsx has six months of sales and targets, in thousands, for four regions — one row per region and month.

From the workbook to the deck The sales sheet is summarised with pandas, a title slide is added, then for each region a slide with a headline, a native chart and a table, and finally the deck is rendered to PDF for checking. Summarise pandas per region Title slide period and date Region slides headline, chart, table Save .pptx native charts Render check PDF via LibreOffice

Step 1 — Summarise the data once

Compute everything the slides will say in pandas, before touching PowerPoint: the monthly series per region and the headline numbers.

Python
import pandas as pd

sales = pd.read_excel("regional_sales.xlsx")
last = sales["Month"].max()
summary = (sales[sales["Month"] == last].set_index("Region")[["Sales", "Target"]]
           .assign(Variance=lambda d: d["Sales"] - d["Target"],
                   Pct=lambda d: d["Sales"] / d["Target"] - 1))
first = sales[sales["Month"] == sales["Month"].min()].set_index("Region")["Sales"]
summary["SixMonthGrowth"] = summary["Sales"] / first - 1
print(last)
print(summary.round(3))

East is the standout, 21% over target in September; West is 18% under and the only region smaller than six months ago. Keeping the numbers in a DataFrame means the same summary can be written to the workbook, used for the slide headlines, and tested — the deck becomes a view of data that was calculated once.

Step 2 — A title slide from the default layouts

A new Presentation() comes with the standard Office layouts. Layout 0 is the title slide:

Python
from pptx import Presentation
from pptx.util import Inches, Pt

prs = Presentation()
prs.slide_width, prs.slide_height = Inches(13.333), Inches(7.5)       # 16:9
title_slide = prs.slides.add_slide(prs.slide_layouts[0])
title_slide.shapes.title.text = "Regional sales review"
title_slide.placeholders[1].text = f"Six months to {pd.Period(last).strftime('%B %Y')}"
print([layout.name for layout in prs.slide_layouts][:6])

The default template is 4:3; setting the width and height to 13.333 × 7.5 inches makes it 16:9. For a real company deck, open the corporate template instead — Presentation("company_template.pptx") — and use its layouts: fonts, colours and logos then come from the template, and the script only supplies content. The layout names printed above are the ones to look for in a corporate template too.

Step 3 — One slide per region with a native chart

For each region, add a slide with a headline, a column-and-line chart of sales against target, and the numbers. CategoryChartData holds the chart's data; PowerPoint stores it in an embedded workbook, which is why the chart remains editable:

Python
from pptx.chart.data import CategoryChartData
from pptx.dml.color import RGBColor
from pptx.enum.chart import XL_CHART_TYPE, XL_LEGEND_POSITION

INDIGO, GREY = RGBColor(0x43, 0x38, 0xCA), RGBColor(0x94, 0xA3, 0xB8)

def layout(name):
    for candidate in prs.slide_layouts:
        if candidate.name == name:
            return candidate
    raise KeyError(f"no slide layout named {name!r}: {[l.name for l in prs.slide_layouts]}")

def headline(region, row):
    direction = "above" if row.Variance >= 0 else "below"
    return f"{region}: {row.Sales:,.0f}k in {pd.Period(last).strftime('%B')}, {abs(row.Pct):.0%} {direction} target"

for region, row in summary.iterrows():
    slide = prs.slides.add_slide(layout("Title Only"))
    title = slide.shapes.title
    title.text = headline(region, row)
    title.left, title.top, title.width, title.height = Inches(0.6), Inches(0.4), Inches(12.1), Inches(1)
    title.text_frame.paragraphs[0].runs[0].font.size = Pt(30)
    series = sales[sales["Region"] == region]
    data = CategoryChartData()
    data.categories = [pd.Period(m).strftime("%b") for m in series["Month"]]
    data.add_series("Sales", series["Sales"].tolist())
    data.add_series("Target", series["Target"].tolist())
    frame = slide.shapes.add_chart(XL_CHART_TYPE.COLUMN_CLUSTERED, Inches(0.6), Inches(1.6),
                                   Inches(8), Inches(5.2), data)
    chart = frame.chart
    chart.has_legend = True
    chart.legend.position = XL_LEGEND_POSITION.BOTTOM
    chart.legend.include_in_layout = False
    chart.plots[0].series[0].format.fill.solid()
    chart.plots[0].series[0].format.fill.fore_color.rgb = INDIGO
    chart.plots[0].series[1].format.fill.solid()
    chart.plots[0].series[1].format.fill.fore_color.rgb = GREY
    chart.value_axis.has_major_gridlines = True
    chart.value_axis.tick_labels.font.size = Pt(12)
    chart.category_axis.tick_labels.font.size = Pt(12)
print(len(prs.slides), "slides so far")

The headline is computed from the data, so it is always true: "West: 259k in September, 18% below target" is written by the script from the same row the chart shows. The "Title Only" layout gives a title placeholder and an empty body, which suits a chart and a table side by side. It is looked up by name rather than by position, because corporate templates order their layouts differently — index 5 is "Title Only" in the default template and could be anything in yours — and a missing name fails with a list of the names that do exist.

The title is repositioned on every slide for a reason worth knowing: the default template's layouts are drawn for 4:3 slides, and changing the slide size to 16:9 does not move their placeholders. Left alone, the headline sits in a 4:3-wide box in the middle of a wide slide and wraps onto two lines. A proper 16:9 template avoids this; with the default one, set the title's position and width yourself.

What each region slide contains Three layers of one slide. A headline computed from the data across the top, a native column chart of sales against target, and a small table of the last month's figures. Headline — West: 259k in September, 18% below target computed from the same row as the chart Native column chart Sales vs Target, April to September Table Sales 259k · Target 315k · Variance −56k · 6-month growth −14%

Step 4 — Add a table of the figures

A small table beside the chart gives the exact numbers. Format them as text in Python — PowerPoint tables hold text, not numbers with formats:

Python
from pptx.enum.text import PP_ALIGN

for slide, (region, row) in zip(list(prs.slides)[1:], summary.iterrows()):
    shape = slide.shapes.add_table(4, 2, Inches(9), Inches(2.2), Inches(3.6), Inches(2))
    table = shape.table
    cells = [("Sales", f"{row.Sales:,.0f}k"), ("Target", f"{row.Target:,.0f}k"),
             ("Variance", f"{row.Variance:+,.0f}k"), ("6-month growth", f"{row.SixMonthGrowth:+.0%}")]
    for r, (label, value) in enumerate(cells):
        table.cell(r, 0).text = label
        table.cell(r, 1).text = value
        for c in (0, 1):
            para = table.cell(r, c).text_frame.paragraphs[0]
            para.runs[0].font.size = Pt(16)
            if c == 1:
                para.alignment = PP_ALIGN.RIGHT
prs.save("regional_review.pptx")
print("saved", len(prs.slides), "slides")

f"{row.Variance:+,.0f}k" puts an explicit sign on the variance, so a positive number reads as "+12k" rather than just "12k". The first table row gets the default table style's header formatting — white on blue in the render — which is why the Sales figure stands out; with a corporate template, the table picks up that template's table style instead. If the first row should not look like a header, set shape.table.first_row = False. Font sizes are set per run, because a PowerPoint table cell's text is a paragraph made of runs, like any other text box.

Step 5 — Check the deck

Read the deck back to confirm its structure, then render it to PDF with LibreOffice to look at it:

Python
check = Presentation("regional_review.pptx")
for i, slide in enumerate(check.slides):
    kinds = sorted({s.shape_type for s in slide.shapes if s.shape_type is not None}, key=str)
    print(i, slide.shapes.title.text if slide.shapes.title else "", [k.name for k in kinds])
Bash
soffice -env:UserInstallation=file://$PWD/lo_profile --headless --convert-to pdf regional_review.pptx > /dev/null 2>&1
python -c "from pypdf import PdfReader; print(len(PdfReader('regional_review.pdf').pages), 'pages rendered')"

The deck has five slides — a title and four regions — each region slide carrying a chart and a table, and LibreOffice renders all five. The PDF is also a convenient format to email alongside the editable deck, for readers who only need to look at it.

Native charts or pasted pictures Two panels. A native python-pptx chart stores its data in the deck, stays sharp at any size and can be restyled and edited in PowerPoint. A pasted image of a chart is fixed, blurs when enlarged and cannot be edited. Native chart (python-pptx) data embedded, editable takes the template's styling sharp at any size Picture of a chart fixed pixels restyling means regenerating fine for matplotlib-only visuals

Performance notes

python-pptx writes a deck of dozens of slides with native charts in well under a second; each chart embeds a small workbook with its data, so a slide with a chart adds tens of kilobytes rather than the megabytes a high-resolution picture would. The slow part of the check in Step 5 is LibreOffice's start-up. When decks are produced per region or per client, build them all in one run and convert every .pptx to PDF in a single soffice call, exactly as for workbooks.

Common pitfalls

  • Pasting pictures instead of native charts. Use add_chart with CategoryChartData so charts stay editable and sharp.
  • Layouts by index in a corporate template. Index 5 is "Title Only" in the default template, not necessarily in yours; find layouts by name.
  • Numbers in tables. Table cells are text; format them in Python.
  • Headline text typed by hand. Compute it from the same data as the chart so it cannot drift.
  • 4:3 slides. The default presentation is 4:3; set 16:9 dimensions or use a 16:9 template.

Conclusion

Summarise the workbook's data with pandas, then build the deck with python-pptx: a title slide from the template's layouts, one slide per region with a headline computed from the data, a native chart from CategoryChartData coloured to the house style, and a table of formatted figures. Save it, read it back to check its structure, and render it to PDF with LibreOffice before it goes out.

Frequently asked questions

How do I create PowerPoint slides from Excel data with Python? Read the data with pandas, then use python-pptx: Presentation() or your template, add_slide with a layout, and add titles, charts and tables to each slide before saving with prs.save("deck.pptx").

Can python-pptx create charts? Yes. slide.shapes.add_chart with a CategoryChartData object creates a native PowerPoint chart — column, bar, line, pie and others — whose data is embedded in the file, so it can be edited in PowerPoint.

How do I use my company's PowerPoint template? Open it with Presentation("template.pptx") and add slides using its layouts, found by name in prs.slide_layouts. Fonts, colours and logos come from the template.

How do I check the deck without PowerPoint? Read it back with python-pptx to count slides and shapes, and convert it to PDF with headless LibreOffice to look at the pages.

More on report layouts & templates

More on report layouts & templates →