01Turn a deck into a Markdown outline and find slides without a titleVerified

For every slide, take the title (shapes.title) and the paragraphs of the other placeholders, indent them by paragraph.level, and write one Markdown file per deck. Slides whose title placeholder is missing or empty are listed as (no title). Useful for reviewing structure or preparing text before handing it to Copilot.

Approach and steps
  1. Iterate over Presentation(path).slides.
  2. Get the title placeholder with slide.shapes.title; it is None when the slide has no title placeholder.
  3. From slide.placeholders, take paragraphs of shapes whose placeholder_format.type is not TITLE or CENTER_TITLE and that have a text frame.
  4. Join the runs of each paragraph, indent by paragraph.level, and prefix with a dash.
  5. Print the numbers and layout names (slide.slide_layout.name) of untitled slides and save out_outline_<name>.md.
PythonRuns locally
"""Turn each deck into a Markdown outline (title + bullets by level) and flag untitled slides."""
from pathlib import Path

from pptx import Presentation
from pptx.enum.shapes import PP_PLACEHOLDER

DECKS = sorted(Path("samples").glob("*.pptx"))
TITLE_TYPES = {PP_PLACEHOLDER.TITLE, PP_PLACEHOLDER.CENTER_TITLE}


def slide_outline(slide):
    title = slide.shapes.title  # None when the slide has no title placeholder
    text = title.text_frame.text.strip() if title is not None else ""
    lines = [text or "(no title)"]
    for shape in slide.placeholders:
        if shape.placeholder_format.type in TITLE_TYPES or not shape.has_text_frame:
            continue
        for p in shape.text_frame.paragraphs:
            body = "".join(r.text for r in p.runs).strip()
            if body:
                lines.append("  " * p.level + "- " + body)
    return lines


def main():
    for path in DECKS:
        prs = Presentation(path)
        md, untitled = ["# " + path.stem], []
        for i, slide in enumerate(prs.slides, 1):
            lines = slide_outline(slide)
            if lines[0] == "(no title)":
                untitled.append("%d (%s)" % (i, slide.slide_layout.name))
            md += ["", "## %d. %s" % (i, lines[0])] + lines[1:]
        out = Path("out_outline_%s.md" % path.stem)
        out.write_text("\n".join(md) + "\n", encoding="utf-8")
        print("%s: slides %d, untitled: %s -> %s" % (path.name, len(prs.slides), ", ".join(untitled) or "none", out.name))


if __name__ == "__main__":
    main()
Output(2026-09-12)
deck_a.pptx: slides 3, untitled: none -> out_outline_deck_a.md
deck_b.pptx: slides 2, untitled: none -> out_outline_deck_b.md
docs_deck_c.pptx: slides 4, untitled: 3 (Blank) -> out_outline_docs_deck_c.md
Example instruction for Copilot

Using python-pptx, write code that writes a Markdown outline per .pptx in a folder: slide title from shapes.title, then the paragraphs of the other placeholders indented by paragraph.level. List the number and layout name of every slide whose title is missing or empty.

Caution

Only placeholder text is collected, so manually placed text boxes, shapes, tables and text inside groups are left out (the batch-check recipe covers those). A slide whose title is a plain text box shows up as (no title). When you pass the outline to Copilot, compare it with the slides to catch gaps.

Availability
Tested with python-pptx 1.0.2 (Python 3.12 venv on Windows 11); the official docs are published for 1.0.0. PyPI lists Python 3.8 or later. Works on .pptx; legacy .ppt files cannot be opened.
Requires
python-pptx 1.0.2
Tested
run and checked locally (2026-09-12, Python 3.12.10 (venv) / Windows 11, python-pptx 1.0.2)
Practical edition
PowerPoint ── Summarize a deck and find key slides
Source
python-pptx, "Shapes"
python-pptx, "Working with placeholders"
python-pptx, "Text-related objects"
Verified
2026-09-12 (v1)
Supporting passages from the sources
The title placeholder shape on the slide. None if the slide has no title placeholder.
0 represents a top-level paragraph and is the default value.
The title placeholder will always have idx 0 if present and any other placeholders will follow in sequence, top to bottom and left to right.

02Export slide tables to CSV and mark merged and empty cellsVerified

Take shape.table from every shape where has_table is True and write one CSV row per grid coordinate. Merge-origin cells get their height and width, cells hidden under a merge are marked spanned, and visible empty cells are flagged, which helps catch gaps in price lists and similar tables.

