mrkeyoor.com_
Thu 06 Aug 10:54 UTC
PyPIUtilsupdated 06 Aug 2026

prettytable

prettytable turns rows of Python values into an ASCII table with plus signs and pipes, the kind you see in a MySQL shell. You make a PrettyTable, set field_names, push rows in, and print it. Beyond the default look it can emit Markdown, reStructuredText, Org mode, HTML, JSON, CSV, LaTeX, and MediaWiki from the same object, sort and filter rows at render time, wrap long cells to a width budget, and pull data in from a CSV file or a DB-API cursor. It is one class plus a handful of helper functions, with wcwidth as its only runtime dependency.

Verdict

Still the least ceremony way to print an aligned table from a script, and the multi-format output is genuinely useful when the same rows need to reach a terminal, a wiki, and a JSON consumer. If your project already pulls in rich, use rich.table and skip this one.

API stability4/5The core PrettyTable surface has been stable across the whole 3.x line and additions like paginate and row_filter are backwards compatible, but the project does keep deprecating older spellings such as the module level ALL and MARKDOWN constants, so long-lived scripts collect warnings.
Docs4/5The README is a full tutorial with worked examples and complete option tables for every table level and column level setting. There is no hosted API reference or changelog narrative beyond GitHub release notes, so behaviour questions sometimes end in the source.
Maintenance5/5Actively maintained under the hugovk umbrella with frequent releases (3.18.0 in June 2026), the repository pushed August 2026, only about 17 open issues against 33 open issues and PRs total, and classifiers already listing Python 3.14 and beyond.
Ecosystem4/5Around 13.2M downloads a week and a long tail of CLI tools that depend on it, but it is a leaf utility with no plugin ecosystem, and newer projects increasingly reach for rich instead.

Use it if

  • You are writing a CLI or an admin script that prints a small result set and you want columns to line up without hand-rolling str.ljust arithmetic
  • You need the same data in more than one shape: print an ASCII table for humans, then call get_json_string() or get_csv_string() for a machine, or set_style(TableStyle.MARKDOWN) to paste into a pull request
  • Your rows contain CJK or emoji characters and naive padding breaks alignment; the wcwidth dependency exists specifically to measure display width instead of character count
  • You want one dependency with no C extension and no transitive tree, which matters for scripts that run inside constrained or air-gapped environments
Skip it if

Setup reality

pip install prettytable and you are done: it pulls only wcwidth, needs no compiler, and imports in milliseconds. The friction is API archaeology rather than installation. Tutorials and Stack Overflow answers from the 0.7 and early 3.x era use module level constants such as prettytable.MSWORD_FRIENDLY, prettytable.ALL, and prettytable.FRAME; those now emit deprecation warnings and the current spellings are TableStyle.MSWORD_FRIENDLY, HRuleStyle.ALL, and VRuleStyle.FRAME. Older material also references table.printt(), removed long ago. Two more things surprise people: styling attributes such as border and align persist on the object across prints until you reset them, and calling get_string(sortby=...) silently discards any dividers you added with add_divider(), because sorting reorders the rows the dividers were attached to.

Patterns

Build a table row by row and print itbuild-a-basic-table

from prettytable import PrettyTable

table = PrettyTable(["Service", "Replicas", "CPU"])
table.add_row(["api", 4, 0.75])
table.add_row(["worker", 2, 1.5])
table.add_rows([
    ["cron", 1, 0.1],
    ["web", 6, 0.25],
])

print(table)

Field names can go in the constructor or via table.field_names. Every add_row must match the field count exactly or you get a ValueError, so build rows as lists, not zipped dicts.

Align columns and format numbers per columnalign-and-format-columns

from prettytable import PrettyTable

table = PrettyTable(["Region", "Users", "Revenue"])
table.add_row(["apac", 128394, 45210.5])
table.add_row(["emea", 9021, 1200.0])

table.align = "r"            # every column
table.align["Region"] = "l"  # one column
table.int_format["Users"] = ",d"
table.float_format["Revenue"] = ".2f"
table.none_format = "n/a"

print(table)

Columns are centred by default, which looks wrong for numbers. int_format and float_format are old-style percent format specs applied per column, and they only fire when the cell is actually an int or float, not a numeric string.

Sort and filter at render timesort-and-filter-rows

print(table.get_string(sortby="Revenue", reversesort=True))

# persistent instead of per-call
table.sortby = "Users"
table.sort_key = lambda row: (row[0] is None, row[0])

# drop rows without touching the data
print(table.get_string(row_filter=lambda row: row[1] > 10000))

# only some columns, only some rows
print(table.get_string(fields=["Region", "Users"], start=0, end=5))

sortby uses plain Python comparison, so a column holding both numbers and None raises TypeError; guard it with sort_key. Sorting also removes any dividers you added.

Switch to Markdown, RST, or a box-drawing styleapply-a-built-in-style

from prettytable import PrettyTable, TableStyle

table = PrettyTable(["Flag", "Meaning"])
table.add_row(["--dry-run", "print, do not apply"])

table.set_style(TableStyle.MARKDOWN)
print(table)

for style in (TableStyle.SINGLE_BORDER, TableStyle.RST, TableStyle.ORGMODE):
    table.set_style(style)
    print(table)

