Intermediate

You have a list of dictionaries, a query result, or a DataFrame you want to print to the terminal — and the default Python output looks like a wall of brackets and commas. Every developer has been there. Debugging data pipelines, writing CLI tools, generating quick reports: at some point you need output that a human can actually read without squinting. Python’s tabulate library solves this in one line.

The tabulate package converts Python data structures into formatted ASCII tables with customizable styles, alignment, and number formatting. It handles lists of lists, lists of dicts, NumPy arrays, Pandas DataFrames, and more. Installation is a single pip command, and it has no mandatory dependencies — making it one of the easiest wins in your CLI toolkit.

In this article you will learn how to install and use tabulate for common data structures, explore the most useful table formats, control alignment and number precision, handle missing values gracefully, and combine tabulate with real-world data in a practical CLI report. By the end you will know exactly when and how to reach for tabulate instead of print.

Python tabulate: Quick Example

Here is the simplest possible tabulate usage — a list of dictionaries printed as a neat grid:

# tabulate_quick.py
from tabulate import tabulate

data = [
    {"name": "Alice", "score": 95, "grade": "A"},
    {"name": "Bob",   "score": 82, "grade": "B"},
    {"name": "Carol", "score": 74, "grade": "C"},
]

print(tabulate(data, headers="keys"))

Output:

name      score  grade
------  -------  -------
Alice        95  A
Bob          82  B
Carol        74  C

The headers="keys" argument tells tabulate to use dictionary keys as column headers. The library automatically right-aligns numeric columns and left-aligns strings — sensible defaults that make the table immediately readable. Keep reading to see how to customize styles, alignment, and formatting for more demanding use cases.

What Is tabulate and Why Use It?

The tabulate library is a pure-Python utility that transforms sequences of records into formatted text tables. Think of it as str.format() for tabular data: you hand it a list of rows and a header, and it figures out column widths, alignment, and separators automatically.

Without tabulate, you would write custom formatting code every time: calculate max column widths, pad strings, build separator lines. That is tedious and error-prone. Tabulate does all of that and adds support for 30+ table styles including plain text, GitHub Markdown, HTML, LaTeX, and more.

Use CaseTabulate Format
Terminal output / CLI toolsgrid, simple, rounded_grid
GitHub README or PR commentsgithub, pipe
HTML email or web pagehtml, unsafehtml
Academic papers / LaTeXlatex, latex_booktabs
Minimal / screenreader friendlyplain, tsv
tabulate tutorial
Fifteen columns, a thousand rows. tabulate in 0.002s.

Installing tabulate

Install from PyPI with pip:

# terminal
pip install tabulate

Verify the installation:

# verify_tabulate.py
import tabulate
print(tabulate.__version__)

Output:

0.9.0

Choosing a Table Format

The tablefmt parameter controls the visual style. Here are the most useful formats side by side:

# tabulate_formats.py
from tabulate import tabulate

rows = [["Python", 3.12, "Interpreted"], ["Rust", 1.75, "Compiled"], ["Go", 1.21, "Compiled"]]
headers = ["Language", "Version", "Type"]

for fmt in ["simple", "grid", "github", "plain"]:
    print(f"\n--- {fmt} ---")
    print(tabulate(rows, headers=headers, tablefmt=fmt))

Output (selected formats):

--- simple ---
Language      Version  Type
----------  ---------  -----------
Python           3.12  Interpreted
Rust             1.75  Compiled
Go               1.21  Compiled

--- grid ---
+------------+-----------+-------------+
| Language   |   Version | Type        |
+============+===========+=============+
| Python     |      3.12 | Interpreted |
+------------+-----------+-------------+
| Rust       |      1.75 | Compiled    |
+------------+-----------+-------------+
| Go         |      1.21 | Compiled    |
+------------+-----------+-------------+

--- github ---
| Language   |   Version | Type        |
|:-----------|----------:|:------------|
| Python     |      3.12 | Interpreted |
| Rust       |      1.75 | Compiled    |
| Go         |      1.21 | Compiled    |

