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

df.to_html("table.html")

That writes exactly this:

<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

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 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.
  • Access per report: private, team, anyone signed in, or link holders with view, comment or edit — sharing & access control.

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, folium maps, ydata-profiling.

From a script or a schedule

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, or publish from the job that already builds it — publishing from 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 and 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 →

Related