OPF
DocsHow-to

Generate a presentation in Python

Build the deck as JSON in Python, then let the OPF CLI validate it and write PowerPoint and PDF.

How it works

To generate a presentation in Python, build the deck as a dictionary (by hand, from your data, or from a language model's structured output), save it as OPF JSON, and call the OPF CLI with subprocess to validate it and write PowerPoint (PPTX) or PDF. There is no OPF package for Python; the CLI is a Node.js program, so the machine also needs Node 22 or later. No PowerPoint or LibreOffice install is needed.

Checked on October 29, 2026 with Python 3.9.6, Node 26.7.0, OPF CLI 0.29.1 and pydantic 2.13.5: both scripts on this page ran and printed the output shown. A unit test in this site's CI runs the export script against the installed CLI.

Set up once

In the folder where the script will run, install the CLI, the renderer, the PowerPoint exporter and the fonts they measure text with. Each line is a separate command; see the first local run for details.

shell
npm init -y
npm install --save-dev @openpresentation/cli@0.31.0 @openpresentation/opf-render@0.31.0 @openpresentation/opf-pptx@0.31.0
npm install --save-dev @expo-google-fonts/roboto@0.4.3 @expo-google-fonts/roboto-mono@0.4.2 @expo-google-fonts/caladea@0.4.2 @expo-google-fonts/arimo@0.4.3 @expo-google-fonts/tinos@0.4.2 @expo-google-fonts/cousine@0.4.3 @expo-google-fonts/gelasio@0.4.1 @expo-google-fonts/noto-sans@0.4.2

The script

The script writes deck.opf.json, runs opf validate, and converts the deck to PPTX and PDF. Every opf command prints a JSON report, so Python reads the result instead of parsing text. Only the Python standard library is needed.

export_deck.py
"""Validate an OPF deck and export it to PPTX and PDF with the OPF CLI.

Run it in a folder where the OPF CLI and its export packages are installed.
"""
import json
import subprocess
import sys
from pathlib import Path

deck = {
    "$schema": "https://openpresentation.org/schema/opf/v1",
    "name": "Q2 retention update",
    "language": "en-US",
    "slides": [
        {"title": "Q2 retention update", "subtitle": "Board of Directors"},
        {"title": "What changed", "items": ["Renewals rose in the pilot", "Fewer support escalations"]},
        {"title": "Next quarter", "text": "Extend the pilot to two more regions."},
    ],
}


def opf(*args):
    """Run an opf command and return its JSON report. Exit if the command failed."""
    result = subprocess.run(["npx", "opf", *args], capture_output=True, text=True)
    report = json.loads(result.stdout)
    if result.returncode != 0:
        sys.exit(f"opf {args[0]} failed: {report.get('error') or report['findings']}")
    return report


Path("deck.opf.json").write_text(json.dumps(deck, indent=2))

report = opf("validate", "deck.opf.json")
print("valid:", report["valid"], report["counts"])

for target in ("deck.pptx", "deck.pdf"):
    result = opf("convert", "deck.opf.json", target, "--force")
    print("wrote", target, result["outputs"][0]["bytes"], "bytes")
python3 export_deck.py
valid: True {'error': 0, 'warning': 0, 'info': 0}
wrote deck.pptx 87338 bytes
wrote deck.pdf 18693 bytes

If the deck is invalid, opf validate exits 1 and the script stops with the findings. For example, an unknown top-level field:

output (shortened)
opf validate failed: [{'ruleId': 'opf/schema', 'severity': 'error', 'category': 'format', 'scope': 'document', 'path': '/x', 'message': 'must NOT have additional properties', ...}]

Swap deck.pdf for slides/deck.png to get one PNG per slide. The CLI guide lists every command and flag.

Check model output

When a model writes the deck, parse its JSON with a Pydantic model before you save it. This catches a missing title or a wrong type in Python, and exclude_none drops the empty fields some structured-output modes return as null. The CLI still validates the result against the full OPF schema.

check_deck.py
from typing import List, Optional

from pydantic import BaseModel, Field


class Slide(BaseModel):
    title: str
    subtitle: Optional[str] = None
    text: Optional[str] = None
    items: Optional[List[str]] = None


class Deck(BaseModel):
    schema_: str = Field("https://openpresentation.org/schema/opf/v1", alias="$schema")
    name: str
    language: str = "en-US"
    slides: List[Slide]


# The JSON text your model returned.
llm_output = '{"name": "Q2 retention update", "slides": [{"title": "What changed", "items": ["Renewals rose", "Fewer escalations"], "text": null}]}'

deck = Deck.model_validate_json(llm_output).model_dump(by_alias=True, exclude_none=True)
print(deck)
python3 check_deck.py
{'$schema': 'https://openpresentation.org/schema/opf/v1', 'name': 'Q2 retention update', 'language': 'en-US', 'slides': [{'title': 'What changed', 'items': ['Renewals rose', 'Fewer escalations']}]}

Each provider has its own rules for structured-output schemas. The OpenAI and Claude guides have starter schemas that follow them; pass the same schema from Python.

OPF or python-pptx?

python-pptx is a Python library that creates and edits .pptx files through PowerPoint's object model: slides, shapes and text frames. It needs no Node.js, and it is the better fit when you fill shapes in an existing PowerPoint template or change a file in place. It writes .pptx only; it does not render PDF or images.

OPF fits when a program or a model writes the deck: the source is one JSON document that can be validated, diffed and reviewed, and the same file exports to PPTX, PDF and PNG. See OPF vs other formats.