Use simple for most terminal output. Use github when generating Markdown for READMEs or PR descriptions. Use grid when you want clear visual cell boundaries. For HTML output, use html — tabulate generates proper <table> markup with <th> and <td> tags.

tabulate tutorial
Pick your format once. Swap it in one argument.

Column Alignment and Number Precision

Tabulate auto-aligns columns but you can override this with the colalign parameter. Number formatting uses floatfmt and intfmt:

# tabulate_alignment.py
from tabulate import tabulate

portfolio = [
    ["AAPL",  182.63,  1500000.25,  "+2.3%"],
    ["GOOGL", 140.12, 14000000.00,  "-0.8%"],
    ["MSFT",  378.85,  9500000.50,  "+1.1%"],
]
headers = ["Ticker", "Price", "Market Cap", "Change"]

print(tabulate(
    portfolio,
    headers=headers,
    tablefmt="simple",
    colalign=("left", "right", "right", "center"),
    floatfmt=("", ".2f", ",.2f", ""),
))

Output:

Ticker      Price     Market Cap    Change
--------  -------  -------------  --------
AAPL       182.63  1,500,000.25   +2.3%
GOOGL      140.12  14,000,000.00  -0.8%
MSFT       378.85  9,500,000.50   +1.1%

The colalign tuple takes one value per column: "left", "right", or "center". The floatfmt tuple follows Python’s format spec mini-language — ".2f" for two decimal places, ",.2f" adds thousands separator. Pass an empty string to skip formatting for that column.

Handling Missing Values

Real-world data is messy — rows may have different lengths or None values. Tabulate handles both gracefully with the missingval parameter:

# tabulate_missing.py
from tabulate import tabulate

rows = [
    ["Alice",  "Engineering", 95000],
    ["Bob",    "Marketing",   None],
    ["Carol",  None,          72000],
    ["Dave",   "Engineering"],
]

headers = ["Name", "Department", "Salary"]

print(tabulate(rows, headers=headers, tablefmt="simple", missingval="N/A"))

Output:

Name    Department      Salary
------  ------------  --------
Alice   Engineering      95000
Bob     Marketing          N/A
Carol   N/A              72000
Dave    Engineering        N/A

The missingval parameter fills in any None or missing cell with the string you provide. This makes tabulate safe to use with real database queries where columns can be NULL — no need to pre-clean the data before display.

tabulate tutorial
missingval=’N/A’. Your None values now have a polite face.

Working with Different Data Sources

Tabulate accepts multiple input formats. Here is how it works with each:

# tabulate_sources.py
from tabulate import tabulate

# 1. List of lists
rows_lol = [[1, "alpha", 3.14], [2, "beta", 2.71]]
print("List of lists:")
print(tabulate(rows_lol, headers=["ID", "Name", "Value"], tablefmt="simple"))

# 2. List of dicts
rows_dicts = [
    {"city": "Sydney", "pop": 5300000, "country": "AU"},
    {"city": "London", "pop": 9000000, "country": "UK"},
]
print("\nList of dicts:")
print(tabulate(rows_dicts, headers="keys", tablefmt="simple"))

# 3. Dict of lists (column-oriented)
rows_cols = {"x": [1, 2, 3], "y": [10, 20, 30], "z": [100, 200, 300]}
print("\nDict of lists:")
print(tabulate(rows_cols, headers="keys", tablefmt="simple"))

Output:

List of lists:
  ID  Name      Value
----  ------  -------
   1  alpha      3.14
   2  beta       2.71

List of dicts:
city        pop  country
------  -------  ---------
Sydney  5300000  AU
London  9000000  UK

Dict of lists:
  x    y    z
---  ---  ---
  1   10  100
  2   20  200
  3   30  300

For headers="keys" with dicts, tabulate uses the dictionary keys in insertion order (Python 3.7+). For column-oriented data (a dict of lists), tabulate handles it correctly without any conversion on your part.

tabulate tutorial
40 lines of Python. Your ops team thinks it took a week.

Real-Life Example: CLI Package Dependency Report

Here is a practical script that reads installed packages and generates a formatted report — useful for auditing environments before deployment:

