Skip to content

Table.to_pandas() permanently mutates Table.header.names in-place, corrupting column names and downstream exports #5241

Open

Description

@codeCraft-Ritik

Description of the bug


Description of the bug *

Table.to_pandas() exports an extracted table as a pandas DataFrame. To handle duplicate or missing column names, it modifies names:

1888:         hdr = self.header
1889:         names = self.header.names
1890:         hdr_len = len(names)
1891:         # ensure uniqueness of column names
1892:         for i in range(hdr_len):
1893:             name = names[i]
1894:             if not name:
1895:                 names[i] = f"Col{i}"
1896:         if hdr_len != len(set(names)):
1897:             for i in range(hdr_len):
1898:                 name = names[i]
1899:                 if name != f"Col{i}":
1900:                     names[i] = f"{i}-{name}"

In Python, names = self.header.names binds names directly to the TableHeader object's internal list self.header.names.

Mutating names[i] on lines 1895 and 1900 permanently alters the table's internal header state:

  1. If duplicate headers exist (e.g. ['Total', 'Total']), calling tab.to_pandas() permanently mutates tab.header.names to ['0-Total', '1-Total'].
  2. Subsequent calls to other export methods (such as tab.to_markdown()) or accessing tab.header.names output the prefixed strings instead of the original extracted text.
  3. An export/read operation should never produce destructive mutations on the instance.

How to reproduce the bug

Highlight in Code

src/table.py:1888-1901:

1888:         hdr = self.header
1889: >>>>>   names = self.header.names    # BUG: Direct reference to internal list!
1890:         hdr_len = len(names)
1891:         # ensure uniqueness of column names
1892:         for i in range(hdr_len):
1893:             name = names[i]
1894:             if not name:
1895: >>>>>       names[i] = f"Col{i}"    # BUG: Mutates self.header.names in-place!
1896:         if hdr_len != len(set(names)):
1897:             for i in range(hdr_len):
1898:                 name = names[i]
1899:                 if name != f"Col{i}":
1900: >>>>>           names[i] = f"{i}-{name}"    # BUG: Mutates self.header.names in-place!

Steps & Reproduction Code

import pymupdf

doc = pymupdf.open()
page = doc.new_page()
page.draw_rect([50, 50, 200, 100])
page.draw_line([50, 75], [200, 75])
page.draw_line([125, 50], [125, 100])
page.insert_text([60, 65], "Total")
page.insert_text([135, 65], "Total")
page.insert_text([60, 90], "10")
page.insert_text([135, 90], "20")

tabs = page.find_tables()
tab = tabs[0]
print("Initial header names:", tab.header.names)
# ['Total', 'Total']

tab.to_pandas()
print("Header names after to_pandas():", tab.header.names)
# ['0-Total', '1-Total'] (mutated in-place)

# Downstream export is corrupted:
print("Markdown output:\n", tab.to_markdown())
# Header row shows |0-Total|1-Total| instead of |Total|Total|

Expected Behavior

Table.to_pandas() should operate on a shallow copy of self.header.names (e.g. names = list(self.header.names)), leaving self.header.names completely unmodified.

Actual Behavior

self.header.names is mutated in-place and permanently alters table metadata.

Suggested Fix

--- a/src/table.py
+++ b/src/table.py
@@ -1886,7 +1886,7 @@ class Table:
         pd_dict = {}
         extract = self.extract()
         hdr = self.header
-        names = self.header.names
+        names = list(self.header.names)
         hdr_len = len(names)
         # ensure uniqueness of column names
         for i in range(hdr_len):

PyMuPDF version

2.1

Operating system

Windows

Python version

3.14

Activity

  1. JorjMcKie commented on Nov 2, 2026

    @JorjMcKie
    Collaborator

    Thanks for your submission. We will include this fix in one of the next versions.

  2. self-assigned this
    on Nov 2, 2026
  3. added a commit that references this issue on Nov 4, 2026
    1a7d932
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions