Placeholders & Notes
A placeholder is a pre-formatted container the template designer put on a layout, and which every slide using that layout inherits. Filling one costs a single assignment and comes out matching the deck's design; drawing your own box in the same spot costs a dozen lines and comes out subtly wrong.
What a placeholder is
Placeholders cut across shape types rather than being one of them. An autoshape (p:sp), a picture (p:pic) and a graphic frame (p:graphicFrame) can all be placeholders; groups, connectors and content parts cannot. OOXML defines 18 placeholder types — title, centre title, subtitle, body, content, picture, chart, table, SmartArt, media clip, date, footer, slide number, header, and the vertical variants for languages such as Japanese.
A placeholder is empty ("unpopulated") until content goes in. Empty ones show prompt text in PowerPoint and, for the rich-content types, the grid of insert buttons. That prompt text is a template artefact — it is never part of your saved content and never appears in presentation mode.
The inheritance model
Placeholders exist at three levels, and the whole subsystem is a property-inheritance chain across them:
- Slide master — inheritee only. Nothing on a master inherits from anything.
- Slide layout — both. Inherits from the master placeholder of the same type.
- Slide — inheritor only. Inherits from the layout placeholder with the same idx.
Note the asymmetry: layout-from-master matches on type, slide-from-layout matches on idx. That is why idx, not name and not position in the collection, is the stable identity of a placeholder.
What gets inherited is essentially everything — fill, line, font, bullet style, and position and size. Directly applied formatting on the slide overrides the inherited value for that one property. This is the mechanism that makes a template a template: change the layout and every slide built on it moves.
Finding a placeholder
Every placeholder is a shape, so it shows up in slide.shapes. Butslide.placeholders is the collection you want, because it is keyed byidx:
from power_pptx import Presentation
prs = Presentation()
slide = prs.slides.add_slide(prs.slide_layouts[8]) # "Picture with Caption"
for shape in slide.placeholders:
phf = shape.placeholder_format
print(f"idx={phf.idx:<3} type={phf.type!s:<14} name={shape.name}")
# idx=0 type=TITLE (1) name=Title 1
# idx=1 type=PICTURE (18) name=Picture Placeholder 2
# idx=2 type=BODY (2) name=Text Placeholder 3Item access is dictionary-like, not list-like. The integer you pass is an idxvalue, not an ordinal, and idx values are not contiguous — a miss raisesKeyError:
title = slide.placeholders[0] # idx key, NOT position
caption = slide.placeholders[2]
slide.placeholders[7] # KeyError: no placeholder on this slide
# with idx == 7Placeholders from PowerPoint's built-in layouts use idx 0–5, with the title always at 0 and the rest running top-to-bottom, left-to-right. Placeholders a designer added by hand start at 10. In the bundled default template, the date, footer and slide-number placeholders sit at idx 10, 11 and 12 on every layout — and are deliberatelynot cloned onto new slides, which is why slide.placeholders is usually shorter than layout.placeholders.
Identifying one
A placeholder's shape_type is unconditionallyMSO_SHAPE_TYPE.PLACEHOLDER no matter what it holds, so it tells you nothing useful. The real detail lives on placeholder_format, which every shape has but which raises ValueError on a non-placeholder — pair it withis_placeholder:
for shape in slide.shapes:
print(shape.shape_type) # PLACEHOLDER (14) for every one of them
for shape in slide.shapes:
if shape.is_placeholder: # guard: the property raises otherwise
phf = shape.placeholder_format
print(phf.idx, phf.type)
textbox = slide.shapes.add_textbox(0, 0, 100, 100)
textbox.placeholder_format # ValueError: shape is not a placeholderInherited position and size — the thing people get wrong
A placeholder on a slide normally carries no geometry of its own. Theleft, top, width and height you read back are resolved from the layout placeholder at access time:
from power_pptx import Presentation
from power_pptx.util import Emu
prs = Presentation()
slide = prs.slides.add_slide(prs.slide_layouts[1]) # "Title and Content"
body = slide.placeholders[1]
# The slide's own <p:sp> carries no position at all ...
print(body._element.x, body._element.y) # None None
# ... yet the shape reports one, resolved from the layout placeholder.
print(Emu(body.left).inches, Emu(body.top).inches) # 0.5 1.75
print(Emu(body.width).inches) # 9.0That resolution is why a placeholder is worth using at all — it is what keeps a hundred slides aligned. It is also the source of two recurring bugs.
First: code that inspects shape.left on a slide and then writes the same number back has just converted an inherited value into a hard-coded one. The slide now looks identical and stops tracking the template forever.
Second, and much sharper: position and size are stored aspairs in the XML — <a:off x= y=> and<a:ext cx= cy=>. Writing one member of a pair materialises the element, and the sibling attribute it did not have a value for defaults to zero. So settingwidth alone on an inheriting placeholder silently collapses its height:
from power_pptx import Presentation
from power_pptx.util import Emu, Inches
prs = Presentation()
slide = prs.slides.add_slide(prs.slide_layouts[1])
body = slide.placeholders[1]
print(Emu(body.top).inches, Emu(body.height).inches) # 1.75 4.95
body.width = Inches(4) # only ONE of the pair
print(Emu(body.width).inches, Emu(body.height).inches) # 4.0 0.0 <- height goneNothing raises; the shape simply becomes a zero-height sliver at save time. The fix is to always write both members of the pair, reading the inherited values first when you only mean to change one:
from power_pptx import Presentation
from power_pptx.util import Emu, Inches
prs = Presentation()
slide = prs.slides.add_slide(prs.slide_layouts[1])
body = slide.placeholders[1]
# Read the inherited values first, then write the whole pair back.
top, height = body.top, body.height
body.left, body.top = Inches(1), top
body.width, body.height = Inches(6), height
print(Emu(body.left).inches, Emu(body.top).inches) # 1.0 1.75
print(Emu(body.width).inches, Emu(body.height).inches) # 6.0 4.95This applies only the first time — once the placeholder has explicit geometry, both attributes are present and single-property assignment behaves normally. Which is exactly what makes the bug hard to spot in a test that runs twice.
Title and body text
Text placeholders take content the same way any autoshape does. The title is common enough to have its own shortcut on the shape tree — slide.shapes.title, which returnsNone when the layout has no title placeholder:
from power_pptx import Presentation
prs = Presentation()
slide = prs.slides.add_slide(prs.slide_layouts[1]) # "Title and Content"
# The title has a dedicated shortcut on the shape tree.
slide.shapes.title.text = "Air-speed Velocity of Unladen Swallows"
# A body placeholder's text frame behaves like any other text frame.
body = slide.placeholders[1].text_frame
body.text = "African swallow" # writes the first paragraph
second = body.add_paragraph()
second.text = "European swallow"
second.level = 1 # indent one bullet levelBecause the placeholder brings its own font size, alignment and bullet styling from the layout, this is usually all the formatting you need. Where a placeholder's box is fixed by the template but your text is generated and variable-length, reach fortext_frame.fit_text(...) — seeSpace-Aware Authoring.
Picture, table and chart placeholders
The rich-content placeholder types have dedicated insertion methods:PicturePlaceholder.insert_picture(),TablePlaceholder.insert_table() andChartPlaceholder.insert_chart(). Inserting a picture stretches it proportionally and crops it to fill the placeholder exactly, so a 1600×600 image dropped into a 4:3 box loses 25% off each side rather than distorting:
from power_pptx import Presentation
from power_pptx.util import Emu
prs = Presentation()
slide = prs.slides.add_slide(prs.slide_layouts[8]) # "Picture with Caption"
placeholder = slide.placeholders[1] # idx key, not position
picture = placeholder.insert_picture("wide.jpg") # use the RETURN VALUE
print(type(picture).__name__) # PlaceholderPicture
print(Emu(picture.width).inches, Emu(picture.height).inches) # 6.0 4.5
print(picture.crop_left, picture.crop_right) # 0.25 0.25Always use the return value. Inserting rich content replaces the placeholder's XML element — p:sp becomes p:pic for a picture, orp:graphicFrame for a table or chart. Your original variable is left pointing at an element that is no longer on the slide. It does not always raise; writes to it can succeed and then vanish at save time. The replacement is the return value, and it is also reachable from slide.placeholders[idx] under the same key.
insert_table and insert_chart both return aPlaceholderGraphicFrame, not the table or chart itself — reach through.table / .chart. An inserted table keeps the placeholder's position and width but sizes its height from the row count; an inserted chart takes the placeholder's full box.
The bundled default template has no table or chart placeholder on any layout, so these are only usable against a template that provides one:
from power_pptx.enum.shapes import PP_PLACEHOLDER
# The built-in template ships no table or chart placeholder, so find the
# idx values in your own template before hard-coding them.
for layout in prs.slide_layouts:
for ph in layout.placeholders:
if ph.placeholder_format.type in (PP_PLACEHOLDER.TABLE, PP_PLACEHOLDER.CHART):
print(layout.name, ph.placeholder_format.idx, ph.placeholder_format.type)Speaker notes
Speaker notes live on a separate part — a notes slide, which has its own placeholder collection cloned from the notes master. The notes text itself sits in that notes slide'sBODY placeholder.
slide.notes is the post-fork shortcut over all of that, and it is asymmetric on purpose: reading never creates a notes slide, assigning does.
from power_pptx import Presentation
prs = Presentation()
slide = prs.slides.add_slide(prs.slide_layouts[1])
slide.shapes.title.text = "Q4 Review"
print(slide.has_notes_slide) # False - nothing created yet
print(repr(slide.notes)) # '' - and reading did NOT create one
print(slide.has_notes_slide) # still False
slide.notes = "Open with the revenue number, then hand over to Priya."
print(slide.has_notes_slide) # True - assigning created the notes slideThe distinction matters when you are walking a deck to extract content. Touchingslide.notes_slide on every slide would mint a notes part for each one and bloat the file; slide.has_notes_slide and slide.notes both leave the deck untouched.
For anything beyond a flat string — multiple paragraphs, run-level formatting — go throughnotes_slide.notes_text_frame, which is an ordinary TextFrame. It returns None in the rare case where the notes master has no body placeholder:
text_frame = slide.notes_slide.notes_text_frame # creates the notes slide
para = text_frame.add_paragraph()
para.text = "Timing: 90 seconds."
print([p.text for p in text_frame.paragraphs])
print(repr(slide.notes)) # paragraphs joined with newlinesMasterPlaceholderCollision
The linter has a rule aimed squarely at the failure mode this page exists to prevent. If a non-placeholder shape's bounding box lines up with an unused layout placeholder — within about 0.05 inch on all four edges — it firesMasterPlaceholderCollision at warning severity:
from power_pptx import Presentation
prs = Presentation()
layout = prs.slide_layouts[5] # "Title Only"
slide = prs.slides.add_slide(layout)
# The anti-pattern: drop the inherited title, hand-draw a box in its place.
slide.shapes.title.delete()
title_ph = layout.placeholders[0]
box = slide.shapes.add_textbox(
title_ph.left, title_ph.top, title_ph.width, title_ph.height
)
box.text_frame.text = "Q4 Review"
for issue in slide.lint().issues:
print(issue.severity.value, issue.code, "->", issue.message)
# warning MasterPlaceholderCollision -> Shape 'TextBox 2' sits at the position
# of layout placeholder idx=0; it likely should have inherited from the
# placeholder instead of redrawing it.The shape looks right today. What it has lost is the inheritance: it will not pick up a font change on the master, it will not move when the layout moves, PowerPoint's outline view and screen readers will not see it as the slide's title, and reapplying the layout will restore the real placeholder underneath it. The rule catches generated decks that reconstruct a template's geometry with add_textbox instead of filling what is already there.
It is deliberately not auto-fixable — moving content from a hand-drawn box into a placeholder is a judgement call about which formatting you meant to keep. Fix it by filling the placeholder instead, or silence it per-run withslide.lint(disable=["MasterPlaceholderCollision"]) when the redraw is intentional. See Lint & Audit for the full rule set.