mrkeyoor.com_
Thu 06 Aug 00:57 UTC
PyPICLI & Toolingupdated 05 Aug 2026

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.

Verdict

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.

API stability4/5tabulate() has grown keyword arguments for years without breaking existing calls, and 0.10.0 only added alignment options; the caveat is the 3.10 floor and the removal of module-level globals in favor of function arguments.
Docs4/5The README is a long doctested reference with a rendered example of every one of the thirty-plus formats, an honest performance section with numbers, and an ANSI-handling explanation; there is no docs site and finding one option among that wall of text takes searching.
Maintenance3/50.10.0 shipped March 2026 after a gap of more than three years since 0.9.0, the repo was last pushed March 2026, and 63 open issues sit on top of open PRs; the scope is small enough that slow is survivable.
Ecosystem5/5About 56M weekly downloads, and pandas DataFrame.to_markdown calls it directly, so it is a hard dependency of a large amount of data tooling whether or not people know they use it.

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
Skip it if

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 simple

The 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 wcwidth

Column 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

PackageRegistryPick it when
richPyPIYour CLI needs color, box drawing, live updates, and progress bars, not just a formatted string
prettytablePyPIYou want a mutable table object you add rows to over time and sort or filter before printing
texttablePyPIYou want explicit per-column width and type control rather than tabulate's automatic type deduction