tabulate
tabulate turns a list of lists, a list of dicts, a list of dataclasses, a NumPy array, or a pandas DataFrame into a formatted text table, and it does it with one function call. The tablefmt argument decides what the output looks like: simple and grid for terminals, github and pipe for Markdown, rst for Sphinx, latex, html, jira, mediawiki, and about thirty more. Along the way it inspects each column, decides whether it holds text or numbers, right-aligns numbers on the decimal point, and pads everything to a consistent width. It also installs a tabulate command that reads CSV, JSON lines, or whitespace-separated data from a file or stdin and prints it in any of those formats.
The fastest way to get a decent-looking text or Markdown table out of Python, and the reason pandas.to_markdown exists at all. Just know that number parsing is on by default and that the release cadence is measured in years, not months.
Use it if
- You are printing a summary table at the end of a CLI run and want decimal-aligned numbers without writing format string arithmetic
- You need to generate Markdown or reStructuredText tables from data, for a README, a changelog, or a generated docs page: tablefmt='github' is the whole implementation
- Your rows arrive as dicts or dataclasses and you want the keys or field names to become headers automatically with headers='keys'
- You want a zero-dependency install. The only optional extra is wcwidth for CJK width handling, so it drops into a locked-down environment without argument
- You want color, spinners, progress bars, or panels alongside the table. tabulate emits a plain string and nothing else; rich gives you styled tables plus everything around them and is the better base for a modern CLI
- You are formatting large tables or calling it in a hot loop. The README's own benchmark puts a 10x10 table at 553 microseconds versus 12 microseconds for joining with tabs, roughly 46 times slower, and it holds the entire table in memory and transposes it twice
- Your string columns look numeric. tabulate parses cells as numbers by default, so '007' prints as 7 and '1e23' becomes scientific notation; you need disable_numparse or a per-column guard, and people discover this in production output
- You need Python 3.9 or older. Version 0.10.0 raised the floor to 3.10, so older runtimes are pinned to 0.9.0 from October 2022
- You want a project on a predictable release schedule. There was a gap of over three years between 0.9.0 and 0.10.0, PyPI still carries the Development Status :: 4 - Beta classifier after eleven years, and the changelog already has an unreleased 0.11.0 section on master
Setup reality
pip install tabulate and you are done: no compiled parts, no required dependencies, and a single pure-Python wheel. Add pip install 'tabulate[widechars]' if your data contains fullwidth CJK glyphs, which pulls wcwidth and turns on WIDE_CHARS_MODE automatically; it is on whenever wcwidth is importable, so another package installing wcwidth silently changes your column widths and you disable it by setting tabulate.WIDE_CHARS_MODE = False. The genuine friction is behavioral rather than installation: number parsing is on by default, the parsed type is deduced per column so one stray string changes how the whole column renders, and picking a tablefmt from a list of more than thirty options is guesswork until you print a few. The package also puts a tabulate executable on PATH, which can collide with other tooling in a shared environment.
Patterns
Print a list of lists with headersbasic-table
from tabulate import tabulate
rows = [['Mercury', 2439.7], ['Venus', 6051.8], ['Earth', 6371.0]]
print(tabulate(rows, headers=['Planet', 'R (km)']))Default tablefmt is 'simple', which is the two-rule style you see in most CLI output; numbers right-align and strings left-align without you asking.
Use dict keys or dataclass fields as headersheaders-from-keys
from tabulate import tabulate
rows = [{'name': 'alice', 'age': 24}, {'name': 'bob', 'age': 19}]
print(tabulate(rows, headers='keys'))
# same trick for the first row of a raw list
print(tabulate(csv_rows, headers='firstrow'))With a list of dicts, columns come from the first row's keys; a later row with an extra key does not add a column, so normalize your rows first if the shape can vary.
Generate a Markdown tablemarkdown-output
from tabulate import tabulate
md = tabulate(rows, headers='keys', tablefmt='github')
open('README.md', 'a').write(md + '\n')Use 'github' for GitHub-flavored Markdown and 'pipe' for Pandoc; they differ in how the alignment row is written, and GitHub renders both but Pandoc only respects 'pipe'.
Stop tabulate from turning strings into numbersdisable-number-parsing
from tabulate import tabulate
rows = [['v1.0', '007'], ['v2.0', '1e23']]
print(tabulate(rows, disable_numparse=True))
# or per column: parse column 1, leave column 0 alone
print(tabulate(rows, disable_numparse=[0]))This is the single most common surprise in the library: version strings, zip codes, and IDs get reformatted as numbers. disable_numparse accepts True or a list of column indices to leave alone.
Control float, integer, and missing value renderingnumber-formatting
from tabulate import tabulate
print(tabulate(
[['pi', 3.14159265, 1000], ['e', 2.71828, None]],
headers=['const', 'value', 'count'],
floatfmt='.4f',
intfmt=',',
missingval='n/a',
))floatfmt and intfmt also take a tuple with one format per column; missingval only applies to None, not to empty strings or NaN in a way you might expect from pandas.
Override the automatic alignmentcolumn-alignment
from tabulate import tabulate
print(tabulate(
rows,
headers=['Name', 'Qty', 'Price'],
colalign=('left', 'right', 'decimal'),
headersalign=('left', 'right', 'right'),
))Values are 'left', 'right', 'center', 'decimal', 'global', or None; 'decimal' only means anything for numeric columns. colglobalalign and headersglobalalign set the default for unlisted columns and were added in 0.10.0.
Cap column width and wrap the overflowwrap-long-cells
from tabulate import tabulate
print(tabulate(
[['John Smith', 'Middle Manager, Widgets Division']],
headers=['Name', 'Title'],
tablefmt='grid',
maxcolwidths=[None, 20],
break_long_words=False,
))maxcolwidths is a list positional to your columns, and None means unlimited for that one. Wrapping needs a format that draws row separators, so 'simple' will run the wrapped lines together and 'grid' will not.
Insert a rule between groups of rowsseparating-line
from tabulate import tabulate, SEPARATING_LINE
table = [
['Earth', 6371],
['Mars', 3390],
SEPARATING_LINE,
['Moon', 1737],
]
print(tabulate(table, tablefmt='simple'))SEPARATING_LINE is a sentinel string you drop in as a row; it works in formats that have a horizontal rule and is silently ignored in ones that do not, such as 'plain'.
Print a pandas DataFrame without the indexdataframe-to-table
from tabulate import tabulate
print(tabulate(df, headers='keys', tablefmt='psql', showindex=False))
# equivalent shortcut that calls tabulate internally
print(df.to_markdown(index=False))showindex defaults to 'default', which means DataFrames show their index and lists of lists do not; pass showindex=False to suppress it or an iterable to supply your own row labels.
Discover the available table formatslist-formats
from tabulate import tabulate_formats
print(tabulate_formats)
# ['asciidoc', 'colon_grid', 'double_grid', 'fancy_grid', 'github', 'grid', ...]tabulate_formats is part of the module's __all__ alongside tabulate and simple_separated_format, so it is safe to rely on. 'colon_grid' is new in 0.10.0.
Format data from the shellcli-usage
tabulate -1 -f github data.csv -r csv
cat events.jsonl | tabulate -r jsonl --headers keys -f psql
ps aux | tabulate -1 -f simpleThe default input parser is whitespace-separated, so pass -r csv or -r jsonl explicitly. Anything that streams gets fully buffered first, since tabulate needs every row before it can compute widths.
Align tables containing CJK text or color codeswide-chars-and-ansi
# pip install 'tabulate[widechars]'
import tabulate as T
from tabulate import tabulate
print(tabulate([['\u6771\u4eac', 37], ['Osaka', 19]], headers=['City', 'Pop']))
T.WIDE_CHARS_MODE = False # opt out without uninstalling wcwidthColumn widths already ignore ANSI escape sequences when measuring, so colored cells align correctly and keep their styling; wide-character handling is the part that needs wcwidth, and it turns itself on the moment that package is importable.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| rich | PyPI | Your CLI needs color, box drawing, live updates, and progress bars, not just a formatted string |
| prettytable | PyPI | You want a mutable table object you add rows to over time and sort or filter before printing |
| texttable | PyPI | You want explicit per-column width and type control rather than tabulate's automatic type deduction |