2025, Nov 26 09:00

Indent totals and subtotals in Jupyter pandas DataFrames using non-breaking spaces or CSS

Learn why leading spaces vanish in Jupyter and how to indent totals and subtotals using non-breaking spaces or pandas Styler CSS: text-indent and borders.

Rendering subtotals and totals with visual hierarchy in a Jupyter Notebook sounds trivial until you try to indent labels with plain spaces. Pandas shows the values, but the apparent indentation vanishes in the rendered HTML. Below is a concise walkthrough of the issue, plus two practical ways to get consistent indentation in the notebook output.

The setup and where it goes wrong

The data arrives as a MultiIndex Series from a database. The goal is to aggregate to subtotals and totals, emit a flat table, and visually indent the labels to reflect structure.

from pandas import MultiIndex, DataFrame, Series

mi_idx = MultiIndex.from_tuples(
    [
        ('total', 'subtotal', 'a'),
        ('total', 'subtotal', 'b'),
        ('total', 'subtotal', 'c'),
        ('total', 'subtotal', 'd'),
        ('total', 'foo', 'foo'),
        ('total', 'bar', 'bar')
    ],
    names=['module_1', 'module_2', 'module_3']
)

raw_musd = [
    106.564338488296,
    60.5686589737704,
    311.695156173571,
    -90.3794796906721,
    29.6147863260859,
    -49.0048344046974
]

base_df = Series(raw_musd, index=mi_idx, name='musd').to_frame()

scheme_df = DataFrame(
    [
        ("a", "module_3", 4),
        ("b", "module_3", 4),
        ("c", "module_3", 4),
        ("d", "module_3", 4),
        ("subtotal", "module_2", 2),
        ("foo", "module_3", 2),
        ("bar", "module_3", 2),
        ("total", "module_1", 0)
    ],
    columns=["name", "sum_over", "indent"]
)

out_dict = {
    f"{' ' * depth}{label}": base_df.query(f"{lvl} == '{label}'")["musd"].sum()
    for label, lvl, depth in scheme_df.values
}

pretty_df = DataFrame(out_dict, index=[0]).T.reset_index().set_axis(['', 'musd'], axis=1)

pretty_df.style.format(precision=1)

The computed values are fine and the strings in the first column do contain the spaces. However, visible indentation doesn’t appear in the rendered table even if you left-align that column.

What’s actually happening

In the notebook’s HTML rendering of a styled DataFrame, padding with regular spaces does not show as indentation. Left alignment alone won’t help either. To make indentation visible in the output, you need either non-breaking spaces or CSS-driven indentation.

Two straightforward fixes

If you just need indentation quickly, you can replace the leading spaces with non-breaking spaces and left-align the first column. If you also want italics, bold, and borders while keeping content separate from styling, use CSS via pandas Styler.

Quick fix with non-breaking spaces:

import pandas as pd

table_quick = pd.DataFrame(
    list({
        4 * " " + "a": 106.6,
        4 * " " + "b": 60.6,
        2 * " " + "subtotal": 388.4,
        0 * " " + "total": 369.1
    }.items()),
    columns=["", "musd"]
)

view_quick = table_quick.style.set_properties(subset=[""], **{"text-align": "left"})
view_quick

CSS-first approach for indentation, italics, bold, and borders:

import pandas as pd

vals_css = {"a": 106.6, "b": 60.6, "subtotal": 388.4, "total": 369.1}
table_css = pd.DataFrame(list(vals_css.items()), columns=["", "musd"])

sty_css = table_css.style.format(precision=1).set_properties(
    subset=[""], **{"text-align": "left"}
)

sty_css = sty_css.set_properties(
    subset=pd.IndexSlice[0:1, :],
    **{"text-indent": "2em", "font-style": "italic"}
)

sty_css = sty_css.set_properties(
    subset=pd.IndexSlice[2, :],
    **{"text-indent": "1em"}
)

sty_css = sty_css.set_properties(
    subset=pd.IndexSlice[3, :],
    **{
        "border-top": "1px solid black",
        "border-bottom": "3px double black",
        "font-weight": "bold"
    }
)

sty_css

If you prefer to hide the index column, you can add sty_css.hide(axis="index") before rendering.

Why this is worth knowing

Data scientists and engineers spend a lot of time communicating results inside notebooks. Small details—indentation for hierarchy, emphasis for totals, clean borders—make tables faster to read and less error-prone. Knowing that the notebook’s HTML view won’t honor leading spaces the way you might expect saves time and directs you toward techniques that are reliable and repeatable.

Final thoughts

For quick wins, swap leading spaces for   and set left alignment. For more control and a cleaner separation of content from presentation, lean on CSS via pandas Styler: text-indent for hierarchy, font-style and font-weight for emphasis, and borders for visual separation. Both approaches play well in Jupyter and give you predictable indentation without changing your aggregation logic.