Approach and steps
  1. Loop over slide.shapes and take shape.table where shape.has_table is True.
  2. Read cells in grid order with table.rows and row.cells.
  3. Record span_height and span_width for cell.is_merge_origin, and spanned for cell.is_spanned.
  4. Flag a cell as empty when it is not hidden by a merge and has no text.
  5. Ask the deck owner to confirm the empty rows in out_tables.csv.
PythonRuns locally
"""Export every table in each deck to CSV, marking merged and empty cells."""
import csv
from pathlib import Path

from pptx import Presentation

DECKS = sorted(Path("samples").glob("*.pptx"))


def iter_tables(prs):
    for s, slide in enumerate(prs.slides, 1):
        for shape in slide.shapes:
            if shape.has_table:
                yield s, shape.name, shape.table


def cell_rows(table):
    for r, row in enumerate(table.rows):
        for c, cell in enumerate(row.cells):
            if cell.is_spanned:
                state = "spanned"  # hidden under a merged cell
            elif cell.is_merge_origin:
                state = "merged %dx%d" % (cell.span_height, cell.span_width)
            else:
                state = ""
            empty = "empty" if not cell.is_spanned and not cell.text.strip() else ""
            yield r, c, state, empty, cell.text.strip()


def main():
    rows = []
    for path in DECKS:
        for s, name, table in iter_tables(Presentation(path)):
            rows += [(path.name, s, name) + rec for rec in cell_rows(table)]
    with open("out_tables.csv", "w", encoding="utf-8-sig", newline="") as f:
        w = csv.writer(f)
        w.writerow(["deck", "slide", "shape", "row", "col", "state", "flag", "text"])
        w.writerows(rows)
    tables = {(r[0], r[1], r[2]) for r in rows}
    print("tables:", len(tables), "cells:", len(rows), "empty cells:", sum(r[6] == "empty" for r in rows))
    for r in rows:
        if r[5] or r[6]:
            print(" | ".join(str(x) for x in r))


if __name__ == "__main__":
    main()
Output(2026-09-12)
tables: 2 cells: 13 empty cells: 1
docs_deck_c.pptx | 4 | Table 2 | 0 | 0 | merged 1x3 |  | プラン別の月額(税別、架空)
docs_deck_c.pptx | 4 | Table 2 | 0 | 1 | spanned |  | 
docs_deck_c.pptx | 4 | Table 2 | 0 | 2 | spanned |  | 
docs_deck_c.pptx | 4 | Table 2 | 2 | 2 |  | empty | 
Example instruction for Copilot

Using python-pptx, write code that exports every table in the .pptx files of a folder to a CSV with deck, slide number, shape name, row, column, merge state, empty flag and text. Use is_merge_origin and is_spanned to tell merged cells apart.

Caution

The docs note that python-pptx addresses cells by grid coordinates, which may not match where a cell appears visually. Text left in spanned cells is hidden on the slide. This example only looks at top-level shapes, so tables inside groups are skipped (reuse the recursive helper from the group recipe). Confirm header rows and units before using the values in any calculation.

Availability
Tested with python-pptx 1.0.2 (Python 3.12 venv on Windows 11); the official docs are published for 1.0.0. PyPI lists Python 3.8 or later. Works on .pptx; legacy .ppt files cannot be opened.
Requires
python-pptx 1.0.2
Tested
run and checked locally (2026-09-12, Python 3.12.10 (venv) / Windows 11, python-pptx 1.0.2)
Source
python-pptx, "Working with tables"
python-pptx, "Shapes"
Verified
2026-09-12 (v1)
Supporting passages from the sources
Access to a table cell in python-pptx is always via that cell’s coordinates in the cell grid, which may not conform to its visual location (or lack thereof) in the table.
In python-pptx a merge-origin cell can be identified with the _Cell.is_merge_origin property.
True if this shape is a graphic frame containing a table object.

03Read speaker notes safely with has_notes_slide and list gaps and flagged termsVerified

Accessing slide.notes_slide creates a notes slide when none exists. Check has_notes_slide first and read notes_text_frame only where notes already exist, then collect slides without notes, empty notes and style-list hits in one CSV.

