# Share a Pandas DataFrame as HTML — `to_html()` Is a Fragment, Not a Page

Canonical: https://commareports.com/share-pandas-dataframe-html
Published: 2026-08-28

> `df.to_html()` writes a bare <table> with no <head> and no CSS. What to send instead, how to keep the formatting with Styler, and how to turn the table into a link people can comment on.

# Share a pandas DataFrame as HTML

The table is right there in the notebook and someone wants it. The
export step has one trap in it.

## `to_html()` gives you a fragment

```python
df.to_html("table.html")
```

That writes exactly this:

```html
<table border="1" class="dataframe">
  <thead>
    …
  </thead>
  <tbody>
    …
  </tbody>
</table>
```

No `<html>`, no `<head>`, no CSS. Browsers are forgiving enough to
render it, which is why the result looks like an unstyled table rather
than an error — and why it looks nothing like what you saw in the
notebook.

## `Styler` gives you a page

```python
styled = (
    df.style
      .format({"revenue": "${:,.0f}", "margin": "{:.1%}"})
      .background_gradient(subset=["margin"], cmap="RdYlGn")
      .hide(axis="index")
)
styled.to_html("table.html", doctype_html=True)
```

`Styler.to_html()` emits the generated `<style>` block with the table,
and `doctype_html=True` wraps it in a real document. Everything you set
in the notebook — number formats, conditional colouring, hidden index —
comes through. This is the one to send.

Useful arguments either way:

- `escape=False` — render HTML you built in a cell (links, badges)
  instead of escaping it.
- `max_rows=` / `max_cols=` — truncate with an ellipsis row rather than
  emitting a table nobody can scroll.
- `index=False` on `to_html()`, `.hide(axis="index")` on the Styler.

## Publish it instead of sending the file

Drag `table.html` into [Comma](https://commareports.com/) and you get a
URL:

- Renders faithfully, including your Styler CSS.
- Opens on a phone, which an `.html` attachment does not — see
  [someone sent me an HTML file](/open-html-file-online).
- Access per report: private, team, anyone signed in, or link holders
  with view, comment or edit — [sharing & access control](/docs/sharing).

If you built a fuller page — a couple of tables, a chart, some prose —
publish that page instead. Plotly figures, folium maps and profiling
reports all publish the same way:
[Plotly HTML](/share-plotly-html),
[folium maps](/share-folium-map),
[ydata-profiling](/share-ydata-profiling-report).

## From a script or a schedule

```python
import requests

html = df.style.format("{:,.2f}").to_html(doctype_html=True)
requests.patch(
    f"https://commareports.com/api/v1/reports/{report_id}",
    headers={"Authorization": f"Bearer {token}"},
    json={"title": "Daily revenue by region", "html": html},
    timeout=30,
).raise_for_status()
```

Same id, same URL, one revision per run — so "revenue by region" is a
living link rather than a new attachment every morning. Put it on a
timer with [scheduled HTML reports](/features/routines/scheduled-html-reports),
or publish from the job that already builds it —
[publishing from CI](/docs/ci).

## Then: comment on the cell

Anchored threads mean a correction lands on the number it's about and
survives the next run —
[commenting on HTML](/comment-on-html) and
[stop screenshotting reports](/stop-screenshotting-reports).

## Limits

Entry HTML 5 MB — which a wide table hits at roughly tens of thousands
of rows. Truncate with `max_rows`, or publish the aggregate. Assets:
25 MB per file, 250 MB and 500 files per report.

## Try it

Comma is free — unlimited reports, unlimited commenters, unlimited
revision history.

**[Publish a table →](https://commareports.com/)**

### Related

- [Share a Jupyter notebook as HTML](/share-jupyter-notebook-html) · [Share an EDA report](/share-eda-report)
- [Share a Plotly figure](/share-plotly-html) · [Share an Altair chart](/share-altair-chart)
- [Upload an HTML file and get a link](/upload-html-file-get-link)
