Skip to main content
paper-pptx logo

paper-pptx

PyPI Python versions Test

An import-compatible, agent-first structure editor for PowerPoint files, designed to prevent silent corruption when editing existing decks.

paper-pptx is a drop-in hard fork of python-pptx v1.0.2 for safely inspecting, editing, composing, and verifying existing PowerPoint (.pptx) presentations. It keeps python-pptx's package layer, XML mapping, and object model. It adds the rendered values a deck shows, edits that survive PowerPoint's run fragmentation, and refusals in place of guesses.

from pptx import Presentation   # the import name is unchanged (see "Drop-in by design")

The fork exists to prevent silent corruption: a deck that opens without error but is wrong. Automated systems cannot eyeball a slide, so every added operation returns its outcome as typed, machine-readable data, and an operation that cannot proceed safely raises a typed refusal and leaves the presentation byte-for-byte unchanged.


Why paper-pptx exists

python-pptx is excellent at creating decks. Its lossless package layer, disciplined XML mapping, and a decade of absorbed edge cases are why this fork builds on it.

The harder problem is changing an existing deck without flattening formatting, stranding relationships, or losing content outside the slide body. Stock run.font.size returns None for any value inherited through the placeholder chain, the only stock write path flattens every run in a paragraph, and there is no public clone, delete, or reorder for slides. An agent cannot look at the result, so it needs the deck's structure and every edit outcome as typed data, and it needs the library to refuse unsafe edits.

Quick start

from pptx import Presentation
from pptx.diff import diff_decks
from pptx.edit import replace_text

prs = Presentation("deck.pptx")

run = prs.slides[0].shapes.title.text_frame.paragraphs[0].runs[0]
font = run.effective_font()
print(font.size.value_pt)              # resolved through layout/master/theme

replace_text(prs, "FY25", "FY26")      # preserves untouched run formatting
prs.save("deck.v2.pptx")               # atomic on a path: the old file survives any failure

delta = diff_decks("deck.pptx", "deck.v2.pptx", detail="text")
print(len(delta.slide_changes), "slides changed")

What paper-pptx adds

Perceive: read what the deck renders

  • pptx.inspect effective values, with provenance. effective_font(), effective_paragraph_format(), and effective_shape_format() resolve size, typeface, color, alignment, line spacing, and bullets through the run, paragraph, placeholder, layout, master, and theme chain, and report which rung supplied each value. Bullet typeface and size resolve on their own chains, because the schema inherits them separately from the glyph. Unresolved values are reported as unresolved.
  • Visibility-complete text inspection. inspect_text() reaches nested groups and table cells, which iterating top-level shapes misses, and returns structural anchors that resolve a slide-unique shape or table cell before validating a full content fingerprint. Regions it cannot survey are reported as blind blocks rather than skipped.
  • A deterministic deck manifest. inspect_deck() emits a versioned, JSON-friendly structural manifest: slides, shapes, z-order, geometry, and placeholder identity.

Edit: change one deck without flattening it

  • Anchored, formatting-preserving replacement. pptx.edit.replace_text and replace_text_at rewrite text while leaving untouched runs byte-identical. The stock shape.text = ... flattens every run's formatting in the paragraph.
  • Relationship-safe slide lifecycle. slides.clone/delete/move/reorder maintain sections, custom shows, and relationships. Upstream has no public equivalent, and the folk XML recipes strand both.
  • Real bullets. paragraph.bullet authors genuine a:buChar and a:buAutoNum state, including bullet typeface and size, instead of typing - or into the text.
  • Autofit made explicit. text_frame.normalize_autofit() freezes PowerPoint's invisible normAutofit scale percentages so an edit does not resize text without warning.
  • Notes without side effects. slide.read_notes_text() never creates a notes part; reading slide.notes_slide upstream does.
  • Typed, group-aware lookup. shape_by_name(), picture_by_name(), table_by_name(), and chart_by_name() recurse into groups and refuse a duplicate rather than returning the first match.
  • Surgery that keeps the package consistent. SlideShapes.delete/move/add_copy, table row and column insert and delete, Picture.replace_image(), and Chart.replace_data_safe() maintain relationships and owned parts.
  • Batched validation. with prs.batch(): validates once at block exit instead of once per mutating call, and discards every edit in the block if that check fails.
  • SlideLayouts.remove() hardening. Same signature, stricter semantics: stale or foreign proxies and unsafe states refuse atomically instead of partially mutating.

