mrkeyoor.com_
Thu 06 Aug 10:53 UTC
PyPIUtilsupdated 06 Aug 2026

python-pptx

python-pptx reads and writes PowerPoint .pptx files directly, without PowerPoint or any Microsoft software installed. You open a presentation (a blank one or your company template), add slides from the template's layouts, and fill in placeholders, text boxes, tables, pictures, autoshapes, and charts, then save. Reading works the same way in reverse: iterate slides, iterate shapes, pull out text runs and images, which is why so much document-processing and retrieval tooling depends on it. It maps the Open XML file format onto Python objects fairly literally, so once you understand that a slide holds shapes, a shape may hold a text frame, a text frame holds paragraphs, and a paragraph holds runs, the API stops surprising you. All positions and sizes are English Metric Units, and the pptx.util module gives you Inches, Cm, Pt, and Emu to avoid doing that arithmetic yourself.

Verdict

Still the only credible open-source way to build PowerPoint files from Python, and it does the core job well enough that an entire generation of reporting and document-parsing tools sits on top of it. Go in knowing it has been dormant since August 2024: pin the version, accept the missing slide operations, and plan a LibreOffice step if you need PDFs.

API stability5/5Nothing has changed since 1.0.2 in August 2024, and the 0.6.x to 1.0 step was mostly packaging, typing, and dropping old Pythons rather than API churn. Code written against the 0.6 docs generally still runs. Stability by dormancy is still stability from a caller's point of view.
Docs5/5readthedocs has a quickstart with screenshots for every example, conceptual pages explaining shapes, placeholders, and text frames, a full API reference, and generated pages for every enum such as MSO_SHAPE and XL_CHART_TYPE. This is a large part of why people can still use a package with no active maintainer.
Maintenance1/5Last commit 6 August 2024 and last release 7 August 2024, with 447 open issues (534 counting PRs) accumulating behind them. No security fixes, no Python 3.14 testing, no response to reports. The library is finished in the sense that it is not being worked on, not in the sense that it is complete.
Ecosystem4/5Around 15.7 million downloads a week, and it is the layer underneath a great deal of document-extraction and report-generation tooling, so it is not going away. But there is no plugin story, community add-ons are scattered gists rather than packages, and the missing pieces such as PDF rendering and slide reordering have never been filled by anyone else.

Use it if

  • You generate decks from data on a schedule or per request: a weekly metrics deck, a per-customer proposal, a report built from a database query and returned as a file download
  • You have a branded corporate template and want output that matches it, because slides added from that template's layouts inherit its fonts, colours, and placeholder positions
  • You need to extract text or images from a corpus of existing decks for search indexing, migration, or feeding a retrieval pipeline
  • You are doing find-and-replace templating: a designer builds the deck with placeholder tokens, and your code swaps in the real values while keeping every bit of formatting
  • You need this to run on a Linux server with no Office licence and no GUI, which rules out COM automation entirely
Skip it if

Setup reality

pip install python-pptx pulls lxml, Pillow, XlsxWriter, and typing-extensions, and needs Python 3.8 or newer. lxml is compiled, but wheels exist for every mainstream platform so it is normally a non-event; on an unusual architecture you are building libxml2 bindings. XlsxWriter is there because every chart embeds a real Excel worksheet inside the pptx, which also means chart data lives in two places and can drift. The real setup cost is the template. The bundled default template is 4:3 (10 by 7.5 inches), so a deck you generate with Presentation() and no arguments looks wrong on every modern screen until you set slide_width and slide_height yourself, and it carries generic Office styling. Any serious use starts by saving a .pptx from PowerPoint with your fonts, colours, and layouts, then opening that file instead. Expect to spend the first hour printing layout indexes and placeholder idx values, because those numbers vary per template and there is no way to guess them. Version 1.0.0 added a py.typed marker, so editor completion is decent, which helps a lot given how deep the object graph goes.

Patterns

Build a deck from the default templatecreate-first-deck

from pptx import Presentation

prs = Presentation()
slide = prs.slides.add_slide(prs.slide_layouts[0])   # 0 = title slide
slide.shapes.title.text = "Q3 Review"
slide.placeholders[1].text = "Generated automatically"

prs.save("deck.pptx")

Layout indexes are positions in whatever template you opened; 0 through 6 happen to be title, title-and-content, section header, two content, comparison, title only, and blank in the bundled default, but a corporate template will differ. Never hardcode an index without printing the layout names first.

Fix the 4:3 default before anyone sees itwidescreen-slides

from pptx import Presentation
from pptx.util import Inches

prs = Presentation()
prs.slide_width = Inches(13.333)
prs.slide_height = Inches(7.5)

The template bundled with python-pptx is 4:3 (10 by 7.5 inches), so every deck built from Presentation() with no argument comes out in a shape nobody has used since about 2012. Set the size before you place any shapes, since coordinates you have already computed do not move.

Discover the layouts and placeholders in your own templateinspect-a-template

from pptx import Presentation

prs = Presentation("brand-template.pptx")
for i, layout in enumerate(prs.slide_layouts):
    print(i, layout.name)
    for ph in layout.placeholders:
        print("   idx", ph.placeholder_format.idx,
              ph.placeholder_format.type, ph.name)

Run this once per template and paste the output into a comment. Placeholder idx values are not sequential and are not the same as the position in slide.placeholders, which is the single most common source of a KeyError when moving code between templates.

Write multi-level bullets into a body placeholderbulleted-text

from pptx import Presentation
from pptx.util import Pt

prs = Presentation()
slide = prs.slides.add_slide(prs.slide_layouts[1])
slide.shapes.title.text = "Findings"

tf = slide.placeholders[1].text_frame
tf.text = "Top level bullet"          # replaces paragraph 0

p = tf.add_paragraph()
p.text = "Second level"
p.level = 1

p = tf.add_paragraph()
p.text = "Emphasised"
p.level = 2
p.font.bold = True
p.font.size = Pt(16)

Setting text_frame.text deletes every paragraph except the first, so do it before add_paragraph, never after. level goes from 0 to 8 and the actual indent and bullet glyph come from the layout, not from this code. Check shape.has_text_frame before touching text_frame on shapes you did not create.

Place an image without distorting itadd-picture

from pptx import Presentation
from pptx.util import Inches

prs = Presentation()
slide = prs.slides.add_slide(prs.slide_layouts[6])   # blank

# give exactly one dimension and aspect ratio is preserved
slide.shapes.add_picture("chart.png", Inches(1), Inches(1), width=Inches(6))

# from a file-like object works too
with open("logo.png", "rb") as f:
    slide.shapes.add_picture(f, Inches(8), Inches(0.5), height=Inches(0.75))

Passing both width and height stretches the image; passing neither uses the image's native size at its stored DPI, which is usually far too large. Identical images added repeatedly are stored once in the package, so a hundred-slide deck with the same logo does not carry a hundred copies.

Insert a table and set column widthsadd-table

from pptx import Presentation
from pptx.util import Inches

prs = Presentation()
slide = prs.slides.add_slide(prs.slide_layouts[5])   # title only

rows, cols = 3, 2
table = slide.shapes.add_table(
    rows, cols, Inches(1), Inches(2), Inches(8), Inches(1.5)
).table

table.columns[0].width = Inches(5)
table.columns[1].width = Inches(3)
table.cell(0, 0).text = "Metric"
table.cell(0, 1).text = "Value"
table.cell(1, 0).text = "Signups"
table.cell(1, 1).text = "1,204"

add_table returns a graphic frame, not the table, hence the trailing .table. Rows and columns are fixed at creation: there is no add_row, so count your data first. Cell text formatting goes through cell.text_frame.paragraphs[0].runs, the same as any other text.

Add a native PowerPoint chart with its own dataadd-chart

from pptx import Presentation
from pptx.chart.data import CategoryChartData
from pptx.enum.chart import XL_CHART_TYPE
from pptx.util import Inches

prs = Presentation()
slide = prs.slides.add_slide(prs.slide_layouts[5])

data = CategoryChartData()
data.categories = ["East", "West", "Midwest"]
data.add_series("Q1", (19.2, 21.4, 16.7))
data.add_series("Q2", (22.3, 28.6, 15.2))

frame = slide.shapes.add_chart(
    XL_CHART_TYPE.COLUMN_CLUSTERED,
    Inches(2), Inches(2), Inches(6), Inches(4.5), data
)
chart = frame.chart
chart.has_legend = True

add_chart returns the graphic frame; the chart itself is frame.chart. Every chart embeds a real xlsx worksheet, which is why XlsxWriter is a dependency and why the file grows. Most 2D chart types are supported and 3D types are not. Use XyChartData or BubbleChartData for continuous X values.

Draw autoshapes and colour themshape-fill-and-color

from pptx import Presentation
from pptx.dml.color import RGBColor
from pptx.enum.shapes import MSO_SHAPE
from pptx.util import Inches

prs = Presentation()
slide = prs.slides.add_slide(prs.slide_layouts[5])

shape = slide.shapes.add_shape(
    MSO_SHAPE.ROUNDED_RECTANGLE,
    Inches(1), Inches(2), Inches(2.5), Inches(1)
)
shape.text = "Step 1"
shape.fill.solid()
shape.fill.fore_color.rgb = RGBColor(0x1F, 0x77, 0xB4)
shape.line.color.rgb = RGBColor(0xFF, 0xFF, 0xFF)

fill.solid() has to be called before fore_color is available, otherwise you get an attribute error about the fill type. Setting an explicit rgb opts the shape out of theme colours, so it will not follow a template recolour later; use fore_color.theme_color if you want it to.

Pull every piece of text out of an existing deckextract-all-text

from pptx import Presentation

prs = Presentation("input.pptx")
for n, slide in enumerate(prs.slides, start=1):
    for shape in slide.shapes:
        if shape.has_text_frame:
            for para in shape.text_frame.paragraphs:
                text = "".join(run.text for run in para.runs)
                if text.strip():
                    print(n, text)
        if shape.has_table:
            for row in shape.table.rows:
                print(n, [c.text for c in row.cells])

Grouped shapes are containers: shape.shapes has to be walked recursively or you lose everything inside a group. Speaker notes live separately on slide.notes_slide.notes_text_frame, and touching that property creates a notes slide if one does not exist.

Fill a designer-built template by replacing tokensfind-and-replace

def replace_text(prs, mapping):
    for slide in prs.slides:
        for shape in slide.shapes:
            if not shape.has_text_frame:
                continue
            for para in shape.text_frame.paragraphs:
                full = "".join(r.text for r in para.runs)
                new = full
                for k, v in mapping.items():
                    new = new.replace(k, str(v))
                if new != full and para.runs:
                    para.runs[0].text = new
                    for r in para.runs[1:]:
                        r.text = ""

Replacing run by run fails constantly because PowerPoint splits {{name}} across runs at spellcheck and language boundaries. Rebuilding the paragraph from run 0 fixes the matching but flattens mixed formatting within that paragraph onto the first run's style, so keep one token per paragraph in the template.

Remove a slide, since there is no API for itdelete-a-slide

R_NS = "{http://schemas.openxmlformats.org/officeDocument/2006/relationships}id"

def delete_slide(prs, index):
    id_list = prs.slides._sldIdLst
    entries = list(id_list)
    rId = entries[index].get(R_NS)
    prs.part.drop_rel(rId)
    id_list.remove(entries[index])

This reaches into a private attribute and the relationship table, and it is the accepted answer because no public method exists. Reordering slides is the same trick with id_list.insert. Since the project is dormant the private names are unlikely to change, but this is the part of your code that will break first.

Return a generated deck from a web handlersave-to-memory

from io import BytesIO
from pptx import Presentation

def build_deck() -> bytes:
    prs = Presentation("brand-template.pptx")
    prs.slides.add_slide(prs.slide_layouts[0]).shapes.title.text = "Report"

    buf = BytesIO()
    prs.save(buf)
    buf.seek(0)
    return buf.getvalue()

# content type:
# application/vnd.openxmlformats-officedocument.presentationml.presentation

save accepts any file-like object, so nothing has to touch disk. Presentation() also accepts a file-like object, which lets you load a template from object storage. Forgetting buf.seek(0) is the reason a download arrives as a zero-byte file.

Alternatives

PackageRegistryPick it when
pptxgenjsnpmYour generator can live in Node or the browser; it is actively released and produces decks client-side with no server round trip.
Spire.PresentationPyPIYou need conversion to PDF or images, or support for shape types python-pptx does not model, and a commercial licence is acceptable.
markitdownPyPIYou only want the text out of a deck as Markdown for indexing or an LLM, and never need to write a .pptx.