Approach and steps
  1. For each slide, check slide.has_notes_slide. If it is False, record no notes and leave notes_slide alone.
  2. If it is True, read slide.notes_slide.notes_text_frame.text, treating a None text frame as empty.
  3. Match the notes text against the terms in style_terms.csv.
  4. Write deck, slide number, title, state (none / empty / text), notes and matched terms to out_notes.csv.
PythonRuns locally
"""List speaker notes per slide without creating empty notes slides, and flag style-list terms."""
import csv
from pathlib import Path

from pptx import Presentation

DECKS = sorted(Path("samples").glob("*.pptx"))
TERMS = Path("samples/style_terms.csv")


def load_terms(path):
    with open(path, encoding="utf-8-sig", newline="") as f:
        return [row["term"] for row in csv.DictReader(f)]


def notes_text(slide):
    """None when the slide has no notes slide; never creates one."""
    if not slide.has_notes_slide:  # slide.notes_slide would create a notes slide
        return None
    frame = slide.notes_slide.notes_text_frame
    return frame.text.strip() if frame is not None else ""


def main():
    terms = load_terms(TERMS)
    rows = []
    for path in DECKS:
        missing = []
        for i, slide in enumerate(Presentation(path).slides, 1):
            title = slide.shapes.title.text_frame.text.strip() if slide.shapes.title is not None else ""
            notes = notes_text(slide)
            state = "none" if notes is None else ("empty" if not notes else "text")
            if state != "text":
                missing.append(i)
            found = [t for t in terms if notes and t in notes]
            rows.append((path.name, i, title, state, notes or "", " ".join(found)))
        print("%s: slides without notes: %s" % (path.name, missing or "none"))
    with open("out_notes.csv", "w", encoding="utf-8-sig", newline="") as f:
        w = csv.writer(f)
        w.writerow(["deck", "slide", "title", "state", "notes", "terms"])
        w.writerows(rows)
    for r in rows:
        if r[5]:
            print("term in notes:", r[0], "slide", r[1], "|", r[5], "|", r[4])


if __name__ == "__main__":
    main()
Output(2026-09-12)
deck_a.pptx: slides without notes: [3]
deck_b.pptx: slides without notes: [1]
docs_deck_c.pptx: slides without notes: [2, 3]
term in notes: deck_a.pptx slide 2 | 必ず | 必ず短縮できるとは言わないこと。
Example instruction for Copilot

Using python-pptx, write code that checks has_notes_slide before reading each slide's speaker notes, and writes a CSV with the notes state (none, empty, text) and any terms from style_terms.csv found in the notes. Never call notes_slide unconditionally, and never save the file.

Caution

Calling notes_slide first in an audit creates notes on slides that had none, and the file changes if it is saved. Notes are often written for the presenter only, so review them before a deck is distributed. Matching is plain substring search, so reminders such as 'do not promise X' are also reported.

Availability
Tested with python-pptx 1.0.2 (Python 3.12 venv on Windows 11); the official docs are published for 1.0.0. PyPI lists Python 3.8 or later. Works on .pptx; legacy .ppt files cannot be opened.
Requires
python-pptx 1.0.2
Tested
run and checked locally (2026-09-12, Python 3.12.10 (venv) / Windows 11, python-pptx 1.0.2)
Practical edition
PowerPoint ── Generate speaker notes
Source
python-pptx, "Slides"
python-pptx, "Working with Notes Slides"
Verified
2026-09-12 (v1)
Supporting passages from the sources
A notes slide is created by notes_slide when one doesn’t exist; use this property to test for a notes slide without the possible side effect of creating one.
Each slide can have zero or one notes slide.
None if there is no notes placeholder.

04Walk group shapes recursively so text inside groups is not missedVerified

A group shape has no text frame of its own, so has_text_frame is always False. Checks that only look at top-level slide.shapes miss text boxes inside groups. Recursing into shape.shapes whenever shape_type is GROUP lists every member shape with the path of group names that contains it.

Approach and steps
  1. Loop over slide.shapes and pass shape.shapes back into the same function when shape.shape_type is MSO_SHAPE_TYPE.GROUP.
  2. Yield every non-group shape together with the path of group names above it.
  3. Print how many text-bearing shapes sit at the top level and how many sit inside groups.
  4. Show each piece of text found inside a group with slide number, path, shape name and shape_id.
PythonRuns locally
"""Find text hidden inside group shapes by walking the shape tree recursively."""
from pathlib import Path

from pptx import Presentation
from pptx.enum.shapes import MSO_SHAPE_TYPE

DECKS = sorted(Path("samples").glob("*.pptx"))


def iter_shapes(shapes, path=()):
    """Yield (group path, shape) for every shape, descending into groups."""
    for shape in shapes:
        if shape.shape_type == MSO_SHAPE_TYPE.GROUP:
            yield from iter_shapes(shape.shapes, path + (shape.name,))
        else:
            yield path, shape


def has_text(shape):
    return shape.has_text_frame and shape.text_frame.text.strip()


def main():
    for deck in DECKS:
        top = nested = 0
        for i, slide in enumerate(Presentation(deck).slides, 1):
            top += sum(1 for s in slide.shapes if has_text(s))
            for path, shape in iter_shapes(slide.shapes):
                if path and has_text(shape):
                    nested += 1
                    print("slide %d | %s > %s (id %d) | %s" % (
                        i, " > ".join(path), shape.name, shape.shape_id, shape.text_frame.text.strip()))
        print("%s: text shapes at top level %d, in groups: %d" % (deck.name, top, nested))


if __name__ == "__main__":
    main()
Output(2026-09-12)
deck_a.pptx: text shapes at top level 6, in groups: 0
slide 2 | Group 5 > TextBox 2 (id 3) | 申込
slide 2 | Group 5 > TextBox 3 (id 4) | 設定
slide 2 | Group 5 > TextBox 4 (id 5) | 最短 3 日で運用開始出来る
docs_deck_c.pptx: text shapes at top level 4, in groups: 3
Example instruction for Copilot

Using python-pptx, write a generator that walks slide shapes recursively into groups (MSO_SHAPE_TYPE.GROUP), yielding the group path and the shape, and print text that exists only inside groups with slide number, shape name and shape_id.

Caution

Groups nested inside groups are handled by the same recursion. Tables and pictures can also sit inside groups, so reuse the helper when searching for them. Use shape_id, which is unique within a slide, when you need to identify a shape. Text inside charts is not read this way.

Availability
Tested with python-pptx 1.0.2 (Python 3.12 venv on Windows 11); the official docs are published for 1.0.0. PyPI lists Python 3.8 or later. Works on .pptx; legacy .ppt files cannot be opened.
Requires
python-pptx 1.0.2
Tested
run and checked locally (2026-09-12, Python 3.12.10 (venv) / Windows 11, python-pptx 1.0.2)
Source
python-pptx, "Shapes"
python-pptx, "Understanding Shapes"
Verified
2026-09-12 (v1)
Supporting passages from the sources
A group shape does not have a textframe and cannot itself contain text. This does not impact the ability of shapes contained by the group to each have their own text.
Unconditionally MSO_SHAPE_TYPE.GROUP in this case
The id of a shape is unique among all shapes on a slide.

05Extract slide images, drop duplicates by SHA1, and list them in a CSVVerified

The image property of a Picture shape exposes the bytes (blob), extension (ext), MIME type, pixel size, dpi and SHA1 of the image. Group pictures by SHA1 to see where the same image is reused, save each unique image once to out_images, and list every placement in a CSV.

Approach and steps
  1. Loop over slide.shapes, recurse into groups, and collect Picture shapes.
  2. Key by picture.image.sha1 and write the blob of each first-seen image to out_images/<deck>_s<n>_<sha1 prefix>.<ext>.
  3. Record content_type, size (pixels) and dpi as well.
  4. Keep later occurrences of the same sha1 in the CSV, marked duplicate.
  5. Use out_pictures.csv to spot reused images and low-resolution ones.
PythonRuns locally
"""Extract pictures from decks, skip duplicates by SHA1, and list every placement in a CSV."""
import csv
from pathlib import Path

from pptx import Presentation
from pptx.enum.shapes import MSO_SHAPE_TYPE
from pptx.shapes.picture import Picture

DECKS = sorted(Path("samples").glob("*.pptx"))
OUT_DIR = Path("out_images")


def iter_pictures(shapes):
    for shape in shapes:
        if shape.shape_type == MSO_SHAPE_TYPE.GROUP:
            yield from iter_pictures(shape.shapes)
        elif isinstance(shape, Picture):
            yield shape


def main():
    OUT_DIR.mkdir(exist_ok=True)
    rows, saved = [], {}
    for deck in DECKS:
        for i, slide in enumerate(Presentation(deck).slides, 1):
            for pic in iter_pictures(slide.shapes):
                img = pic.image
                dup = "duplicate" if img.sha1 in saved else ""
                if not dup:
                    saved[img.sha1] = "%s_s%d_%s.%s" % (deck.stem, i, img.sha1[:8], img.ext)
                    (OUT_DIR / saved[img.sha1]).write_bytes(img.blob)
                w, h = img.size
                rows.append((deck.name, i, pic.name, img.content_type, "%dx%d" % (w, h),
                             "%dx%d" % img.dpi, saved[img.sha1], dup))
    with open("out_pictures.csv", "w", encoding="utf-8-sig", newline="") as f:
        wr = csv.writer(f)
        wr.writerow(["deck", "slide", "shape", "content_type", "pixels", "dpi", "file", "flag"])
        wr.writerows(rows)
    print("pictures:", len(rows), "unique files:", len(saved))
    for r in rows:
        print(" | ".join(str(x) for x in r))


if __name__ == "__main__":
    main()
Output(2026-09-12)
pictures: 3 unique files: 2
docs_deck_c.pptx | 3 | Picture 1 | image/png | 64x40 | 72x72 | docs_deck_c_s3_dc9edf4b.png | 
docs_deck_c.pptx | 3 | Picture 2 | image/png | 64x40 | 72x72 | docs_deck_c_s3_dc9edf4b.png | duplicate
docs_deck_c.pptx | 3 | Picture 3 | image/png | 64x40 | 72x72 | docs_deck_c_s3_ece03d63.png | 
Example instruction for Copilot

Using python-pptx, write code that extracts pictures (Picture shapes, including those inside groups) from every .pptx in a folder, saves each unique image once to out_images based on image.sha1, and writes deck, slide, shape name, MIME type, pixel size, dpi, file name and a duplicate flag to a CSV.

Caution

The docs state that dpi defaults to (72, 72) when the image file does not specify it. size is in pixels; the displayed size on the slide comes from shape.width and shape.height in EMU. Images used as a shape's picture fill are not Picture shapes and are not extracted. Extracted images keep the usage restrictions of the source deck, such as licensed stock material.

Availability
Tested with python-pptx 1.0.2 (Python 3.12 venv on Windows 11); the official docs are published for 1.0.0. PyPI lists Python 3.8 or later. Works on .pptx; legacy .ppt files cannot be opened.
Requires
python-pptx 1.0.2
Tested
run and checked locally (2026-09-12, Python 3.12.10 (venv) / Windows 11, python-pptx 1.0.2)
Source
python-pptx, "Image"
python-pptx, "Shapes"
python-pptx, "Understanding Shapes"
Verified
2026-09-12 (v1)
Supporting passages from the sources
The Image object provides access to detailed properties of the image itself, including the bytes of the image file itself.
A default value of (72, 72) is used if the dpi is not specified in the image file.
Note that an auto shape can have a picture fill

06Batch-check every deck in a folder against a style list into one CSVVerified

Open every .pptx in a folder, gather text from text boxes, placeholders, groups, table cells and speaker notes, and write every style-list hit to a single CSV. A file that cannot be opened is recorded as a row instead of stopping the run.

Approach and steps
  1. Collect files with FOLDER.glob("*.pptx") and load style_terms.csv.
  2. Take slide shapes from a queue; when a shape is a group, put its members back at the front of the queue.
  3. Read text_frame.text for shapes with a text frame, and the text of non-spanned cells for tables.
  4. Read speaker notes only when has_notes_slide is True.
  5. Write deck, slide number, location, term, action and text to out_deck_check.csv and print counts per deck.
PythonRuns locally
"""Check every .pptx in a folder against a style list: text, groups, tables and notes."""
import csv
from collections import Counter
from pathlib import Path

from pptx import Presentation
from pptx.enum.shapes import MSO_SHAPE_TYPE

FOLDER = Path("samples")
TERMS = FOLDER / "style_terms.csv"


def load_terms(path):
    with open(path, encoding="utf-8-sig", newline="") as f:
        return list(csv.DictReader(f))


def texts(slide):
    """Yield (where, text) for text frames, table cells (also inside groups) and notes."""
    queue = [(shape, shape.name) for shape in slide.shapes]
    while queue:
        shape, where = queue.pop(0)
        if shape.shape_type == MSO_SHAPE_TYPE.GROUP:
            queue[:0] = [(s, where + ">" + s.name) for s in shape.shapes]
        elif shape.has_text_frame:
            yield where, shape.text_frame.text
        elif shape.has_table:
            for r, row in enumerate(shape.table.rows):
                for c, cell in enumerate(row.cells):
                    if not cell.is_spanned:
                        yield "%s[r%dc%d]" % (where, r, c), cell.text
    if slide.has_notes_slide and slide.notes_slide.notes_text_frame is not None:
        yield "notes", slide.notes_slide.notes_text_frame.text


