Add page number using python-docx

Question:

I am trying to add a page number in the footer of a word doc using python-docx. So far, I haven’t been able to find how to do so. This question address how to find a page number (or how you cannot). This one talks about creating a template and adding page numbers there. Is there a way to add page numbers on a document I created using doc = Document()?

Asked By: max_max_mir

||

Answers:

I think adding PageNumber is a feature that has not yet implemented.

However…

  1. If it is an existing document you want to add headers and footers to
    you can call a VBA-macro. I recently posted a way to do that
    (https://stackoverflow.com/a/44767400/7386332)
  2. If it is a new document then you can indeed go on and create a
    template document first and then open it up and continue editing as
    described by scanny.
  3. This refers to this use case in its docs but doesn’t demonstrate
    how
    https://python-docx.readthedocs.io/en/latest/dev/analysis/features/header.html?highlight=page%20number
  4. Or you can try this
    https://github.com/python-openxml/python-docx/issues/498
Answered By: Syafiqur__

An automatic page-number in a footer is implemented as a field. Fields do not yet have API support in python-docx, so you cannot do what you want with a document created from the default template (document = Document()), at least not by making an API call.

The two possible approaches are to create a template document that already has a page-number in the footer and start from there:

document = Document("my-template.docx")

Or to create a workaround function that adds in the XML using low-level lxml calls on an XML element object obtained from a python-docx object, like paragraph._p.

The links provided by Syafiqur__ in his answer can help you with this latter approach.

Answered By: scanny

Thanks to Syafiqur__ and scanny, I came up with a solution to add page numbers.

def create_element(name):
    return OxmlElement(name)

def create_attribute(element, name, value):
    element.set(ns.qn(name), value)


def add_page_number(run):
    fldChar1 = create_element('w:fldChar')
    create_attribute(fldChar1, 'w:fldCharType', 'begin')

    instrText = create_element('w:instrText')
    create_attribute(instrText, 'xml:space', 'preserve')
    instrText.text = "PAGE"

    fldChar2 = create_element('w:fldChar')
    create_attribute(fldChar2, 'w:fldCharType', 'end')

    run._r.append(fldChar1)
    run._r.append(instrText)
    run._r.append(fldChar2)

doc = Document()
add_page_number(doc.sections[0].footer.paragraphs[0].add_run())
doc.save("your_doc.docx")
Answered By: max_max_mir

I was able to make it appear in the centre by setting the footer paragraph’s alignment. So I would modify the last few lines of @max_max_mir’s answer to read

doc = Document()
add_page_number(doc.sections[0].footer.paragraphs[0].add_run())
doc.sections[0].footer.paragraphs[0].alignment = WD_PARAGRAPH_ALIGNMENT.CENTER
doc.save("your_doc.docx")

More generally, I was able to display ‘Page x of y’ in the footer by modifying the answer above:

def create_element(name):
    return OxmlElement(name)


def create_attribute(element, name, value):
    element.set(nsqn(name), value)


def add_page_number(paragraph):
    paragraph.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER

    page_run = paragraph.add_run()
    t1 = create_element('w:t')
    create_attribute(t1, 'xml:space', 'preserve')
    t1.text = 'Page '
    page_run._r.append(t1)

    page_num_run = paragraph.add_run()

    fldChar1 = create_element('w:fldChar')
    create_attribute(fldChar1, 'w:fldCharType', 'begin')

    instrText = create_element('w:instrText')
    create_attribute(instrText, 'xml:space', 'preserve')
    instrText.text = "PAGE"

    fldChar2 = create_element('w:fldChar')
    create_attribute(fldChar2, 'w:fldCharType', 'end')

    page_num_run._r.append(fldChar1)
    page_num_run._r.append(instrText)
    page_num_run._r.append(fldChar2)

    of_run = paragraph.add_run()
    t2 = create_element('w:t')
    create_attribute(t2, 'xml:space', 'preserve')
    t2.text = ' of '
    of_run._r.append(t2)

    fldChar3 = create_element('w:fldChar')
    create_attribute(fldChar3, 'w:fldCharType', 'begin')

    instrText2 = create_element('w:instrText')
    create_attribute(instrText2, 'xml:space', 'preserve')
    instrText2.text = "NUMPAGES"

    fldChar4 = create_element('w:fldChar')
    create_attribute(fldChar4, 'w:fldCharType', 'end')

    num_pages_run = paragraph.add_run()
    num_pages_run._r.append(fldChar3)
    num_pages_run._r.append(instrText2)
    num_pages_run._r.append(fldChar4)

doc = Document()
add_page_number(doc.sections[0].footer.paragraphs[0])
doc.save("your_doc.docx")
Answered By: Utkarsh Dalal

I do not have "reputation points" to comment on "Syafiqur__ and scanny" max_max_mir’s solution, so I am forced to write a brand new comment. Given the complicated xml solution, I deviced a trick to add a text of my choice to the footer, and then align the page numbering at the footer’s side the way I want.

So, I create the footer’s text by using a run, and I align it accordingly by using tabs. Then I call max_max_mir’s function:

my_footer_run = footer.paragraphs[0].add_run()
my_footer_run.text = "Copyright MyCompany  All Rights Reserved.tt"
add_page_number(my_footer_run)

… and the page number is shown in the appropriate corner. In the above example, the page numbering is shown on the right while the original text is shown on the left.

Many thanks for the original solution!

Answered By: Sp_V

Thank you max_max_mir and Utkarsh Dalal. This is wonderful. I made few changes I am sharing it here for people who need it:

  1. set different first page (cover page)
  2. start counting pages from 0 (cover page is not counted)
import docx
from docx.enum.text import WD_PARAGRAPH_ALIGNMENT
from docx.oxml import OxmlElement, ns

def create_element(name):
    return OxmlElement(name)
        
def create_attribute(element, name, value):
    element.set(ns.qn(name), value)
        
def add_page_number(run):
    fldStart = create_element('w:fldChar')
    create_attribute(fldStart, 'w:fldCharType', 'begin')

    instrText = create_element('w:instrText')
    create_attribute(instrText, 'xml:space', 'preserve')
    instrText.text = "PAGE"

    fldChar1 = create_element('w:fldChar')
    create_attribute(fldChar1, 'w:fldCharType', 'separate')

    fldChar2 = create_element('w:t')
    fldChar2.text = "2"

    fldEnd = create_element('w:fldChar')
    create_attribute(fldEnd, 'w:fldCharType', 'end')

    run._r.append(fldStart)

    run._r.append(instrText)
    run._r.append(fldChar1)
    run._r.append(fldChar2)

    run._r.append(fldEnd)


    add_page_number(doc.sections[0].footer.paragraphs[0].add_run())
    doc.sections[0].footer.paragraphs[0].alignment = WD_PARAGRAPH_ALIGNMENT.CENTER

    doc.sections[0].different_first_page_header_footer = True
    sectPr = doc.sections[0]._sectPr
        
    pgNumType = OxmlElement('w:pgNumType')
    pgNumType.set(ns.qn('w:start'), "0")
    sectPr.append(pgNumType)
Answered By: mhagr

What I found easiest was to prepare the template in Word as I wanted it to be, with page numbers, colors, etc; then read it; then modify it and save it

from docx import Document

folder_data = 'C:\Users\...\Data\'
folder_output = 'C:\Users\...\Output\'

client_ = 'Client 1'; price_ = 99.99

document_ = Document(f'{folder_data}invoiceTemplate.docx')
document_.paragraphs[3].add_run(f'{price_} EUR')

# ... more code ...

document_.save(f'{folder_output}{client_} invoice.docx')
Answered By: amargulis
Categories: questions Tags: , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.