How to Fill PDF Forms with Python
Interactive PDF forms (AcroForm) are everywhere — contracts, applications, invoices, tax documents. Python offers several mature libraries to fill them programmatically: pypdf (successor to PyPDF2), pdfrw, and the high-level wrapper fillpdf. This guide covers all three with real-world examples.
Understanding AcroForm
PDF forms store interactive fields in the /AcroForm dictionary. Each field has:
/T— field name (the key you use to fill it)/FT— field type:/Tx(text),/Btn(button/checkbox),/Ch(list/combo)/V— current value/AP— appearance stream (may need updating after changes)
Installation
pip install pypdf fillpdf pdfrw
For encrypted PDFs or advanced overlays:
pip install reportlab
Inspecting Form Fields
Before filling a form, discover what fields it contains:
from pypdf import PdfReader
def list_fields(pdf_path):
reader = PdfReader(pdf_path)
fields = reader.get_fields()
if not fields:
print("PDF has no AcroForm fields")
return
for name, field in fields.items():
ftype = field.get('/FT', 'unknown')
value = field.get('/V', '(empty)')
print(f"Field: {name!r:40} Type: {ftype} Value: {value}")
list_fields("form.pdf")
Typical output:
Field: 'full_name' Type: /Tx Value: (empty)
Field: 'date_of_birth' Type: /Tx Value: (empty)
Field: 'accept_terms' Type: /Btn Value: /Off
Field: 'account_type' Type: /Ch Value: (empty)
Filling with pypdf
from pypdf import PdfReader, PdfWriter
from pypdf.generic import NameObject
def fill_pdf_pypdf(input_path, output_path, data):
reader = PdfReader(input_path)
writer = PdfWriter()
for page in reader.pages:
writer.add_page(page)
# Copy AcroForm dictionary
if "/AcroForm" in reader.trailer["/Root"]:
writer._root_object.update({
NameObject("/AcroForm"): reader.trailer["/Root"]["/AcroForm"]
})
# Fill fields on first page
writer.update_page_form_field_values(
writer.pages[0],
data,
auto_regenerate=False
)
with open(output_path, "wb") as f:
writer.write(f)
print(f"Saved: {output_path}")
data = {
"full_name": "Alice Johnson",
"date_of_birth": "03/15/1990",
"city": "New York",
"email": "alice@example.com",
}
fill_pdf_pypdf("blank_form.pdf", "filled_form.pdf", data)
Filling Multiple Pages
from pypdf import PdfReader, PdfWriter
def fill_all_pages(input_path, output_path, data):
reader = PdfReader(input_path)
writer = PdfWriter()
for page in reader.pages:
writer.add_page(page)
for page in writer.pages:
writer.update_page_form_field_values(page, data)
with open(output_path, "wb") as f:
writer.write(f)
Checkboxes with pypdf
Checkboxes use /Yes or /Off as values:
from pypdf import PdfReader, PdfWriter
from pypdf.generic import NameObject
def set_checkbox(writer, field_name, checked=True):
value = "/Yes" if checked else "/Off"
for page in writer.pages:
if "/Annots" in page:
for annot in page["/Annots"]:
obj = annot.get_object()
if obj.get("/T") == field_name:
obj.update({NameObject("/V"): NameObject(value)})
obj.update({NameObject("/AS"): NameObject(value)})
reader = PdfReader("form.pdf")
writer = PdfWriter()
for page in reader.pages:
writer.add_page(page)
set_checkbox(writer, "accept_terms", checked=True)
set_checkbox(writer, "receive_newsletter", checked=False)
with open("output.pdf", "wb") as f:
writer.write(f)
Filling with fillpdf (Easiest)
fillpdf is the most user-friendly option for straightforward forms:
from fillpdf import fillpdfs
# List fields
fields = fillpdfs.get_form_fields("form.pdf")
print(fields)
# Fill
data = {
"full_name": "Bob Smith",
"ssn": "123-45-6789",
"phone": "(555) 123-4567",
"accept_terms": True, # checkbox
}
fillpdfs.write_fillable_pdf(
"form.pdf",
"filled_form.pdf",
data,
flatten=False # True = flatten to non-editable static PDF
)
Flattening the PDF
# Create a non-editable version for archiving or sending
fillpdfs.flatten_pdf("filled_form.pdf", "final_form.pdf")
Filling with pdfrw
pdfrw operates directly on the PDF tree — very fast for batch processing:
import pdfrw
ANNOT_KEY = '/Annots'
ANNOT_FIELD_KEY = '/T'
SUBTYPE_KEY = '/Subtype'
WIDGET_SUBTYPE = '/Widget'
def fill_pdf_pdfrw(input_path, output_path, data):
template = pdfrw.PdfReader(input_path)
for page in template.pages:
annotations = page[ANNOT_KEY]
if not annotations:
continue
for annotation in annotations:
if annotation[SUBTYPE_KEY] == WIDGET_SUBTYPE:
if annotation[ANNOT_FIELD_KEY]:
name = annotation[ANNOT_FIELD_KEY][1:-1]
if name in data:
annotation.update(
pdfrw.PdfDict(V=data[name], AS=data[name])
)
template.Root.AcroForm.update(
pdfrw.PdfDict(NeedAppearances=pdfrw.PdfObject('true'))
)
pdfrw.PdfWriter().write(output_path, template)
print(f"Saved: {output_path}")
data = {
"employee_name": "Carol White",
"department": "Engineering",
"start_date": "04/24/2026",
}
fill_pdf_pdfrw("contract.pdf", "signed_contract.pdf", data)
Batch Processing from CSV
Fill hundreds of forms automatically:
import csv
from fillpdf import fillpdfs
from pathlib import Path
def batch_fill(template_pdf, csv_data, output_dir):
output_dir = Path(output_dir)
output_dir.mkdir(exist_ok=True)
with open(csv_data, encoding='utf-8') as f:
reader = csv.DictReader(f)
for i, row in enumerate(reader, 1):
file_id = row.get('id', f'form_{i:04d}')
out_path = output_dir / f"{file_id}.pdf"
fillpdfs.write_fillable_pdf(
str(template_pdf),
str(out_path),
row,
flatten=True
)
print(f"[{i}] Generated: {out_path.name}")
# CSV headers: id, full_name, ssn, address, date
batch_fill("employee_form.pdf", "employees.csv", "generated_forms/")
Overlaying Text on Non-Form PDFs
If the PDF has no interactive fields, overlay text using reportlab:
from reportlab.pdfgen import canvas
from reportlab.lib.pagesizes import letter
from pypdf import PdfReader, PdfWriter
import io
def overlay_text(input_pdf, output_pdf, fields):
"""
fields: list of (text, x, y, font_size)
Coordinates in points (72 pts = 1 inch), origin at bottom-left
"""
packet = io.BytesIO()
c = canvas.Canvas(packet, pagesize=letter)
for text, x, y, size in fields:
c.setFont("Helvetica", size)
c.drawString(x, y, text)
c.save()
packet.seek(0)
overlay = PdfReader(packet)
reader = PdfReader(input_pdf)
writer = PdfWriter()
for i, page in enumerate(reader.pages):
if i < len(overlay.pages):
page.merge_page(overlay.pages[i])
writer.add_page(page)
with open(output_pdf, "wb") as f:
writer.write(f)
overlay_text(
"invoice_template.pdf",
"invoice_filled.pdf",
[
("Acme Corp LLC", 150, 680, 12),
("EIN: 12-3456789", 150, 665, 10),
("04/24/2026", 420, 720, 10),
("$1,250.00", 440, 580, 12),
]
)
Library Comparison
| Feature | pypdf | fillpdf | pdfrw |
|---|---|---|---|
| Ease of use | Medium | High | Medium-low |
| Checkboxes | Yes (manual) | Yes (automatic) | Yes (manual) |
| Dropdowns | Yes | Yes | Yes |
| Batch speed | Medium | Medium | High |
| Encrypted PDFs | Yes | Limited | No |
| Flatten support | Yes | Yes | No |
| Dependencies | Minimal | pypdf + pdfrw | Minimal |
Troubleshooting
Fields filled but text invisible:
template.Root.AcroForm.update(
pdfrw.PdfDict(NeedAppearances=pdfrw.PdfObject('true'))
)
Write-protected PDF:
from pypdf import PdfReader, PdfWriter
reader = PdfReader("protected.pdf")
if reader.is_encrypted:
reader.decrypt("") # try empty password
writer = PdfWriter()
writer.clone_reader_document_root(reader)
Dropdown selection:
from pypdf.generic import NameObject, TextStringObject
for page in writer.pages:
if "/Annots" not in page:
continue
for annot in page["/Annots"]:
obj = annot.get_object()
if obj.get("/T") == "account_type":
obj.update({
NameObject("/V"): TextStringObject("Premium"),
NameObject("/DV"): TextStringObject("Premium"),
})
Additional Resource
For PDF conversions (PDF→Word, PDF→image, PDF→text) without any coding, use KaijuConverter — fast, free, and no account required.
Related conversions
Document conversions that follow this topic naturally: