Skip to content

usage

usage

Which styles a document actually uses.

The interesting part is getting "used" right. A naive scan of w:pStyle in word/document.xml gets the wrong answer in several ways, and each of them is a real document:

  • Every story part, not just the body. Headers, footers, footnotes, endnotes and comments are separate parts with their own content and their own style references.
  • Indirect references. A style can be reachable without ever being applied — as the w:basedOn of a used style, as its w:next, as its w:link, from a numbering level's w:pStyle, or from the w:tblStylePr conditional formatting inside a table style.
  • The default styles. The style carrying w:default="1" applies to every paragraph with no w:pStyle at all. It is used by definition and has zero direct references.

So "used" is a reachability closure, not a membership test: seed from the direct applications, then follow the reference edges until the set stops growing.

The closure runs on style ids, which is what the XML references; docx.styles.styles.Styles keys on names, which is what the API exposes, and BabelFish translates the built-ins between the two spellings. Mixing the two is a recurring source of bugs, so the translation happens only at the boundary.

StyleUsage

Bases: NamedTuple

A report of which styles a document defines and which of them it uses.

Iterating yields the style ids in use. str() gives the one-paragraph summary that print shows.

unused property

unused: Tuple[str, ...]

Style ids defined but not reachable, in document order.

compute_usage

compute_usage(
    styles_elm: CT_Styles,
    document_part: DocumentPart | None,
    keep: Iterable[str] = (),
    *,
    seed_defaults: bool = True,
) -> StyleUsage

The style-usage report for styles_elm as used by document_part.

keep names extra style ids to treat as used along with their own closure — for a caller who plans to apply a style that nothing references yet.

seed_defaults puts the w:default="1" styles into the closure whether or not anything references them, which is the truthful reading: they apply to content that names no style at all. Passing False answers the narrower question of what is reachable by reference alone, which is what a caller deliberately pruning the defaults needs.

With no document_part there is no content to scan, so only the defaults and keep seed the closure. That is the honest answer for a styles part reached on its own rather than a claim that nothing is used.

Source code in src/docx/styles/usage.py
def compute_usage(
    styles_elm: CT_Styles,
    document_part: DocumentPart | None,
    keep: Iterable[str] = (),
    *,
    seed_defaults: bool = True,
) -> StyleUsage:
    """The style-usage report for `styles_elm` as used by `document_part`.

    `keep` names extra style ids to treat as used along with their own closure — for a
    caller who plans to apply a style that nothing references yet.

    `seed_defaults` puts the `w:default="1"` styles into the closure whether or not
    anything references them, which is the truthful reading: they apply to content that
    names no style at all. Passing |False| answers the narrower question of what is
    reachable by reference alone, which is what a caller deliberately pruning the
    defaults needs.

    With no `document_part` there is no content to scan, so only the defaults and `keep`
    seed the closure. That is the honest answer for a styles part reached on its own
    rather than a claim that nothing is used.
    """
    by_id = {s.styleId: s for s in styles_elm.style_lst if s.styleId}
    defined = tuple(s.styleId for s in styles_elm.style_lst if s.styleId)

    counts = _direct_reference_counts(document_part) if document_part is not None else {}

    # -- seed: everything directly applied, every default style, and `keep` --
    pending = set(counts) | set(keep)
    if seed_defaults:
        pending |= _default_style_ids(styles_elm)
    # -- Word's "Normal" is repaired into a document that lacks it, and the repair
    # -- dialogue is worse than the bloat, so it is never dropped --
    if "Normal" in by_id:
        pending.add("Normal")

    # -- reachability closure. A dangling edge — a `w:basedOn` naming a style that is
    # -- not defined — is a dead end rather than an error; it is legal and common. The
    # -- visited set is what makes cycles terminate, and `w:next` pointing at its own
    # -- style is the normal case rather than a pathology. --
    used: Set[str] = set()
    while pending:
        style_id = pending.pop()
        if style_id in used or style_id not in by_id:
            continue
        used.add(style_id)
        pending.update(_style_edges(by_id[style_id]))

    latent = tuple(
        name
        for name in styles_elm.xpath("./w:latentStyles/w:lsdException/@w:name")
        if name not in by_id
    )

    return StyleUsage(
        defined=defined,
        used=tuple(style_id for style_id in defined if style_id in used),
        reference_counts=counts,
        latent=latent,
    )

latent_exception_names

latent_exception_names(
    styles_elm: CT_Styles,
) -> Tuple[str, ...]

Every w:lsdException/@w:name in the latent-styles block, defined or not.

Source code in src/docx/styles/usage.py
def latent_exception_names(styles_elm: CT_Styles) -> Tuple[str, ...]:
    """Every `w:lsdException/@w:name` in the latent-styles block, defined or not."""
    return tuple(styles_elm.xpath("./w:latentStyles/w:lsdException/@w:name"))

is_default_style

is_default_style(
    styles_elm: CT_Styles, style: CT_Style
) -> bool

True when style is the w:default="1" style of its type.

Source code in src/docx/styles/usage.py
def is_default_style(styles_elm: CT_Styles, style: CT_Style) -> bool:
    """|True| when `style` is the `w:default="1"` style of its type."""
    return bool(style.styleId) and style.styleId in _default_style_ids(styles_elm)