TableLoader, TableLoaderConfig, InMemoryTableLoader
On this page
Module: table_loader — the Table Reader
[!NOTE] Bilingual source Like
graph_loader, this module's comments and docstrings are written primarily in Arabic. This page documents the described behavior in English.
Overview
A format-agnostic table extractor: pulls tabular data out of Excel, CSV/TSV, PDF, HTML, and Markdown files, and renders each table as a LoadedDocument in your choice of output format (Markdown, CSV, JSON, or plain text) — either one document per whole table, or one document per row.
TableLoader is not wired into EXTENSION_MAP/AutoLoader/DirectoryLoader and must be used explicitly. It overlaps in scope with CSVLoader/ExcelLoader (for CSV/Excel) and HTMLLoader's table extraction (for HTML), but is distinguished by its uniform multi-format output formatting and explicit per-table vs. per-row document granularity control.
TableLoaderConfig
@dataclass
class TableLoaderConfig:
output_format: str = "markdown" # markdown | csv | json | text
per_row: bool = False
sheet_names: Optional[List[str]] = None
skip_empty_tables: bool = True
max_rows_per_doc: Optional[int] = None
include_header_in_rows: bool = True
column_separator: str = " | "
pdf_extraction_method: str = "pdfplumber" # pdfplumber | camelot
html_parser: str = "html.parser"
encoding: str = "utf-8"
TableLoader
Extends BaseFileLoader
Signature
class TableLoader(BaseFileLoader):
SUPPORTED_EXTENSIONS = [
".xlsx", ".xls", ".xlsm", ".csv", ".tsv", ".pdf", ".html", ".htm", ".md", ".markdown",
]
def __init__(
self,
file_path: str,
config: Optional[TableLoaderConfig] = None,
*,
output_format: str = "markdown",
per_row: bool = False,
encoding: str = "utf-8",
): ...
def load(self) -> List[LoadedDocument]: ...
def lazy_load(self) -> Iterator[LoadedDocument]: ...
[!WARNING]
output_format/per_rowalways overrideconfigSame pattern asGraphLoader:__init__unconditionally doesself.config.output_format = output_formatandself.config.per_row = per_rowafter settingself.config, regardless of whether a fully-customizedconfig=was passed. To get a non-defaultoutput_format/per_rowwhile also passing a customconfig, you must set them as direct keyword arguments too — values inside a passed-inconfigobject for these two specific fields are always overwritten.
Parameters
Returns
List[LoadedDocument] — count and shape depend on per_row and the number of tables/sheets/pages found.
Raises
FileNotFoundError/ValueError— at constructionImportError—openpyxl(Excel),pdfplumberorcamelot-py(PDF, depending onpdf_extraction_method),beautifulsoup4(HTML) — each with a clear install messageValueError— unrecognized extension reachinglazy_load()(defensive)
Internal workflow — format dispatch
ext = self.file_path.suffix.lower()
if ext in (".xlsx", ".xls", ".xlsm"): yield from self._load_excel()
elif ext in (".csv", ".tsv"): yield from self._load_csv(delimiter=...)
elif ext == ".pdf": yield from self._load_pdf()
elif ext in (".html", ".htm"): yield from self._load_html()
elif ext in (".md", ".markdown"): yield from self._load_markdown()
Format: Excel — _load_excel()
Opens with openpyxl.load_workbook(read_only=True, data_only=True). Iterates config.sheet_names or all sheets; for each, reads all rows via iter_rows(values_only=True), treats the first row as headers, the rest as data, and builds a _RawTable per sheet → _table_to_docs().
Format: CSV/TSV — _load_csv(delimiter)
Reads the whole file as a single table (first row = headers, rest = data rows) via csv.reader. Unlike CSVLoader, this treats the entire CSV as one table object rather than one document per row by default (though per_row=True produces the same per-row granularity).
Format: PDF — _load_pdf()
Dispatches to _pdf_pdfplumber() or _pdf_camelot() based on config.pdf_extraction_method:
- pdfplumber: iterates pages, calls
page.extract_tables(), skips tables with fewer than 2 rows (tbl[0]= header, rest = data); each table getspage_numberand an incrementingtable_idxin metadata. - camelot:
camelot.read_pdf(path, pages="all"); each detected table's pandasDataFramehas its first row treated as headers; metadata includes camelot's ownaccuracyscore (a model-reported extraction confidence metric) andpage(camelot reports this on the table object itself, e.g. as a string like"3").
Format: HTML — _load_html()
For each <table>: headers from <thead><tr> if present, else falls back to treating the first <tbody> row as headers (headers = cells when row_idx == 0 and no <thead> headers were found). Skips empty tables if skip_empty_tables.
Format: Markdown — _load_markdown()
Parses GitHub-Flavored-Markdown tables via the static method _parse_markdown_tables(text), which uses a regex (\|.+\|\n\|[-:| ]+\|\n(?:\|.+\|\n)*) to find header-row + separator-row + data-rows blocks, splitting each |-delimited line into cells.
[!NOTE] Markdown table regex limitations The regex requires lines to end with
\n(so a table at the very end of a file without a trailing newline may not match) and expects a strict| cell | cell |structure per line; tables using alternate Markdown table syntaxes (e.g. without leading/trailing pipes) will not be detected.
_RawTable (internal dataclass)
@dataclass
class _RawTable:
headers: List[str]
rows: List[List[str]]
metadata: Dict[str, Any] = field(default_factory=dict)
An internal intermediate representation, not part of the public API, used to decouple "got this table from somewhere" from "render this table into a LoadedDocument".
_table_to_docs(table) → one or more LoadedDocument
- If
config.skip_empty_tablesand the table has no rows: yields nothing. - If
config.per_row: delegates to_per_row_docs()(one document per row). - Else: delegates to
_whole_table_doc()(one document for the entire table).
_whole_table_doc(table) — output formatting
Renders the entire table (respecting max_rows_per_doc as a truncation limit) using one of four formatters based on config.output_format:
_per_row_docs(table) — per-row formatting
For each row: if include_header_in_rows, renders as "header1: val1 | header2: val2 | ..."; otherwise just joins raw values with column_separator. Each row gets its own LoadedDocument with row_index in metadata.
Metadata produced
Varies by format and granularity, but generally includes: source, file_format ("excel", "csv", "pdf", "html", "markdown"), sheet_name/page_number/table_index (as applicable), total_rows, total_cols, loader_type: "TableLoader", output_format (or "row" in per-row mode), and row_index (per-row mode only).
Complexity
- Time: O(rows × columns) per table, summed across all tables/sheets/pages.
- Space: Each format builds the full table in memory (
_RawTable) before rendering — there's no row-level streaming within a single table's extraction, thoughlazy_load()does yield each table's document(s) as soon as that table is processed, before moving to the next sheet/page.
Thread safety
No shared mutable state; safe across concurrent instances on different files.
Examples
Basic — all Excel tables as Markdown:
from zerda.document_loaders import TableLoader
docs = TableLoader("data.xlsx").load()
print(docs[0].page_content) # GFM markdown table
Per-row extraction, plain text format:
from zerda.document_loaders import TableLoader, TableLoaderConfig
config = TableLoaderConfig(per_row=True, output_format="text")
loader = TableLoader("report.pdf", config=config)
for doc in loader.lazy_load():
print(doc.page_content)
JSON output for downstream structured processing:
docs = TableLoader("inventory.csv", output_format="json").load()
import json
records = json.loads(docs[0].page_content)
Specific Excel sheets only:
config = TableLoaderConfig(sheet_names=["Q1", "Q2"])
docs = TableLoader("annual.xlsx", config=config).load()
PDF tables via camelot instead of pdfplumber:
config = TableLoaderConfig(pdf_extraction_method="camelot")
docs = TableLoader("financial_statement.pdf", config=config).load()
Truncate huge tables to a manageable document size:
config = TableLoaderConfig(max_rows_per_doc=200)
docs = TableLoader("big_export.xlsx", config=config).load()
Error handling — missing PDF table extraction dependency:
try:
docs = TableLoader("report.pdf").load()
except ImportError as e:
print(f"Install pdfplumber: {e}")
Production usage — Arabic table data, per-row for RAG with row-level citations:
config = TableLoaderConfig(per_row=True, include_header_in_rows=True)
docs = TableLoader("بيانات.xlsx", config=config).load()
for doc in docs:
# doc.page_content e.g. "الاسم: أحمد | العمر: 30 | المدينة: القاهرة"
index_for_retrieval(doc)
Best practices
- Use
per_row=Truewhen you need row-level retrieval granularity (e.g. "find the row about product X"); use the default whole-table mode when the table's meaning depends on seeing all rows together (e.g. a small comparison table). - For PDF tables, try
pdfplumberfirst (the default, lighter dependency); fall back tocamelotonly if extraction quality is insufficient, sincecamelothas heavier dependencies (OpenCV via[cv]extras). - Always pass
output_format/per_rowas direct keyword arguments toTableLoader(...), not only inside aconfig=object, per the override warning above.
Common mistakes
- Passing
TableLoaderConfig(output_format="json")viaconfig=without also passingoutput_format="json"directly — it gets silently reset to"markdown". - Expecting
max_rows_per_docto limitper_rowmode — it only affects whole-table document rendering; per-row mode inherently produces one document per row with no built-in cap (useskip_rows/row-count logic upstream if you need to limit total rows processed). - Assuming the Markdown table parser handles every valid Markdown table syntax variant — see the regex limitation note above.
InMemoryTableLoader
Extends BaseDocumentLoader directly
Overview
Loads a table passed directly as Python lists — no file required.
Signature
class InMemoryTableLoader(BaseDocumentLoader):
def __init__(
self,
headers: List[str],
rows: List[List[str]],
config: Optional[TableLoaderConfig] = None,
source_name: str = "in_memory_table",
): ...
def load(self) -> List[LoadedDocument]: ...
def lazy_load(self) -> Iterator[LoadedDocument]: ...
Parameters
Internal workflow
lazy_load() constructs an uninitialized TableLoader instance via TableLoader.__new__(TableLoader) (bypassing __init__ entirely, since there's no real file to validate) and manually sets just the two attributes (config, file_path) that _table_to_docs() needs, then reuses that method directly. This is a deliberate code-reuse technique to avoid duplicating the formatting logic (_to_markdown, _to_csv_text, etc.) between TableLoader and InMemoryTableLoader.
Examples
Basic:
from zerda.document_loaders import InMemoryTableLoader
loader = InMemoryTableLoader(
headers=["الاسم", "العمر", "المدينة"],
rows=[["أحمد", "30", "القاهرة"], ["سارة", "25", "الإسكندرية"]],
)
docs = loader.load()
Programmatically-generated table (e.g. from a database query):
from zerda.document_loaders import InMemoryTableLoader, TableLoaderConfig
rows = [[str(r["id"]), r["name"], str(r["revenue"])] for r in query_results]
config = TableLoaderConfig(output_format="markdown")
docs = InMemoryTableLoader(headers=["id", "name", "revenue"], rows=rows, config=config).load()
Per-row, for individual record retrieval:
config = TableLoaderConfig(per_row=True)
docs = InMemoryTableLoader(headers=["sku", "qty"], rows=inventory_rows, config=config).load()
Best practices
- Use this when table data already exists as in-memory Python structures (query results, API responses) rather than round-tripping through a temporary CSV file and
TableLoader.
Common mistakes
- Mismatched row lengths vs.
headers— the formatting methods (_to_markdown, etc.) generally pad short rows but this is on a best-effort basis per formatter; verify your row lengths match your header count for predictable output.
Related APIs
GraphLoader— the analogous module for graph-structured (not tabular) dataCSVLoader,ExcelLoader— narrower, format-specific alternativesHTMLLoader— also extracts HTML tables, as a side feature of general HTML loadingPDFMultimodalLoader— heuristic (less reliable) PDF table detection as a side featureLoadedDocument