Beginner

When you print a list of dictionaries or a set of results in a Python script, you get a wall of text that is hard to read. You can spend time manually formatting columns with str.ljust() and calculating widths, or you can use PrettyTable and have a properly aligned table in three lines. It is one of those small libraries that makes CLI tools and scripts significantly more professional-looking with almost no effort.

PrettyTable is a Python library for printing formatted ASCII tables to the terminal. It handles column alignment, sorting, padding, and borders automatically. You just add your headers and rows. Beyond basic display, it can export tables to CSV, JSON, and HTML — so the same table you print in the terminal can become a web page element without any reformatting.

This article covers installation, building tables by rows and columns, sorting, selecting specific columns, border styles, and exporting to other formats. It ends with a real-world example — a system report script that formats multiple metrics into a clean terminal display. By the end you will have a reusable pattern for any script that needs to display structured data cleanly.

Printing a Python Table with PrettyTable: Quick Example

Here is the minimal path from data to formatted table:

# quick_prettytable.py
from prettytable import PrettyTable

table = PrettyTable()
table.field_names = ["Name", "Language", "Stars"]

table.add_row(["Flask", "Python", 67000])
table.add_row(["Django", "Python", 78000])
table.add_row(["FastAPI", "Python", 74000])
table.add_row(["Express", "JavaScript", 62000])

print(table)

Output:

+---------+------------+-------+
| Name    | Language   | Stars |
+---------+------------+-------+
| Flask   | Python     | 67000 |
| Django  | Python     | 78000 |
| FastAPI | Python     | 74000 |
| Express | JavaScript | 62000 |
+---------+------------+-------+

PrettyTable calculates column widths automatically, aligns text left and numbers right, and draws the border. No manual padding, no f-string gymnastics. The sections below show sorting, filtering columns, styling borders, and exporting to HTML and JSON.

What Is PrettyTable and When Should You Use It?

PrettyTable is a pure-Python library for generating formatted plain-text tables. It is designed for command-line tools, scripts, and any situation where you want to display tabular data without the weight of a full terminal UI framework. The tables it produces are plain ASCII, making them compatible with any terminal, log file, or text-based report.

LibraryOutputSortingExportBest For
print() + f-stringsManual alignmentManualManualSimple one-off output
PrettyTableASCII table, auto-alignBuilt-inCSV, HTML, JSONCLI tools, scripts, reports
tabulateMany formats (grid, pipe, markdown)NoLimitedMarkdown docs, pandas output
rich TableColor, styled, unicodeNoNoColorful terminal apps

PrettyTable is the right choice when you want a simple, dependency-light table with sorting and export capabilities. Use the rich library instead when you need color and visual styling. Use tabulate when you need Markdown or pipe-delimited output for documentation.

Installing PrettyTable

# terminal
pip install prettytable

Output:

Successfully installed prettytable-3.10.0 wcwidth-0.2.13

PrettyTable has no heavy dependencies. It installs in seconds and works with Python 3.7 and above. Import it with from prettytable import PrettyTable.

Building Tables Row by Row and Column by Column

You can build a PrettyTable either row by row (the most common approach) or column by column. Column-by-column is useful when your data is already organized in lists per field:

# build_methods.py
from prettytable import PrettyTable

# Method 1: Add rows one at a time
table1 = PrettyTable(["City", "Country", "Population"])
table1.add_row(["Tokyo", "Japan", 13960000])
table1.add_row(["Delhi", "India", 32940000])
table1.add_row(["Shanghai", "China", 28516000])
table1.add_row(["Sao Paulo", "Brazil", 22430000])
print("Row method:")
print(table1)

print()

# Method 2: Add all rows at once with add_rows()
table2 = PrettyTable(["Product", "Price", "Stock"])
table2.add_rows([
    ["Widget A", 9.99, 150],
    ["Widget B", 24.99, 45],
    ["Widget C", 4.49, 500],
])
print("add_rows() method:")
print(table2)

Output:

Row method:
+-----------+---------+------------+
| City      | Country | Population |
+-----------+---------+------------+
| Tokyo     | Japan   |   13960000 |
| Delhi     | India   |   32940000 |
| Shanghai  | China   |   28516000 |
| Sao Paulo | Brazil  |   22430000 |
+-----------+---------+------------+

add_rows() method:
+----------+-------+-------+
| Product  | Price | Stock |
+----------+-------+-------+
| Widget A |  9.99 |   150 |
| Widget B | 24.99 |    45 |
| Widget C |  4.49 |   500 |
+----------+-------+-------+

Notice that PrettyTable automatically right-aligns numeric columns and left-aligns text columns. The Population and Price columns are right-aligned without any configuration. You can override alignment per column with table.align["City"] = "r" if needed.

Sorting Tables

PrettyTable sorts on any column by name. You can also reverse the order and sort at print time or at data-entry time:

# sorting_tables.py
from prettytable import PrettyTable

table = PrettyTable(["Package", "Version", "Downloads/Month"])
table.add_rows([
    ["requests",   "2.31",  "450000000"],
    ["numpy",      "1.26",  "180000000"],
    ["pandas",     "2.1",   "150000000"],
    ["boto3",      "1.34",  "120000000"],
    ["setuptools", "69.0",  "380000000"],
])

# Sort by downloads descending
table.sortby = "Downloads/Month"
table.reversesort = True
print("Sorted by downloads (desc):")
print(table)
print()

# Sort at print time with get_string()
print("Sorted by Package name:")
print(table.get_string(sortby="Package"))

Output:

Sorted by downloads (desc):
+------------+---------+-----------------+
| Package    | Version | Downloads/Month |
+------------+---------+-----------------+
| requests   | 2.31    |     450000000   |
| setuptools | 69.0    |     380000000   |
| numpy      | 1.26    |     180000000   |
| pandas     | 2.1     |     150000000   |
| boto3      | 1.34    |     120000000   |
+------------+---------+-----------------+

Sorted by Package name:
+------------+---------+-----------------+
| Package    | Version | Downloads/Month |
+------------+---------+-----------------+
| boto3      | 1.34    |     120000000   |
| numpy      | 1.26    |     180000000   |
| pandas     | 2.1     |     150000000   |
| requests   | 2.31    |     450000000   |
| setuptools | 69.0    |     380000000   |
+------------+---------+-----------------+

Setting table.sortby makes the sort persistent — subsequent print(table) calls will always use that sort. Using get_string(sortby=...) applies a one-time sort without changing the table’s default. This is useful when you want different views of the same data without modifying the table object.

Selecting Specific Columns and Rows

The get_string() method accepts fields to show only selected columns, and start/end to paginate rows:

# filter_display.py
from prettytable import PrettyTable

table = PrettyTable(["ID", "Name", "Department", "Salary", "Start Date"])
table.add_rows([
    [1, "Alice Chen",    "Engineering", 95000, "2021-03-15"],
    [2, "Bob Torres",    "Marketing",   72000, "2020-07-01"],
    [3, "Carol White",   "Engineering", 88000, "2022-01-10"],
    [4, "David Kim",     "HR",          65000, "2019-11-20"],
    [5, "Eva Martinez",  "Engineering", 102000, "2023-06-01"],
])

# Show only selected columns
print("Engineering salary view:")
print(table.get_string(fields=["Name", "Department", "Salary"],
                       sortby="Salary", reversesort=True))
print()

# Paginate: show rows 1-3 only
print("First 3 rows only:")
print(table.get_string(start=0, end=3))

Output:

Engineering salary view:
+--------------+-------------+--------+
| Name         | Department  | Salary |
+--------------+-------------+--------+
| Eva Martinez | Engineering | 102000 |
| Alice Chen   | Engineering |  95000 |
| Carol White  | Engineering |  88000 |
| Bob Torres   | Marketing   |  72000 |
| David Kim    | HR          |  65000 |
+--------------+-------------+--------+

First 3 rows only:
+----+-------------+-------------+--------+------------+
| ID | Name        | Department  | Salary | Start Date |
+----+-------------+-------------+--------+------------+
|  1 | Alice Chen  | Engineering |  95000 | 2021-03-15 |
|  2 | Bob Torres  | Marketing   |  72000 | 2020-07-01 |
|  3 | Carol White | Engineering |  88000 | 2022-01-10 |
+----+-------------+-------------+--------+------------+

Border Styles

PrettyTable includes several built-in junction characters for different visual styles. You can also disable the border entirely for tab-separated output:

# border_styles.py
from prettytable import PrettyTable, SINGLE_BORDER, DOUBLE_BORDER, MARKDOWN

table = PrettyTable(["Name", "Score"])
table.add_rows([["Alice", 95], ["Bob", 87], ["Carol", 92]])

print("Default (classic ASCII):")
print(table)

print("\nSingle line border:")
table.set_style(SINGLE_BORDER)
print(table)

print("\nDouble line border:")
table.set_style(DOUBLE_BORDER)
print(table)

print("\nMarkdown format:")
table.set_style(MARKDOWN)
print(table)

Output:

Default (classic ASCII):
+-------+-------+
| Name  | Score |
+-------+-------+
| Alice |    95 |
| Bob   |    87 |
| Carol |    92 |
+-------+-------+

Single line border:
+-------+-------+
| Name  | Score |
+-------+-------+
...

Markdown format:
| Name  | Score |
| ----- | ----- |
| Alice |    95 |
| Bob   |    87 |
| Carol |    92 |

The MARKDOWN style is particularly useful when generating documentation or writing to a file that will be rendered as Markdown. SINGLE_BORDER and DOUBLE_BORDER use Unicode box-drawing characters for a more polished look in terminals that support them.

Exporting Tables to HTML, JSON, and CSV

PrettyTable can render the same data as HTML, JSON, or CSV with no extra code:

# export_formats.py
from prettytable import PrettyTable

table = PrettyTable(["Country", "Capital", "Population"])
table.add_rows([
    ["Germany", "Berlin",  3677000],
    ["France",  "Paris",   2148000],
    ["Italy",   "Rome",    2873000],
    ["Spain",   "Madrid",  3305000],
])

# Export to HTML
html_output = table.get_html_string()
print("HTML output (first 300 chars):")
print(html_output[:300])
print()

# Export to JSON
json_output = table.get_json_string()
print("JSON output (first 200 chars):")
print(json_output[:200])
print()

# Export to CSV
csv_output = table.get_csv_string()
print("CSV output:")
print(csv_output)

Output:

HTML output (first 300 chars):
<table>
    <thead>
        <tr>
            <th>Country</th>
            <th>Capital</th>
            <th>Population</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td>Germany</td>
...

