power-pptx

API: Text

Signature reference for the text stack. The prose, motivation and worked patterns live onText & Typography; measurement and the fit guarantee are covered underSpace-Aware Authoring.

Text nests three levels deep: shape.text_frameTextFrame.paragraphs_Paragraph.runs. Only runs hold characters; only a run's Font is character-level formatting. Classes with a leading underscore are never constructed directly — you always reach them from a shape.

TextFrame

from power_pptx import Presentation
from power_pptx.enum.text import MSO_AUTO_SIZE, MSO_VERTICAL_ANCHOR
from power_pptx.util import Inches

prs = Presentation()
slide = prs.slides.add_slide(prs.slide_layouts[6])
shape = slide.shapes.add_textbox(Inches(1), Inches(1), Inches(8), Inches(2))

shape.has_text_frame                # False for pictures, connectors, ...
tf = shape.text_frame

tf.text = "Line one\nLine two"      # \n -> paragraph, \v -> line break
tf.word_wrap = True
tf.auto_size = MSO_AUTO_SIZE.TEXT_TO_FIT_SHAPE
tf.vertical_anchor = MSO_VERTICAL_ANCHOR.MIDDLE
tf.margin_left = Inches(0.1)        # also margin_right / _top / _bottom
tf.column_count = 2                 # 1..16
tf.column_spacing = Inches(0.25)

tf.paragraphs                       # tuple[_Paragraph, ...] - fresh objects!
tf.add_paragraph()                  # -> _Paragraph, appended
tf.clear()                          # all paragraphs but one empty one
MemberTypeNotes
textstrRead/write. Paragraphs joined by "\n", line breaks as "\v". Assignment replaces everything
paragraphstuple[_Paragraph, ...]Read-only; always at least one. New wrapper objects on every access
word_wrapbool | NoneNone inherits from the style hierarchy
auto_sizeMSO_AUTO_SIZE | NoneNONE, SHAPE_TO_FIT_TEXT, TEXT_TO_FIT_SHAPE
vertical_anchorMSO_VERTICAL_ANCHOR | NoneTOP / MIDDLE / BOTTOM
margin_left, margin_right, margin_top, margin_bottomLengthText insets; default 0.1" left/right, 0.05" top/bottom
column_countint1..16, else ValueError. Reads 1 when unset; assigning 1 removes the attribute
column_spacingLength | NoneGutter between columns
add_paragraph()_ParagraphAppends and returns a new empty paragraph
clear()NoneRemoves all paragraphs but one, and empties it
fit_text(...)int | NoneSee below
set_paragraph_defaults(...)NoneSee below

set_paragraph_defaults fork

TextFrame.set_paragraph_defaults(
    *,
    font_name: str | None = None,
    size: Length | None = None,
    bold: bool | None = None,
    italic: bool | None = None,
    color: object | None = None,     # RGBColor | "#RRGGBB" | (r, g, b)
) -> None

Keyword-only. Applies each supplied property to every paragraph's a:defRPr and to every run inside it, only where that property is currently unset — explicit per-run formatting is preserved verbatim. A run carrying any explicit colour (RGB, theme, preset or system) is skipped by color. Operates on the paragraphs that exist at call time, so run it after the text is in place.

from power_pptx.util import Pt

tf.text = "Total contract value\nUp 27% QoQ\nChurn below 2%"
tf.paragraphs[0].runs[0].font.bold = True      # explicit, must survive

tf.set_paragraph_defaults(font_name="Inter", size=Pt(14), color="#222222")

[(p.runs[0].font.size.pt, p.runs[0].font.bold) for p in tf.paragraphs]
# [(14.0, True), (14.0, None), (14.0, None)]

_Paragraph

from power_pptx.enum.text import PP_ALIGN
from power_pptx.util import Inches, Pt

p = tf.paragraphs[0]
p.text = "Hello"                   # replaces all runs with one
p.alignment = PP_ALIGN.CENTER      # None inherits
p.level = 1                        # indent level, int 0..8
p.line_spacing = 1.2               # float (multiple) or Length
p.space_before = Pt(6)
p.space_after = Pt(6)
p.rtl = True                       # right-to-left; tri-state
p.font                             # Font for a:defRPr - run defaults
p.runs                             # tuple[_Run, ...]

p.add_line_break()                 # soft return, reads back as "\v"
p.clear()                          # drop content, keep paragraph properties

p.set_numbered("romanLcPeriod", start_at=3)   # auto-numbered list
p.start_at                         # 3 (None = no explicit start)

