Skip to content

sdt

sdt

Custom element classes for structured document tags, aka "content controls".

A w:sdt element wraps a region of a document, marking it as a form field, a template placeholder, a building block, or a region bound to a data source. Its content lives inside a w:sdtContent child, so anything that walks a container's children without looking through that wrapper simply does not see it.

The schema defines four w:sdt variants — block, run, row and cell — differing only in the content model of w:sdtContent. lxml dispatches on tag name alone, so one element class serves all four; what a particular w:sdt contains is discovered from its children rather than declared.

CT_Sdt

Bases: BaseOxmlElement

w:sdt element, a structured document tag ("content control").

alias_val property

alias_val: str | None

The friendly name shown on the control in Word, or None if not set.

content_control_type property

content_control_type: WD_CONTENT_CONTROL_TYPE | None

Member of WdContentControlType this control is, or None.

None when w:sdtPr is absent or names no type, which Word treats as a rich-text control but is not the same as saying so explicitly.

id_val property

id_val: int | None

Value of ./w:sdtPr/w:id/@w:val, or None if not present.

Read from the attribute directly rather than through a registered element class; w:id appears in several unrelated places in the schema and is not this library's to claim globally.

showing_placeholder property

showing_placeholder: bool

True when the control currently displays its placeholder text.

The text inside such a control is the prompt ("Click here to enter text."), not a value the user supplied.

tag_val property

tag_val: str | None

Value of ./w:sdtPr/w:tag/@w:val, or None if not present.

The tag is the programmatic identifier of the control; unlike the alias it is not shown to the user and is what code binding to a template matches on.

Named tag_val rather than tag because .tag is lxml's element tag name.

text property

text: str

The text of everything inside this content control.

CT_SdtContent

Bases: BaseOxmlElement

w:sdtContent element, the content region of a w:sdt.

text property

text: str

The text of this content region.

Block-level content contributes one line per paragraph, including paragraphs inside a table; run-level content is concatenated as it would be in a paragraph.

CT_SdtPr

Bases: BaseOxmlElement

w:sdtPr element, the properties of a w:sdt.

iter_block_content

iter_block_content(
    element: BaseOxmlElement,
) -> Iterator[CT_P | CT_Tbl]

Generate each w:p and w:tbl child of element, in document order.

A w:sdt child is looked through rather than skipped: the block-level content of its w:sdtContent is generated in its place, recursively, so a content control nested in another content control is seen as well.

So is a w:customXml, which the schema defines in a block-level flavour (CT_CustomXmlBlock) as well as the run-level one — it wraps whole paragraphs and tables, and skipping it drops them from the document entirely.

Source code in src/docx/oxml/sdt.py
def iter_block_content(element: BaseOxmlElement) -> Iterator[CT_P | CT_Tbl]:
    """Generate each `w:p` and `w:tbl` child of `element`, in document order.

    A `w:sdt` child is looked through rather than skipped: the block-level content of
    its `w:sdtContent` is generated in its place, recursively, so a content control
    nested in another content control is seen as well.

    So is a `w:customXml`, which the schema defines in a block-level flavour
    (`CT_CustomXmlBlock`) as well as the run-level one — it wraps whole paragraphs and
    tables, and skipping it drops them from the document entirely.
    """
    for child in element.iterchildren():
        if child.tag in (qn("w:p"), qn("w:tbl")):
            yield cast("CT_P | CT_Tbl", child)
        elif child.tag in TRANSPARENT_WRAPPER_TAGS:
            yield from iter_block_content(cast("BaseOxmlElement", child))
        elif child.tag == qn("w:sdt"):
            sdtContent = child.find(qn("w:sdtContent"))
            if sdtContent is not None:
                yield from iter_block_content(sdtContent)

iter_run_content

iter_run_content(
    element: BaseOxmlElement,
) -> Iterator[CT_R | CT_Hyperlink]

Generate each w:r and w:hyperlink child of element, in document order.

This is the document as it now reads, which for a document carrying tracked changes means with every revision accepted.

As with iter_block_content, a run-level w:sdt is looked through. So is a w:fldSimple, whose runs hold the result text the field displays; skipping it would drop a page number or a cross-reference from the paragraph's text. So is an insertion (w:ins, w:moveTo), whose runs are part of the text. A deletion (w:del, w:moveFrom) is skipped: its text is no longer part of the document, and it is held in w:delText rather than w:t for exactly that reason. Use docx.oxml.revision.iter_original_run_content for the other reading.

Source code in src/docx/oxml/sdt.py
def iter_run_content(element: BaseOxmlElement) -> Iterator[CT_R | CT_Hyperlink]:
    """Generate each `w:r` and `w:hyperlink` child of `element`, in document order.

    This is the document as it now reads, which for a document carrying tracked changes
    means with every revision accepted.

    As with :func:`iter_block_content`, a run-level `w:sdt` is looked through. So is a
    `w:fldSimple`, whose runs hold the result text the field displays; skipping it would
    drop a page number or a cross-reference from the paragraph's text. So is an
    insertion (`w:ins`, `w:moveTo`), whose runs are part of the text. A deletion
    (`w:del`, `w:moveFrom`) is skipped: its text is no longer part of the document, and
    it is held in `w:delText` rather than `w:t` for exactly that reason. Use
    :func:`docx.oxml.revision.iter_original_run_content` for the other reading.
    """
    for child in element.iterchildren():
        if child.tag in (qn("w:r"), qn("w:hyperlink")):
            yield cast("CT_R | CT_Hyperlink", child)
        elif child.tag in _LOOK_THROUGH_TAGS:
            yield from iter_run_content(cast("BaseOxmlElement", child))
        elif child.tag in _SKIP_TAGS:
            continue
        elif child.tag == qn("w:sdt"):
            sdtContent = child.find(qn("w:sdtContent"))
            if sdtContent is not None:
                yield from iter_run_content(sdtContent)