tabulate formats list-of-lists or list-of-dicts as text tables. pip install tabulate. Basic: from tabulate import tabulate. print(tabulate([[1,"Alice",99.5],[2,"Bob",87.2]], headers=["id","name","score"])). Dict list: tabulate([{"a":1,"b":2},{"a":3,"b":4}], headers="keys"). Format: tablefmt="plain" (default) | "simple" (lines) | "grid" | "pipe" (Markdown) | "github" | "rst" | "latex" | "html" | "tsv" | "rounded_grid" | "heavy_grid" | "double_grid". Numbers: floatfmt=".2f" — 2 decimal places. intfmt="," — thousands separator. Alignment: colalign=("right","left","decimal"). showindex: showindex=True — 0-based row numbers. showindex="always". showindex=range(1,N). Headers style: headers=["Name","Score"]. headers="keys" for dicts. headers="firstrow" — first list row is header. Missing: missingval="N/A". numalign: numalign="right" (default) | "left" | "center" | "decimal". stralign: stralign="left". Width: maxcolwidths=30 — truncate. disable_numparse=True — treat numbers as strings. pandas: tabulate(df, headers="keys", tablefmt="pipe", showindex=False). Tuple input. Generator input. Single column: tabulate([[v] for v in values], headers=["Value"]). Claude Code generates tabulate table formatters, Markdown output functions, and CLI report formatters.
CLAUDE.md for tabulate
## tabulate Stack
- Version: tabulate >= 0.9 | pip install tabulate
- Basic: tabulate(data, headers=["A","B"]) where data is list-of-lists or list-of-dicts
- Dict: tabulate(list_of_dicts, headers="keys") — auto-extracts column names
- Formats: "simple" | "grid" | "pipe" (Markdown) | "github" | "html" | "rst" | "latex"
- Numbers: floatfmt=".2f" | intfmt="," | numalign="decimal"
- Align: colalign=("right","left","center") — per-column
- Truncate: maxcolwidths=30 — wrap/truncate long cell content
tabulate Table Formatting Pipeline
# app/tables.py — tabulate formatting for CLI, Markdown, HTML, and reports
from __future__ import annotations
from dataclasses import dataclass
from typing import Any, Sequence
from tabulate import tabulate
# ─────────────────────────────────────────────────────────────────────────────
# 1. All table formats demonstrated
# ─────────────────────────────────────────────────────────────────────────────
SAMPLE_DATA = [
{"name": "Alice", "score": 97.5, "rank": 1, "passed": True},
{"name": "Bob", "score": 84.2, "rank": 2, "passed": True},
{"name": "Carol", "score": 61.0, "rank": 3, "passed": False},
{"name": "Dave", "score": 58.9, "rank": 4, "passed": False},
]
def show_all_formats(data: list[dict] = SAMPLE_DATA) -> None:
"""Print the same table in several common formats."""
formats = [
("plain", "Plain (no borders)"),
("simple", "Simple (lines under header)"),
("grid", "Grid (full ASCII grid)"),
("rounded_grid", "Rounded Grid"),
("pipe", "Pipe (Markdown)"),
("github", "GitHub Flavored Markdown"),
("rst", "reStructuredText"),
("html", "HTML <table>"),
]
for fmt, label in formats:
print(f"\n── {label} (tablefmt={fmt!r}) ──")
print(tabulate(data, headers="keys", tablefmt=fmt, floatfmt=".1f"))
# ─────────────────────────────────────────────────────────────────────────────
# 2. Number formatting
# ─────────────────────────────────────────────────────────────────────────────
def format_financial_table(records: list[dict]) -> str:
"""Format a financial table with currency, percent, and integer formatting."""
rows = [
[
r["product"],
r["revenue"],
r["cost"],
r["margin"],
r["units"],
]
for r in records
]
return tabulate(
rows,
headers=["Product", "Revenue ($)", "Cost ($)", "Margin %", "Units"],
tablefmt="simple",
floatfmt=("", ",.2f", ",.2f", ".1%", ""),
intfmt=",",
colalign=("left", "right", "right", "right", "right"),
)
# ─────────────────────────────────────────────────────────────────────────────
# 3. Markdown tables for docs and GitHub PRs
# ─────────────────────────────────────────────────────────────────────────────
def to_markdown(
data: list[dict],
floatfmt: str = ".2f",
showindex: bool = False,
) -> str:
"""
tablefmt="pipe" produces GitHub/CommonMark Markdown tables.
Paste directly into README.md, PR descriptions, or Confluence pages.
"""
return tabulate(
data,
headers="keys",
tablefmt="pipe",
floatfmt=floatfmt,
showindex=showindex,
)
def benchmark_results_markdown(results: list[dict]) -> str:
"""Format benchmark results as a Markdown table for CI comments."""
rows = sorted(results, key=lambda r: r.get("mean_ms", 0))
return tabulate(
rows,
headers={
"name": "Test",
"mean_ms": "Mean (ms)",
"min_ms": "Min (ms)",
"stddev": "Std Dev",
"ops": "Ops/sec",
},
tablefmt="github",
floatfmt=".3f",
intfmt=",",
numalign="decimal",
colalign=("left", "right", "right", "right", "right"),
)
# ─────────────────────────────────────────────────────────────────────────────
# 4. HTML output
# ─────────────────────────────────────────────────────────────────────────────
def to_html_table(
data: list[dict],
floatfmt: str = ".2f",
table_id: str = "data-table",
) -> str:
"""
tablefmt="html" produces a <table> element.
Wrap in a style block for reports or emails.
"""
raw = tabulate(data, headers="keys", tablefmt="html", floatfmt=floatfmt)
# Inject id for CSS targeting
return raw.replace("<table>", f'<table id="{table_id}">', 1)
# ─────────────────────────────────────────────────────────────────────────────
# 5. CLI report — with row index and truncated columns
# ─────────────────────────────────────────────────────────────────────────────
def cli_report(
data: list[dict],
title: str = "",
maxcolwidths: int = 40,
showindex: bool = True,
) -> str:
"""
maxcolwidths=N wraps long strings in cells.
showindex=True adds a 0-based row number column.
"""
lines: list[str] = []
if title:
lines.append(title)
lines.append("=" * len(title))
table = tabulate(
data,
headers="keys",
tablefmt="rounded_grid",
showindex=showindex,
maxcolwidths=maxcolwidths,
missingval="—",
)
lines.append(table)
return "\n".join(lines)
# ─────────────────────────────────────────────────────────────────────────────
# 6. pandas DataFrame integration
# ─────────────────────────────────────────────────────────────────────────────
def dataframe_table(df, tablefmt: str = "pipe", floatfmt: str = ".3f") -> str:
"""
tabulate works directly with pandas DataFrames.
headers="keys" uses column names; showindex=False omits the DataFrame index.
"""
return tabulate(
df,
headers="keys",
tablefmt=tablefmt,
floatfmt=floatfmt,
showindex=False,
)
# ─────────────────────────────────────────────────────────────────────────────
# 7. Mixed-type data with missing values
# ─────────────────────────────────────────────────────────────────────────────
def format_mixed(data: list[dict]) -> str:
"""Handle None/missing values and mixed types gracefully."""
return tabulate(
data,
headers="keys",
tablefmt="simple",
missingval="N/A",
floatfmt=".2f",
numalign="decimal",
stralign="left",
)
# ─────────────────────────────────────────────────────────────────────────────
# 8. Comparison table (diff results)
# ─────────────────────────────────────────────────────────────────────────────
def diff_table(
before: list[dict],
after: list[dict],
key: str,
metric: str,
) -> str:
"""
Side-by-side comparison: before vs after for a given metric.
Adds a delta column.
"""
before_map = {r[key]: r[metric] for r in before}
after_map = {r[key]: r[metric] for r in after}
all_keys = sorted(set(before_map) | set(after_map))
rows = []
for k in all_keys:
b = before_map.get(k)
a = after_map.get(k)
delta = (a - b) if (a is not None and b is not None) else None
rows.append({
key: k,
"before": b,
"after": a,
"delta": delta,
"change %": (delta / b * 100) if b and delta is not None else None,
})
return tabulate(
rows,
headers="keys",
tablefmt="simple",
floatfmt=".2f",
missingval="N/A",
numalign="decimal",
colalign=("left", "right", "right", "right", "right"),
)
# ─────────────────────────────────────────────────────────────────────────────
# Demo
# ─────────────────────────────────────────────────────────────────────────────
if __name__ == "__main__":
print("=== Sample formats ===")
for fmt in ["simple", "grid", "pipe", "github"]:
print(f"\n[{fmt}]")
print(tabulate(SAMPLE_DATA, headers="keys", tablefmt=fmt, floatfmt=".1f"))
print("\n=== Financial table ===")
financial = [
{"product": "Widget A", "revenue": 15_234.50, "cost": 8_900.00, "margin": 0.416, "units": 508},
{"product": "Widget B", "revenue": 28_750.00, "cost": 19_200.00, "margin": 0.332, "units": 575},
{"product": "Gadget X", "revenue": 9_100.25, "cost": 4_050.00, "margin": 0.555, "units": 607},
]
print(format_financial_table(financial))
print("\n=== Benchmark markdown ===")
bench = [
{"name": "sorted()", "mean_ms": 0.042, "min_ms": 0.038, "stddev": 0.004, "ops": 23_000},
{"name": "insertion_sort", "mean_ms": 0.315, "min_ms": 0.290, "stddev": 0.025, "ops": 3_173},
{"name": "bubble_sort", "mean_ms": 1.840, "min_ms": 1.750, "stddev": 0.090, "ops": 543},
]
print(benchmark_results_markdown(bench))
print("\n=== Missing values ===")
mixed = [
{"name": "Alice", "score": 97.5, "note": None},
{"name": "Bob", "score": None, "note": "absent"},
]
print(format_mixed(mixed))
print("\n=== Diff table ===")
before = [{"module": "auth", "p50": 12.4}, {"module": "api", "p50": 45.1}]
after = [{"module": "auth", "p50": 10.2}, {"module": "api", "p50": 48.7}]
print(diff_table(before, after, key="module", metric="p50"))
For the Rich Table alternative — Rich’s Table renders ANSI-colored, box-drawing-character tables directly to a terminal with full color support and dynamic layout, while tabulate outputs plain strings that can be printed, written to files, embedded in Markdown documentation, pasted into GitHub PR comments (tablefmt="pipe"), compiled into LaTeX reports (tablefmt="latex"), or emailed as HTML (tablefmt="html") — the table string is just a string, so it composes with logging, file output, and string formatting without a console object. For the print + format() alternative — manually right-padding strings with f"{val:>10}" requires calculating column widths, handling Unicode width, aligning numeric columns, and producing a consistent format across different data shapes, while tabulate(data, headers="keys") handles all alignment automatically, detects numeric columns for right-alignment, and switches from ASCII dashes to proper box-drawing characters with tablefmt="rounded_grid". The Claude Skills 360 bundle includes tabulate skill sets covering all major tablefmt values (simple/grid/pipe/github/html/rst/latex), floatfmt and intfmt number formatting, colalign per-column alignment, showindex row numbering, missingval for None handling, maxcolwidths for truncation, pandas DataFrame tabulation, Markdown table generation for PR comments, HTML table output, benchmark comparison tables, and side-by-side diff tables. Start with the free tier to try table formatting code generation.