Skip to content

numbering

numbering

The list-numbering API — reading the number a list paragraph displays, and restarting.

The number a reader sees against a list paragraph — "1.", "a)", "iii." — is nowhere in the document body. Word computes it from numbering.xml at display time, so anything converting a document to text, Markdown or HTML has to compute it too. That computation is compute_list_numbers, and it is the part of this module worth being careful about; the rest is a straightforward model over the numbering part.

The model mirrors the two-level indirection described in docx.oxml.numbering: a NumberingDefinition is a w:num, a concrete list, and it resolves each of its nine NumberingLevel objects from the w:abstractNum it points at, with any w:lvlOverride of its own applied on top.

Restarting a list means creating a second w:num on the same w:abstractNum carrying a w:startOverride, not resetting a counter; see Paragraph.restart_numbering.

What is not computed here: a level whose format is one of the locale-specific ones — Japanese counting, Korean chosung and the rest — falls back to decimal, because rendering those correctly is a localisation problem rather than a document-model one. NumberingLevel.is_renderable says which is which.

NumberingLevel

NumberingLevel(
    ilvl: int,
    lvl: CT_Lvl | None,
    start_override: int | None = None,
)

One of the nine levels of a list, with any instance overrides applied.

Reached through NumberingDefinition.levels or NumberingDefinition.level.

Source code in src/docx/numbering.py
def __init__(self, ilvl: int, lvl: CT_Lvl | None, start_override: int | None = None):
    self._ilvl = ilvl
    self._lvl = lvl
    self._start_override = start_override

ilvl property

ilvl: int

The zero-based level number, 0 being the outermost.

is_bullet property

is_bullet: bool

True when this level shows a bullet rather than a number.

is_renderable property

is_renderable: bool

True when format_number can render this level's format faithfully.

False for the locale-specific formats, where format_number falls back to decimal. Worth checking before presenting a computed number as authoritative.

level_text property

level_text: str

The pattern this level displays, e.g. "%1.".

A %n is the counter of one-based level n. Defaults to "%{ilvl+1}.", which is what Word shows for a level that does not say.

number_format property

number_format: WD_NUMBER_FORMAT

Member of WdNumberFormat this level renders its counter as.

DECIMAL when the level does not say, which is Word's default.

restart_after_level property

restart_after_level: int | None

The one-based level whose increment restarts this one.

None means Word's default: restart whenever any higher level increments. 0 means never restart.

start property

start: int

The number this level counts from.

A w:startOverride on the concrete list wins over the abstract definition's w:start, which is how a restarted list begins again at 1. Defaults to 1.

style_id property

style_id: str | None

The paragraph style linked to this level, or None.

A paragraph with this style takes this level even with no w:numPr of its own.

is_legal: bool

True when this level renders every placeholder as decimal.

Word's "legal numbering" option, which turns "1.a.i" into "1.1.1".

suffix property

suffix: str

What separates the number from the text: "tab", "space" or "nothing".

"tab" when the level does not say, which is Word's default.

indent property

indent: Length | None

The left indent this level applies, or None when it sets none.

hanging_indent property

hanging_indent: Length | None

The hanging indent this level applies, or None when it sets none.

This is what keeps the wrapped text of a list item lined up under the first line rather than under the bullet.

set

set(
    *,
    start: int | None = None,
    number_format: WD_NUMBER_FORMAT | str | None = None,
    level_text: str | None = None,
    suffix: str | None = None,
    alignment: str | None = None,
    indent: Length | None = None,
    hanging_indent: Length | None = None,
    restart_after_level: int | None = None,
    style_id: str | None = None,
    is_legal: bool | None = None,
) -> NumberingLevel

Change this level's definition; return self for chaining.

Only the arguments given are written, so a call sets what it names and leaves the rest of the level alone:

level.set(number_format=WD_NUMBER_FORMAT.LOWER_LETTER, level_text="%2)")

The change is made to the abstract definition, which is shared: every list pointing at it changes with it. Use Numbering.add_definition for a list of your own rather than editing a definition the document already had.

Raises ValueError for a level that has no definition to write to — one of the nine levels an abstract definition does not define.

Source code in src/docx/numbering.py
def set(
    self,
    *,
    start: int | None = None,
    number_format: WD_NUMBER_FORMAT | str | None = None,
    level_text: str | None = None,
    suffix: str | None = None,
    alignment: str | None = None,
    indent: Length | None = None,
    hanging_indent: Length | None = None,
    restart_after_level: int | None = None,
    style_id: str | None = None,
    is_legal: bool | None = None,
) -> NumberingLevel:
    """Change this level's definition; return self for chaining.

    Only the arguments given are written, so a call sets what it names and leaves
    the rest of the level alone::

        level.set(number_format=WD_NUMBER_FORMAT.LOWER_LETTER, level_text="%2)")

    The change is made to the *abstract* definition, which is shared: every list
    pointing at it changes with it. Use :meth:`.Numbering.add_definition` for a list
    of your own rather than editing a definition the document already had.

    Raises |ValueError| for a level that has no definition to write to — one of the
    nine levels an abstract definition does not define.
    """
    if self._lvl is None:
        raise ValueError(
            "level %d has no definition in this list; only levels the abstract"
            " definition defines can be changed" % self._ilvl
        )
    lvl = self._lvl

    if start is not None:
        lvl.start = start
    if number_format is not None:
        lvl.num_fmt = number_format
    if level_text is not None:
        lvl.lvl_text = level_text
    if suffix is not None:
        lvl.suffix = suffix
    if alignment is not None:
        lvl.jc = alignment
    if restart_after_level is not None:
        lvl.lvl_restart = restart_after_level
    if style_id is not None:
        lvl.p_style = style_id
    if is_legal is not None:
        lvl.is_lgl = is_legal
    if indent is not None or hanging_indent is not None:
        pPr = lvl.get_or_add_pPr()
        if indent is not None:
            pPr.ind_left = indent  # pyright: ignore[reportAttributeAccessIssue]
        if hanging_indent is not None:
            # -- a hanging indent is a negative first-line indent, which is how
            # -- `CT_PPr.first_line_indent` already spells it --
            pPr.first_line_indent = Length(  # pyright: ignore[reportAttributeAccessIssue]
                -hanging_indent
            )
    return self

format_number

format_number(value: int) -> str

value rendered in this level's number format.

Falls back to decimal for a format this library does not render; see is_renderable.

Source code in src/docx/numbering.py
def format_number(self, value: int) -> str:
    """`value` rendered in this level's number format.

    Falls back to decimal for a format this library does not render; see
    :attr:`is_renderable`.
    """
    fmt = self.number_format
    if fmt == WD_NUMBER_FORMAT.UPPER_ROMAN:
        return _to_roman(value)
    if fmt == WD_NUMBER_FORMAT.LOWER_ROMAN:
        return _to_roman(value).lower()
    if fmt == WD_NUMBER_FORMAT.UPPER_LETTER:
        return _to_letter(value)
    if fmt == WD_NUMBER_FORMAT.LOWER_LETTER:
        return _to_letter(value).lower()
    if fmt == WD_NUMBER_FORMAT.ORDINAL:
        return _to_ordinal(value)
    if fmt == WD_NUMBER_FORMAT.HEX:
        return format(value, "X")
    if fmt == WD_NUMBER_FORMAT.DECIMAL_ZERO:
        return f"{value:02d}"
    if fmt == WD_NUMBER_FORMAT.CHICAGO:
        return _CHICAGO[(value - 1) % len(_CHICAGO)] if value > 0 else str(value)
    if fmt == WD_NUMBER_FORMAT.NONE:
        return ""
    return str(value)

NumberingDefinition

NumberingDefinition(num: CT_Num, numbering: Numbering)

A concrete list — a w:num — and the levels it resolves to.

Two definitions pointing at the same abstract definition are two independent sequences that happen to look alike; that is how a restarted list is represented.

Source code in src/docx/numbering.py
def __init__(self, num: CT_Num, numbering: Numbering):
    self._num = num
    self._numbering = numbering

abstract_num_id property

abstract_num_id: int | None

The id of the abstract definition this list takes its formatting from.

num_id property

num_id: int

The id a paragraph's w:numPr/w:numId refers to this list by.

levels property

levels: List[NumberingLevel]

The nine levels of this list, outermost first.

level

level(ilvl: int) -> NumberingLevel

The level ilvl of this list, with any instance override applied.

A level the definition says nothing about is still returned, carrying Word's defaults, because a paragraph can legitimately refer to it.

Source code in src/docx/numbering.py
def level(self, ilvl: int) -> NumberingLevel:
    """The level `ilvl` of this list, with any instance override applied.

    A level the definition says nothing about is still returned, carrying Word's
    defaults, because a paragraph can legitimately refer to it.
    """
    abstract = self._abstract_num
    lvl = None if abstract is None else abstract.lvl_having_ilvl(ilvl)

    start_override = None
    lvlOverride = self._num.lvlOverride_having_ilvl(ilvl)
    if lvlOverride is not None:
        start_override = lvlOverride.start_override
        if lvlOverride.lvl is not None:
            lvl = lvlOverride.lvl

    return NumberingLevel(ilvl, lvl, start_override)

Numbering

Numbering(numbering: CT_Numbering, part: DocumentPart)

Bases: ElementProxy

The numbering definitions of a document.

Reached through Document.numbering. Supports len(), iteration over the concrete list definitions, and lookup by num_id.

Source code in src/docx/numbering.py
def __init__(self, numbering: CT_Numbering, part: DocumentPart):
    super().__init__(numbering)
    self._element = numbering
    self._part = part

get

get(num_id: int) -> NumberingDefinition | None

The list definition with num_id, or None when there is none.

Source code in src/docx/numbering.py
def get(self, num_id: int) -> NumberingDefinition | None:
    """The list definition with `num_id`, or |None| when there is none."""
    try:
        num = self._element.num_having_numId(num_id)
    except KeyError:
        return None
    return NumberingDefinition(num, self)

add_definition

add_definition(
    levels: Sequence[Mapping[str, object]] | None = None,
    *,
    multi_level_type: str | None = None,
) -> NumberingDefinition

Define a new list and return it.

Until now a list could be applied and restarted but not defined, so a format the template did not already contain — 1) where the template has 1., a custom bullet character, a particular per-level indent — meant hand-building w:abstractNum XML:

definition = document.numbering.add_definition([
    {"number_format": WD_NUMBER_FORMAT.DECIMAL, "level_text": "%1)"},
    {"number_format": WD_NUMBER_FORMAT.LOWER_LETTER, "level_text": "%2)"},
])
paragraph.set_numbering(definition.num_id, level=0)

levels is one mapping per level, outermost first, of the keyword arguments NumberingLevel.set takes. A level given as an empty mapping takes the defaults. With levels of None the definition gets nine decimal levels, which is what Word's plain numbered list is; add_bulleted_definition and add_numbered_definition are the shorthands for the two common cases.

multi_level_type is written to w:multiLevelType when given; Word uses it to decide how to present the list in its gallery and is content without it.

A fresh w:abstractNum and a w:num pointing at it are created, both with ids free in this document. w:nsid and w:tmpl are deliberately not written — they are what Word uses to recognise a definition as one of its own gallery entries, and inventing values would claim a provenance this definition does not have.

Raises ValueError for more than nine levels, which is all OOXML admits.

Source code in src/docx/numbering.py
def add_definition(
    self,
    levels: Sequence[Mapping[str, object]] | None = None,
    *,
    multi_level_type: str | None = None,
) -> NumberingDefinition:
    """Define a new list and return it.

    Until now a list could be *applied* and *restarted* but not defined, so a format
    the template did not already contain — `1)` where the template has `1.`, a
    custom bullet character, a particular per-level indent — meant hand-building
    `w:abstractNum` XML::

        definition = document.numbering.add_definition([
            {"number_format": WD_NUMBER_FORMAT.DECIMAL, "level_text": "%1)"},
            {"number_format": WD_NUMBER_FORMAT.LOWER_LETTER, "level_text": "%2)"},
        ])
        paragraph.set_numbering(definition.num_id, level=0)

    `levels` is one mapping per level, outermost first, of the keyword arguments
    :meth:`.NumberingLevel.set` takes. A level given as an empty mapping takes the
    defaults. With `levels` of |None| the definition gets nine decimal levels, which
    is what Word's plain numbered list is; :meth:`add_bulleted_definition` and
    :meth:`add_numbered_definition` are the shorthands for the two common cases.

    `multi_level_type` is written to `w:multiLevelType` when given; Word uses it to
    decide how to present the list in its gallery and is content without it.

    A fresh `w:abstractNum` and a `w:num` pointing at it are created, both with ids
    free in this document. `w:nsid` and `w:tmpl` are deliberately not written — they
    are what Word uses to recognise a definition as one of its own gallery entries,
    and inventing values would claim a provenance this definition does not have.

    Raises |ValueError| for more than nine levels, which is all OOXML admits.
    """
    from docx.oxml.numbering import CT_AbstractNum

    if levels is None:
        levels = [{} for _ in range(_MAX_LEVELS)]
    elif len(levels) > _MAX_LEVELS:
        raise ValueError(
            "a list definition has at most %d levels, got %d"
            % (_MAX_LEVELS, len(levels))
        )

    abstract = CT_AbstractNum.new(self._next_abstract_num_id())
    self._insert_abstract_num(abstract)
    if multi_level_type is not None:
        abstract.multi_level_type = multi_level_type

    for ilvl, spec in enumerate(levels):
        lvl = abstract.add_level(ilvl)
        NumberingLevel(ilvl, lvl).set(
            **{
                "number_format": WD_NUMBER_FORMAT.DECIMAL,
                "level_text": "%%%d." % (ilvl + 1),
                "start": 1,
                **spec,  # pyright: ignore[reportArgumentType]
            }
        )

    num = self._element.add_num(abstract.abstractNumId)
    return NumberingDefinition(num, self)

