python-pptx: the practical guide for production document automation
A working python-pptx guide: text replacement, tables from data, charts from numbers, the library's limits, and when to graduate to a platform.
A data engineer at an analytics firm told me the python-pptx script she wrote in 2021 still runs every Monday. It pulls KPIs from the warehouse, drops them into a fifteen-slide template, writes a deck, and emails it. Five years, no maintenance, no drama. It’s the cleanest production pipeline in her stack.
She also said she’d never use python-pptx for the company’s customer-facing decks. Those go through a different system because the brand designer signed off on the master and the Monday-deck pipeline doesn’t preserve the master cleanly enough — image placeholders crop wrong, a custom font drops back to the default on one machine, the chart colours land close to brand but not exact. For internal reporting, fine. For something a customer sees, the python pptx path is the wrong layer.
That’s the honest frame for this guide. python-pptx is excellent for a real class of jobs and has a fidelity ceiling for another class. This piece walks through the things developers actually need — text replacement, tables from data, charts from numbers — with working code, then is honest about where the library stops paying. For the broader category treatment, read the PowerPoint automation.
What python-pptx is, briefly
python-pptx is a Python library for reading and writing PowerPoint .pptx files. It works directly on the OOXML structure inside the file — open a deck, walk slides, mutate shapes, save. No PowerPoint installation required; no Office automation; no headless browser. Pure file-format manipulation.
The library’s mental model is shape-centric. A slide has shapes; shapes have text frames or tables or charts or images; text frames have paragraphs; paragraphs have runs. Most of what python pptx work consists of is finding the right shape on the right slide and mutating its content.
Install it once:
pip install python-pptx
The three operations below cover most of what production pipelines actually do.
1. Text replacement in placeholders
The most common python-pptx job is “open a template, fill in some fields, save.” The naive way works for prototypes and breaks the moment the template has formatting.
The naive version, which works but loses run-level formatting:
from pptx import Presentation
prs = Presentation("template.pptx")
for slide in prs.slides:
for shape in slide.shapes:
if not shape.has_text_frame:
continue
for para in shape.text_frame.paragraphs:
for run in para.runs:
if "{customer_name}" in run.text:
run.text = run.text.replace("{customer_name}", "Acme Corp")
if "{report_date}" in run.text:
run.text = run.text.replace("{report_date}", "May 2026")
prs.save("output.pptx")
This works when the placeholder text sits inside a single run. PowerPoint sometimes splits text across runs invisibly — the same {customer_name} placeholder you see as one string on screen can be two or three runs in the file, especially after editing. When that happens the replace silently fails to find a match and the placeholder ships into the output.
The robust version finds placeholder by name (using PowerPoint’s named placeholders), which sidesteps the run-splitting problem entirely:
from pptx import Presentation
def replace_placeholder_text(slide, placeholder_name, new_text):
"""Replace text in a named placeholder, preserving run-level formatting."""
for shape in slide.placeholders:
if shape.name == placeholder_name:
tf = shape.text_frame
# Take the formatting from the first run, blank the frame,
# write the new text into a single run that inherits formatting.
if tf.paragraphs and tf.paragraphs[0].runs:
first_run = tf.paragraphs[0].runs[0]
font = first_run.font
tf.clear()
p = tf.paragraphs[0]
run = p.add_run()
run.text = new_text
# Copy the font properties.
run.font.name = font.name
run.font.size = font.size
run.font.bold = font.bold
run.font.italic = font.italic
if font.color and font.color.type:
run.font.color.rgb = font.color.rgb
return True
return False
prs = Presentation("template.pptx")
data = {
"customer_name_placeholder": "Acme Corp",
"report_date_placeholder": "May 2026",
"owner_placeholder": "J. Patel",
}
for slide in prs.slides:
for placeholder_name, value in data.items():
replace_placeholder_text(slide, placeholder_name, value)
prs.save("output.pptx")
This approach assumes the template’s placeholders have meaningful names — which is a discipline you set when designing the template. Open the template in PowerPoint, select a placeholder, use the Selection Pane to give it a name like customer_name_placeholder. The python pptx code then targets by name rather than by position or by string-match.
The tradeoff: this preserves single-run formatting and loses inline run-level formatting (a bolded word inside a paragraph). For most reporting use cases where the placeholder is a name or a date, that’s fine. For long narrative text with mixed formatting, the run-iteration approach is needed and the replacements have to handle run-splitting carefully.
2. Inserting tables from data
The second-most common python-pptx job is dropping a table of data into a slide. Two approaches: replace an existing table the designer drew in the template, or add a new table from scratch.
Replacing an existing table is the cleaner approach because the designer has already styled it — the borders, the header row, the cell padding. The code just fills the cells:
from pptx import Presentation
from pptx.util import Inches
prs = Presentation("template.pptx")
# Customers per region, the kind of thing a monthly report contains.
rows_data = [
{"region": "Americas", "customers": 142, "arr": "$1.8M", "growth": "+12%"},
{"region": "EMEA", "customers": 98, "arr": "$1.2M", "growth": "+8%"},
{"region": "APAC", "customers": 71, "arr": "$760K", "growth": "+18%"},
{"region": "LATAM", "customers": 24, "arr": "$240K", "growth": "+22%"},
]
# Find a slide named "regional_breakdown" — set this up in the template
# by naming the slide via the slide's notes or by index.
target_slide = prs.slides[2] # Or look up by name if you tag slides.
# Find the table on the slide.
table_shape = None
for shape in target_slide.shapes:
if shape.has_table:
table_shape = shape
break
if not table_shape:
raise RuntimeError("No table found on target slide")
table = table_shape.table
# Assume the template has a header row and 4 data rows pre-styled.
# Fill the data rows.
for row_idx, row in enumerate(rows_data, start=1): # row 0 is the header
cells = [row["region"], str(row["customers"]), row["arr"], row["growth"]]
for col_idx, value in enumerate(cells):
cell = table.cell(row_idx, col_idx)
# Replace text while preserving the cell's existing formatting.
cell.text = value
# The .text setter rebuilds the text frame, which loses cell
# formatting on some templates. The robust path is the run-level
# approach below.
prs.save("output.pptx")
The cell.text = value shortcut is the fast path and the right one for simple cell content. When the cell has rich formatting in the template — a specific font, a colour, alignment — set the run-level properties explicitly:
def set_cell_text_preserving_format(cell, new_text):
"""Set cell text without nuking cell-level formatting."""
tf = cell.text_frame
if tf.paragraphs and tf.paragraphs[0].runs:
first_run = tf.paragraphs[0].runs[0]
font_name = first_run.font.name
font_size = first_run.font.size
font_bold = first_run.font.bold
else:
font_name = font_size = font_bold = None
tf.clear()
p = tf.paragraphs[0]
run = p.add_run()
run.text = new_text
if font_name:
run.font.name = font_name
if font_size:
run.font.size = font_size
if font_bold is not None:
run.font.bold = font_bold
# Then fill cells with this helper.
for row_idx, row in enumerate(rows_data, start=1):
set_cell_text_preserving_format(table.cell(row_idx, 0), row["region"])
set_cell_text_preserving_format(table.cell(row_idx, 1), str(row["customers"]))
set_cell_text_preserving_format(table.cell(row_idx, 2), row["arr"])
set_cell_text_preserving_format(table.cell(row_idx, 3), row["growth"])
The other table approach — adding a table from scratch — is needed when the data shape is dynamic (the row count changes). python-pptx lets you add a table to a slide:
from pptx import Presentation
from pptx.util import Inches, Pt
prs = Presentation("template.pptx")
slide = prs.slides[2]
rows_data = [
{"region": "Americas", "customers": 142, "arr": "$1.8M"},
{"region": "EMEA", "customers": 98, "arr": "$1.2M"},
{"region": "APAC", "customers": 71, "arr": "$760K"},
]
# Add a table — rows includes header.
rows = len(rows_data) + 1
cols = 3
left = Inches(1)
top = Inches(2)
width = Inches(8)
height = Inches(0.4 * rows)
table_shape = slide.shapes.add_table(rows, cols, left, top, width, height)
table = table_shape.table
# Header row.
headers = ["Region", "Customers", "ARR"]
for col_idx, h in enumerate(headers):
cell = table.cell(0, col_idx)
cell.text = h
cell.text_frame.paragraphs[0].runs[0].font.bold = True
cell.text_frame.paragraphs[0].runs[0].font.size = Pt(11)
# Data rows.
for row_idx, row in enumerate(rows_data, start=1):
table.cell(row_idx, 0).text = row["region"]
table.cell(row_idx, 1).text = str(row["customers"])
table.cell(row_idx, 2).text = row["arr"]
prs.save("output.pptx")
The catch with adding tables from scratch is that you’re now writing layout in code. Column widths, row heights, cell colours, borders, alignment — every styling decision becomes a Python call instead of a designer decision. For uniform tables, this is fine. For tables that have to match the rest of the deck’s design, the replace-an-existing-table approach is the right one.
3. Chart generation from numbers
The third common python-pptx job is generating charts from data. The library has a chart submodule that produces native PowerPoint charts — the same kind PowerPoint draws when you insert a chart from Excel — which means the user can open the deck and edit the chart’s data the way they would any chart.
A bar chart from a list of numbers:
from pptx import Presentation
from pptx.util import Inches
from pptx.chart.data import CategoryChartData
from pptx.enum.chart import XL_CHART_TYPE
prs = Presentation("template.pptx")
slide = prs.slides[3]
# Chart data — categories and one or more series.
chart_data = CategoryChartData()
chart_data.categories = ["Jan", "Feb", "Mar", "Apr", "May"]
chart_data.add_series("New customers", (38, 42, 51, 47, 64))
chart_data.add_series("Churned", (5, 7, 4, 6, 8))
# Position the chart.
x, y, cx, cy = Inches(1), Inches(2), Inches(8), Inches(4.5)
slide.shapes.add_chart(
XL_CHART_TYPE.COLUMN_CLUSTERED,
x, y, cx, cy,
chart_data,
)
prs.save("output.pptx")
For chart types beyond bar — line, area, pie, doughnut — the same CategoryChartData works with a different XL_CHART_TYPE constant. For more advanced shapes (combo charts, secondary axes, scatter plots with XY data), the API has XyChartData and combination chart support, with more verbose code paths.
The honest tradeoff with python-pptx charts: they’re real PowerPoint charts, which is a feature for editability and a problem for visual fidelity. PowerPoint applies its default theming on top of your data, and the colours don’t always land exactly on your brand palette without explicit overrides. For brand-exact charts, set the colours per series:
from pptx.dml.color import RGBColor
chart = slide.shapes.add_chart(
XL_CHART_TYPE.COLUMN_CLUSTERED, x, y, cx, cy, chart_data
).chart
for series in chart.series:
fill = series.format.fill
fill.solid()
if "New customers" in series.name:
fill.fore_color.rgb = RGBColor(0x2E, 0x7D, 0x32) # brand green
elif "Churned" in series.name:
fill.fore_color.rgb = RGBColor(0xC6, 0x28, 0x28) # brand red
Set the title, the axis labels, the legend position the same way — through the chart’s properties. It’s verbose but deterministic. For a recurring monthly report, write the styling once and the chart looks the same every run.
What python-pptx can’t do
The library has a real ceiling. The places it stops paying:
Image auto-sizing in placeholders. PowerPoint’s picture placeholders crop and zoom uploaded images to fit the placeholder’s aspect ratio. python-pptx’s image insertion doesn’t always replicate this behaviour — images come in at their native size and need explicit sizing/cropping. For decks with photo placeholders the design relies on, this is a real gap.
Animations and transitions. The library doesn’t expose slide transitions, animation timelines, or build effects. If your template has animations, they survive a python-pptx round-trip only if the library leaves them untouched — modifying a slide can sometimes drop animation data depending on which shapes are touched.
Complex slide masters with theme colour inheritance. Slide masters in PowerPoint use a theme colour palette. Shapes can inherit colours from the master rather than holding explicit colour values. python-pptx exposes the explicit colours well and the theme inheritance less well — colour fidelity for theme-coloured shapes is sometimes off.
Picture cropping fidelity. The crop coordinates that PowerPoint applies to images are encoded in the OOXML, but the library’s API for them is minimal. Round-tripping a deck that has carefully cropped images can lose the crops in edge cases.
Merge with external services for things like font substitution. python-pptx writes fonts by name. If the rendering machine doesn’t have the font, PowerPoint substitutes when the file is opened. For deterministic font rendering, the team has to manage fonts at the OS level — not a python-pptx problem to solve.
Complex layouts with conditional sections that push later content. The library is a low-level OOXML manipulator, not a layout engine. If your template has “show this section conditionally and reflow later sections,” that logic is yours to write. Manageable for simple cases; painful for nested conditionals.
When to use python-pptx vs alternatives
A short version, in order.
Use python-pptx when: the deck is mostly text replacement, tables, and charts; the visual fidelity bar is internal-reporting standard; the team has Python in the stack already; the volume justifies a script. Monday morning KPI decks, sales report dashboards, internal updates — the library is excellent for these.
Use the Office.js or Open XML SDK when: you’re in a .NET stack, you want tighter Office integration, or the team is already building inside the Office ecosystem. The same operations are possible with different tradeoffs.
Use the Google Slides API instead when: the deck lives in Google Drive, the team works in Slides, and the collaborative editing matters. The Slides API model fits cloud-native pipelines better than file-based python-pptx.
Use a template-preserving platform when: brand fidelity is the value of the deck. The customer-facing report, the board pack, the QBR. python pptx will get you eighty percent there and the last twenty percent — image cropping, theme colours, picture placeholder fidelity — is where teams reach for a layer that’s specifically built to walk a designer-owned master without breaking it. The template-driven document automation category solves this; SourceToDocs is in it.
The pattern most mature teams end up at: python-pptx for internal reporting, a template-driven platform for customer-facing output. Different tools for different fidelity bars. Don’t try to make python-pptx be both.
For the broader treatment of PowerPoint automation, including the comparison with Office.js and the Open XML SDK, read the PowerPoint automation. For the Slides-API counterpart of this guide, see google slides api practical guide.