Find-and-Replace in python-pptx Breaks Your Fonts. Here's the Run-Safe Fix.
PowerPoint splits a single string across multiple runs, so python-pptx find-and-replace either misses the keyword or resets the font to default. Here's why it happens — and a one-call API that swaps the text while keeping every bit of formatting.
Posted by
Related reading
Turn Markdown Docs into a PowerPoint Deck
Have a README or .md documentation file? Turn it into a .pptx presentation in one API call — or let an AI agent do it over MCP. Here's exactly how.
The most common thing developers reach for python-pptx to do is also the thing it quietly does worst: take a branded template, find {{client_name}} or a placeholder string, and swap in real data. It looks like a two-line find-and-replace. Then you open the deck and your bold headings are regular, your brand font is Calibri, and half your placeholders never got replaced at all. This is the single most recurring python-pptx complaint, spread across several open issues that will never be fixed upstream — the library is a file-format reader/writer, and its maintainer has said as much.
Why the naive replace eats your formatting
PowerPoint does not store a paragraph as one string. It stores a list of <a:r> runs, and it splits text into a new run every time anything changes — including invisible things like a spell-check boundary or an edit you made months ago. So the word {{client_name}} is very often not one run but three: {{clien, t_na, me}}. Two failure modes fall out of that:
- The keyword is never found. Your shape.text.replace("{{client_name}}", ...) matches nothing, because no single run contains the whole string.
- The formatting is wiped. The fix everyone tries next — overwriting run.text — drops the run back to the paragraph default. As one maintainer thread puts it, after the swap "the font has become the default font." Bold, color, size, and typeface are gone.
Doing it correctly by hand means walking the run tree, detecting a keyword that straddles run boundaries, rewriting the first run's text, deleting the middle runs, and copying the original run's <a:rPr> properties onto the result — per shape, per table cell, per slide. It is a lot of fragile XML for what should be a template fill.
The run-safe fix: replace_text
replace_text does the run-walking for you and preserves each run's formatting, so a swap is one HTTP call. It matches keywords even when they are split across runs, and the output opens cleanly in PowerPoint. Two modes:
# placeholders mode — fill {{key}} markers anywhere in the deck.
# Multipart upload; keys map to {{client_name}}, {{quarter}}, {{arr}}.
curl -s https://powerpointengine.io/api/powerpoint/replace \
-F 'file=@quarterly_template.pptx' \
-F 'replaceMode=placeholders' \
-F 'replacements={"client_name":"Northwind Traders","quarter":"Q3 2026","arr":"$4.2M"}' \
| jq -r '.result.downloadUrl'
# add -F 'sessionId=YOUR_ACCOUNT_ID' to use your plan (no watermark)The bold stays bold, the brand font stays the brand font, and {{arr}} is replaced even though PowerPoint had chopped it into {{a + rr}} behind your back. Need to replace the entire text of a named shape instead of a marker? Switch to "objects" mode and key the replacements by shape name.
Fill a template, then keep going
Because every tool takes and returns a URL, a template fill is usually step one of a chain: replace the placeholders, duplicate the summary slide for each region, and export a PDF — without a local PowerPoint or LibreOffice anywhere. The slide-surgery post covers the duplicate/delete/move side, and the MCP post shows an AI agent doing the whole chain in one conversation.
The free tier returns a watermarked file so you can test the exact before/after on your own deck; Pro removes the watermark. Full request and response shape is in the docs.