def check_deck(path, terms):
    try:
        prs = Presentation(path)
    except Exception as e:  # keep going when one file cannot be opened
        return [(path.name, "", "", "", "", "open failed: %s" % type(e).__name__)]
    rows = []
    for i, slide in enumerate(prs.slides, 1):
        for where, text in texts(slide):
            for t in terms:
                if t["term"] in text:
                    rows.append((path.name, i, where, t["term"], t["action"], text.strip().replace("\n", " / ")))
    return rows


def main():
    terms = load_terms(TERMS)
    decks = sorted(FOLDER.glob("*.pptx"))
    rows = [r for path in decks for r in check_deck(path, terms)]
    with open("out_deck_check.csv", "w", encoding="utf-8-sig", newline="") as f:
        w = csv.writer(f)
        w.writerow(["deck", "slide", "where", "term", "action", "text"])
        w.writerows(rows)
    print("decks:", len(decks), "hits:", len(rows))
    for deck, n in Counter(r[0] for r in rows).most_common():
        print("  %s %d" % (deck, n))
    for r in rows[:8]:
        print(" | ".join(str(x) for x in r[:5]))


if __name__ == "__main__":
    main()
Output(2026-09-12)
decks: 3 hits: 5
  deck_a.pptx 4
  docs_deck_c.pptx 1
deck_a.pptx | 1 | Content Placeholder 2 | 業界初 | 根拠を確認する
deck_a.pptx | 2 | notes | 必ず | 言い換える
deck_a.pptx | 3 | Content Placeholder 2 | 唯一 | 根拠を確認する
deck_a.pptx | 3 | Content Placeholder 2 | 出来る | 「できる」に統一
docs_deck_c.pptx | 2 | Group 5>TextBox 4 | 出来る | 「できる」に統一
Example instruction for Copilot

Using python-pptx, write code that checks text boxes, placeholders, shapes inside groups, table cells and speaker notes (checking has_notes_slide first) in every .pptx in a folder against the terms in style_terms.csv and collects all hits in one CSV. If a file cannot be opened, record the error name and continue.

Caution

Matching is plain substring search, so negated or quoted uses also appear; a person decides what to change. Text in charts, text inside images, and text on slide masters and layouts is not read. The decks are opened read-only in effect (nothing is saved), so the originals stay unchanged. Treat the CSV with the same confidentiality as the decks it came from.

Availability
Tested with python-pptx 1.0.2 (Python 3.12 venv on Windows 11); the official docs are published for 1.0.0. PyPI lists Python 3.8 or later. Works on .pptx; legacy .ppt files cannot be opened.
Requires
python-pptx 1.0.2
Tested
run and checked locally (2026-09-12, Python 3.12.10 (venv) / Windows 11, python-pptx 1.0.2)
Source
python-pptx, "Working with text"
python-pptx, "Shapes"
python-pptx, "Slides"
Verified
2026-09-12 (v1)
Supporting passages from the sources
Auto shapes and table cells can contain text. Other shapes can’t. Text is always manipulated the same way, regardless of its container.
use this property to test for a notes slide without the possible side effect of creating one.
A spanned cell can be identified with its _Cell.is_spanned property.

07Build a new summary deck from the titles and counts of several decksVerified

Read slide titles, table counts and the number of slides with notes from several decks, then write a cover slide, one title list per deck and a totals table into a new deck created with Presentation(). Layouts are picked by name with get_by_name() rather than by index, which is less error-prone when the template changes.

Approach and steps
  1. Open each source deck and count slide titles from slide.shapes.title, shapes with has_table, and slides that have notes.
  2. Create a new deck with Presentation() and pick layouts with slide_layouts.get_by_name("Title Slide") and so on; stop if a layout is missing.
  3. Add one Title and Content slide per deck and list its slide titles as level-1 paragraphs in the body placeholder.
  4. Finish with a Title Only slide holding a totals table created with shapes.add_table.
  5. Save as out_summary.pptx, reopen it and check the slide count.