Compose: assemble decks across files

  • Cross-deck import with explicit fidelity modes. Presentation.import_slide() and append_deck() make the inheritance trade-off a required argument: adopt_theme, keep_appearance, or bake. Each returns an ImportReport naming every part added, reused, and dropped.
  • Layout rebind with a shift report. Slide.rebind_layout() moves a slide under explicit placeholder and orphan policies and reports every run whose resolved font changed. Placeholder geometry and text direction are inherited from the layout and sit outside that comparison.
  • Real fields, not static text. apply_footers() writes genuine a:fld slide-number and date fields, so PowerPoint refreshes them.

Verify: prove what changed

  • A semantic deck diff. pptx.diff.diff_decks() matches slides by permanent slide ID and top-level shapes within them by slide-wide shape ID, so slide or shape z-order does not manufacture add/remove or facet changes. Within each stable text container, schema v5 reports one exact snapshot hunk bounded by the longest common prefix and suffix: insertion, deletion, one-paragraph replacement, or changed_region. It never infers paragraph moves or a historical location for duplicate content. Table changes report exact dimensions, while detail="full" limits per-run and bullet shifts to unique unchanged paragraphs. package_changes retains semantic package-level evidence for unsupported facets.
  • Byte-minimal saves and a package oracle. pptx.package.patch_save() writes semantically unchanged parts back with their original bytes, so a one-line edit diffs as a few parts rather than all of them, and diff_package() reports exactly which parts differ.

Package intake and save

  • Guarded package intake. Opening a .pptx rejects ambiguous or unsafe ZIP archives: duplicate or case-colliding member names, path traversal, encrypted or exotically-compressed members, lying size headers, an archive that does not span its file exactly, and any member that resolves to no content type. A part that nothing references is kept; PowerPoint opens that package and drops the part on its next save, and so does save().
  • Atomic save. Saving to a path writes a sibling temporary file and replaces the destination only after serialization succeeds, preserving the existing file's permission bits and resolving symlinks. Stream saves stage the whole package first, so a serialization failure emits nothing, and restore the destination on failure when it can be read and rewound; a write-only sink keeps whatever landed.

Safety contract

Every added operation either does exactly what it claims or refuses atomically. Mutating operations run inside a package transaction: whatever the operation touched is restored if it refuses, and the deck-wide check runs before anything commits. Some operations, including apply_footers(), append_deck(), import_slide() and slide clone, also validate in full before touching anything.

A refusal raises a typed error from the hierarchy rooted at pptx.errors.PaperRefusal and leaves the presentation byte-for-byte unchanged in memory and on disk. The subclasses say what went wrong: PackageLimitError, TargetNotFoundError, StaleAnchorError, AmbiguousTargetError, UnsupportedStructureError, RelationshipPolicyError, and BoundaryViolationError. Programmer mistakes remain plain ValueError or TypeError, so callers can catch PaperRefusal separately.

A refused edit is a success mode; the worst outcome this library can produce is a file that opens without error but is wrong. Held proxies survive a refusal, and a stale handle raises TargetNotFoundError instead of editing a neighbor. Each documented refusal condition has a test asserting both that the typed refusal is raised and that output bytes equal input bytes.

Drop-in by design

Only the distribution and repository are renamed. The importable package stays pptx. This is the same distribution/import split as Pillow (pip install pillow, import PIL), and it preserves existing code, snippets, and model priors.

  • GitHub repository / PyPI distribution: paper-pptx
  • Python import: pptx
  • Fork sentinel: pptx.__paper_version__

Every documented upstream API behaves as upstream documents it: from pptx import Presentation, pptx.util, pptx.chart.data, placeholder access, shape trees. New upstream releases are merged, never rebased, so the fork retains its history and compatibility.

Installation

Requires Python 3.9+. Upstream v1.0.2 supported Python 3.8.

python -m pip uninstall -y python-pptx paper-pptx
python -m pip install paper-pptx

The clean uninstall is required when migrating from python-pptx. Both distributions own the same pptx import package, and pip cannot safely overlay or uninstall two distributions that own the same files. If both are installed, import pptx refuses with an ImportError rather than running an unverifiable mix of the two.

Confirm the install:

paper-pptx-doctor

Documentation

The Sphinx docs extend the upstream python-pptx documentation to cover the fork's additions: start with docs/user/paper-additions.rst and the docs/api/*.rst reference pages. Everything inherited from python-pptx works as documented at the python-pptx documentation.

Current limitations

Converting a documented typed refusal into a correct operation is the sanctioned growth path of this package. The known gaps:

  • Notes parts are never created. read_notes_text and replace_notes_text work only on existing notes.
  • Table-cell effective values refuse until the table-style resolution walk is built.
  • Chart data replacement supports single-plot category charts. XY, bubble, stock, surface, radar, 3-D, and multi-plot combos refuse.
  • Bullet color is not resolved, and bullet diffing needs detail="full" and skips table cells.
  • diff_decks assumes lineage, matching slides by permanent ID and top-level shapes by slide-wide shape ID. It is not a visual or edit-history diff; group-boundary shape moves remain add/remove, paragraph moves are not inferred, and deleted IDs reused by new same-kind shapes cannot be distinguished without a persistent identifier general PPTX files do not carry.
  • Fixture provenance. The test corpus is generated and LibreOffice round-tripped, not authored by PowerPoint.

Deliberate non-goals: no rendering or layout-geometry computation, no SmartArt authoring (opaque preservation only), and no animation or transition authoring.

Testing

  • Upstream's pytest and behave suites run on every change to check compatibility with existing behavior.
  • A frozen, hash-pinned fixture corpus includes generated presentations and LibreOffice round-trips.
  • The contract harness checks refusal atomicity and validates the fixture corpus with a headless LibreOffice load smoke.

Contributing

Contributions are welcome. See CONTRIBUTING.md for the engineering discipline this fork runs on. The short version: the upstream suite must remain green; persistence changes need saved-and-reopened assertions and exact package-delta checks; guarded refusals must be atomic and leave bytes unchanged; and a refusal message must name what was found, why it is unsafe, and what to do about it.

Community

Acknowledgments

paper-pptx exists because python-pptx is excellent. Steve Canny and the python-pptx contributors built the lossless package layer, the disciplined XML mapping, and a decade of absorbed edge cases that make safe deck editing possible at all. This fork stands on that work and keeps their API intact.

Citation

If you reference paper-pptx in research or writing:

@software{paper_pptx,
  title   = {paper-pptx: an agent-first structure editor for PowerPoint files},
  author  = {{Paper Instruments, Inc.}},
  year    = {2026},
  url     = {https://github.com/paper-instruments/paper-pptx}
}

Cite it as a fork of python-pptx by Steve Canny and contributors.

License

MIT, inherited from python-pptx. Original work © 2013 Steve Canny and the python-pptx contributors; fork additions © 2026 Paper Instruments, Inc. This fork preserves the upstream license and attribution. See LICENSE.

Release files for paper-pptx 0.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for paper-pptx 0.2.0
File Size Uploaded
paper_pptx-0.2.0.tar.gz 11.4 MB Details

Built distribution (wheel)

Table of built distributions (wheels) for paper-pptx 0.2.0
File Interpreter ABI Platform
paper_pptx-0.2.0-py3-none-any.whl Python 3 none any Details

Total release size: 12.0 MB

Release files / paper_pptx-0.2.0.tar.gz

Download URL paper_pptx-0.2.0.tar.gz
Size 11.4 MB
Tags Source
SHA-256 checksum
How to use checksums
719723e97d21aecc7bd77c269fb33838cb803042276d743fafb310d2c97ed45a
BLAKE2b-256 checksum
How to use checksums
d01debe0168e429fa3ec3326ac9f960322ae827a606d1d696d4ae096cc5f6062
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.13

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 24, 2026.

Transparency log

Release files / paper_pptx-0.2.0-py3-none-any.whl

Download URL paper_pptx-0.2.0-py3-none-any.whl
Size 617.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
8fc5fe9a52f37448aba8304cf2e2b8952dda6e1a1a2154d9c1f95651c6e9f9d0
BLAKE2b-256 checksum
How to use checksums
ea02aeabf96191e9a532d64619338547bcfbdae6a42ca6e179ae2b414f2d8d08
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.13

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 24, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 release files

0.1.2

2 release files

0.1.1

2 release files

0.1.0

2 release files

0.0.1

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page