Skip to content

docx_plus.controls.read

Read and modify the values of existing content controls. read_controls(doc) returns a flat dict[str, ControlValue] keyed by tag (default) or alias; set_control_value / clear_control mutate single controls.

The five typed errors are all dual-base (DocxPlusError plus a stdlib exception) so callers can match either contract — see ARCHITECTURE.md §9.

docx_plus.controls.read

Read and modify content controls (SDTs) in an existing document.

The companion to :mod:docx_plus.controls.builder. Where builder writes w:sdt elements, this module discovers them, reports their values, sets new values, or resets them to placeholder state.

The read side is intentionally schema-tolerant: it works on any document with content controls, not just ones built by :class:FormBuilder. Type detection dispatches on the marker child of w:sdtPr (w:text, w:dropDownList, w:comboBox, w:date, w14:checkbox).

ControlType module-attribute

ControlType = Literal['text', 'dropdown', 'combobox', 'date', 'checkbox']

ControlValue dataclass

ControlValue(
    tag: str,
    alias: str | None,
    control_type: ControlType,
    value: ControlValueT | None,
    is_placeholder: bool,
)

A single content control's identity, type, and current value.

Attributes:

Name Type Description
tag str

The control's w:tag value (machine identifier).

alias str | None

The control's w:alias value (UI label), or None.

control_type ControlType

One of text, dropdown, combobox, date, checkbox.

value ControlValueT | None

The current value:

  • text/dropdown/combobox: str if filled, None if showing placeholder.
  • date: :class:~datetime.datetime if filled, None otherwise.
  • checkbox: always bool (no placeholder concept).
is_placeholder bool

True if the control is showing its placeholder text (w:showingPlcHdr present in sdtPr). Always False for checkboxes.

ControlNotFoundError

Bases: DocxPlusError, KeyError

Raised when no content control with the requested tag exists.

Subclasses KeyError so existing except KeyError: clauses still catch it; also subclasses :class:DocxPlusError per SPEC §9.7.

DuplicateTagError

Bases: DocxPlusError, ValueError

Raised by :func:read_controls when two controls share a key.

ValueNotInListError

Bases: DocxPlusError, ValueError

Raised by :func:set_control_value when a dropdown value has no match.

ControlTypeError

Bases: DocxPlusError, TypeError

Raised when a value's Python type does not match the control's type.

read_controls

read_controls(
    doc: Document, *, by: Literal["tag", "alias"] = "tag"
) -> dict[str, ControlValue]

Return every content control in doc keyed by tag (or alias).

Parameters:

Name Type Description Default
doc Document

The python-docx Document to inspect.

required
by Literal['tag', 'alias']

Either "tag" (default) — key on w:tag, every control included — or "alias" — key on w:alias, controls without an alias are skipped.

'tag'

Returns:

Type Description
dict[str, ControlValue]

Mapping from key to :class:ControlValue.

Raises:

Type Description
DuplicateTagError

If two controls share the same key.

Source code in docx_plus/controls/read.py
def read_controls(
    doc: Document,
    *,
    by: Literal["tag", "alias"] = "tag",
) -> dict[str, ControlValue]:
    """Return every content control in ``doc`` keyed by tag (or alias).

    Args:
        doc: The python-docx Document to inspect.
        by: Either ``"tag"`` (default) — key on ``w:tag``, every control
            included — or ``"alias"`` — key on ``w:alias``, controls without
            an alias are skipped.

    Returns:
        Mapping from key to :class:`ControlValue`.

    Raises:
        DuplicateTagError: If two controls share the same key.
    """
    out: dict[str, ControlValue] = {}
    for sdt in _iter_sdts(doc):
        info = _read_sdt(sdt)
        if info is None:
            continue
        key = info.tag if by == "tag" else info.alias
        if key is None:
            continue
        if key in out:
            raise DuplicateTagError(
                f"duplicate {by} {key!r} encountered while reading controls",
            )
        out[key] = info
    return out

set_control_value

set_control_value(doc: Document, tag: str, value: ControlValueT) -> None

Set the value of a control identified by tag.

Parameters:

Name Type Description Default
doc Document

The python-docx Document to modify.

required
tag str

The control's w:tag value.

required
value ControlValueT

The new value. Type must match the control type:

  • text: str
  • dropdown / combobox: str
  • date: :class:~datetime.datetime
  • checkbox: bool
required

Raises:

