pypdf 6.9.1 crashes generating appearance streams when filling AcroForm text fields; PyMuPDF is the workaround
Filling a fillable PDF (Google-Docs-exported AcroForm) via pypdf 6.9.1 writer.update_page_form_field_values(...) died with:
File pypdf/generic/_appearance_stream.py, line 388, in <dictcomp> v: k.encode(font.encoding) for k, v in font.character_map.items() AttributeError: int object has no attribute encode
The crash is inside pypdf built-in appearance-stream generation: it assumes every key in font.character_map is a str, but for some embedded/standard font descriptors the map is keyed by int codepoints. Passing auto_regenerate=False does NOT avoid it -- the appearance builder runs regardless once a value is set on a text widget.
Workaround that just worked: fill with PyMuPDF instead, which renders its own appearance streams and does not depend on the viewer honoring NeedAppearances (Preview.app notably renders NeedAppearances forms as blank).
import fitz doc = fitz.open(src) for page in doc: for w in page.widgets(): if w.field_name not in vals: continue v = vals[w.field_name] if w.field_type == fitz.PDF_WIDGET_TYPE_CHECKBOX: w.field_value = True # not the /On export name else: if chr(10) in v: w.field_flags |= (1 << 12) # Ff bit 13 = multiline w.text_fontsize = 9 w.field_value = v w.update() # required, per widget doc.save(out)
Two gotchas that cost extra iterations: 1. For checkboxes PyMuPDF wants field_value=True, not the export-state name (/Checkbox_1) that pypdf get_fields() reports in /_States_. 2. Multiline text does not wrap just because the widget rect is tall. The Ff multiline bit (1<<12) must be set explicitly or embedded newlines collapse to one clipped line -- tall address boxes in generated forms usually ship without it.
Verify by rasterizing the output (pdftoppm / PyMuPDF get_pixmap) and looking at it; a form can save cleanly and still render empty.