table.set_style(TableStyle.DEFAULT)  # undo

Use the TableStyle enum. The old module level names such as prettytable.MARKDOWN still work but emit a DeprecationWarning, and MSWORD_FRIENDLY, PLAIN_COLUMNS, DOUBLE_BORDER and RANDOM live on the enum too.

Emit JSON, CSV, HTML, or LaTeX from the same tableexport-other-formats

print(table.get_json_string())
print(table.get_csv_string())
print(table.get_html_string(attributes={"id": "report", "class": "zebra"}))
print(table.get_latex_string())
print(table.get_mediawiki_string())

# let the caller pick
def render(table, out_format="text"):
    return table.get_formatted_string(out_format)

get_formatted_string accepts text, html, json, csv, latex, and mediawiki and forwards extra keyword arguments to the underlying renderer. get_html_string(format=True) inlines CSS that mimics your border and alignment settings.

Fill a table from a CSV file or a DB-API cursorimport-from-csv-or-database

import sqlite3
from prettytable import from_csv, from_db_cursor, from_json

with open("export.csv") as fp:
    table = from_csv(fp)

conn = sqlite3.connect("app.db")
cur = conn.cursor()
cur.execute("SELECT id, email, created_at FROM users LIMIT 20")
table = from_db_cursor(cur)

round_trip = from_json(table.get_json_string())

from_db_cursor reads column names off cursor.description, so a driver that leaves description empty (some bulk or DDL statements) returns None instead of a table. from_csv takes an open file object, not a path.

Add a title and group rows into sectionsadd-title-and-dividers

from prettytable import PrettyTable

table = PrettyTable(["Stage", "Duration"])
table.title = "Build report\n2026-08-06"

table.add_row(["checkout", "3s"])
table.add_row(["install", "41s"], divider=True)
table.add_row(["test", "2m10s"])
table.add_divider()
table.add_row(["total", "2m54s"])

print(table)

Titles may span lines with \n and each line is centred separately. add_divider() attaches the rule to the row above it, and any sort wipes dividers out.

Wrap long cells inside a width budgetcontrol-table-width

from prettytable import PrettyTable

table = PrettyTable(["Path", "Error"])
table.add_row([
    "services/ingest/handlers/webhook.py",
    "ConnectionResetError while flushing the batch to the upstream queue",
])

table.max_table_width = 80
table.max_width["Error"] = 40
table.min_width["Path"] = 12
table.break_on_hyphens = False
table.valign = "t"

print(table)

Wrapping inserts real newlines inside cells, so the printed table is taller than the row count suggests; set valign to t or the wrapped text centres itself vertically and reads oddly.

Strip borders for grep-friendly outputborderless-output

from prettytable import PrettyTable, HRuleStyle, VRuleStyle

table = PrettyTable(["pod", "status"], border=False, header=True, padding_width=2)
table.add_row(["api-7f9", "Running"])
table.align = "l"
print(table)

# or keep the frame and drop only the inner rules
table.border = True
table.hrules = HRuleStyle.FRAME
table.vrules = VRuleStyle.NONE
print(table)

hrules and vrules take the HRuleStyle and VRuleStyle enums now; the bare ALL and NONE names imported from prettytable are deprecated. preserve_internal_border=True keeps column separators while border=False.

Print a coloured table with ColorTablecolored-table

from prettytable.colortable import ColorTable, Theme, Themes

table = ColorTable(["Check", "Result"], theme=Themes.OCEAN)
table.add_row(["lint", "pass"])
table.add_row(["tests", "fail"])
print(table)

custom = Theme(default_color="37", vertical_color="34", junction_color="36")
table.theme = custom
print(table)

ColorTable colours the border characters, not the cell contents. For coloured cells use custom_format with a colour library, and remember ANSI codes go into pipes and files unless you check sys.stdout.isatty() first.

Format cells with your own callablecustom-cell-formatting

from prettytable import PrettyTable

def human(field: str, value) -> str:
    if field == "Bytes" and isinstance(value, int):
        return f"{value / 1_048_576:.1f} MiB"
    if value is None:
        return "-"
    return str(value)

table = PrettyTable(["File", "Bytes"])
table.custom_format = human
table.add_row(["dump.sql", 734003200])
table.add_row(["empty.log", None])
print(table)

The callable receives (field_name, value) and must return a string. Set it as a dict keyed by column name to format only some columns; it runs after int_format and float_format, so do not set both on the same column.

Split a long table into pagespaginate-long-output

pages = table.paginate(page_length=20, line_break="\n\n--- more ---\n\n")
print(pages)

# add a running index column first if rows need numbering
table.add_autoindex("#")

paginate returns one string with the separator embedded, not a list of pages, and it repeats the header on each page. The default separator is a form feed, which looks like garbage in most terminals.

Alternatives

PackageRegistryPick it when
richPyPIYour CLI wants colour, styled cell content, progress bars, and terminal-aware widths, not just aligned text.
tabulatePyPIYou want a one-line function call over a list of lists or a DataFrame, with a wide menu of output formats and no object to configure.
texttablePyPIYou want a tiny dependency-free renderer and per-column type hints for number formatting.