Skip to content

DX trap: paragraph.font.size writes defRPr, causing PowerPoint UI font-size bounce-back #1135

@YTH-coding

Description

@YTH-coding

Not a bug — behavior is OOXML-correct — but a significant DX / documentation gap that silently breaks the primary use case of generating editable .pptx files.

Summary

When users set paragraph.font.size = Pt(14) in python-pptx, the generated .pptx appears correct when opened. However, manual editing in PowerPoint fails: changing the font size from 14pt to 18pt in the UI causes it to snap back to 14pt. Changing to 19pt or higher "works" but actually triggers clipping instead of resizing. This behavior is highly confusing because the symptom depends on font size, text box dimensions, and line count — making it appear non-deterministic.

Root Cause

paragraph.font.size writes to <a:defRPr> (default run properties at the paragraph level), not to <a:rPr> (explicit run properties). PowerPoint's UI treats defRPr sz="1400" as an anchor value — when the user attempts to override it through the UI, PowerPoint evaluates whether the new size fits within the text box boundaries and, at certain thresholds, reverts to the defRPr size.

In contrast, run.font.size writes directly to <a:rPr sz="1800">, which PowerPoint treats as the actual run size and allows free adjustment.

python-pptx API OOXML output PowerPoint editing behavior
p.font.size = Pt(14) <a:defRPr sz="1400"> Font size resets on manual edit
run.font.size = Pt(14) <a:rPr sz="1400"> Fully editable

Why This Matters

The entire point of generating .pptx (as opposed to .pdf) is that the recipient can edit it. Academic reports, business presentations, and course assignments nearly always require manual touch-ups after generation. This API behavior silently breaks that workflow — and the symptoms are so counterintuitive that users typically spend hours debugging autofit settings, text box properties, and font configurations before discovering the real cause.

Proposed Solutions

  1. Documentation (minimal): Add a warning in the paragraph.font.size docstring and the user guide that paragraph-level font sizes create defRPr, which affects PowerPoint UI editability. Recommend run.font.size as the preferred approach.

  2. API convenience (ideal): Provide a Paragraph.set_font_at_run_level(size, bold, color, ...) method that transparently creates a run with explicit <a:rPr> properties, or optionally make paragraph.font.size set all existing runs' sizes when the paragraph already contains text.

  3. Working pattern users can adopt today:

from pptx.util import Pt

def set_paragraph_text(p, text, size, bold=False, color=None):
    """Clear paragraph and add a single run with explicit font properties."""
    p.clear()
    run = p.add_run()
    run.text = text
    run.font.size = Pt(size)
    run.font.bold = bold
    if color:
        run.font.color.rgb = color

Verification

Attached is a minimal reproduction script that generates a slide with one paragraph using p.font.size and one using run.font.size. The behavior difference is immediately apparent upon manual editing.

from pptx import Presentation
from pptx.util import Inches, Pt
from pptx.dml.color import RGBColor

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

# Text box using p.font.size (will bounce back on manual edit)
tb1 = slide.shapes.add_textbox(Inches(1), Inches(1), Inches(5), Inches(1))
tf1 = tb1.text_frame
p1 = tf1.paragraphs[0]
p1.text = "paragraph.font.size — try changing to 18pt in PowerPoint"
p1.font.size = Pt(14)
p1.font.color.rgb = RGBColor(0x33, 0x33, 0x33)

# Text box using run.font.size (works fine)
tb2 = slide.shapes.add_textbox(Inches(1), Inches(2.5), Inches(5), Inches(1))
tf2 = tb2.text_frame
p2 = tf2.paragraphs[0]
p2.clear()
run = p2.add_run()
run.text = "run.font.size — try changing to 18pt in PowerPoint"
run.font.size = Pt(14)
run.font.color.rgb = RGBColor(0x33, 0x33, 0x33)

prs.save("repro.pptx")

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions