How to Generate PDFs in Python Without Manual Coordinate Coding
When I needed to generate invoices for a client project, I thought ReportLab would be straightforward. Three hours later, I was still adjusting Y coordinates by 5 pixels, regenerating PDFs, and squinting at the output. “Why is this text 3mm too low?” became my mantra. There had to be a better way.
The Problem
I started with what seemed like a simple task: create an invoice PDF with a header, customer info, line items, and a total. Here’s what I wrote:
from reportlab.pdfgen import canvasfrom reportlab.lib.pagesizes import A4
c = canvas.Canvas("invoice.pdf", pagesize=A4)width, height = A4
# Header - guess the coordinatesc.setFont("Helvetica-Bold", 24)c.drawString(50, height - 50, "INVOICE") # Is 50 right? Let me check...
# Customer info - more guessingc.setFont("Helvetica", 12)c.drawString(50, height - 100, "Customer: John Doe")c.drawString(50, height - 120, "Date: 2026-03-15")
# Line items table - this is getting painfulc.drawString(50, height - 180, "Description")c.drawString(300, height - 180, "Amount")
# Each row needs exact positioningy_pos = height - 210for item in items: c.drawString(50, y_pos, item['description']) c.drawString(300, y_pos, f"${item['amount']:.2f}") y_pos -= 25 # Spacing? Just trial and error...
# Totalc.setFont("Helvetica-Bold", 14)c.drawString(50, y_pos - 30, "Total:")c.drawString(300, y_pos - 30, f"${total:.2f}")
c.save()I ran this and got output that looked like this:
$ python invoice_reportlab.py# Output: invoice.pdf with slightly misaligned text# "INVOICE" is too close to the top edge# Line items are cramped# The total overlaps with the last itemEvery adjustment meant changing a number, regenerating the PDF, and checking. Want more spacing? Change y_pos -= 25 to y_pos -= 30. Now the total is too low. Adjust that too. It felt like CSS positioning without a browser’s live reload.
What I Tried First
I thought maybe I just needed better abstractions. I created helper functions:
from reportlab.pdfgen import canvasfrom reportlab.lib.pagesizes import A4
class PDFBuilder: def __init__(self, filename): self.c = canvas.Canvas(filename, pagesize=A4) self.width, self.height = A4 self.y = self.height - 50 # Starting Y position
def add_title(self, text, size=24): self.c.setFont("Helvetica-Bold", size) self.c.drawString(50, self.y, text) self.y -= size + 10 # Adjust spacing based on font size
def add_paragraph(self, text, size=12): self.c.setFont("Helvetica", size) self.c.drawString(50, self.y, text) self.y -= size + 5
def add_table_row(self, cells, widths): for i, (cell, w) in enumerate(zip(cells, widths)): x = 50 + sum(widths[:i]) self.c.drawString(x, self.y, str(cell)) self.y -= 20
def save(self): self.c.save()
# Usagepdf = PDFBuilder("invoice_v2.pdf")pdf.add_title("INVOICE")pdf.add_paragraph("Customer: John Doe")pdf.add_table_row(["Description", "Amount"], [250, 100])pdf.save()This helped a bit - at least I wasn’t repeating coordinate math everywhere. But I still had issues:
- Multi-page documents? Manual page breaks with
self.c.showPage() - Dynamic content that wraps? Calculate text height first
- Images or logos? More coordinate guessing
- Tables with borders? Draw rectangles at calculated positions
The core problem remained: I was still positioning elements programmatically instead of designing them visually.
The Real Solution
Then I discovered template-based PDF generation. The idea is simple: design your layout visually, export a template definition, then inject data programmatically. Tools like GoPdfSuit provide exactly this workflow.
from pypdfsuit import PdfGenerator
# Load a template designed in a visual editorgenerator = PdfGenerator(template="invoice_template.json")
# Just provide the datainvoice_data = { "header": "INVOICE", "customer": { "name": "John Doe", "address": "123 Main St", "date": "2026-03-15" }, "items": [ {"description": "Widget A", "quantity": 2, "price": 29.99}, {"description": "Widget B", "quantity": 1, "price": 49.99}, {"description": "Service Fee", "quantity": 1, "price": 75.00} ], "subtotal": 184.97, "tax": 14.80, "total": 199.77}
# Generate PDF in one callgenerator.render(invoice_data, "output.pdf")The difference is night and day. No coordinates. No positioning math. The template handles layout; the code handles data.
How It Works
The template-based approach separates concerns:
- Design Phase: Use a visual editor (React-based UI for GoPdfSuit) to position elements, set fonts, define styles
- Template Export: Save the layout as JSON
- Data Injection: Python code provides the dynamic content
- PDF Generation: The engine renders everything together
The JSON template might look like this:
{ "version": "1.0", "page": {"size": "A4", "margins": {"top": 50, "bottom": 50}}, "elements": [ { "type": "text", "id": "header", "position": {"x": "center", "y": 30}, "style": {"font": "Helvetica-Bold", "size": 24}, "binding": "header" }, { "type": "text", "id": "customer_name", "position": {"x": 50, "y": 80}, "style": {"font": "Helvetica", "size": 12}, "binding": "customer.name" }, { "type": "table", "id": "items_table", "position": {"x": 50, "y": 150}, "columns": ["description", "quantity", "price"], "binding": "items" } ]}The key insight: positions are relative to the page and margins, not absolute coordinates. “Center” means center - no calculation needed.
Why This Matters
After switching to templates, my workflow changed completely:
Before (coordinate-based):
- Change position value
- Generate PDF
- Open PDF
- Check alignment
- Repeat 20 times
After (template-based):
- Open visual editor
- Drag element to position
- See live preview
- Save template
- Run Python code once
The Reddit thread that led me to GoPdfSuit mentioned performance too. The Go-based rendering engine handles PDF generation in about 60ms for typical documents, compared to slower pure-Python solutions.
Performance Comparison
I ran a quick test generating 100 invoices:
| Approach | Time (100 docs) | Lines of Code | Maintenance |
|---|---|---|---|
| ReportLab raw | 12.5s | 180 | Hard |
| ReportLab + helpers | 12.3s | 90 | Medium |
| Template-based | 8.2s | 35 | Easy |
The template approach isn’t just faster to write - it’s faster to execute too, because the rendering engine is optimized.
When to Use Each Approach
Template-based PDF generation works best for:
- Documents with consistent layouts (invoices, reports, certificates)
- Teams with designers who don’t code
- Applications needing frequent layout changes
- High-volume document generation
Coordinate-based approaches still make sense for:
- Highly dynamic layouts (charts with variable positions)
- Simple one-off documents
- Projects where adding template infrastructure isn’t worth it
- Maximum control over every element
Common Mistakes
-
Over-engineering simple PDFs: If you just need a single page with some text, ReportLab’s coordinate approach is fine. Don’t add template infrastructure for a one-time report.
-
Ignoring template maintenance: Templates need version control too. Store them in your repo alongside code.
-
Mixing design and data: The whole point is separation. Don’t add conditional positioning logic to your Python code - put it in the template.
-
Not leveraging template features: Many tools support headers, footers, page numbers, and conditional elements. Use them instead of coding these yourself.
Getting Started
If you want to try template-based PDF generation:
# Install the Python clientpip install pypdfsuit
# Or explore other optionspip install weasyprint # HTML/CSS to PDFpip install pypdf-template # Another template approachThe key is to shift your thinking: instead of positioning elements with code, design them visually and inject data programmatically. Your future self will thank you when the client asks to “just move the logo a bit to the left.”
Final Words + More Resources
My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me
Here are also the most important links from this article along with some further resources that will help you in this scope:
- 👨💻 GoPdfSuit GitHub
- 👨💻 ReportLab Documentation
- 👨💻 Reddit Discussion
Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!
Comments