# package_report.py
import subprocess
import sys
from tabulate import tabulate

def get_installed_packages():
    result = subprocess.run(
        [sys.executable, "-m", "pip", "list", "--format=json"],
        capture_output=True, text=True
    )
    import json
    packages = json.loads(result.stdout)
    return packages

def get_outdated_packages():
    result = subprocess.run(
        [sys.executable, "-m", "pip", "list", "--outdated", "--format=json"],
        capture_output=True, text=True
    )
    import json
    if result.stdout.strip():
        return {p["name"].lower(): p["latest_version"] for p in json.loads(result.stdout)}
    return {}

packages = get_installed_packages()
outdated = get_outdated_packages()

rows = []
for pkg in sorted(packages, key=lambda p: p["name"].lower()):
    name = pkg["name"]
    version = pkg["version"]
    latest = outdated.get(name.lower(), "")
    status = "OUTDATED" if latest else "ok"
    rows.append([name, version, latest or "--", status])

print("\nInstalled Packages Report")
print(f"Total: {len(rows)} packages, {len(outdated)} outdated\n")

print(tabulate(
    rows,
    headers=["Package", "Installed", "Latest", "Status"],
    tablefmt="simple",
    colalign=("left", "right", "right", "center"),
    missingval="--",
))

Output (example):

Installed Packages Report
Total: 47 packages, 3 outdated

Package      Installed    Latest    Status
---------  -----------  --------  --------
certifi         2023.11  2024.02   OUTDATED
pip              23.3.2   24.0.0   OUTDATED
requests          2.31.0  --          ok
tabulate          0.9.0   --          ok
urllib3           2.1.0   2.2.0    OUTDATED

This script shows how to combine tabulate with subprocess output for a genuinely useful DevOps tool. Extend it by adding a --html flag that changes tablefmt="html" and writes to a file, or filter to show only outdated packages by slicing the rows list before passing to tabulate.

Frequently Asked Questions

Can I use tabulate directly with a Pandas DataFrame?

Yes — pass the DataFrame and headers="keys": tabulate(df, headers="keys", tablefmt="grid"). DataFrames also have a built-in df.to_markdown() method that wraps tabulate internally. For Jupyter notebooks, the built-in HTML rendering is usually more appropriate than tabulate.

How do I handle very wide tables that wrap in the terminal?

Use maxcolwidths (added in tabulate 0.9.0) to truncate long values: tabulate(data, headers="keys", maxcolwidths=30) caps each column at 30 characters. For very wide tables, consider using tablefmt="plain" (no borders) or transpose the data so columns become rows.

How do I add a row index to my table?

Pass showindex=True for an auto-incrementing index, or pass a list for custom indices: tabulate(rows, headers=headers, showindex=range(1, len(rows)+1)). You can also pass showindex="always" which works for any input type including DataFrames.

Is tabulate fast enough for large datasets?

Tabulate is optimized for display, not batch processing — it is fine for hundreds to a few thousand rows. For tables with 10,000+ rows you will notice slowdown because it must scan all values to compute column widths. Display only a sample (data[:100]) or use a fixed column width you specify manually.

Can I add colors or bold text to tabulate output?

Tabulate itself does not add ANSI color codes, but you can add them to your data strings before passing to tabulate. Libraries like colorama work alongside tabulate. Note that ANSI codes add invisible characters that can throw off alignment — test carefully. If you want colors plus rich formatting, consider using the rich library’s built-in Table class as an alternative.

Conclusion

The tabulate library turns the chore of formatting tabular data into a single function call. The key patterns: pass lists of dicts with headers="keys" for the quickest readable output; use tablefmt="github" for Markdown output; use colalign and floatfmt for precise numeric formatting; and missingval for handling None values from real-world data sources.

The real-life example — a package dependency reporter — shows how to combine tabulate with subprocess to build a useful DevOps tool in about 40 lines. Extend it with a --html output option and automated email delivery for a lightweight dependency audit system.

For the full list of format names and options, see the tabulate documentation on PyPI and the GitHub repository which includes format screenshots for every style.