add_numbered_definition

add_numbered_definition(
    depth: int = 9,
    *,
    formats: Sequence[WD_NUMBER_FORMAT] | None = None,
    indent_step: Length | None = None,
) -> NumberingDefinition

Define a decimal-with-sublevels list and return it.

The tedious half of add_definition is assembling nine levels by hand, so this does it: each level shows its own counter followed by a period, in the format formats gives it, indented indent_step further than the level above.

formats cycles when it is shorter than depth; the default of decimal, lower letter and lower roman is Word's familiar 1. / a. / i. alternation. For the cumulative "1.1.1" style, pass levels to add_definition with level_text of "%1.%2." and so on. indent_step defaults to a quarter inch, which is what Word uses.

Raises ValueError for a depth above nine, as add_definition does.

Source code in src/docx/numbering.py
def add_numbered_definition(
    self,
    depth: int = 9,
    *,
    formats: Sequence[WD_NUMBER_FORMAT] | None = None,
    indent_step: Length | None = None,
) -> NumberingDefinition:
    """Define a decimal-with-sublevels list and return it.

    The tedious half of :meth:`add_definition` is assembling nine levels by hand, so
    this does it: each level shows its own counter followed by a period, in the
    format `formats` gives it, indented `indent_step` further than the level above.

    `formats` cycles when it is shorter than `depth`; the default of decimal, lower
    letter and lower roman is Word's familiar 1. / a. / i. alternation. For the
    cumulative "1.1.1" style, pass `levels` to :meth:`add_definition` with
    `level_text` of ``"%1.%2."`` and so on. `indent_step` defaults to a quarter
    inch, which is what Word uses.

    Raises |ValueError| for a `depth` above nine, as :meth:`add_definition` does.
    """
    from docx.shared import Inches

    if formats is None:
        formats = (
            WD_NUMBER_FORMAT.DECIMAL,
            WD_NUMBER_FORMAT.LOWER_LETTER,
            WD_NUMBER_FORMAT.LOWER_ROMAN,
        )
    step = Inches(0.25) if indent_step is None else indent_step

    levels: List[Dict[str, object]] = []
    for ilvl in range(depth):
        levels.append(
            {
                "number_format": formats[ilvl % len(formats)],
                "level_text": "%%%d." % (ilvl + 1),
                "start": 1,
                "indent": Length(step * (ilvl + 2)),
                "hanging_indent": step,
            }
        )
    return self.add_definition(levels, multi_level_type="multilevel")

add_bulleted_definition

add_bulleted_definition(
    depth: int = 9,
    *,
    bullets: Sequence[str] = ("•", "o", "§"),
    indent_step: Length | None = None,
) -> NumberingDefinition

Define a bulleted list and return it.

bullets cycles when it is shorter than depth; the default is Word's own bullet, circle and square sequence. indent_step defaults to a quarter inch. Raises ValueError for a depth above nine.

Note the bullet characters Word writes are glyphs of the Symbol and Wingdings fonts rather than the Unicode characters they resemble. The defaults here are the Unicode ones, which render in whatever font the paragraph uses and so do not depend on a font being installed.

Source code in src/docx/numbering.py
def add_bulleted_definition(
    self,
    depth: int = 9,
    *,
    bullets: Sequence[str] = ("•", "o", "§"),
    indent_step: Length | None = None,
) -> NumberingDefinition:
    """Define a bulleted list and return it.

    `bullets` cycles when it is shorter than `depth`; the default is Word's own
    bullet, circle and square sequence. `indent_step` defaults to a quarter inch.
    Raises |ValueError| for a `depth` above nine.

    Note the bullet characters Word writes are glyphs of the Symbol and Wingdings
    fonts rather than the Unicode characters they resemble. The defaults here are
    the Unicode ones, which render in whatever font the paragraph uses and so do not
    depend on a font being installed.
    """
    from docx.shared import Inches

    step = Inches(0.25) if indent_step is None else indent_step

    levels: List[Dict[str, object]] = []
    for ilvl in range(depth):
        levels.append(
            {
                "number_format": WD_NUMBER_FORMAT.BULLET,
                "level_text": bullets[ilvl % len(bullets)],
                "indent": Length(step * (ilvl + 2)),
                "hanging_indent": step,
            }
        )
    return self.add_definition(levels, multi_level_type="hybridMultilevel")

restart

restart(
    num_id: int, ilvl: int = 0, start: int = 1
) -> NumberingDefinition

Return a new list definition restarting the list num_id at start.

The new definition points at the same abstract definition, so it looks identical, and carries a w:startOverride for level ilvl. Assign its num_id to a paragraph to make the list begin again there; Paragraph.restart_numbering does that in one step.

Raises KeyError if num_id names no list.

Source code in src/docx/numbering.py
def restart(self, num_id: int, ilvl: int = 0, start: int = 1) -> NumberingDefinition:
    """Return a new list definition restarting the list `num_id` at `start`.

    The new definition points at the same abstract definition, so it looks identical,
    and carries a `w:startOverride` for level `ilvl`. Assign its :attr:`num_id` to a
    paragraph to make the list begin again there; :meth:`.Paragraph.restart_numbering`
    does that in one step.

    Raises |KeyError| if `num_id` names no list.
    """
    source = self.get(num_id)
    if source is None:
        raise KeyError(f"no numbering definition with num_id {num_id}")
    abstract_num_id = source.abstract_num_id
    if abstract_num_id is None:
        raise KeyError(f"numbering definition {num_id} names no abstract definition")

    num = self._element.add_num(abstract_num_id)
    num.add_lvlOverride(ilvl).add_startOverride(start)
    return NumberingDefinition(num, self)

ParagraphNumbering

ParagraphNumbering(
    num_id: int,
    level: int,
    numbering: Numbering,
    from_style: bool,
)

The list membership of a paragraph — which list it is in, and at what level.

Reached through Paragraph.numbering, which is None for a paragraph that is not in a list at all.

Source code in src/docx/numbering.py
def __init__(self, num_id: int, level: int, numbering: Numbering, from_style: bool):
    self._num_id = num_id
    self._level = level
    self._numbering = numbering
    self._from_style = from_style

definition property

definition: NumberingDefinition | None

The list this paragraph belongs to, None if num_id names none.

from_style property

from_style: bool

True when this numbering comes from the paragraph's style, not the paragraph.

A paragraph numbered through its style has no w:numPr of its own, so changing its level means giving it one.

level property

level: int

The zero-based list level of this paragraph, 0 being the outermost.

level_definition property

level_definition: NumberingLevel | None

The NumberingLevel governing this paragraph, None if unresolvable.

num_id property

num_id: int

The id of the list this paragraph belongs to.

get_paragraph_numbering

get_paragraph_numbering(
    p: CT_P, part: DocumentPart
) -> Tuple[int, int, bool] | None

(num_id, ilvl, from_style) for p, or None when it is not in a list.

A direct w:pPr/w:numPr on the paragraph wins. Failing that the paragraph's style hierarchy is walked — a style's own w:numPr, then the style it is based on — which is how the built-in "List Number" and "List Bullet" styles number a paragraph that carries no numbering markup at all.

w:numId of 0 means "explicitly not numbered" and is honoured as such: Word uses it to switch numbering off for a paragraph whose style would otherwise apply it.

