Of course! Using Python with the Bottle web framework to generate and serve PDFs is a common task. The best and most standard way to do this is by using the ReportLab library to create the PDF content dynamically.

Here’s a complete guide covering:
- The Scenario: A simple web page with a button to generate a PDF report.
- Setup: Installing the necessary libraries.
- Backend (Bottle + ReportLab): The Python code to generate the PDF and send it to the browser.
- Frontend (HTML/JS): The user interface.
- Alternative: Serving a pre-existing PDF file.
The Scenario
We'll build a simple web application with one page:
- : Displays a welcome message and a "Generate PDF Report" button.
/generate_pdf: A hidden URL that our button will call. This route will generate a PDF in memory and send it to the browser for download.
Setup
First, you need to install Bottle and ReportLab. You can do this using pip:
pip install bottle reportlab
Backend (Bottle + ReportLab)
This is the core of our application. We'll create a Python script that sets up the Bottle server and defines our routes.

The key to this is using Python's io module to create the PDF in memory (a BytesIO object) instead of writing it to a file on the disk. This is much more efficient for web applications.
Create a file named app.py:
import bottle
from bottle import route, run, static_file, response
from reportlab.pdfgen import canvas
from reportlab.lib.pagesizes import letter
from reportlab.lib.units import inch
from io import BytesIO
# --- Route for the main page ---
@route('/')
def index():
"""Serves the main HTML page."""
return '''
<h1>Welcome to the PDF Generator!</h1>
<p>Click the button below to generate a PDF report.</p>
<button onclick="generatePDF()">Generate PDF Report</button>
'''
# --- Route to generate and serve the PDF ---
@route('/generate_pdf')
def generate_pdf():
"""
Generates a PDF in memory and sends it to the browser for download.
"""
# 1. Create a BytesIO buffer to hold the PDF in memory
buffer = BytesIO()
# 2. Create a canvas object to draw on the PDF
p = canvas.Canvas(buffer, pagesize=letter)
width, height = letter
# 3. Draw content on the PDF
# You can add text, images, tables, etc.
p.setFont("Helvetica-Bold", 24)
p.drawString(inch, height - inch, "Sales Report")
p.setFont("Helvetica", 12)
p.drawString(inch, height - 2 * inch, "This report was dynamically generated.")
p.drawString(inch, height - 2.5 * inch, "Date: 2025-10-27")
# Draw a simple line
p.line(inch, height - 3 * inch, width - inch, height - 3 * inch)
# Add some sample data
data = [
("Product A", 150),
("Product B", 200),
("Product C", 75),
]
y = height - 4 * inch
p.drawString(inch, y, "Product")
p.drawString(3 * inch, y, "Sales")
y -= 0.5 * inch
for product, sales in data:
p.drawString(inch, y, product)
p.drawString(3 * inch, y, f"${sales}")
y -= 0.5 * inch
# 4. Close the canvas and save the PDF to the buffer
p.save()
# 5. Get the value of the buffer and rewind it
pdf_data = buffer.getvalue()
buffer.close()
# 6. Set the HTTP response headers
# - Content-Type: 'application/pdf' tells the browser it's a PDF.
# - Content-Disposition: 'attachment' prompts a "Save As" dialog.
# The 'filename=' parameter suggests a default name for the file.
response.content_type = 'application/pdf'
response.headers['Content-Disposition'] = 'attachment; filename="report.pdf"'
# 7. Return the PDF data as the response body
return pdf_data
# --- To run the application ---
if __name__ == '__main__':
# Use reloader=True for development to restart the server on code changes
run(host='localhost', port=8080, reloader=True)
Code Breakdown:
BytesIO(): Creates an in-memory binary stream. It acts like a file, but it doesn't touch your disk.canvas.Canvas(buffer, ...): We tell ReportLab to draw our PDF onto our in-memory buffer.p.drawString(...),p.line(...): These are ReportLab functions for drawing content. You can get very creative here, adding tables, charts, images, etc.p.save(): Finalizes the PDF document and writes it to the buffer.buffer.getvalue(): Retrieves the raw bytes of the PDF from the buffer.response.content_type = 'application/pdf': This is a critical header. It tells the browser how to interpret the data it's receiving. Without it, the browser might try to display the raw PDF code as text.response.headers['Content-Disposition'] = 'attachment; ...': This header tells the browser to treat the response as a file to be downloaded, rather than a page to be displayed. Thefilename="report.pdf"part is what appears in the "Save As..." dialog.
Frontend (HTML/JS)
The frontend is very simple. We have a button that, when clicked, makes a GET request to our /generate_pdf route. The browser will then handle the response according to the headers we set.

The app.py already includes the HTML for the main page, but here it is again for clarity. You could also save this as index.html and use Bottle's static_file route to serve it.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">PDF Generator App</title>
<style>
body { font-family: sans-serif; text-align: center; margin-top: 50px; }
button { padding: 10px 20px; font-size: 16px; cursor: pointer; }
</style>
</head>
<body>
<h1>Welcome to the PDF Generator!</h1>
<p>Click the button below to generate a PDF report.</p>
<!-- This button calls our backend route -->
<button onclick="window.location.href='/generate_pdf'">Generate PDF Report</button>
</body>
</html>
How to Run and Test
- Save the Python code as
app.py. - Open your terminal or command prompt.
- Navigate to the directory where you saved
app.py. - Run the script:
python app.py - You will see output like this:
Bottle server starting up (using WSGIRefServer)... Listening on http://localhost:8080/ - Open your web browser and go to
http://localhost:8080. - You should see the welcome page. Click the "Generate PDF Report" button.
- Your browser should prompt you to download a file named
report.pdf. Open it, and you'll see the dynamically generated content!
Alternative: Serving a Pre-existing PDF File
Sometimes, you don't need to generate the PDF on the fly; you just need to serve a file that already exists. For this, Bottle's static_file route is perfect.
Let's say you have a PDF file named my_document.pdf in a folder named static.
-
Project Structure:
/my_project |-- app.py |-- /static |-- my_document.pdf -
app.pyCode:import bottle from bottle import route, run, static_file, response # Route for the main page @route('/') def index(): return ''' <h1>PDF File Server</h1> <p><a href="/download_pdf">Click here to download my_document.pdf</a></p> ''' # Route to serve a static file @route('/download_pdf') def download_pdf(): # The 'root' argument is the path to the directory containing the file. # The 'filename' is the name of the file to serve. # The 'download' argument prompts a "Save As" dialog. return static_file('my_document.pdf', root='static', download=True) if __name__ == '__main__': run(host='localhost', port=8080, reloader=True)
This approach is simpler and more efficient if the PDF content doesn't change. The download=True argument conveniently sets the Content-Disposition header for you.