PythonRuns locally
"""Build a new summary deck: one slide per source deck listing its slide titles, plus a totals table."""
from pathlib import Path

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

DECKS = sorted(Path("samples").glob("deck_*.pptx"))
OUT = Path("out_summary.pptx")


def describe(path):
    titles, tables, notes = [], 0, 0
    for slide in Presentation(path).slides:
        t = slide.shapes.title
        titles.append(t.text_frame.text.strip() if t is not None else "(no title)")
        tables += sum(1 for s in slide.shapes if s.has_table)
        if slide.has_notes_slide and slide.notes_slide.notes_text_frame.text.strip():
            notes += 1
    return {"name": path.name, "titles": titles, "tables": tables, "notes": notes}


def layout(prs, name):
    found = prs.slide_layouts.get_by_name(name)
    if found is None:
        raise SystemExit("layout not found in template: %s" % name)
    return found


def build(infos, out):
    prs = Presentation()  # built-in default template; open your own .pptx to reuse its layouts
    cover = prs.slides.add_slide(layout(prs, "Title Slide"))
    cover.shapes.title.text = "Deck summary"
    cover.placeholders[1].text = "%d decks" % len(infos)
    for info in infos:
        slide = prs.slides.add_slide(layout(prs, "Title and Content"))
        slide.shapes.title.text = info["name"]
        tf = slide.placeholders[1].text_frame
        tf.text = "%d slides" % len(info["titles"])
        for i, title in enumerate(info["titles"], 1):
            p = tf.add_paragraph()
            p.text = "%d. %s" % (i, title)
            p.level = 1
    last = prs.slides.add_slide(layout(prs, "Title Only"))
    last.shapes.title.text = "Totals"
    n = len(infos) + 1
    table = last.shapes.add_table(n, 4, Inches(0.5), Inches(1.8), Inches(9), Inches(0.4) * n).table
    for c, head in enumerate(["deck", "slides", "tables", "slides with notes"]):
        table.cell(0, c).text = head
    for r, info in enumerate(infos, 1):
        for c, value in enumerate([info["name"], len(info["titles"]), info["tables"], info["notes"]]):
            table.cell(r, c).text = str(value)
            table.cell(r, c).text_frame.paragraphs[0].runs[0].font.size = Pt(14)
    prs.save(out)


def main():
    if any(OUT.resolve() == p.resolve() for p in DECKS):
        raise SystemExit("output must not overwrite an input")
    infos = [describe(p) for p in DECKS]
    build(infos, OUT)
    print("saved", OUT, "slides:", len(Presentation(OUT).slides))
    for info in infos:
        print("  %s: %d slides, %d tables, %d with notes" % (
            info["name"], len(info["titles"]), info["tables"], info["notes"]))


if __name__ == "__main__":
    main()
Output(2026-09-12)
saved out_summary.pptx slides: 4
  deck_a.pptx: 3 slides, 1 tables, 2 with notes
  deck_b.pptx: 2 slides, 0 tables, 1 with notes
Example instruction for Copilot

Using python-pptx, write code that collects slide titles, table counts and the number of slides with notes from deck_*.pptx files, then builds a new presentation with a cover, one title list per deck and a totals table. Select layouts by name with slide_layouts.get_by_name() and stop if one is missing. Do not re-save the source files.

Caution

Layout names depend on the template. With a company template, open it with Presentation(path) and print the names in prs.slide_layouts before choosing. This script only copies titles and does not write summaries of the content; if you need prose summaries, use Copilot's summary feature and have a person review the result.

Availability
Tested with python-pptx 1.0.2 (Python 3.12 venv on Windows 11); the official docs are published for 1.0.0. PyPI lists Python 3.8 or later. Works on .pptx; legacy .ppt files cannot be opened.
Requires
python-pptx 1.0.2
Tested
run and checked locally (2026-09-12, Python 3.12.10 (venv) / Windows 11, python-pptx 1.0.2)
Practical edition
PowerPoint ── Summarize a deck and find key slides
Source
python-pptx, "Slides"
python-pptx, "Getting Started"
python-pptx, "Presentations"
Verified
2026-09-12 (v1)
Supporting passages from the sources
Return SlideLayout object having name , or default if not found.
If pptx is missing or None, the built-in default presentation “template” is loaded.
Not all shapes can contain text, but those that do always have at least one paragraph, even if that paragraph is empty and no text is visible within the shape.