97

I'm looking for a way to pretty-print tables like this:

=======================
| column 1 | column 2 |
=======================
| value1   | value2   |
| value3   | value4   |
=======================

I've found the asciitable library but it doesn't do the borders, etc. I don't need any complex formatting of data items, they're just strings. I do need it to auto-size columns.

Do other libraries or methods exist, or do I need to spend a few minutes writing my own?

TrebledJ
  • 8,713
  • 7
  • 26
  • 48
kdt
  • 27,905
  • 33
  • 92
  • 139
  • Why not use docutils to do this for you? – S.Lott May 06 '11 at 10:19
  • What do you call a table ? How is the data organised in a table ? Are value1, value2, value3, value4... the successive values in a list ? I think that fomat() is enough to obtain such a simple display, no need to learn during a long time a tutorial that explains how to gain time by using a library – eyquem May 06 '11 at 10:40
  • 2
    @korona: No, I wasn't making a suggestion. I was asking a question. I have no clue what @kdt knows or doesn't know. Rather than assume, I feel compelled to ask. – S.Lott May 06 '11 at 11:27
  • 6
    Sounded to me like you were in fact assuming that he knows about docutils. Maybe he doesn't? – korona May 09 '11 at 13:28
  • 2
    @S.Lott I've looked at docutils, and while it is of course great for converting text to html, latex, etc, I don't see a way to *generate* nice text tables, with columns that line up and look pretty with fixed-width fonts. Did you misunderstand kdt's goal, or am I missing something? – nealmcb Oct 30 '12 at 14:02
  • Possible duplicate of [Printing tabular data in Python](http://stackoverflow.com/questions/5122347/printing-tabular-data-in-python) – Martin Thoma Jul 24 '16 at 09:40

13 Answers13

97

I've read this question long time ago, and finished writing my own pretty-printer for tables: tabulate.

My use case is:

  • I want a one-liner most of the time
  • which is smart enough to figure the best formatting for me
  • and can output different plain-text formats

Given your example, grid is probably the most similar output format:

from tabulate import tabulate
print tabulate([["value1", "value2"], ["value3", "value4"]], ["column 1", "column 2"], tablefmt="grid")
+------------+------------+
| column 1   | column 2   |
+============+============+
| value1     | value2     |
+------------+------------+
| value3     | value4     |
+------------+------------+

Other supported formats are plain (no lines), simple (Pandoc simple tables), pipe (like tables in PHP Markdown Extra), orgtbl (like tables in Emacs' org-mode), rst (like simple tables in reStructuredText). grid and orgtbl are easily editable in Emacs.

Performance-wise, tabulate is slightly slower than asciitable, but much faster than PrettyTable and texttable.

P.S. I'm also a big fan of aligning numbers by a decimal column. So this is the default alignment for numbers if there are any (overridable).

Community
  • 1
  • 1
sastanin
  • 40,473
  • 13
  • 103
  • 130
  • 4
    I just happened to need a tabulating solution and was fortunate enough to find your library! Works like a charm :D In case you're listening, just wanted to say **Thank You** :) – deepak Oct 06 '16 at 05:02
  • 2
    Yes, I'm listening. Thank you for the kind words. It's really nice to get positive feedback. – sastanin Oct 17 '16 at 09:54
  • 1
    Hi, @sastanin First of all, thank you very much for such a nice library. May I know is there any option to print table to span whole width of terminal? – Validus Oculus Oct 30 '16 at 06:40
  • 1
    Hello sastanin, just wanted to drop a word here to thank you for this very handy package. Works like a charm and saved me the trouble of writing my own. Thanks a lot for sharing ! – Valentin B. Aug 30 '18 at 13:47
  • 1
    Your feature list is an understatement. Tried ansi escaped stuff, works perfect. Thanks for this! – Red Pill Oct 05 '20 at 11:42
  • [`pandas` 1.0.0 or higher can print a dataframe in markdown format directly](https://pandas.pydata.org/docs/whatsnew/v1.0.0.html#converting-to-markdown) with the help of `tabulate`. It's handy when data can be managed in `pandas` dataframe and get printed directly. – Edward May 22 '21 at 10:59
40

Here's a quick and dirty little function I wrote for displaying the results from SQL queries I can only make over a SOAP API. It expects an input of a sequence of one or more namedtuples as table rows. If there's only one record, it prints it out differently.

It is handy for me and could be a starting point for you:

def pprinttable(rows):
  if len(rows) > 1:
    headers = rows[0]._fields
    lens = []
    for i in range(len(rows[0])):
      lens.append(len(max([x[i] for x in rows] + [headers[i]],key=lambda x:len(str(x)))))
    formats = []
    hformats = []
    for i in range(len(rows[0])):
      if isinstance(rows[0][i], int):
        formats.append("%%%dd" % lens[i])
      else:
        formats.append("%%-%ds" % lens[i])
      hformats.append("%%-%ds" % lens[i])
    pattern = " | ".join(formats)
    hpattern = " | ".join(hformats)
    separator = "-+-".join(['-' * n for n in lens])
    print hpattern % tuple(headers)
    print separator
    _u = lambda t: t.decode('UTF-8', 'replace') if isinstance(t, str) else t
    for line in rows:
        print pattern % tuple(_u(t) for t in line)
  elif len(rows) == 1:
    row = rows[0]
    hwidth = len(max(row._fields,key=lambda x: len(x)))
    for i in range(len(row)):
      print "%*s = %s" % (hwidth,row._fields[i],row[i])

Sample output:

pkid                                 | fkn                                  | npi
-------------------------------------+--------------------------------------+----
405fd665-0a2f-4f69-7320-be01201752ec | 8c9949b9-552e-e448-64e2-74292834c73e | 0
5b517507-2a42-ad2e-98dc-8c9ac6152afa | f972bee7-f5a4-8532-c4e5-2e82897b10f6 | 0
2f960dfc-b67a-26be-d1b3-9b105535e0a8 | ec3e1058-8840-c9f2-3b25-2488f8b3a8af | 1
c71b28a3-5299-7f4d-f27a-7ad8aeadafe0 | 72d25703-4735-310b-2e06-ff76af1e45ed | 0
3b0a5021-a52b-9ba0-1439-d5aafcf348e7 | d81bb78a-d984-e957-034d-87434acb4e97 | 1
96c36bb7-c4f4-2787-ada8-4aadc17d1123 | c171fe85-33e2-6481-0791-2922267e8777 | 1
95d0f85f-71da-bb9a-2d80-fe27f7c02fe2 | 226f964c-028d-d6de-bf6c-688d2908c5ae | 1
132aa774-42e5-3d3f-498b-50b44a89d401 | 44e31f89-d089-8afc-f4b1-ada051c01474 | 1
ff91641a-5802-be02-bece-79bca993fdbc | 33d8294a-053d-6ab4-94d4-890b47fcf70d | 1
f3196e15-5b61-e92d-e717-f00ed93fe8ae | 62fa4566-5ca2-4a36-f872-4d00f7abadcf | 1

Example

>>> from collections import namedtuple
>>> Row = namedtuple('Row',['first','second','third'])
>>> data = Row(1,2,3)
>>> data
Row(first=1, second=2, third=3)
>>> pprinttable([data])
 first = 1
second = 2
 third = 3
>>> pprinttable([data,data])
first | second | third
------+--------+------
    1 |      2 |     3
    1 |      2 |     3
arulraj.net
  • 4,579
  • 3
  • 36
  • 37
MattH
  • 37,273
  • 11
  • 82
  • 84
  • @MattH can you show the usage of this function with an example? – theAlse Sep 12 '12 at 14:48
  • 1
    @MattH thanks, but big number seems to crash it right away. TypeError: object of type 'int' has no len(). – theAlse Sep 12 '12 at 18:06
  • @Alborz: I posted this as a starting point for others, customise it to deal with your data types if you want to. Though depending on which line that error came from, you might not be calling the function as intended – MattH Sep 12 '12 at 19:00
  • 1
    @theAlse I fixed the bug you identified, by making it `len(str(max(...)))` in the lens.append line. So now if a number in a column is wider than the column header, we're still good. BTW, MattH - cute use of the "key" argument to max()! – nealmcb Oct 30 '12 at 14:40
20

For some reason when I included 'docutils' in my google searches I stumbled across texttable, which seems to be what I'm looking for.

ewwink
  • 18,382
  • 2
  • 44
  • 54
kdt
  • 27,905
  • 33
  • 92
  • 139
  • 2
    Nice one. Lacks automatic column width detection; use: http://pastebin.com/SAsPJUxM – Kos Nov 02 '12 at 11:57
14

I too wrote my own solution to this. I tried to keep it simple.

https://github.com/Robpol86/terminaltables

from terminaltables import AsciiTable
table_data = [
    ['Heading1', 'Heading2'],
    ['row1 column1', 'row1 column2'],
    ['row2 column1', 'row2 column2']
]
table = AsciiTable(table_data)
print table.table
+--------------+--------------+
| Heading1     | Heading2     |
+--------------+--------------+
| row1 column1 | row1 column2 |
| row2 column1 | row2 column2 |
+--------------+--------------+

table.inner_heading_row_border = False
print table.table
+--------------+--------------+
| Heading1     | Heading2     |
| row1 column1 | row1 column2 |
| row2 column1 | row2 column2 |
+--------------+--------------+

table.inner_row_border = True
table.justify_columns[1] = 'right'
table.table_data[1][1] += '\nnewline'
print table.table
+--------------+--------------+
| Heading1     |     Heading2 |
+--------------+--------------+
| row1 column1 | row1 column2 |
|              |      newline |
+--------------+--------------+
| row2 column1 | row2 column2 |
+--------------+--------------+
Robpol86
  • 1,594
  • 21
  • 18
13

I just released termtables for this purpose. For example, this

import termtables as tt

tt.print(
    [[1, 2, 3], [613.23236243236, 613.23236243236, 613.23236243236]],
    header=["a", "bb", "ccc"],
    style=tt.styles.ascii_thin_double,
    padding=(0, 1),
    alignment="lcr"
)

gets you

+-----------------+-----------------+-----------------+
| a               |       bb        |             ccc |
+=================+=================+=================+
| 1               |        2        |               3 |
+-----------------+-----------------+-----------------+
| 613.23236243236 | 613.23236243236 | 613.23236243236 |
+-----------------+-----------------+-----------------+

By default, the table is rendered with Unicode box-drawing characters,

┌─────────────────┬─────────────────┬─────────────────┐
│ a               │       bb        │             ccc │
╞═════════════════╪═════════════════╪═════════════════╡
│ 1               │        2        │               3 │
├─────────────────┼─────────────────┼─────────────────┤
│ 613.23236243236 │ 613.23236243236 │ 613.23236243236 │
└─────────────────┴─────────────────┴─────────────────┘

termtables are very configurable; check out the tests for more examples.

Nico Schlömer
  • 53,797
  • 27
  • 201
  • 249
8

If you want a table with column and row spans, then try my library dashtable

from dashtable import data2rst

table = [
        ["Header 1", "Header 2", "Header3", "Header 4"],
        ["row 1", "column 2", "column 3", "column 4"],
        ["row 2", "Cells span columns.", "", ""],
        ["row 3", "Cells\nspan rows.", "- Cells\n- contain\n- blocks", ""],
        ["row 4", "", "", ""]
    ]

# [Row, Column] pairs of merged cells
span0 = ([2, 1], [2, 2], [2, 3])
span1 = ([3, 1], [4, 1])
span2 = ([3, 3], [3, 2], [4, 2], [4, 3])

my_spans = [span0, span1, span2]

print(data2rst(table, spans=my_spans, use_headers=True))

Which outputs:

+----------+------------+----------+----------+
| Header 1 | Header 2   | Header3  | Header 4 |
+==========+============+==========+==========+
| row 1    | column 2   | column 3 | column 4 |
+----------+------------+----------+----------+
| row 2    | Cells span columns.              |
+----------+----------------------------------+
| row 3    | Cells      | - Cells             |
+----------+ span rows. | - contain           |
| row 4    |            | - blocks            |
+----------+------------+---------------------+
dmodo
  • 221
  • 3
  • 15
7

Version using w3m designed to handle the types MattH's version accepts:

import subprocess
import tempfile
import html
def pprinttable(rows):
    esc = lambda x: html.escape(str(x))
    sour = "<table border=1>"
    if len(rows) == 1:
        for i in range(len(rows[0]._fields)):
            sour += "<tr><th>%s<td>%s" % (esc(rows[0]._fields[i]), esc(rows[0][i]))
    else:
        sour += "<tr>" + "".join(["<th>%s" % esc(x) for x in rows[0]._fields])
        sour += "".join(["<tr>%s" % "".join(["<td>%s" % esc(y) for y in x]) for x in rows])
    with tempfile.NamedTemporaryFile(suffix=".html") as f:
        f.write(sour.encode("utf-8"))
        f.flush()
        print(
            subprocess
            .Popen(["w3m","-dump",f.name], stdout=subprocess.PIPE)
            .communicate()[0].decode("utf-8").strip()
        )

from collections import namedtuple
Row = namedtuple('Row',['first','second','third'])
data1 = Row(1,2,3)
data2 = Row(4,5,6)
pprinttable([data1])
pprinttable([data1,data2])

results in:

┌───────┬─┐
│ first │1│
├───────┼─┤
│second │2│
├───────┼─┤
│ third │3│
└───────┴─┘
┌─────┬───────┬─────┐
│first│second │third│
├─────┼───────┼─────┤
│1    │2      │3    │
├─────┼───────┼─────┤
│4    │5      │6    │
└─────┴───────┴─────┘
Janus Troelsen
  • 20,267
  • 14
  • 135
  • 196
7

You can try BeautifulTable. It does what you want to do. Here's an example from it's documentation

>>> from beautifultable import BeautifulTable
>>> table = BeautifulTable()
>>> table.columns.header = ["name", "rank", "gender"]
>>> table.rows.append(["Jacob", 1, "boy"])
>>> table.rows.append(["Isabella", 1, "girl"])
>>> table.rows.append(["Ethan", 2, "boy"])
>>> table.rows.append(["Sophia", 2, "girl"])
>>> table.rows.append(["Michael", 3, "boy"])
>>> print(table)
+----------+------+--------+
|   name   | rank | gender |
+----------+------+--------+
|  Jacob   |  1   |  boy   |
+----------+------+--------+
| Isabella |  1   |  girl  |
+----------+------+--------+
|  Ethan   |  2   |  boy   |
+----------+------+--------+
|  Sophia  |  2   |  girl  |
+----------+------+--------+
| Michael  |  3   |  boy   |
+----------+------+--------+
Priyam Singh
  • 816
  • 2
  • 11
  • 10
  • ```/usr/local/lib/python3.8/site-packages/beautifultable/utils.py:113: FutureWarning: 'BeautifulTable.column_headers' has been deprecated in 'v1.0.0' and will be removed in 'v1.2.0'. Use 'BTColumnCollection.header' instead. warnings.warn(message, FutureWarning)``` – evandrix Dec 13 '20 at 03:20
3
from sys import stderr, stdout    
def create_table(table: dict, full_row: bool = False) -> None:

        min_len = len(min((v for v in table.values()), key=lambda q: len(q)))
        max_len = len(max((v for v in table.values()), key=lambda q: len(q)))

        if min_len < max_len:
            stderr.write("Table is out of shape, please make sure all columns have the same length.")
            stderr.flush()
            return

        additional_spacing = 1

        heading_separator = '| '
        horizontal_split = '| '

        rc_separator = ''
        key_list = list(table.keys())
        rc_len_values = []
        for key in key_list:
            rc_len = len(max((v for v in table[key]), key=lambda q: len(str(q))))
            rc_len_values += ([rc_len, [key]] for n in range(len(table[key])))

            heading_line = (key + (" " * (rc_len + (additional_spacing + 1)))) + heading_separator
            stdout.write(heading_line)

            rc_separator += ("-" * (len(key) + (rc_len + (additional_spacing + 1)))) + '+-'

            if key is key_list[-1]:
                stdout.flush()
                stdout.write('\n' + rc_separator + '\n')

        value_list = [v for vl in table.values() for v in vl]

        aligned_data_offset = max_len

        row_count = len(key_list)

        next_idx = 0
        newline_indicator = 0
        iterations = 0

        for n in range(len(value_list)):
            key = rc_len_values[next_idx][1][0]
            rc_len = rc_len_values[next_idx][0]

            line = ('{:{}} ' + " " * len(key)).format(value_list[next_idx], str(rc_len + additional_spacing)) + horizontal_split

            if next_idx >= (len(value_list) - aligned_data_offset):
                next_idx = iterations + 1
                iterations += 1
            else:
                next_idx += aligned_data_offset

            if newline_indicator >= row_count:
                if full_row:
                    stdout.flush()
                    stdout.write('\n' + rc_separator + '\n')
                else:
                    stdout.flush()
                    stdout.write('\n')

                newline_indicator = 0

            stdout.write(line)
            newline_indicator += 1

        stdout.write('\n' + rc_separator + '\n')
        stdout.flush()

Example:

table = {
        "uid": ["0", "1", "2", "3"],
        "name": ["Jon", "Doe", "Lemma", "Hemma"]
    }

create_table(table)

Output:

uid   | name       | 
------+------------+-
0     | Jon        | 
1     | Doe        | 
2     | Lemma      | 
3     | Hemma      | 
------+------------+-
Lepstr
  • 31
  • 1
2

I know it the question is a bit old but here's my attempt at this:

https://gist.github.com/lonetwin/4721748

It is a bit more readable IMHO (although it doesn't differentiate between single / multiple rows like @MattH's solutions does, nor does it use NamedTuples).

lonetwin
  • 971
  • 10
  • 17
2

Here's my solution:

def make_table(columns, data):
    """Create an ASCII table and return it as a string.

    Pass a list of strings to use as columns in the table and a list of
    dicts. The strings in 'columns' will be used as the keys to the dicts in
    'data.'

    Not all column values have to be present in each data dict.

    >>> print(make_table(["a", "b"], [{"a": "1", "b": "test"}]))
    | a | b    |
    |----------|
    | 1 | test |
    """
    # Calculate how wide each cell needs to be
    cell_widths = {}
    for c in columns:
        values = [str(d.get(c, "")) for d in data]
        cell_widths[c] = len(max(values + [c]))

    # Used for formatting rows of data
    row_template = "|" + " {} |" * len(columns)

    # CONSTRUCT THE TABLE

    # The top row with the column titles
    justified_column_heads = [c.ljust(cell_widths[c]) for c in columns]
    header = row_template.format(*justified_column_heads)
    # The second row contains separators
    sep = "|" + "-" * (len(header) - 2) + "|"
    # Rows of data
    rows = []
    for d in data:
        fields = [str(d.get(c, "")).ljust(cell_widths[c]) for c in columns]
        row = row_template.format(*fields)
        rows.append(row)

    return "\n".join([header, sep] + rows)
Luke Taylor
  • 8,631
  • 8
  • 54
  • 92
2

I use this small utility function.

def get_pretty_table(iterable, header):
    max_len = [len(x) for x in header]
    for row in iterable:
        row = [row] if type(row) not in (list, tuple) else row
        for index, col in enumerate(row):
            if max_len[index] < len(str(col)):
                max_len[index] = len(str(col))
    output = '-' * (sum(max_len) + 1) + '\n'
    output += '|' + ''.join([h + ' ' * (l - len(h)) + '|' for h, l in zip(header, max_len)]) + '\n'
    output += '-' * (sum(max_len) + 1) + '\n'
    for row in iterable:
        row = [row] if type(row) not in (list, tuple) else row
        output += '|' + ''.join([str(c) + ' ' * (l - len(str(c))) + '|' for c, l in zip(row, max_len)]) + '\n'
    output += '-' * (sum(max_len) + 1) + '\n'
    return output

print get_pretty_table([[1, 2], [3, 4]], ['header 1', 'header 2'])

output

-----------------
|header 1|header 2|
-----------------
|1       |2       |
|3       |4       |
-----------------
thavan
  • 2,409
  • 24
  • 32
  • 1
    You're adding a space between each column in `output += '|' + ''.join([h + ' ' * (l - len(h)) + '|' for h, l in zip(header, max_len)]) + '\n'` but not in the separator lines. Can extend that row of `-`s with something as simple as `output = '-' * (sum(max_len) + 1 + len(header)) + '\n'` – ochawkeye Nov 08 '16 at 17:27
  • Should be called `get_shitty_table()` – MathCrackExchange Jul 09 '23 at 00:55
-1

This can be done with only builtin modules fairly compactly using list and string comprehensions. Accepts a list of dictionaries all of the same format...

def tableit(dictlist):
    lengths = [ max(map(lambda x:len(x.get(k)), dictlist) + [len(k)]) for k in dictlist[0].keys() ]
    lenstr = " | ".join("{:<%s}" % m for m in lengths)
    lenstr += "\n"

    outmsg = lenstr.format(*dictlist[0].keys())
    outmsg += "-" * (sum(lengths) + 3*len(lengths))
    outmsg += "\n"
    outmsg += "".join(
        lenstr.format(*v) for v in [ item.values() for item in dictlist ]
    )
    return outmsg
MattK
  • 1,431
  • 13
  • 14
  • Please submit a example how to use your code. `tableit([dict(a='1', b='2', c='3'), dict(a='x', b='y', c='z')])` – iuridiniz Jan 11 '21 at 20:34
  • You code does not work without changes in python 3, line 2 must be: `lengths = [ max(list(map(lambda x:len(x.get(k)), dictlist)) + [len(k)]) for k in dictlist[0].keys() ]` – iuridiniz Jan 11 '21 at 20:34