p.tab_stops.add_tab_stop(Inches(3.0), "right")
len(p.tab_stops), p.tab_stops[0].position, p.tab_stops[0].alignment
MemberTypeNotes
textstrRead/write. Assignment replaces all runs with a single run
runstuple[_Run, ...]Fresh wrapper objects on every access
fontFontThe paragraph's a:defRPr — defaults its runs inherit
alignmentPP_ALIGN | NoneHorizontal; None inherits
levelintIndent / outline level, 0..8. Bullet glyph and indent come from the master's list style for that level
line_spacingint | float | Length | NoneA number is a multiple of single spacing; a Length is exact
space_before, space_afterLength | NoneSpace above / below the paragraph
rtlbool | NoneRight-to-left layout direction; tri-state
start_atint | NoneFirst number of an auto-numbered list. None both when not numbered and when starting at 1. Assigning an int to a non-numbered paragraph makes it "arabicPeriod"
set_numbered(scheme="arabicPeriod", start_at=None)NoneReplaces any existing bullet. Schemes include "arabicPeriod", "arabicParenR", "romanLcPeriod", "alphaUcParenR"
tab_stopsTabStopsSee below
add_run()_RunAppends and returns a new run
add_line_break()NoneSoft return inside the paragraph — one bullet, one spacing
clear()_ParagraphRemoves runs, breaks and fields; paragraph properties survive

TabStops and TabStop

  • TabStops — sequence over a:tabLst: __len__, __iter__, __getitem__
  • TabStops.add_tab_stop(position: Length, alignment: str = "left") -> TabStop"left", "center", "right" or "decimal"
  • TabStop.positionLength | None, read/write
  • TabStop.alignmentstr, read/write

_Run and Font

_Run has exactly three members: text (str, read/write),font (Font) and hyperlink (_Hyperlink). Everything else is on the font.

from power_pptx.enum.dml import MSO_THEME_COLOR
from power_pptx.enum.lang import MSO_LANGUAGE_ID
from power_pptx.util import Pt

r = p.add_run()
r.text = " world"

f = r.font                         # also p.font (paragraph-level defaults)
f.name = "Inter"
f.size = Pt(18)                    # Length; read back with .pt
f.bold = True
f.italic = False
f.underline = True                 # or an MSO_UNDERLINE member
f.color.rgb = "#0B5CFF"            # hex / tuple / RGBColor
f.color.theme_color = MSO_THEME_COLOR.ACCENT_1     # ... or a theme colour
f.fill                             # full FillFormat (gradient, pattern)

f.all_caps = True                  # cap="all"
f.small_caps = True                # shares cap=, so this replaces all_caps
f.strikethrough = True             # strike="sngStrike"
f.superscript = True               # baseline=30%
f.subscript = True                 # baseline=-25%, and replaces superscript
f.letter_spacing = Pt(1.5)         # tracking; negative tightens
f.language_id = MSO_LANGUAGE_ID.FRENCH
f.caps                             # raw accessor: "none" | "small" | "all"

f.outline.color.rgb = "FF0000"     # LineFormat over the glyphs
f.outline.width = Pt(1)
f.shadow.blur_radius = Pt(3)       # ShadowFormat
f.glow.radius = Pt(6)              # GlowFormat
PropertyTypeXMLNotes
namestr | Nonea:latin/@typefaceNone = inherit the theme typeface
sizeLength | NoneszAssign Pt(n); read back .pt
bold, italicbool | Noneb, iTri-state
underlinebool | MSO_TEXT_UNDERLINE_TYPE | NoneuTrue = single. Reads back True/False for single/none, else the enum member
colorColorFormata:solidFillNon-mutating reads — nothing is written until you assign
fillFillFormatfill groupThe full fill surface behind color
all_capsbool | Nonecap="all"One cap attribute, so mutually exclusive. False writes cap="none"
small_capsbool | Nonecap="small"
capsstr | NonecapRaw accessor: "none" / "small" / "all"
strikethroughbool | NonestrikeTrue writes sngStrike; reads True for either strike variant
superscriptbool | Nonebaseline > 0One baseline attribute. True = +30% / -25%. Clearing one never cancels the other
subscriptbool | Nonebaseline < 0
letter_spacingLength | NonespcTracking; negative tightens
language_idMSO_LANGUAGE_ID | NonelangReads back MSO_LANGUAGE_ID.NONE — not Python None — when unset
outlineLineFormata:lnGlyph stroke: .color, .width
shadowShadowFormata:effectLstLazy — no effect XML until assigned
glowGlowFormata:effectLst
link = p.add_run()
link.text = "read the appendix"
link.hyperlink.address = "https://example.com"   # http/https/mailto/file
link.hyperlink.address = None                    # remove, drop the rel

jump = p.add_run()
jump.text = "Go to the detail slide"
jump.hyperlink.target_slide = prs.slides[3]      # internal slide jump
jump.hyperlink.target_slide                      # Slide, or None if external
  • addressstr | None. One setter for add, change and remove; assigning None deletes the a:hlinkClick and drops the relationship. For a run holding an internal jump this reads back as the target part name (e.g. "slide2.xml"), not None.
  • target_slideSlide | None. Writes appaction://hlinksldjump action. Returns None when there is no hyperlink or when it is external, so this — not address — is the correct test for "is this an internal link".

fit_text fork

TextFrame.fit_text(
    font_family: str | None = None,   # None -> "Calibri"
    max_size: int = 18,
    bold: bool = False,
    italic: bool = False,
    font_file: str | None = None,     # pin the metrics to a .ttf
    strict: bool = False,             # raise instead of estimating
) -> int | None                       # applied point size, None if empty

Measures the frame's actual string with Pillow font metrics and writes the largest integer point size that fits into the XML before save. Also sets word_wrap = True andauto_size = MSO_AUTO_SIZE.NONE, and applies the family, size, bold and italic to every run in the frame. Returns the applied size, or None when the frame is empty. Raises ValueError when the text does not fit at any size down to 1pt.

When font_file is None the installed file for font_familyis looked up. If neither is found, measurement falls back to Pillow's bundled default face and the result is an estimate: naming a family that is not installed emitspower_pptx.exc.FontMetricsWarning, and strict=True raisesValueError instead. Omitting font_family entirely does not warn.

from power_pptx.text.layout import TextFitter
from power_pptx.util import Inches, Pt

# The engine behind fit_text: measure once, style it yourself
best_pt = TextFitter.best_fit_font_size(
    text="Q4 2026 Customer Outcomes Review",
    extents=(Inches(8), Inches(1.5)),   # available (width, height)
    max_size=44,
    font_file=None,        # a .ttf path pins the metrics; None = Pillow default
)                          # -> int, or None when even 1pt overflows

if best_pt is not None:
    tf.paragraphs[0].runs[0].font.size = Pt(best_pt)

power_pptx.text.fonts

from power_pptx.text.fonts import (
    find_font_file, font_is_installed, installed_font_families,
)

font_is_installed("Inter")                    # -> bool
font_is_installed("Inter", bold=True)         # per style, not per family
find_font_file("Inter", bold=True)            # -> str path, or None
installed_font_families()                     # -> tuple[str, ...], sorted
  • font_is_installed(family_name, bold=False, italic=False) -> bool
  • find_font_file(family_name, bold=False, italic=False) -> str | None — the non-raising form of FontFiles.find
  • installed_font_families() -> tuple[str, ...] — sorted; files with no family name are skipped
  • FontFiles.find(family_name, is_bold, is_italic) -> str — raises KeyError when not installed

The scan walks the conventional font directories for the platform and is cached on the class after the first call. Style matters: font_is_installed("Inter") can beTrue while font_is_installed("Inter", bold=True) isFalse.

import warnings
from power_pptx.exc import FontMetricsWarning

# Fail the build rather than ship a guessed size
warnings.simplefilter("error", FontMetricsWarning)

Text enumerations

EnumImport fromMembers (selection)
MSO_AUTO_SIZEpower_pptx.enum.textNONE, SHAPE_TO_FIT_TEXT, TEXT_TO_FIT_SHAPE
PP_ALIGNpower_pptx.enum.textLEFT, CENTER, RIGHT, JUSTIFY, …
MSO_VERTICAL_ANCHOR (alias MSO_ANCHOR)power_pptx.enum.textTOP, MIDDLE, BOTTOM
MSO_UNDERLINEpower_pptx.enum.textNONE, SINGLE_LINE, DOUBLE_LINE, WAVY_LINE, …
MSO_LANGUAGE_IDpower_pptx.enum.langNONE, ENGLISH_US, FRENCH, JAPANESE, …
MSO_THEME_COLORpower_pptx.enum.dmlACCENT_1ACCENT_6, DARK_1, LIGHT_1, …