GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — Restart numbering of an ordered list in document. · Issue #25 · python-openxml/python-docx

Restart numbering of an ordered list in document. #25

New issue
Copy link
New issue
Copy link
Open
Open
Restart numbering of an ordered list in document.#25
Copy link
Labels
numbering

Description

@proofit404
proofit404
opened on Mar 18, 2014
Issue body actions

We can easily add ordered list with document.add_paragraph(style='ListNumber') code. But how can we restart its numbering?

Reacted by Adam Stauffer, H0r53, palash kulshreshtha, Jesus Alber, csdevignes, pemontto, Peter Zaitcev / [USSX] Hares and Adam Zhu

Activity

Next
  1. scanny
    modified the milestones: This milestone has been deleted, This milestone has been deleted on May 1, 2014
  2. scanny
    modified the milestones: This milestone has been deleted, This milestone has been deleted on May 13, 2014
  3. scanny
    added
    numbering
    on Jun 17, 2014
  4. lafolle commented on Aug 6, 2014

    @lafolle
    lafolle
    on Aug 6, 2014
    More actions

    +1

  5. ilnurgi commented on Aug 15, 2014

    @ilnurgi
    ilnurgi
    on Aug 15, 2014
    More actions

    +1 :)

  6. downtown12 commented on Jul 6, 2015

    @downtown12
    downtown12
    on Jul 6, 2015
    More actions

    Hi @scanny . I have a problem with workaround way you mentioned in stackoverflow.com. In order to restarting the list numbering, you said one need to locate the numbering definition of the style as well as the abstract numbering definition it points to. I tried but failed to locate these two definitions. Can you explain in detail how they can be located? Cuz I haven't found any documents about them.

  7. scanny commented on Jul 6, 2015

    @scanny
    scanny
    on Jul 6, 2015
    Contributor
    More actions

    Can you link to the SO response? that would save me from taking the time to search it up.

  8. downtown12 commented on Jul 7, 2015

    @downtown12
    downtown12
    on Jul 7, 2015
    More actions

    Here is the link, the same as the link you answered for issue #87 : http://stackoverflow.com/questions/23446268/python-docx-how-to-restart-list-lettering/23464442#23464442

  9. scanny commented on Jul 7, 2015

    @scanny
    scanny
    on Jul 7, 2015
    Contributor
    More actions

    @downtown12 I don't have time to develop step-by-step instructions for you, but if you're looking for the right direction to start looking for yourself, these might be helpful:

    • The document object provides access to the numbering part for the document (numbering.xml). This call should get you there: document.part.numbering_part
    • The NumberingPart object provides access to a sequence of NumberingDefinition objects: numbering_part.numbering_definitions
    • The NumberingDefinitions class looks like where API support ends at the moment. You can get the w:numbering element from it though, which is the parent of all the numbering definitions: numbering_definitions._numbering. From there you'll need to work with lxml calls to get it's children using XPath and so on.
    Reacted by soxfmr, Joseph Fox-Rabinovitz and Dmitry Selivanov
    Reacted by Sean Gillespie
  10. downtown12 commented on Jul 7, 2015

    @downtown12
    downtown12
    on Jul 7, 2015
    More actions

    Thanks a lot @scanny .Your instructions help me connect the attributes you mentioned above with the elements of numbering.xml. They are pretty helpful.

  11. yurac commented on Sep 15, 2015

    @yurac
    yurac
    on Sep 15, 2015
    More actions

    @downtown12, I also use this feature. I see that all infrastructure is already in python-docx. Enabling it is easy - Below is a patch. I also added accessors for numId, ilvl and ind.left for
    convenience

    diff --git a/docx/document.py b/docx/document.py
    index 655a70e..4a0ec06 100644
    --- a/docx/document.py
    +++ b/docx/document.py
    @@ -51,6 +51,12 @@ class Document(ElementProxy):
             paragraph.add_run().add_break(WD_BREAK.PAGE)
             return paragraph
    
    +    def get_new_list(self, abstractNumId):
    +        """
    +        Returns a new numId that references given abstractNumId
    +        """
    +        return self.numbering.numbering_definitions.add_num(abstractNumId, True)
    +
         def add_paragraph(self, text='', style=None):
             """
             Return a paragraph newly added to the end of the document, populated
    @@ -157,6 +163,14 @@ class Document(ElementProxy):
             return self._part.styles
    
         @property
    +    def numbering(self):
    +        """
    +        A "Provides access to numbering part
    +        """
    +        x=self._part.numbering_part
    +        return self._part.numbering_part
    +
    +    @property
         def tables(self):
             """
             A list of |Table| instances corresponding to the tables in the
    diff --git a/docx/oxml/numbering.py b/docx/oxml/numbering.py
    index aeedfa9..097f3d6 100644
    --- a/docx/oxml/numbering.py
    +++ b/docx/oxml/numbering.py
    @@ -96,13 +96,15 @@ class CT_Numbering(BaseOxmlElement):
         """
         num = ZeroOrMore('w:num', successors=('w:numIdMacAtCleanup',))
    
    -    def add_num(self, abstractNum_id):
    +    def add_num(self, abstractNum_id, restart=False):
             """
             Return a newly added CT_Num (<w:num>) element referencing the
             abstract numbering definition identified by *abstractNum_id*.
             """
             next_num_id = self._next_numId
             num = CT_Num.new(next_num_id, abstractNum_id)
    +        if restart:
    +            num.add_lvlOverride(ilvl=0).add_startOverride(1)
             return self._insert_num(num)
    
         def num_having_numId(self, numId):
    diff --git a/docx/parts/numbering.py b/docx/parts/numbering.py
    index e324c5a..186c6f8 100644
    --- a/docx/parts/numbering.py
    +++ b/docx/parts/numbering.py
    @@ -43,5 +43,8 @@ class _NumberingDefinitions(object):
             super(_NumberingDefinitions, self).__init__()
             self._numbering = numbering_elm
    
    +    def add_num(self, abstractNum_id, restart=False):
    +        return self._numbering.add_num(abstractNum_id, restart).numId
    +
         def __len__(self):
             return len(self._numbering.num_lst)
    
  12. downtown12 commented on Sep 18, 2015

    @downtown12
    downtown12
    on Sep 18, 2015
    More actions

    @yurac Thanks.
    Actually, I solved my issue serveral days ago in another work-around way.
    I didn't walk the way that changing the original docx packages but adding some features in the app of myself.
    But I wrote much more amount of code than you did.
    I read the document_part as well as the numbering_part from the Document object that I was editing.
    Firstly I parse the document_part using lxml, once I extract one numbering list paragraph, I create a abstract number node for it in the numbering_part. So that can make sure every list has its numbering restarted.
    It seems every road lead to Rome.
    Thanks very much for supplying me a new way.

  13. 14 remaining items

  14. scanny
    removed this from the This milestone has been deleted milestone on Apr 9, 2016
  15. akobler commented on Jun 6, 2016

    @akobler
    akobler
    on Jun 6, 2016
    More actions

    +1

  16. ameily commented on Apr 18, 2017

    @ameily
    ameily
    on Apr 18, 2017
    More actions

    I need this functionality for a project. It looks like there are two PRs open, #110 and #210, and both are awaiting tests. Can I step in and help and, if so, which PR should I go off of? I personally like the API in #110 better.

  17. scanny commented on Apr 21, 2017

    @scanny
    scanny
    on Apr 21, 2017
    Contributor
    More actions

    Hi Adam, you're more than welcome to contribute. The key thing that stops most folks is getting the tests done. Python folks just don't seem to be test-driven as a whole and this project is strictly test-driven. No commit gets merged without tests. You can look at the commit history to get an idea the granularity and flow of how these feature additions go. Generally look for a commit that starts with 'docs: document analysis for ...' followed by one starting 'acpt: add scenarios for xxx'. Then the implementation follows and then the pattern repeats.

    The first step is developing the enhancement proposal, also known as the "analysis page". This is where the API is resolved and so it's a natural first step. It's also separately committable, whether you go on to develop the feature or not. After that, you can pick and choose whatever you want from the existing pull requests to use in yours.

    Neither one of the two PRs look like they went in the right direction, which is typically what happens when you don't start with the analysis document. There's a lot to think through and inputs to be collected and understood, like the relevant XML Schema excerpts, Word's own behaviors with respect to lists, what the spec has to say about it (little generally :), and what the MS API is for doing the same from VBA.

    Let me know if you need more to go on :)

    Reacted by David Drinnan
  18. cm-cm-cm-cm commented on Jun 5, 2018

    @cm-cm-cm-cm
    cm-cm-cm-cm
    on Jun 5, 2018
    More actions

    Are there any updates to this?

  19. chaithanyaramkumar commented on Jun 20, 2018

    @chaithanyaramkumar
    chaithanyaramkumar
    on Jun 20, 2018
    More actions

    how to read bullets or numbering in an existing document
    for example
    input is
    1.apple
    2.boy
    output is
    ['1.apple','2.boy']

  20. madphysicist commented on Jun 27, 2018

    @madphysicist
    madphysicist
    on Jun 27, 2018
    More actions

    So I've made a thing that searches the existing abstract numbering schemes for the style of the current paragraph, and sets a reasonable abstract style based on that if possible:

    def list_number(doc, par, prev=None, level=None, num=True):
        """
        Makes a paragraph into a list item with a specific level and
        optional restart.
    
        An attempt will be made to retreive an abstract numbering style that
        corresponds to the style of the paragraph. If that is not possible,
        the default numbering or bullet style will be used based on the
        ``num`` parameter.
    
        Parameters
        ----------
        doc : docx.document.Document
            The document to add the list into.
        par : docx.paragraph.Paragraph
            The paragraph to turn into a list item.
        prev : docx.paragraph.Paragraph or None
            The previous paragraph in the list. If specified, the numbering
            and styles will be taken as a continuation of this paragraph.
            If omitted, a new numbering scheme will be started.
        level : int or None
            The level of the paragraph within the outline. If ``prev`` is
            set, defaults to the same level as in ``prev``. Otherwise,
            defaults to zero.
        num : bool
            If ``prev`` is :py:obj:`None` and the style of the paragraph
            does not correspond to an existing numbering style, this will
            determine wether or not the list will be numbered or bulleted.
            The result is not guaranteed, but is fairly safe for most Word
            templates.
        """
        xpath_options = {
            True: {'single': 'count(w:lvl)=1 and ', 'level': 0},
            False: {'single': '', 'level': level},
        }
    
        def style_xpath(prefer_single=True):
            """
            The style comes from the outer-scope variable ``par.style.name``.
            """
            style = par.style.style_id
            return (
                'w:abstractNum['
                    '{single}w:lvl[@w:ilvl="{level}"]/w:pStyle[@w:val="{style}"]'
                ']/@w:abstractNumId'
            ).format(style=style, **xpath_options[prefer_single])
    
        def type_xpath(prefer_single=True):
            """
            The type is from the outer-scope variable ``num``.
            """
            type = 'decimal' if num else 'bullet'
            return (
                'w:abstractNum['
                    '{single}w:lvl[@w:ilvl="{level}"]/w:numFmt[@w:val="{type}"]'
                ']/@w:abstractNumId'
            ).format(type=type, **xpath_options[prefer_single])
    
        def get_abstract_id():
            """
            Select as follows:
    
                1. Match single-level by style (get min ID)
                2. Match exact style and level (get min ID)
                3. Match single-level decimal/bullet types (get min ID)
                4. Match decimal/bullet in requested level (get min ID)
                3. 0
            """
            for fn in (style_xpath, type_xpath):
                for prefer_single in (True, False):
                    xpath = fn(prefer_single)
                    ids = numbering.xpath(xpath)
                    if ids:
                        return min(int(x) for x in ids)
            return 0
    
        if (prev is None or
                prev._p.pPr is None or
                prev._p.pPr.numPr is None or
                prev._p.pPr.numPr.numId is None):
            if level is None:
                level = 0
            numbering = doc.part.numbering_part.numbering_definitions._numbering
            # Compute the abstract ID first by style, then by num
            anum = get_abstract_id()
            # Set the concrete numbering based on the abstract numbering ID
            num = numbering.add_num(anum)
            # Make sure to override the abstract continuation property
            num.add_lvlOverride(ilvl=level).add_startOverride(1)
            # Extract the newly-allocated concrete numbering ID
            num = num.numId
        else:
            if level is None:
                level = prev._p.pPr.numPr.ilvl.val
            # Get the previous concrete numbering ID
            num = prev._p.pPr.numPr.numId.val
        par._p.get_or_add_pPr().get_or_add_numPr().get_or_add_numId().val = num
        par._p.get_or_add_pPr().get_or_add_numPr().get_or_add_ilvl().val = level
    

    This is in no way comprehensive or particularly robust, but it works pretty well for what I am trying to do.

    Reacted by ocomsoft, Florian Bernard, Aleksandar Bosnjak, magicubedragon, Matt Fisher, meng zhao, Sunghyun, Kyle L. Davis, kgintel, Diogo Fernandes and 3 more
    Reacted by Florian Bernard, Bob Sutor, Sunghyun, Leon Blum, Kyle L. Davis and Adam Zhu
  21. chaithanyaramkumar commented on Jul 3, 2018

    @chaithanyaramkumar
    chaithanyaramkumar
    on Jul 3, 2018
    More actions

    thanks

  22. chaithanyaramkumar commented on Jul 13, 2018

    @chaithanyaramkumar
    chaithanyaramkumar
    on Jul 13, 2018
    More actions

    In a document have one table
    we need to read first row is col header is true otherwise its false
    also first col is row header otherwise its false
    please answer me.

  23. nitinkhosla79 commented on Apr 2, 2020

    @nitinkhosla79
    nitinkhosla79
    on Apr 2, 2020
    More actions

    All kudos to @jlovegren0. He added low-level support for numbering styles
    We are able to generate multilevel bullet lists now.
    PR: #582
    We are helping him test his PR. Requesting everyone to please try his api/sample code(in PR comments) and share your success story as well. OR provide feedback.
    Thank you all for your support.

  24. komawar commented on Apr 2, 2020

    @komawar
    komawar
    on Apr 2, 2020
    More actions

    Yes, excellent work indeed by @jlovegren0

    Nested numbering lists work with correct numbering.

  25. komawar commented on Apr 6, 2020

    @komawar
    komawar
    on Apr 6, 2020
    More actions

    We have a good discussion on PR: #582
    This should lay out different possibilities on the implementation of the problem and some of the corner cases/challenges therein

    again approbation for @jlovegren0 for all the crafted inputs and workarounds

  26. VictorBancho commented on Jul 24, 2023

    @VictorBancho
    VictorBancho
    on Jul 24, 2023
    More actions

    @yurac Thanks. Actually, I solved my issue serveral days ago in another work-around way. I didn't walk the way that changing the original docx packages but adding some features in the app of myself. But I wrote much more amount of code than you did. I read the document_part as well as the numbering_part from the Document object that I was editing. Firstly I parse the document_part using lxml, once I extract one numbering list paragraph, I create a abstract number node for it in the numbering_part. So that can make sure every list has its numbering restarted. It seems every road lead to Rome. Thanks very much for supplying me a new way.

    Hi @downtown12, I know it has been quite a few years since you posted this but I have encountered this issue and wanted to try your solution. Would you mind elaborating on how you parsed the document_part (which property?) using lxml and located the numbering list paragraphs? Furthermoer, how did you manage to create and where/how to insert the abstract number nodes?

    I've iterated through the document.part._element, and its children, but that didn't seem to show anything insightful (no list number objects etc even though the document has lists). Wasn't clear if the docs for python-docx also elaborated on how to do this.

  27. gookie-dev commented on Aug 21, 2025

    @gookie-dev
    gookie-dev
    on Aug 21, 2025
    More actions

    I implemented a helper list_number function that rebinds a paragraph to the correct numbering definition.

    • If prev is None, a fresh numId is created from the appropriate abstract numbering definition, and a level override with startOverride(1) is added. This ensures numbering restarts from 1.
    • Otherwise, the paragraph inherits the numId (and optionally the ilvl) from the previous paragraph, so numbering continues within the same list.
    • The function supports both numbered and bulleted lists by resolving the correct abstract numbering ID via XPath queries on the numbering part.

    Before

    from docx import Document
    
    doc = Document()
    
    doc.add_paragraph("List 1:")
    for txt in ["L1E1", "L1E2", "L1E3", "L1E4"]:
        doc.add_paragraph(txt, style="List Number")
    
    doc.add_paragraph("")
    
    doc.add_paragraph("List 2:")
    for txt in ["L2E1", "L2E2", "L2E3", "L2E4"]:
        doc.add_paragraph(txt, style="List Number")
    
    doc.save("test.docx")
    Image

    After

    from docx import Document
    
    doc = Document()
    
    doc.add_paragraph("List 1:")
    prev = None
    for txt in ["L1E1", "L1E2", "L1E3", "L1E4"]:
        p = doc.add_paragraph(txt, style="List Number")
        list_number(doc, p, prev=prev)
        prev = p
    
    doc.add_paragraph("")
    
    doc.add_paragraph("List 2:")
    prev = None  # restart numbering
    for txt in ["L2E1", "L2E2", "L2E3", "L2E4"]:
        p = doc.add_paragraph(txt, style="List Number")
        list_number(doc, p, prev=prev)
        prev = p
    
    doc.save("test.docx")
    Image
    def list_number(doc, par, prev=None, level=None, numbered=True):
        xpath_options = {
            True: {"single": "count(w:lvl)=1 and ", "level": 0},
            False: {"single": "", "level": level},
        }
    
        def style_xpath(prefer_single=True):
            style = par.style.style_id
            return (
                'w:abstractNum['
                '{single}w:lvl[@w:ilvl="{level}"]/w:pStyle[@w:val="{style}"]'
                ']/@w:abstractNumId'
            ).format(style=style, **xpath_options[prefer_single])
    
        def type_xpath(prefer_single=True):
            t = "decimal" if numbered else "bullet"
            return (
                'w:abstractNum['
                '{single}w:lvl[@w:ilvl="{level}"]/w:numFmt[@w:val="{type}"]'
                ']/@w:abstractNumId'
            ).format(type=t, **xpath_options[prefer_single])
    
        def get_abstract_id(numbering):
            for fn in (style_xpath, type_xpath):
                for prefer_single in (True, False):
                    xp = fn(prefer_single)
                    ids = numbering.xpath(xp)
                    if ids:
                        return min(int(x) for x in ids)
            return 0
    
        if (prev is None or
            prev._p.pPr is None or
            prev._p.pPr.numPr is None or
            prev._p.pPr.numPr.numId is None):
            if level is None:
                level = 0
            numbering = doc.part.numbering_part.numbering_definitions._numbering
            anum = get_abstract_id(numbering)
            num = numbering.add_num(anum)
            num.add_lvlOverride(ilvl=level).add_startOverride(1)
            num_id = num.numId
        else:
            if level is None:
                level = prev._p.pPr.numPr.ilvl.val
            num_id = prev._p.pPr.numPr.numId.val
    
        ppr = par._p.get_or_add_pPr()
        numPr = ppr.get_or_add_numPr()
        numPr.get_or_add_numId().val = num_id
        numPr.get_or_add_ilvl().val = level
    Reacted by David Drinnan
    Reacted by Rick Torres Jr., AldyAlfnsyh, Gandarufuuu, Peter Zaitcev / [USSX] Hares and Параноик
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    numbering

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

      You are visitor 01402503 — GeezerVison does not store the pages you look up.
      Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.