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_frame →TextFrame.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| Member | Type | Notes |
|---|---|---|
text | str | Read/write. Paragraphs joined by "\n", line breaks as "\v". Assignment replaces everything |
paragraphs | tuple[_Paragraph, ...] | Read-only; always at least one. New wrapper objects on every access |
word_wrap | bool | None | None inherits from the style hierarchy |
auto_size | MSO_AUTO_SIZE | None | NONE, SHAPE_TO_FIT_TEXT, TEXT_TO_FIT_SHAPE |
vertical_anchor | MSO_VERTICAL_ANCHOR | None | TOP / MIDDLE / BOTTOM |
margin_left, margin_right, margin_top, margin_bottom | Length | Text insets; default 0.1" left/right, 0.05" top/bottom |
column_count | int | 1..16, else ValueError. Reads 1 when unset; assigning 1 removes the attribute |
column_spacing | Length | None | Gutter between columns |
add_paragraph() | _Paragraph | Appends and returns a new empty paragraph |
clear() | None | Removes all paragraphs but one, and empties it |
fit_text(...) | int | None | See below |
set_paragraph_defaults(...) | None | See 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)
) -> NoneKeyword-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| Member | Type | Notes |
|---|---|---|
text | str | Read/write. Assignment replaces all runs with a single run |
runs | tuple[_Run, ...] | Fresh wrapper objects on every access |
font | Font | The paragraph's a:defRPr — defaults its runs inherit |
alignment | PP_ALIGN | None | Horizontal; None inherits |
level | int | Indent / outline level, 0..8. Bullet glyph and indent come from the master's list style for that level |
line_spacing | int | float | Length | None | A number is a multiple of single spacing; a Length is exact |
space_before, space_after | Length | None | Space above / below the paragraph |
rtl | bool | None | Right-to-left layout direction; tri-state |
start_at | int | None | First 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) | None | Replaces any existing bullet. Schemes include "arabicPeriod", "arabicParenR", "romanLcPeriod", "alphaUcParenR" |
tab_stops | TabStops | See below |
add_run() | _Run | Appends and returns a new run |
add_line_break() | None | Soft return inside the paragraph — one bullet, one spacing |
clear() | _Paragraph | Removes runs, breaks and fields; paragraph properties survive |
TabStops and TabStop
TabStops— sequence overa:tabLst:__len__,__iter__,__getitem__TabStops.add_tab_stop(position: Length, alignment: str = "left") -> TabStop—"left","center","right"or"decimal"TabStop.position—Length | None, read/writeTabStop.alignment—str, 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| Property | Type | XML | Notes |
|---|---|---|---|
name | str | None | a:latin/@typeface | None = inherit the theme typeface |
size | Length | None | sz | Assign Pt(n); read back .pt |
bold, italic | bool | None | b, i | Tri-state |
underline | bool | MSO_TEXT_UNDERLINE_TYPE | None | u | True = single. Reads back True/False for single/none, else the enum member |
color | ColorFormat | a:solidFill | Non-mutating reads — nothing is written until you assign |
fill | FillFormat | fill group | The full fill surface behind color |
all_caps | bool | None | cap="all" | One cap attribute, so mutually exclusive. False writes cap="none" |
small_caps | bool | None | cap="small" | |
caps | str | None | cap | Raw accessor: "none" / "small" / "all" |
strikethrough | bool | None | strike | True writes sngStrike; reads True for either strike variant |
superscript | bool | None | baseline > 0 | One baseline attribute. True = +30% / -25%. Clearing one never cancels the other |
subscript | bool | None | baseline < 0 | |
letter_spacing | Length | None | spc | Tracking; negative tightens |
language_id | MSO_LANGUAGE_ID | None | lang | Reads back MSO_LANGUAGE_ID.NONE — not Python None — when unset |
outline | LineFormat | a:ln | Glyph stroke: .color, .width |
shadow | ShadowFormat | a:effectLst | Lazy — no effect XML until assigned |
glow | GlowFormat | a:effectLst |
_Hyperlink
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 externaladdress—str | None. One setter for add, change and remove; assigningNonedeletes thea:hlinkClickand drops the relationship. For a run holding an internal jump this reads back as the target part name (e.g."slide2.xml"), notNone.target_slide—Slide | None. Writes appaction://hlinksldjumpaction. ReturnsNonewhen there is no hyperlink or when it is external, so this — notaddress— 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 emptyMeasures 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, ...], sortedfont_is_installed(family_name, bold=False, italic=False) -> boolfind_font_file(family_name, bold=False, italic=False) -> str | None— the non-raising form ofFontFiles.findinstalled_font_families() -> tuple[str, ...]— sorted; files with no family name are skippedFontFiles.find(family_name, is_bold, is_italic) -> str— raisesKeyErrorwhen 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
| Enum | Import from | Members (selection) |
|---|---|---|
MSO_AUTO_SIZE | power_pptx.enum.text | NONE, SHAPE_TO_FIT_TEXT, TEXT_TO_FIT_SHAPE |
PP_ALIGN | power_pptx.enum.text | LEFT, CENTER, RIGHT, JUSTIFY, … |
MSO_VERTICAL_ANCHOR (alias MSO_ANCHOR) | power_pptx.enum.text | TOP, MIDDLE, BOTTOM |
MSO_UNDERLINE | power_pptx.enum.text | NONE, SINGLE_LINE, DOUBLE_LINE, WAVY_LINE, … |
MSO_LANGUAGE_ID | power_pptx.enum.lang | NONE, ENGLISH_US, FRENCH, JAPANESE, … |
MSO_THEME_COLOR | power_pptx.enum.dml | ACCENT_1 … ACCENT_6, DARK_1, LIGHT_1, … |