Type Description
ControlNotFoundError

If no control with that tag exists.

ControlTypeError

If value's type does not match the control type.

ValueNotInListError

For a dropdown when value matches neither w:value nor w:displayText of any list item.

Source code in docx_plus/controls/read.py
def set_control_value(
    doc: Document,
    tag: str,
    value: ControlValueT,
) -> None:
    """Set the value of a control identified by ``tag``.

    Args:
        doc: The python-docx Document to modify.
        tag: The control's ``w:tag`` value.
        value: The new value. Type must match the control type:

            - text: ``str``
            - dropdown / combobox: ``str``
            - date: :class:`~datetime.datetime`
            - checkbox: ``bool``

    Raises:
        ControlNotFoundError: If no control with that tag exists.
        ControlTypeError: If ``value``'s type does not match the control type.
        ValueNotInListError: For a dropdown when ``value`` matches neither
            ``w:value`` nor ``w:displayText`` of any list item.
    """
    sdt = _find_sdt_by_tag(doc, tag)
    sdt_pr = _sdt_pr(sdt)
    sdt_content = _sdt_content(sdt)
    control_type = _classify_sdt(sdt)
    if control_type is None:
        raise ControlNotFoundError(
            f"control with tag {tag!r} has no recognised type marker",
        )

    if control_type == "checkbox":
        if not isinstance(value, bool):
            raise ControlTypeError(
                f"checkbox control {tag!r} requires bool; got {type(value).__name__}",
            )
        _set_checkbox(sdt_pr, sdt_content, checked=value)
        return

    if control_type == "date":
        if not isinstance(value, datetime):
            raise ControlTypeError(
                f"date control {tag!r} requires datetime; got {type(value).__name__}",
            )
        _set_date(sdt_pr, sdt_content, value)
        _clear_placeholder_flag(sdt_pr)
        return

    # text / dropdown / combobox
    if not isinstance(value, str):
        raise ControlTypeError(
            f"{control_type} control {tag!r} requires str; got {type(value).__name__}",
        )

    if control_type == "text":
        _replace_sdt_content_text(sdt_content, value)
    elif control_type == "dropdown":
        display = _resolve_dropdown_value(sdt_pr, value, allow_freeform=False, tag=tag)
        _replace_sdt_content_text(sdt_content, display)
    else:  # combobox
        display = _resolve_dropdown_value(sdt_pr, value, allow_freeform=True, tag=tag)
        _replace_sdt_content_text(sdt_content, display)

    _clear_placeholder_flag(sdt_pr)

clear_control

clear_control(doc: Document, tag: str) -> None

Reset a control to its placeholder state.

For text/dropdown/combobox/date: re-adds w:showingPlcHdr to sdtPr and re-applies the PlaceholderText rStyle to every run in sdtContent. The placeholder text itself is preserved in place (whatever sdtContent currently holds).

For checkbox: resets the checked flag to 0 and the glyph to . Checkboxes have no placeholder mode.

Source code in docx_plus/controls/read.py
def clear_control(doc: Document, tag: str) -> None:
    """Reset a control to its placeholder state.

    For text/dropdown/combobox/date: re-adds ``w:showingPlcHdr`` to sdtPr and
    re-applies the ``PlaceholderText`` rStyle to every run in sdtContent. The
    placeholder text itself is preserved in place (whatever sdtContent
    currently holds).

    For checkbox: resets the checked flag to ``0`` and the glyph to
    ``☐``. Checkboxes have no placeholder mode.
    """
    sdt = _find_sdt_by_tag(doc, tag)
    sdt_pr = _sdt_pr(sdt)
    sdt_content = _sdt_content(sdt)
    control_type = _classify_sdt(sdt)
    if control_type is None:
        raise ControlNotFoundError(
            f"control with tag {tag!r} has no recognised type marker",
        )

    if control_type == "checkbox":
        _set_checkbox(sdt_pr, sdt_content, checked=False)
        return

    _set_placeholder_flag(sdt_pr)
    for run in sdt_content.findall(qn("w:r")):
        rpr = run.find(qn("w:rPr"))
        if rpr is None:
            rpr = el("w:rPr")
            run.insert(0, rpr)
        for existing in rpr.findall(qn("w:rStyle")):
            remove(existing)
        rstyle = el("w:rStyle", **{"w:val": _PLACEHOLDER_STYLE_ID})
        rpr.insert(0, rstyle)