Logo

dev-resources.site

for different kinds of informations.

Automating Word Document Creation with Python and FastAPI (Using python-docx-template)

Published at
1/7/2025
Categories
python
microsoftword
fastapi
pythondocxtemplate
Author
huseinkntrc
Author
11 person written this
huseinkntrc
open
Automating Word Document Creation with Python and FastAPI (Using python-docx-template)

Photo by [Ed Hardie](https://unsplash.com/@impelling?utm_source=medium&utm_medium=referral) on [Unsplash](https://unsplash.com?utm_source=medium&utm_medium=referral)

Ever needed to create a Word document filled with specific content? Better yet, have you wished to automate the process so you don’t have to manually update it each time the context changes?

Well, look no further — Python, as always, has got us covered. With a library called “python-docx-template”, dynamically rendering content into a template Word document becomes not only easy but also efficient.

So, what exactly do I mean by dynamically generating Word documents?

Take a basic example: an invoice.

We all know how tiring it can be to manually update the data inside an invoice every time you need to send it to a customer. Sometimes, it’s simply not feasible to do it manually — like when the input data comes from an API call, and you need to auto-fill the details to generate an invoice for the customer.

This is where dynamic Word document generation helps us.

Imagine you have a Word document template that looks like this:

Original Word template

The receiver’s and sender’s company details may change over time, and the items listed? They’ll almost always be different. One day it could be a single item; the next, it could be a hundred.

So, how do we go about generating an invoice dynamically using a template?

First, we need to take our template Word document and modify it to make it Jinja2-compatible. By incorporating Jinja2 templating language features, we gain powerful capabilities like conditional rendering, looping over arrays of data, and more. This allows us to dynamically populate the document based on the context provided.

After modifying the original template and making it Jinja2-compatible, this is what we get:

Jinja2 Word template

Now, I understand that expressions like tr if — endif and tr for — endfor might seem a bit confusing at first, but these are Jinja2-specific syntax for writing conditions and loops.

Basically, anything between “{{ }}” represents a variable that will be rendered with its real value. For example, “{% tr if items %}” ensures that the items variable exists in the context before attempting to render table rows for it. Meanwhile, “{% tr for item in items %}” loops over the provided items, and for each item, it generates a table row containing information like item’s description and item’s amount.

This way if the items array contains 100 objects, then the generated Word document will have 100 items rendered dynamically and so on.

Now, let’s spin up a basic FastApi server and provide context to the Jinja2 template file above using Python.

First, create a new empty folder, and then run the following command inside it to create a virtual environment:

pip3 install virtualenv
virtualenv -p python3 venv
source venv/bin/activate
Enter fullscreen mode Exit fullscreen mode

After creating and activating the virtual environment, install required libraries:

pip install "fastapi[standard]" docx docxtpl pydantic requests
Enter fullscreen mode Exit fullscreen mode

Then, Create a file “main.py” with:

    from fastapi import FastAPI

    app = FastAPI()


    @app.get("/")
    def read_root():
        return {"Hello": "World"}
Enter fullscreen mode Exit fullscreen mode

If you make a request (using Postman, or your browser) to “localhost:8000”, you should get this as the output:

    {
        "Hello": "World"
    }
Enter fullscreen mode Exit fullscreen mode

Now that we have a basic endpoint running, we can start rendering context into Word template file.

Import your Jinja2 modified Word template file into the root of your project like so:

Modify the main.py file to be:

    from fastapi import FastAPI
    from docxtpl import DocxTemplate, InlineImage
    from pydantic import BaseModel
    from typing import List
    from io import BytesIO
    from fastapi.responses import StreamingResponse
    from docx.shared import Mm
    import requests

    app = FastAPI()

    class Company(BaseModel):
        name: str
        logo: str | None = None
        address_line_1: str | None = None
        address_line_2: str | None = None
        address_line_3: str | None = None
        phone_number: str | None = None
        registered_id: str


    class BankInformation(BaseModel):
        name: str
        bank_name: str
        bank_address: str
        bank_swift_code: str
        account_number: str
        iban: str


    class Item(BaseModel):
        description: str
        amount: int


    class VatInformation(BaseModel):
        description: str
        percentage: int
        amount: int

    class InvoiceContext(BaseModel):
        company: Company
        billed_company: Company
        beneficiary: BankInformation
        sender: BankInformation
        items: List[Item]
        vat_info: VatInformation | None = None

        invoice_no: str
        invoice_date: str
        total_amount: int = 0


    def get_image_from_url(image_url: str):
        """Fetches an image from a provided URL and returns it as a BytesIO object."""
        response = requests.get(image_url)
        image = BytesIO(response.content)
        return image


    def process_logo(template, logo_url: str):
        """Process the logo URL and return the InlineImage if a valid URL is provided."""
        if logo_url:
            image = get_image_from_url(logo_url)
            return InlineImage(template, image, width=Mm(21))
        return None


    def process_total_amount(items: List[Item], vat_info: VatInformation | None) -> int:
        """
        Calculates the total amount for the invoice by summing the amounts of all items
        and adding the VAT (if provided).
        """
        total = sum(item.amount for item in items)

        if vat_info:
            # Calculate the VAT amount
            vat_amount = (total * vat_info.percentage) / 100
            total += vat_amount

        return total


    @app.post("/")
    async def create_invoice(context: InvoiceContext):
        # Load the template
        template = DocxTemplate("invoice_tpl.docx")

        context.company.logo = process_logo(template, context.company.logo)
        context.total_amount = process_total_amount(
            context.items, context.vat_info)

        # Render the template with the context
        template.render(context)

        # Save the document into a BytesIO buffer
        result = BytesIO()
        template.save(result)

        # Rewind the buffer to the beginning
        result.seek(0)

        # Return the result as a StreamingResponse
        return StreamingResponse(result,
                                 media_type="application/vnd.openxmlformats-officedocument.wordprocessingml.document",
                                 headers={"Content-Disposition": "attachment; filename=invoice.docx"})
Enter fullscreen mode Exit fullscreen mode

Now, let’s test the endpoint by sending a JSON body and retrieve the generated output.

{
  "company": {
    "name": "Tech Corp",
    "logo": "https://lh3.googleusercontent.com/d_S5gxu_S1P6NR1gXeMthZeBzkrQMHdI5uvXrpn3nfJuXpCjlqhLQKH_hbOxTHxFhp5WugVOEcl4WDrv9rmKBDOMExhKU5KmmLFQVg",
    "address_line_1": "123 Tech Street",
    "address_line_2": "Suite 456",
    "address_line_3": "Tech City",
    "phone_number": "+1234567890",
    "registered_id": "TC12345"
  },
  "billed_company": {
    "name": "Client Inc.",
    "address_line_1": "789 Client Avenue",
    "address_line_2": "Floor 3",
    "address_line_3": "Client City",
    "phone_number": "+0987654321",
    "registered_id": "CI67890"
  },
  "beneficiary": {
    "name": "John Doe",
    "bank_name": "Global Bank",
    "bank_address": "123 Global Road, Cityville",
    "bank_swift_code": "GB1234XYZ",
    "account_number": "1234567890",
    "iban": "GB00BANK1234567890"
  },
  "sender": {
    "name": "Tech Corp",
    "bank_name": "TechBank",
    "bank_address": "456 Tech Avenue, Tech City",
    "bank_swift_code": "TB9876XYZ",
    "account_number": "9876543210",
    "iban": "GB00TBANK9876543210"
  },
  "items": [
    {
      "description": "Consulting services",
      "amount": 500
    },
    {
      "description": "Software license",
      "amount": 1200
    }
  ],
  "vat_info": {
    "description": "VAT at 20%",
    "percentage": 20,
    "amount": 340
  },
  "invoice_no": "INV123456",
  "invoice_date": "2024-12-23"
}
Enter fullscreen mode Exit fullscreen mode

The output examples:

With Vat information provided

Without Vat information

With 5 items

Conclusion

In this guide, we explored how to dynamically generate Word documents using python-docx-template and FastAPI. By combining Jinja2 templating with FastAPI, we created a flexible system to generate documents like invoices with dynamic data. This approach offers a simple, scalable solution for automating document creation based on user input.

  • The upcoming blog (Part 2) will cover how to dynamically render context to PDFs.

You can find both the template file and the main.py file in the repository:
*https://github.com/huseink/docx-dynamic-generation*

Follow me for more:
Husein Kantarci
*Personal portfolio*huseink.dev

*https://www.linkedin.com/in/huseinkantarci/*

*https://github.com/huseink*

*https://gitlab.com/huseinkntrc*

fastapi Article's
30 articles in total
Favicon
Protect Your APIs from Abuse with FastAPI and Redis
Favicon
Making a Todo API with FastAPI and MongoDB
Favicon
The Core of FastAPI: A Deep Dive into Starlette 🌟🌟🌟
Favicon
Python FastAPI quickstart in uv
Favicon
Automating Word Document Creation with Python and FastAPI (Using python-docx-template)
Favicon
🌍💱 New Project: Currency Exchange API
Favicon
Getting Started with FastAPI
Favicon
[HANDS ON] Service Discovery in Prometheus
Favicon
Python's Unstoppable Rise, Dominating The Modern Backend Environment
Favicon
Boost Your App Performance by 10x with Redis Caching!
Favicon
Supercharge Your API Performance with Asynchronous Programming in FastAPI
Favicon
Mastering Real-Time AI: A Developer’s Guide to Building Streaming LLMs with FastAPI and Transformers
Favicon
Integrating LangChain with FastAPI for Asynchronous Streaming
Favicon
Serverless FastAPI Development: Building Player FC API on AWS
Favicon
FastAPI + Uvicorn = Blazing Speed: The Tech Behind the Hype
Favicon
Why is My Multi-Threaded API Still Slow?
Favicon
🧩 Step-by-Step EC2 Deployment: FastAPI, MongoDB, and NGINX Setup
Favicon
🚀 Building a User Management API with FastAPI and SQLite
Favicon
Simplify Authentication with FastAPI!
Favicon
Fastapi
Favicon
The Secret Behind FastAPI’s Speed
Favicon
Maximize Your FastAPI Efficiency: Blazingly Fast Implementation of Caching and Locking with py-cachify
Favicon
Understanding REST APIs: A Beginner’s Guide
Favicon
Mastering Python Async IO with FastAPI
Favicon
Is Flask Dead? Is FastAPI the Future?
Favicon
WSGI vs ASGI: The Crucial Decision Shaping Your Web App’s Future in 2025
Favicon
Fina Categorization API made publicly free
Favicon
Self-Correcting AI Agents: How to Build AI That Learns From Its Mistakes
Favicon
How to Build Smarter AI Agents with Dynamic Tooling
Favicon
🚀 Validating User Input with FastAPI: An Example with Custom Validators

Featured ones: