Repository navigation
Make numbering definitions writable - define a list from scratch #98
Description
#73 gave us a good read side for numbering — Paragraph.numbering, Paragraph.list_number, Document.list_numbers, NumberingLevel.number_format, .level_text, .start, format_number() — plus set_numbering(), remove_numbering() and restart_numbering() on the write side.
What is missing is the ability to define a list. NumberingLevel is read-only: start, num_fmt, lvl_text, lvl_restart, is_lgl and p_style on CT_Lvl are all getters with no setters, and Numbering has no add_definition(). CT_Numbering.add_num() exists but only takes an abstractNum_id that must already be there.
So you can apply a list definition the document already has, and you can restart one, but you cannot create a numbered list with a format the template does not already contain — no 1) when the template only has 1., no custom bullet character, no per-level indent. In practice people fall back to hand-building w:abstractNum XML and appending it, which is exactly the kind of thing the library should be doing for them.
Where the prior art is
jlovegren0/python-docx, branch master (and footnotes), is the most complete numbering-definition layer of any of the forks. It adds constructors and writable attributes for CT_AbstractNum, CT_Lvl, CT_NumFmt, CT_LevelText, CT_LevelSuffix, CT_MultiLevelType, CT_LvlLegacy and CT_NumPicBullet, along with the ST_NumberFormat, ST_LevelSuffix and ST_MultiLevelType simple types in docx/oxml/simpletypes.py and an abstractNum_having_abstractNumId() lookup.
Our CT_AbstractNum and CT_Lvl already exist with the read accessors and lvl_having_ilvl(); WD_NUMBER_FORMAT already covers ST_NumberFormat. This is filling in the write side of classes we have, not new ones.
Yiqing-Zhou/python-docx and revvsales/python-docx-1 (Document.get_new_list(abstractNumId)) both do smaller versions of the same thing.
Shape
- Setters on
CT_Lvlforstart,numFmt,lvlText,lvlRestart,suff,lvlJcand the levelw:pPrindent w:suff(ST_LevelSuffix:tab,space,nothing) is not modelled at all — it is what controls the gap between the number and the text, and is a common askNumbering.add_definition(levels)->NumberingDefinition, allocating a freeabstractNumIdandnumIdand wiring thew:num->w:abstractNumlink- A
NumberingLevelwrite side matching the existing read properties - Probably a helper for the two common cases, a bulleted list and a decimal-with-sublevels list, since assembling nine levels by hand is the tedious part
Watch out for
CT_Numberingis anxsd:sequence— allw:abstractNumelements precede allw:num._tag_seqalready reflects this; new definitions must be placed accordingly and the placement asserted in a test.- Word rejects a
w:abstractNumwhosew:lvlelements are not in ascendingw:ilvlorder. w:nsidandw:tmplare what Word uses to recognise a list definition as one of its own. Checkref/xsd/wml.xsdand decide whether to emit them; documents open fine without them but the list may not round-trip through Word's list gallery.
Activity
toxicphreAK commentedon Aug 5, 2026
Landed in #139. Setters on
CT_Lvlforstart,numFmt,lvlText,lvlRestart,suff,lvlJc,pStyleandisLgl, each placing its child in schema order;w:suffwas not modelled at all and is now.Numbering.add_definition(), plusadd_numbered_definition()andadd_bulleted_definition()for the tedious nine-level part, andNumberingLevel.set().All three warnings are handled: the abstract definition goes before every
w:num, levels are kept in ascendingw:ilvl, and onw:nsid/w:tmpl— they are deliberately not written, since they are what Word uses to claim a definition as one of its own gallery entries and inventing values would assert a provenance the definition does not have.