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 Case | Tabulate Format |
|---|---|
| Terminal output / CLI tools | grid, simple, rounded_grid |
| GitHub README or PR comments | github, pipe |
| HTML email or web page | html, unsafehtml |
| Academic papers / LaTeX | latex, latex_booktabs |
| Minimal / screenreader friendly | plain, tsv |
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.
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.
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.
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.