Collections

One pattern for every list-of-things in a Document

The pattern

Every document-scoped aggregation in hwpapi v2 follows one interface β€” dict-like, iterable, sized, membership-testable, names-exposing, and predicate-filterable. The contract lives in hwpapi.collections.Collection as a typing.Protocol:

from typing import Callable, Iterator, Protocol, runtime_checkable

@runtime_checkable
class Collection(Protocol):
    def __getitem__(self, key): ...
    def __iter__(self) -> Iterator: ...
    def __len__(self) -> int: ...
    def __contains__(self, key) -> bool: ...
    def names(self) -> list[str]: ...
    def filter(self, predicate: Callable[..., bool]) -> list: ...

Every concrete collection under app.doc.* implements this:

app.doc.* Returns Elements keyed by
fields FieldCollection field name
bookmarks BookmarkCollection bookmark name
hyperlinks HyperlinkCollection URL or index
images ImageCollection image name or index
styles StyleCollection style name
paragraphs ParagraphCollection index (int)
tables TableCollection table name or index

The common moves

Because every collection implements the same six methods, the same five operations work on all of them:

coll = app.doc.fields                 # or bookmarks, images, ...

len(coll)                             # count
"author" in coll                      # membership
for item in coll: ...                 # iterate
coll["author"]                        # by key β†’ element
coll.names()                          # list of keys
coll.filter(lambda f: "head" in f.name)   # list[Element] where predicate holds

Collections that support mutation add __setitem__, __delitem__, and clear():

app.doc.fields["author"] = "홍길동"    # write
del app.doc.fields["author"]          # remove one
app.doc.fields.clear()                # remove all

Element value objects

Subscripting a collection returns a lightweight element object:

  • fields[name] β†’ Field β€” .name, .value, .goto()
  • bookmarks[name] β†’ Bookmark β€” .name, .goto()
  • hyperlinks[key] β†’ Hyperlink
  • images[key] β†’ Image
  • paragraphs[i] β†’ Paragraph β€” .text, .style, .charshape, .parashape, .runs
  • styles[name] β†’ Style
  • tables[key] β†’ Table β€” .cell(row, col), .rows, .cols

Elements are value objects: constructing one does not touch COM. Reading a property like field.value or paragraph.text is what triggers the COM call. This matters when you iterate β€” for p in app.doc.paragraphs: p.text pays one COM hit per text read, not one per construction.

Recipes

Bulk-fill form fields from a dict

data = {"author": "홍길동", "date": "2026-04-19", "title": "λ³΄κ³ μ„œ"}

for name, value in data.items():
    if name in app.doc.fields:
        app.doc.fields[name] = value

Find all paragraphs that use a specific style

para = app.doc.paragraphs.filter(lambda p: p.style == "Heading 1")
print(f"{len(para)} heading 1 paragraphs")

Rename a bookmark

if "old-name" in app.doc.bookmarks:
    app.doc.bookmarks.rename("old-name", "new-name")

List every image in the document

for img in app.doc.images:
    print(img.name, img.width, img.height)

Migration note

In v1, these lived on App directly:

v1 v2
app.fields app.doc.fields
app.bookmarks app.doc.bookmarks
app.hyperlinks app.doc.hyperlinks
app.images app.doc.images
app.styles app.doc.styles
app.field_names app.doc.fields.names()
app.field_exists(n) n in app.doc.fields
app.delete_field(n) del app.doc.fields[n]
app.delete_all_fields() app.doc.fields.clear()

See the migration guide for the complete table.

Back to top