Source code in src/docx/numbering.py
def get_paragraph_numbering(p: CT_P, part: DocumentPart) -> Tuple[int, int, bool] | None:
    """`(num_id, ilvl, from_style)` for `p`, or |None| when it is not in a list.

    A direct `w:pPr/w:numPr` on the paragraph wins. Failing that the paragraph's style
    hierarchy is walked — a style's own `w:numPr`, then the style it is based on — which
    is how the built-in "List Number" and "List Bullet" styles number a paragraph that
    carries no numbering markup at all.

    `w:numId` of 0 means "explicitly not numbered" and is honoured as such: Word uses it
    to switch numbering off for a paragraph whose style would otherwise apply it.
    """
    pPr = p.pPr
    if pPr is not None:
        numPr = pPr.numPr
        if numPr is not None:
            num_id = numPr.numId_val
            if num_id is not None:
                if num_id == 0:
                    return None
                return num_id, numPr.ilvl_val or 0, False

    style_id = p.style
    if style_id is None:
        return None
    resolved = _numbering_from_style(style_id, part)
    if resolved is None:
        return None
    num_id, ilvl = resolved
    if num_id == 0:
        return None
    return num_id, ilvl, True

compute_list_numbers

compute_list_numbers(
    paragraphs: Iterator[CT_P], part: DocumentPart
) -> List[Tuple[CT_P, str]]

(paragraph_element, number) for each numbered paragraph, in document order.

paragraphs must be every paragraph of the story in document order, since a list number depends on everything before it. Paragraphs that are not in a list contribute no entry.

The elements are returned rather than used as dictionary keys because an lxml element proxy is created on demand and may be collected and its id() reused; holding the element in the result is what keeps the association valid.

The counters follow ISO/IEC 29500 §17.9. For each numbered paragraph the counter of its own level increments, and every deeper level is restarted — unless its w:lvlRestart says otherwise, where 0 means never restart and n means restart only when the one-based level n increments. Counters are kept per w:num rather than per abstract definition, which is what makes two lists sharing a definition count independently and what makes a w:startOverride restart work.

Source code in src/docx/numbering.py
def compute_list_numbers(paragraphs: Iterator[CT_P], part: DocumentPart) -> List[Tuple[CT_P, str]]:
    """`(paragraph_element, number)` for each numbered paragraph, in document order.

    `paragraphs` must be every paragraph of the story in document order, since a list
    number depends on everything before it. Paragraphs that are not in a list contribute
    no entry.

    The elements are returned rather than used as dictionary keys because an lxml element
    proxy is created on demand and may be collected and its `id()` reused; holding the
    element in the result is what keeps the association valid.

    The counters follow ISO/IEC 29500 §17.9. For each numbered paragraph the counter of
    its own level increments, and every deeper level is restarted — unless its
    `w:lvlRestart` says otherwise, where 0 means never restart and *n* means restart only
    when the one-based level *n* increments. Counters are kept per `w:num` rather than
    per abstract definition, which is what makes two lists sharing a definition count
    independently and what makes a `w:startOverride` restart work.
    """
    if not part.has_numbering_part:
        # -- no numbering part means no list can resolve; say so without adding one --
        return []
    numbering = Numbering(part.numbering_part.element, part)

    # -- counters[num_id][ilvl] is the last number shown at that level. A level absent
    # -- from the dict has not started yet and takes its `start` value next. --
    counters: Dict[int, Dict[int, int]] = {}
    numbers: List[Tuple[CT_P, str]] = []

    for p in paragraphs:
        resolved = get_paragraph_numbering(p, part)
        if resolved is None:
            continue
        num_id, ilvl, _ = resolved

        definition = numbering.get(num_id)
        if definition is None:
            continue

        level = definition.level(ilvl)
        list_counters = counters.setdefault(num_id, {})

        if ilvl in list_counters:
            list_counters[ilvl] += 1
        else:
            list_counters[ilvl] = level.start

        _restart_deeper_levels(definition, list_counters, ilvl)
        numbers.append((p, _render(definition, level, list_counters, ilvl)))

    return numbers

iter_story_paragraphs

iter_story_paragraphs(element) -> Iterator[CT_P]

Generate every w:p in element in document order, tables included.

List numbering counts paragraphs in the order Word lays them out, and a numbered paragraph inside a table cell counts towards the same list as one outside it.

Source code in src/docx/numbering.py
def iter_story_paragraphs(element) -> Iterator[CT_P]:
    """Generate every `w:p` in `element` in document order, tables included.

    List numbering counts paragraphs in the order Word lays them out, and a numbered
    paragraph inside a table cell counts towards the same list as one outside it.
    """
    for p in element.iter(qn("w:p")):
        yield p