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?

6

13 Answers

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).

6

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 
4

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

1

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

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 | +--------------+--------------+ 
1

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.

1

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 | +----------+------------+---------------------+ 
1

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 | +----------+------+--------+ 
2

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 │ └─────┴───────┴─────┘ 

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

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).

0

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 | ----------------- 
1
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 | ------+------------+- 
1

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) 

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 
2