from weasyprint import HTML
from pypdf import PdfReader
rows = "".join(
f"<tr><td>Item {i}</td><td>{i * 10}</td></tr>" for i in range(1, 121)
)
html = f"""
<style>
@page {{ size: Letter; margin: 1in; }}
thead {{ display: table-header-group; }}
tfoot {{ display: table-footer-group; }}
tr {{ break-inside: avoid; }}
</style>
<table>
<thead><tr><th>Name</th><th>Amount</th></tr></thead>
<tbody>{rows}</tbody>
</table>
"""
HTML(string=html).write_pdf("table.pdf")
reader = PdfReader("table.pdf")
print("pages:", len(reader.pages))
for n, page in enumerate(reader.pages, start=1):
print(n, "Name" in page.extract_text())
This example uses WeasyPrint and pypdf. The last loop checks each page's extracted text for the header word, and for this table it prints True on every page.
Why it breaks
CSS 2.1 defines the table-header-group display value, which is what thead gets by default, and says print user agents may repeat header rows on each page spanned by a table. Two common mistakes turn that off. First, the header is a regular tr inside tbody, or the table has no thead at all, so there is no header group to repeat. Second, a stylesheet sets thead to display: block, which takes it out of the table's header group. In a 120-row WeasyPrint test, both variants kept the header on page 1 only, while the thead version showed it on every page.
Caveats
The spec says may, so repetition depends on your renderer. Confirm it with a multi-page test document before relying on it. Also, break-inside: avoid on tr asks the renderer to keep each row on one page instead of splitting it across a page boundary.
Back to All Questions