I am trying to create a nice column list in python for use with commandline admin tools which I create.
Basicly, I want a list like:
[['a', 'b', 'c'], ['aaaaaaaaaa', 'b', 'c'], ['a', 'bbbbbbbbbb', 'c']]To turn into:
a b c
aaaaaaaaaa b c
a bbbbbbbbbb cUsing plain tabs wont do the trick here because I don't know the longest data in each row.
This is the same behavior as 'column -t' in Linux..
$ echo -e "a b c\naaaaaaaaaa b c\na bbbbbbbbbb c"
a b c
aaaaaaaaaa b c
a bbbbbbbbbb c
$ echo -e "a b c\naaaaaaaaaa b c\na bbbbbbbbbb c" | column -t
a b c
aaaaaaaaaa b c
a bbbbbbbbbb cI have looked around for various python libraries to do this but can't find anything useful.
122 Answers
Since Python 2.6+, you can use a format string in the following way to set the columns to a minimum of 20 characters and align text to right.
table_data = [ ['a', 'b', 'c'], ['aaaaaaaaaa', 'b', 'c'], ['a', 'bbbbbbbbbb', 'c']
]
for row in table_data: print("{: >20} {: >20} {: >20}".format(*row))Output:
a b c aaaaaaaaaa b c a bbbbbbbbbb c 8 data = [['a', 'b', 'c'], ['aaaaaaaaaa', 'b', 'c'], ['a', 'bbbbbbbbbb', 'c']]
col_width = max(len(word) for row in data for word in row) + 2 # padding
for row in data: print "".join(word.ljust(col_width) for word in row)
a b c
aaaaaaaaaa b c
a bbbbbbbbbb c What this does is calculate the longest data entry to determine the column width, then use .ljust() to add the necessary padding when printing out each column.
I came here with the same requirements but @lvc and @Preet's answers seems more inline with what column -t produces in that columns have different widths:
>>> rows = [ ['a', 'b', 'c', 'd']
... , ['aaaaaaaaaa', 'b', 'c', 'd']
... , ['a', 'bbbbbbbbbb', 'c', 'd']
... ]
...>>> widths = [max(map(len, col)) for col in zip(*rows)]
>>> for row in rows:
... print " ".join((val.ljust(width) for val, width in zip(row, widths)))
...
a b c d
aaaaaaaaaa b c d
a bbbbbbbbbb c d 5 This is a little late to the party, and a shameless plug for a package I wrote, but you can also check out the Columnar package.
It takes a list of lists of input and a list of headers and outputs a table-formatted string. This snippet creates a docker-esque table:
from columnar import columnar
headers = ['name', 'id', 'host', 'notes']
data = [ ['busybox', 'c3c37d5d-38d2-409f-8d02-600fd9d51239', 'linuxnode-1-292735', 'Test server.'], ['alpine-python', '6bb77855-0fda-45a9-b553-e19e1a795f1e', 'linuxnode-2-249253', 'The one that runs python.'], ['redis', 'afb648ba-ac97-4fb2-8953-9a5b5f39663e', 'linuxnode-3-3416918', 'For queues and stuff.'], ['app-server', 'b866cd0f-bf80-40c7-84e3-c40891ec68f9', 'linuxnode-4-295918', 'A popular destination.'], ['nginx', '76fea0f0-aa53-4911-b7e4-fae28c2e469b', 'linuxnode-5-292735', 'Traffic Cop'],
]
table = columnar(data, headers, no_borders=True)
print(table)Or you can get a little fancier with colors and borders.
To read more about the column-sizing algorithm and see the rest of the API you can check out the link above or see the Columnar GitHub Repo
0You have to do this with 2 passes:
- get the maximum width of each column.
- formatting the columns using our knowledge of max width from the first pass using
str.ljust()andstr.rjust()
Wow, only 17 answers. The zen of python says "There should be one-- and preferably only one --obvious way to do it."
So here is an 18th way to do it: The tabulate package supports a bunch of data types that it can display as tables, here is a simple example adapted from their docs:
from tabulate import tabulate
table = [["Sun",696000,1989100000], ["Earth",6371,5973.6], ["Moon",1737,73.5], ["Mars",3390,641.85]]
print(tabulate(table, headers=["Planet","R (km)", "mass (x 10^29 kg)"]))which outputs
Planet R (km) mass (x 10^29 kg)
-------- -------- -------------------
Sun 696000 1.9891e+09
Earth 6371 5973.6
Moon 1737 73.5
Mars 3390 641.85 1 Transposing the columns like that is a job for zip:
>>> a = [['a', 'b', 'c'], ['aaaaaaaaaa', 'b', 'c'], ['a', 'bbbbbbbbbb', 'c']]
>>> list(zip(*a))
[('a', 'aaaaaaaaaa', 'a'), ('b', 'b', 'bbbbbbbbbb'), ('c', 'c', 'c')]To find the required length of each column, you can use max:
>>> trans_a = zip(*a)
>>> [max(len(c) for c in b) for b in trans_a]
[10, 10, 1]Which you can use, with suitable padding, to construct strings to pass to print:
>>> col_lenghts = [max(len(c) for c in b) for b in trans_a]
>>> padding = ' ' # You might want more
>>> padding.join(s.ljust(l) for s,l in zip(a[0], col_lenghts))
'a b c' To get fancier tables like
---------------------------------------------------
| First Name | Last Name | Age | Position |
---------------------------------------------------
| John | Smith | 24 | Software |
| | | | Engineer |
---------------------------------------------------
| Mary | Brohowski | 23 | Sales |
| | | | Manager |
---------------------------------------------------
| Aristidis | Papageorgopoulos | 28 | Senior |
| | | | Reseacher |
---------------------------------------------------you can use this Python recipe:
'''
From
PSF License
'''
import cStringIO,operator
def indent(rows, hasHeader=False, headerChar='-', delim=' | ', justify='left', separateRows=False, prefix='', postfix='', wrapfunc=lambda x:x): """Indents a table by column. - rows: A sequence of sequences of items, one sequence per row. - hasHeader: True if the first row consists of the columns' names. - headerChar: Character to be used for the row separator line (if hasHeader==True or separateRows==True). - delim: The column delimiter. - justify: Determines how are data justified in their column. Valid values are 'left','right' and 'center'. - separateRows: True if rows are to be separated by a line of 'headerChar's. - prefix: A string prepended to each printed row. - postfix: A string appended to each printed row. - wrapfunc: A function f(text) for wrapping text; each element in the table is first wrapped by this function.""" # closure for breaking logical rows to physical, using wrapfunc def rowWrapper(row): newRows = [wrapfunc(item).split('\n') for item in row] return [[substr or '' for substr in item] for item in map(None,*newRows)] # break each logical row into one or more physical ones logicalRows = [rowWrapper(row) for row in rows] # columns of physical rows columns = map(None,*reduce(operator.add,logicalRows)) # get the maximum of each column by the string length of its items maxWidths = [max([len(str(item)) for item in column]) for column in columns] rowSeparator = headerChar * (len(prefix) + len(postfix) + sum(maxWidths) + \ len(delim)*(len(maxWidths)-1)) # select the appropriate justify method justify = {'center':str.center, 'right':str.rjust, 'left':str.ljust}[justify.lower()] output=cStringIO.StringIO() if separateRows: print >> output, rowSeparator for physicalRows in logicalRows: for row in physicalRows: print >> output, \ prefix \ + delim.join([justify(str(item),width) for (item,width) in zip(row,maxWidths)]) \ + postfix if separateRows or hasHeader: print >> output, rowSeparator; hasHeader=False return output.getvalue()
# written by Mike Brown
#
def wrap_onspace(text, width): """ A word-wrap function that preserves existing line breaks and most spaces in the text. Expects that existing line breaks are posix newlines (\n). """ return reduce(lambda line, word, width=width: '%s%s%s' % (line, ' \n'[(len(line[line.rfind('\n')+1:]) + len(word.split('\n',1)[0] ) >= width)], word), text.split(' ') )
import re
def wrap_onspace_strict(text, width): """Similar to wrap_onspace, but enforces the width constraint: words longer than width are split.""" wordRegex = re.compile(r'\S{'+str(width)+r',}') return wrap_onspace(wordRegex.sub(lambda m: wrap_always(m.group(),width),text),width)
import math
def wrap_always(text, width): """A simple word-wrap function that wraps text on exactly width characters. It doesn't split the text in words.""" return '\n'.join([ text[width*i:width*(i+1)] \ for i in xrange(int(math.ceil(1.*len(text)/width))) ])
if __name__ == '__main__': labels = ('First Name', 'Last Name', 'Age', 'Position') data = \ '''John,Smith,24,Software Engineer Mary,Brohowski,23,Sales Manager Aristidis,Papageorgopoulos,28,Senior Reseacher''' rows = [row.strip().split(',') for row in data.splitlines()] print 'Without wrapping function\n' print indent([labels]+rows, hasHeader=True) # test indent with different wrapping functions width = 10 for wrapper in (wrap_always,wrap_onspace,wrap_onspace_strict): print 'Wrapping function: %s(x,width=%d)\n' % (wrapper.__name__,width) print indent([labels]+rows, hasHeader=True, separateRows=True, prefix='| ', postfix=' |', wrapfunc=lambda x: wrapper(x,width)) # output: # #Without wrapping function # #First Name | Last Name | Age | Position #------------------------------------------------------- #John | Smith | 24 | Software Engineer #Mary | Brohowski | 23 | Sales Manager #Aristidis | Papageorgopoulos | 28 | Senior Reseacher # #Wrapping function: wrap_always(x,width=10) # #---------------------------------------------- #| First Name | Last Name | Age | Position | #---------------------------------------------- #| John | Smith | 24 | Software E | #| | | | ngineer | #---------------------------------------------- #| Mary | Brohowski | 23 | Sales Mana | #| | | | ger | #---------------------------------------------- #| Aristidis | Papageorgo | 28 | Senior Res | #| | poulos | | eacher | #---------------------------------------------- # #Wrapping function: wrap_onspace(x,width=10) # #--------------------------------------------------- #| First Name | Last Name | Age | Position | #--------------------------------------------------- #| John | Smith | 24 | Software | #| | | | Engineer | #--------------------------------------------------- #| Mary | Brohowski | 23 | Sales | #| | | | Manager | #--------------------------------------------------- #| Aristidis | Papageorgopoulos | 28 | Senior | #| | | | Reseacher | #--------------------------------------------------- # #Wrapping function: wrap_onspace_strict(x,width=10) # #--------------------------------------------- #| First Name | Last Name | Age | Position | #--------------------------------------------- #| John | Smith | 24 | Software | #| | | | Engineer | #--------------------------------------------- #| Mary | Brohowski | 23 | Sales | #| | | | Manager | #--------------------------------------------- #| Aristidis | Papageorgo | 28 | Senior | #| | poulos | | Reseacher | #---------------------------------------------The Python recipe page contains a few improvements on it.
pandas based solution with creating dataframe:
import pandas as pd
l = [['a', 'b', 'c'], ['aaaaaaaaaa', 'b', 'c'], ['a', 'bbbbbbbbbb', 'c']]
df = pd.DataFrame(l)
print(df) 0 1 2
0 a b c
1 aaaaaaaaaa b c
2 a bbbbbbbbbb cTo remove index and header values to create output what you want you could use to_string method:
result = df.to_string(index=False, header=False)
print(result) a b c aaaaaaaaaa b c a bbbbbbbbbb c Scolp is a new library that lets you pretty print streaming columnar data easily while auto-adjusting column width.
(Disclaimer: I am the author)
For lazy people
that are using Python 3.* and Pandas/Geopandas; universal simple in-class approach (for 'normal' script just remove self):
Function colorize:
def colorize(self,s,color): s = color+str(s)+"\033[0m" return sHeader:
print('{0:<23} {1:>24} {2:>26} {3:>26} {4:>11} {5:>11}'.format('Road name','Classification','Function','Form of road','Length','Distance') )and then data from Pandas/Geopandas dataframe:
for index, row in clipped.iterrows(): rdName = self.colorize(row['name1'],"\033[32m") rdClass = self.colorize(row['roadClassification'],"\033[93m") rdFunction = self.colorize(row['roadFunction'],"\033[33m") rdForm = self.colorize(row['formOfWay'],"\033[94m") rdLength = self.colorize(row['length'],"\033[97m") rdDistance = self.colorize(row['distance'],"\033[96m") print('{0:<30} {1:>35} {2:>35} {3:>35} {4:>20} {5:>20}'.format(rdName,rdClass,rdFunction,rdForm,rdLength,rdDistance) )Meaning of {0:<30} {1:>35} {2:>35} {3:>35} {4:>20} {5:>20}:
0, 1, 2, 3, 4, 5 -> columns, there are 6 in total in this case
30, 35, 20 -> width of column (note that you'll have to add length of \033[96m - this for Python is a string as well), just experiment :)
>, < -> justify: right, left (there is = for filling with zeros as well)
If you want to distinct e.g. max value, you'll have to switch to special Pandas style function, but suppose that's far enough to present data on terminal window.
Result:
A slight variation on a previous answer (I don't have enough rep to comment on it). The format library lets you specify the width and alignment of an element but not where it starts, ie, you can say "be 20 columns wide" but not "start in column 20". Which leads to this issue:
table_data = [ ['a', 'b', 'c'], ['aaaaaaaaaa', 'b', 'c'], ['a', 'bbbbbbbbbb', 'c']
]
print("first row: {: >20} {: >20} {: >20}".format(*table_data[0]))
print("second row: {: >20} {: >20} {: >20}".format(*table_data[1]))
print("third row: {: >20} {: >20} {: >20}".format(*table_data[2]))Output
first row: a b c
second row: aaaaaaaaaa b c
third row: a bbbbbbbbbb cThe answer of course is to format the literal strings as well, which combines slightly weirdly with the format:
table_data = [ ['a', 'b', 'c'], ['aaaaaaaaaa', 'b', 'c'], ['a', 'bbbbbbbbbb', 'c']
]
print(f"{'first row:': <20} {table_data[0][0]: >20} {table_data[0][1]: >20} {table_data[0][2]: >20}")
print("{: <20} {: >20} {: >20} {: >20}".format(*['second row:', *table_data[1]]))
print("{: <20} {: >20} {: >20} {: >20}".format(*['third row:', *table_data[1]]))Output
first row: a b c
second row: aaaaaaaaaa b c
third row: aaaaaaaaaa b c This sets independent, best-fit column widths based on the max-metric used in other answers.
data = [['a', 'b', 'c'], ['aaaaaaaaaa', 'b', 'c'], ['a', 'bbbbbbbbbb', 'c']]
padding = 2
col_widths = [max(len(w) for w in [r[cn] for r in data]) + padding for cn in range(len(data[0]))]
format_string = "{{:{}}}{{:{}}}{{:{}}}".format(*col_widths)
for row in data: print(format_string.format(*row)) 2 Formatting as a table requires the right padding. One generic solution is to use a python package called prettytable. Though it would be nicer to not depend on a library, but this package takes care of all the edge cases and is simple without any further dependencies.
x = PrettyTable()
x.field_names =["field1", "field2", "field3"]
x.add_row(["col1_content", "col2_content", "col3_content"])
print(x) Building from some other answers, I think I have a fairly readable and robust solution:
data = [['a', 'b', 'c'], ['aaaaaaaa', 'b', 'c'], ['a', 'bbbbbbbbbb', 'ccc']]
padding = 4
# build a format string of max widths
# ex: '{:43}{:77}{:104}'
num_cols = len(data[0])
widths = [0] * num_cols
for row in data: for i, value in enumerate(row): widths[i] = max(widths[i], len(str(value)))
format_string = "".join([f'{{:{w+padding}}}' for w in widths])
# print the data
for row in data: print(format_string.format(*[str(x) for x in row]))This also supports NoneType in the records, by wrapping a few things in str()
Here is a variation of the Shawn Chin's answer. The width is fixed per column, not over all columns. There is also a border below the first row and between the columns. (icontract library is used to enforce the contracts.)
@icontract.pre( lambda table: not table or all(len(row) == len(table[0]) for row in table))
@icontract.post(lambda table, result: result == "" if not table else True)
@icontract.post(lambda result: not result.endswith("\n"))
def format_table(table: List[List[str]]) -> str: """ Format the table as equal-spaced columns. :param table: rows of cells :return: table as string """ cols = len(table[0]) col_widths = [max(len(row[i]) for row in table) for i in range(cols)] lines = [] # type: List[str] for i, row in enumerate(table): parts = [] # type: List[str] for cell, width in zip(row, col_widths): parts.append(cell.ljust(width)) line = " | ".join(parts) lines.append(line) if i == 0: border = [] # type: List[str] for width in col_widths: border.append("-" * width) lines.append("-+-".join(border)) result = "\n".join(lines) return resultHere is an example:
>>> table = [['column 0', 'another column 1'], ['00', '01'], ['10', '11']]
>>> result = packagery._format_table(table=table)
>>> print(result)
column 0 | another column 1
---------+-----------------
00 | 01
10 | 11 updated @Franck Dernoncourt fancy recipe to be python 3 and PEP8 compliant
import io
import math
import operator
import re
import functools
from itertools import zip_longest
def indent( rows, has_header=False, header_char="-", delim=" | ", justify="left", separate_rows=False, prefix="", postfix="", wrapfunc=lambda x: x,
): """Indents a table by column. - rows: A sequence of sequences of items, one sequence per row. - hasHeader: True if the first row consists of the columns' names. - headerChar: Character to be used for the row separator line (if hasHeader==True or separateRows==True). - delim: The column delimiter. - justify: Determines how are data justified in their column. Valid values are 'left','right' and 'center'. - separateRows: True if rows are to be separated by a line of 'headerChar's. - prefix: A string prepended to each printed row. - postfix: A string appended to each printed row. - wrapfunc: A function f(text) for wrapping text; each element in the table is first wrapped by this function.""" # closure for breaking logical rows to physical, using wrapfunc def row_wrapper(row): new_rows = [wrapfunc(item).split("\n") for item in row] return [[substr or "" for substr in item] for item in zip_longest(*new_rows)] # break each logical row into one or more physical ones logical_rows = [row_wrapper(row) for row in rows] # columns of physical rows columns = zip_longest(*functools.reduce(operator.add, logical_rows)) # get the maximum of each column by the string length of its items max_widths = [max([len(str(item)) for item in column]) for column in columns] row_separator = header_char * ( len(prefix) + len(postfix) + sum(max_widths) + len(delim) * (len(max_widths) - 1) ) # select the appropriate justify method justify = {"center": str.center, "right": str.rjust, "left": str.ljust}[ justify.lower() ] output = io.StringIO() if separate_rows: print(output, row_separator) for physicalRows in logical_rows: for row in physicalRows: print( output, prefix + delim.join( [justify(str(item), width) for (item, width) in zip(row, max_widths)] ) + postfix) if separate_rows or has_header: print(output, row_separator) has_header = False return output.getvalue()
# written by Mike Brown
#
def wrap_onspace(text, width): """ A word-wrap function that preserves existing line breaks and most spaces in the text. Expects that existing line breaks are posix newlines (\n). """ return functools.reduce( lambda line, word, i_width=width: "%s%s%s" % ( line, " \n"[ ( len(line[line.rfind("\n") + 1 :]) + len(word.split("\n", 1)[0]) >= i_width ) ], word, ), text.split(" "), )
def wrap_onspace_strict(text, i_width): """Similar to wrap_onspace, but enforces the width constraint: words longer than width are split.""" word_regex = re.compile(r"\S{" + str(i_width) + r",}") return wrap_onspace( word_regex.sub(lambda m: wrap_always(m.group(), i_width), text), i_width )
def wrap_always(text, width): """A simple word-wrap function that wraps text on exactly width characters. It doesn't split the text in words.""" return "\n".join( [ text[width * i : width * (i + 1)] for i in range(int(math.ceil(1.0 * len(text) / width))) ] )
if __name__ == "__main__": labels = ("First Name", "Last Name", "Age", "Position") data = """John,Smith,24,Software Engineer Mary,Brohowski,23,Sales Manager Aristidis,Papageorgopoulos,28,Senior Reseacher""" rows = [row.strip().split(",") for row in data.splitlines()] print("Without wrapping function\n") print(indent([labels] + rows, has_header=True)) # test indent with different wrapping functions width = 10 for wrapper in (wrap_always, wrap_onspace, wrap_onspace_strict): print("Wrapping function: %s(x,width=%d)\n" % (wrapper.__name__, width)) print( indent( [labels] + rows, has_header=True, separate_rows=True, prefix="| ", postfix=" |", wrapfunc=lambda x: wrapper(x, width), ) ) # output: # # Without wrapping function # # First Name | Last Name | Age | Position # ------------------------------------------------------- # John | Smith | 24 | Software Engineer # Mary | Brohowski | 23 | Sales Manager # Aristidis | Papageorgopoulos | 28 | Senior Reseacher # # Wrapping function: wrap_always(x,width=10) # # ---------------------------------------------- # | First Name | Last Name | Age | Position | # ---------------------------------------------- # | John | Smith | 24 | Software E | # | | | | ngineer | # ---------------------------------------------- # | Mary | Brohowski | 23 | Sales Mana | # | | | | ger | # ---------------------------------------------- # | Aristidis | Papageorgo | 28 | Senior Res | # | | poulos | | eacher | # ---------------------------------------------- # # Wrapping function: wrap_onspace(x,width=10) # # --------------------------------------------------- # | First Name | Last Name | Age | Position | # --------------------------------------------------- # | John | Smith | 24 | Software | # | | | | Engineer | # --------------------------------------------------- # | Mary | Brohowski | 23 | Sales | # | | | | Manager | # --------------------------------------------------- # | Aristidis | Papageorgopoulos | 28 | Senior | # | | | | Reseacher | # --------------------------------------------------- # # Wrapping function: wrap_onspace_strict(x,width=10) # # --------------------------------------------- # | First Name | Last Name | Age | Position | # --------------------------------------------- # | John | Smith | 24 | Software | # | | | | Engineer | # --------------------------------------------- # | Mary | Brohowski | 23 | Sales | # | | | | Manager | # --------------------------------------------- # | Aristidis | Papageorgo | 28 | Senior | # | | poulos | | Reseacher | # --------------------------------------------- You can prepare your data and pass it to the real column utility.
Let's assume you have printed data to file /tmp/filename.txt with the tab as a delimeter. Then you can columnize it like this:
import subprocess
result = subprocess.run("cat /tmp/filename.txt | column -N \"col_1,col_2,col_3\" -t -s'\t' -R 2,3", shell=True, stdout=subprocess.PIPE)
print(result.stdout.decode("utf-8"))As you can see, you can use features of column utility, such as right aligning.
table = [['a', 'b', 'c'], ['aaaaaaaaaa', 'b', 'c'], ['a', 'bbbbbbbbbb', 'c']]
def print_table(table): def get_fmt(table): fmt = '' for column, row in enumerate(table[0]): fmt += '{{!s:<{}}} '.format( max(len(str(row[column])) for row in table) + 2) return fmt fmt = get_fmt(table) for row in table: print(fmt.format(*row))
print_table(table) This was a fun little project...
columns.py
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING: from typing import Iterable, Iterator, Sequence, Sized Matrix = Sequence[Sequence]
def all_elem_same_length(list: Sequence) -> bool: length = len(list[0]) for elem in list: if not len(elem) == length: return False return True
def get_col(matrix: Matrix, col_i: int) -> Iterator[Sized]: return (row[col_i] for row in matrix)
def get_cols(matrix: Matrix) -> Iterator[Iterable[Sized]]: return (get_col(matrix, col_i) for col_i in range(len(matrix[0])))
def get_longest_elem(list: Iterable[Sized]) -> Sized: return max(list, key=len)
def get_longest_elem_per_column(matrix: Matrix) -> Iterator[Sized]: return (get_longest_elem(col) for col in get_cols(matrix))
def get_word_pad_fstr(element: Sized, padding: int) -> str: return f"{{:{len(element)+padding}}}"
def get_row_elem_pad_strings(matrix: Matrix, padding: int) -> Iterator[str]: return ( get_word_pad_fstr(word, padding) for word in get_longest_elem_per_column(matrix) )
def print_table(matrix: Matrix, padding=4) -> None: if not all_elem_same_length(matrix): raise ValueError("Table rows must all have the same length.") format_string = "".join(get_row_elem_pad_strings(matrix, padding)) for row in matrix: print(format_string.format(*(str(e) for e in row)))
if __name__ == "__main__": data = [["a", "b", "c"], ["aaaaaaaa", "b", "c"], ["a", "bbbbbbbbbb", "ccc"]] print_table(data) I realize this question is old but I didn't understand Antak's answer and didn't want to use a library so I rolled my own solution.
Solution assumes records is a 2D array, records are all the same length, and that fields are all strings.
def stringifyRecords(records): column_widths = [0] * len(records[0]) for record in records: for i, field in enumerate(record): width = len(field) if width > column_widths[i]: column_widths[i] = width s = "" for record in records: for column_width, field in zip(column_widths, record): s += field.ljust(column_width+1) s += "\n" return s I found this answer super-helpful and elegant, originally from here:
matrix = [["A", "B"], ["C", "D"]]
print('\n'.join(['\t'.join([str(cell) for cell in row]) for row in matrix]))Output
A B
C D