JSON output (first 200 chars):
[{"Country": "Germany", "Capital": "Berlin", "Population": 3677000},
 {"Country": "France", "Capital": "Paris", "Population": 2148000},
 ...

CSV output:
Country,Capital,Population
Germany,Berlin,3677000
France,Paris,2148000
Italy,Rome,2873000
Spain,Madrid,3305000

All three export methods respect the current sort order and selected fields from sortby and field_names. This means you can build one table, display it in the terminal, write the HTML version to a report file, and save the CSV for further processing — all from the same object without any duplication.

Real-Life Example: System Resource Monitor Report

Here is a practical script that collects system metrics and formats them into a terminal report using PrettyTable:

# system_report.py
import os
import shutil
from prettytable import PrettyTable, SINGLE_BORDER

def get_disk_usage():
    """Return disk usage for common paths."""
    paths = ["/", "/tmp"] if os.name != "nt" else ["C:\\", "D:\\"]
    rows = []
    for path in paths:
        if os.path.exists(path):
            total, used, free = shutil.disk_usage(path)
            pct = (used / total) * 100
            rows.append([
                path,
                f"{total // (1024**3)} GB",
                f"{used // (1024**3)} GB",
                f"{free // (1024**3)} GB",
                f"{pct:.1f}%",
            ])
    return rows

def make_env_table(keys):
    """Show selected environment variables."""
    table = PrettyTable(["Variable", "Value"])
    table.set_style(SINGLE_BORDER)
    table.align["Variable"] = "l"
    table.align["Value"] = "l"
    table.max_width["Value"] = 40
    for key in keys:
        val = os.environ.get(key, "(not set)")
        table.add_row([key, val])
    return table

# Disk usage table
disk_table = PrettyTable(["Mount", "Total", "Used", "Free", "Usage %"])
disk_table.set_style(SINGLE_BORDER)
for row in get_disk_usage():
    disk_table.add_row(row)

# Environment variables table
env_table = make_env_table(["HOME", "PATH", "SHELL", "LANG", "USER"])

# Python info table
py_table = PrettyTable(["Setting", "Value"])
py_table.set_style(SINGLE_BORDER)
import sys
py_table.add_rows([
    ["Python version", sys.version.split()[0]],
    ["Platform",       sys.platform],
    ["Prefix",         sys.prefix[:40]],
    ["Executable",     sys.executable[:40]],
])

print("=== Disk Usage ===")
print(disk_table)
print("\n=== Environment Variables ===")
print(env_table)
print("\n=== Python Environment ===")
print(py_table)

Output:

=== Disk Usage ===
+-------+-------+------+------+---------+
| Mount | Total | Used | Free | Usage % |
+-------+-------+------+------+---------+
| /     | 500 GB| 120 GB| 380 GB| 24.0% |
| /tmp  | 500 GB| 120 GB| 380 GB| 24.0%  |
+-------+-------+------+------+---------+

=== Environment Variables ===
+----------+-------------------------------+
| Variable | Value                         |
+----------+-------------------------------+
| HOME     | /home/alice                   |
| PATH     | /usr/local/bin:/usr/bin:/bin  |
| SHELL    | /bin/bash                     |
...

=== Python Environment ===
+----------------+----------------------------+
| Setting        | Value                      |
+----------------+----------------------------+
| Python version | 3.12.1                     |
| Platform       | linux                      |
...

The max_width property on the Value column prevents long PATH entries from wrecking the table layout. table.align["Variable"] = "l" forces left alignment on a column that PrettyTable might otherwise center. You can extend this example by adding a network interfaces section, a process list, or writing the HTML version to a file for a daily system report.

Frequently Asked Questions

Does PrettyTable handle Unicode characters correctly?

Yes, PrettyTable uses the wcwidth library to calculate the display width of Unicode characters correctly, including CJK double-width characters. This means columns with Chinese, Japanese, or Korean text will align properly in the terminal. Make sure your terminal uses a font that supports the characters you are displaying.

Can I build a PrettyTable directly from a CSV file?

Yes. Use from prettytable import from_csv and pass an open file object: table = from_csv(open("data.csv")). There are similar from_json() and from_html_one() constructors for importing from those formats. This makes PrettyTable useful as a quick display layer for any data file without having to parse it manually.

How do I prevent long text from breaking the table layout?

Set table.max_width["ColumnName"] = N to cap the display width of a specific column at N characters. Content longer than N is truncated with an ellipsis. You can also set a global max width with table.max_width = 30. This is essential for columns that might contain long strings like file paths, URLs, or log messages.

Can I add color to PrettyTable output?

PrettyTable itself does not support ANSI color codes natively — it focuses on structure rather than styling. For colorful tables, use the rich library’s Table class instead, which has full color, bold, italic, and Unicode border support. You can also print PrettyTable output inside a rich.console.Console call without modification if you just want to wrap the ASCII output in color.

How does PrettyTable compare to printing a pandas DataFrame?

Pandas DataFrames print as formatted tables natively when you call print(df), but the output uses spaces for alignment without borders. PrettyTable gives you explicit borders, sorting, and export formats that pandas does not. For data analysis workflows already using pandas, tabulate is often a better companion since it directly accepts DataFrames. Use PrettyTable for scripts that are not already using pandas.

Conclusion

PrettyTable is the fastest path from raw data to a readable terminal table. The API is minimal — create a table, set field names, add rows, print. Sorting, column selection, and export to HTML, JSON, and CSV work out of the box. The border style options give you a professional look with one line change.

Try extending the system report example by adding a table of the top 10 running processes sorted by CPU usage, using the psutil library to get process data. Then export the full report as HTML and open it in a browser. Once you have PrettyTable in your CLI toolkit, your debugging scripts and data reports will never look the same.

See the official PrettyTable GitHub repository for the full API reference and changelog.