Migrating from hwpapi v2.x to v3.0

Published

April 29, 2026

Overview

hwpapi v3.0 reorganizes the public API around a three-tier model inspired by xlwings: an App that owns only the HWP COM engine lifecycle, an app.docs collection that owns open-document membership, and a Document instance that owns everything scoped to a single .hwp file. In v2 the App class doubled as a single-document facade β€” app.open(), app.save(), app.close(), and app.doc all lived on App and silently operated on whichever document HWP considered β€œactive”. v3 removes that ambiguity: app.docs.open(path) and app.docs.add() return a Document instance, and all per-document work happens on that instance. Multi-document workflows (merge, bulk convert, A/B comparison) become first-class β€” they were impossible in v2 without dropping to raw COM via app.api.XHwpDocuments.

Compatibility Policy

hwpapi 3.0 is a clean-cut break. There is no deprecation shim.

  • v2 code that calls removed App members (app.open, app.save, app.save_as, app.close, app.doc) raises AttributeError.
  • No runtime warnings. No compatibility layer. No import-level aliases.
  • Reason: v2 conflated process lifecycle with document lifecycle, making app.save() ambiguous in any multi-document scenario. A shim that forwarded app.save() to app.docs.active.save() would preserve that ambiguity and prevent the type system from distinguishing β€œApp operation” from β€œDocument operation”. A clean break is cheaper than a perpetual compat layer, and the migration is a near-mechanical find/replace (see the table below).
  • v2.x users who cannot migrate immediately should pin hwpapi==2.* β€” that line is frozen on the v2.x branch and tagged v2.0.0. Only critical bug fixes will be backported.

The new shape (v3)

hwpapi.App                        # HWP COM engine lifecycle only
β”œβ”€β”€ visible (property)            # window visibility β€” bool r/w
β”œβ”€β”€ version (property)            # engine version
β”œβ”€β”€ quit()                        # tear down the COM engine process
β”œβ”€β”€ docs ─────────────────────────────── DocumentCollection
β”‚   β”œβ”€β”€ open(path) β†’ Document         # open a .hwp / .hwpx file
β”‚   β”œβ”€β”€ add()      β†’ Document         # create a new blank document
β”‚   β”œβ”€β”€ active     β†’ Document         # currently active document
β”‚   β”œβ”€β”€ [i] / ["name.hwp"]            # index / name access
β”‚   β”œβ”€β”€ len(app.docs)                 # number of open documents
β”‚   └── for d in app.docs: ...        # iterate
β”œβ”€β”€ actions                       # raw process-level actions (escape hatch)
β”œβ”€β”€ engine                        # Engine escape hatch
└── api                           # raw COM handle escape hatch

hwpapi.Document                   # everything scoped to one .hwp file
β”œβ”€β”€ name / path / saved           # metadata
β”œβ”€β”€ text (r/w property)           # full document text
β”œβ”€β”€ insert_text(s)
β”œβ”€β”€ insert_picture(path) / insert_table(rows, cols)
β”œβ”€β”€ insert_line_break() / insert_page_break() / insert_paragraph_break()
β”œβ”€β”€ insert_tab()
β”œβ”€β”€ find_text(query) / replace_all(find, replace)
β”œβ”€β”€ select_all() / select_text(start, end) / get_selected_text()
β”œβ”€β”€ copy() / cut() / paste() / delete() / clear()
β”œβ”€β”€ undo() / redo()
β”œβ”€β”€ save()                        # save in place (per-document)
β”œβ”€β”€ save_as(path)                 # save to a new path
β”œβ”€β”€ close(save=False)             # close this document only
β”œβ”€β”€ activate()                    # bring this doc to the foreground
β”œβ”€β”€ cursor                        # per-Document cursor accessor
β”‚   β”œβ”€β”€ goto_page(n)
β”‚   └── in_table()
β”œβ”€β”€ actions                       # doc-scoped actions (auto-activates)
β”œβ”€β”€ fields / bookmarks / hyperlinks / images / paragraphs /
β”‚   styles / tables               # cached collection accessors
└── api                           # per-doc COM handle (XHwpDocument)

1:1 migration table

Rows are grouped by destination. Every removed App member in v3 has a documented landing point.

Renamed/moved to app.docs

v2 call v3 equivalent Notes
app.open(path) doc = app.docs.open(path) Returns the Document instance β€” bind it
app.new() doc = app.docs.add() Same shape; add() matches xlwings
app.doc app.docs.active Active document accessor
(n/a in v2) app.docs[0], app.docs["report.hwp"] Index / name access
(n/a in v2) len(app.docs) Number of open documents
(n/a in v2) for d in app.docs: Iteration

Moved to Document instance

Every per-document operation that lived on App (or app.doc) in v2 now lives on the Document returned by app.docs.open() / app.docs.add().

