Python ASCII-Art Table
In no particular order:


Your approach to default keyword arguments with the **k is unusual. I’d expect to see something more like: 

def ascii_table(table, header=None, align='left', border=False):
    if header is None:
        header = []


Note that I’ve dropped the space between the function name and the opening parens (per PEP 8 style), and also used None as the default for the header parameter – this is to avoid quirks around mutable default arguments.

This style is more in keeping with other Python code, and makes it easier for somebody to see what parameters this function takes. It also makes it easier to spot when somebody is calling this with entirely wrong arguments. 
Newlines are cheap! Don’t put stuff on a single line, especially if … else blocks. For example, this block: 

if border: printable.append('│ ' + ' │ '.join(printrow) + ' │')
else: printable.append(' │ '.join(print row))


would usually be written more like: 

if border:
    printable.append('│ ' + ' │ '.join(printrow) + ' │')
else:
    printable.append(' │ '.join(print row))


That style is more conventional, and makes it easier to see the structure of the code. 
Loop over the elements of a list directly, not its indices. For example, this loop:

for i in range(len(header)):
    assert header[i]
    if align == 'center':
        printrow.append(header[i].center(widths[i]))
    ....
    # do stuff with header[i]




could be better written as something like: 

for h, w in zip(header, widths):
    if align == 'centre':
        printrow.append(h.center(w))
    ...




Iterating directly over lists is more Pythonic, and generally makes for cleaner code. 
Don’t cram too much into a single expression; it becomes difficult to parse and hurts readability. This is on the edge of what I’d consider acceptable: 

for i in range(max(map(len, table))):




This is unreadable: 

for i in range(max(map(len, table))): widths.append(max(max(map(len, [row[i] for row in table if len(row) &gt; i])), len(header[i]) if len(header) &gt; i else 0))




You should break it across multiple lines. 
You can tidy up some of your logical conditions. For example, this: 

if i &gt; 0 and i &lt; max(map(len, table)) - 1:




can be reduced to: 

if 0 &lt; i &lt; max(len(t) for t in table) - 1




which is a bit easier to read. 
This could potentially choke on a large table, because you’re building all the rows in a list printable. I would consider rewriting this as a generator – essentially, replace every printable.append(foo) with yield foo.

Then your function just lazily computes the rows, and passes them to the caller. It’s up to them how they want to use it – whether they want to save it, do some more processing, join it into a string (and then they can choose the newline).

Generators and iteration are a very powerful feature of Python – if you’re not familiar with them already, this PyCon talk is a good introduction.