v2 call v3 equivalent Notes
app.save() doc.save() Per-document save; no ambiguity
app.save_as(path) doc.save_as(path) Per-document save-as
app.close() doc.close(save=False) Pass save=True to save before close
app.doc.text doc.text r/w property
app.doc.insert_text(s) doc.insert_text(s)
app.doc.insert_picture(p) doc.insert_picture(p)
app.doc.insert_line_break() doc.insert_line_break()
app.doc.insert_page_break() doc.insert_page_break()
app.doc.insert_paragraph_break() doc.insert_paragraph_break()
app.doc.insert_tab() doc.insert_tab()
app.doc.find_text(q) doc.find_text(q)
app.doc.replace_all(f, r) doc.replace_all(f, r)
app.doc.select_all() doc.select_all()
app.doc.select_text(s, e) doc.select_text(s, e)
app.doc.get_selected_text() doc.get_selected_text()
app.doc.copy() / cut() / paste() doc.copy() / cut() / paste()
app.doc.delete() / clear() doc.delete() / clear()
app.doc.undo() / redo() doc.undo() / redo()
app.doc.cursor.goto_page(n) doc.cursor.goto_page(n)
app.doc.cursor.in_table() doc.cursor.in_table()
app.doc.fields doc.fields FieldCollection β€” dict-like
app.doc.bookmarks doc.bookmarks BookmarkCollection
app.doc.hyperlinks doc.hyperlinks HyperlinkCollection
app.doc.images doc.images ImageCollection
app.doc.paragraphs doc.paragraphs ParagraphCollection
app.doc.styles doc.styles StyleCollection
app.doc.tables doc.tables TableCollection

Multi-document workflows (NEW)

These were impossible in v2 without dropping to raw COM:

Pattern v3 syntax
Open two files at once a = app.docs.open("a.hwp"); b = app.docs.open("b.hwp")
Create blank alongside existing src = app.docs.open(p); dst = app.docs.add()
Iterate every open document for d in app.docs: ...
Look up by filename app.docs["report.hwp"]
Look up by index app.docs[0]
Switch active document doc.activate()
Count open documents len(app.docs)

actions binding

The raw HAction interface is process-global and always targets the active document. v3 exposes it at two levels with different semantics:

Entry point Activation Use when
app.actions.X.run() Caller is responsible (doc.activate() manually) Single-doc scripts / escape hatch
doc.actions.X.run() Automatic β€” doc.activate() is called immediately before run() Default β€” multi-doc safe

doc.actions is a thin proxy. The auto-activate adds one COM call (microseconds) per run() and is a no-op when the doc is already active. For single-document scripts the cost is negligible; for multi-document scripts it removes a whole class of β€œwrong document was active” bugs.

Examples

Example 1 β€” Single document: open, insert, save

# v2.x
from hwpapi import App
app = App()
app.open("report.hwp")
app.doc.insert_text("Hello, world.\n")
app.save()
app.close()
app.quit()

# v3.0
from hwpapi import App
with App() as app:
    doc = app.docs.open("report.hwp")
    doc.insert_text("Hello, world.\n")
    doc.save()
    doc.close()
# app.quit() called automatically on exit

Example 2 β€” Multi-document: merge two files

# v2.x β€” not directly supported; required app.api.XHwpDocuments

# v3.0
from hwpapi import App

with App() as app:
    src = app.docs.open("source.hwp")
    dst = app.docs.add()                      # blank document

    for para in src.paragraphs:
        dst.insert_text(para.text + "\n")

    dst.save_as("merged.hwp")
    src.close(save=False)                     # leave source untouched
    dst.close()

Example 3 β€” Bulk convert: HWP β†’ PDF for a folder

# v3.0
from hwpapi import App
from pathlib import Path

with App() as app:
    for hwp in Path("inbox").glob("*.hwp"):
        doc = app.docs.open(hwp)
        doc.save_as(hwp.with_suffix(".pdf"))  # extension drives format
        doc.close()                           # release memory before next file

Example 4 β€” Multi-document switching: process N files in parallel

# v3.0
from hwpapi import App
from pathlib import Path

with App() as app:
    docs = [app.docs.open(p) for p in Path("inbox").glob("*.hwp")]

    print(f"{len(app.docs)} documents open")

    for doc in app.docs:
        doc.replace_all("{{YEAR}}", "2026")   # auto-activates per call
        doc.save()

    # Pick a specific one by name and activate it for the user
    app.docs["summary.hwp"].activate()

FAQ

β€œWhat happened to app.doc?” Removed. v2’s β€œactive document” accessor is now app.docs.active. The v2 implicit-active-doc model is replaced by explicit binding: capture the Document returned by app.docs.open(...) / app.docs.add() and work with that variable.

β€œHow do I get the currently active document?” app.docs.active. Note that the active document can change if the user clicks another window in HWP β€” bind your own variable from app.docs.open(...) and use doc.activate() when you need to be sure.

β€œHow do I iterate over all open documents?” for d in app.docs:. len(app.docs) returns the count, and app.docs[i] / app.docs["name.hwp"] index by position or filename.

β€œDoes doc.actions.X cost more than app.actions.X?” Practically no. doc.actions.X.run() calls doc.activate() (one COM call, microseconds) before delegating to app.actions.X.run(). When doc is already active, the activation is a no-op. The benefit is multi-document safety; use app.actions only as an explicit escape hatch.

β€œCan I still pin v2?” Yes β€” pip install "hwpapi==2.*". The v2.x branch is frozen at v2.0.0 and will receive only critical bug fixes.

β€œWhen will v2 stop receiving fixes?” v2 is frozen as of v3.0 release. Only critical bug fixes (data loss, crashes, security) are backported; no new features, no API additions.

See also

Back to top