Вопрос: Как в Python вывести таблицу как в примере?

Лучший ответ:
Вывести в консоль?
Ну я бы подходил к этому так.
1. Преобразовать все выводимые данные в строки и разбить их по переносам строки.
2. Определить наибольшую ширину каждого столбца с учётом переносов строки. Для каждой ячейки вычисляем  наибольшую ширину строки, потом ищем максимум по столбцу. Можно добавить 1-2 символа как "поля", если надо.
3. При выводе строки, выводим строки ячеек вместе, используя itertools.zip_longest(). Т.е. сначала первую строку в каждой ячейке, потом вторую, и т.д. Если получили вместо одной из строк None, значит в этой ячейке строки уже закончились - выводим пробелы.
Ширину вывода каждой ячейки мы знаем из пункта 2, выровнять значение пробелами - тривиально.

# -*- coding: utf-8 -*-
import typing as t
import itertools

def print_table(headers: t.Dict[str, str], data: t.Iterable[t.Dict[str, str]]) -> None:
    keys: t.List[str] = list(headers.keys()) #список ключей в таблице - чтобы сохранять порядок столбцов
    split_data: t.List[t.Dict[str, t.List[str]]] = [] #ячейки, разбитые на строки
    max_widths: t.Dict[str, int] = { key:len(value) for key,value in headers.items() } #ширина каждого столбца в таблице
    for line in data:
        #разбиваем ячейки строки на текстовые строки по \n
        split_line: t.Dict[str, t.List[str]] = { key:value.splitlines() for key,value in line.items() }
        #обновляем ширину столбцов, если надо
        for key in keys:
            new_width = max(map(len, split_line.get(key, [''])))
            if new_width > max_widths[key]:
                max_widths[key] = new_width
        split_data.append(split_line)
    #выводим заголовки
    for key in keys:
        print(f'{{0:<{max_widths[key]}}}'.format(headers[key]), end='|') #можно вместо | поставить пробел
    print()
    print( '+'.join('-'*v for v in max_widths.values()) + '|') #разделитель заголовка и тела таблицы
    #выводим строки таблицы
    for row in split_data:
        for parts in itertools.zip_longest(*(row[key] for key in keys)):
            #parts - кортеж, где каждый элемент либо строка в очередной ячейке, либо None
            for key,part in zip(keys, parts):
                #None означает, что в этой ячейке строки текста уже кончились
                print(f'{{0:<{max_widths[key]}}}'.format(part if part is not None else ''), end='|')
            print()
        print( '+'.join('-'*v for v in max_widths.values())  + '|') #разделитель строк, если надо

data = [
    {'ip':'192.168.0.2', 'model':'DES-3200-26', 'uptime': '3d 12:03:05', 'uplink state': '25: up\n26:up', 'uplink err': '0\n11', 'uplink mcast': '24560\n113'},
    {'ip':'192.168.0.2', 'model':'DES-3200-52', 'uptime': '1d 04:00:15', 'uplink state': '49: up\n50:up\n51:down\n52:down', 'uplink err': '10\n1133\n0\n0', 'uplink mcast': '5497812\n3145\n0\n0'},
]
headers = {'ip': 'IP address', 'model': 'Model', 'uptime': 'Uptime', 'uplink state': 'Uplink state', 'uplink err': 'Uplink errors', 'uplink mcast': 'Uplink M-cast'}
print_table(headers, data)
IP address |Model      |Uptime     |Uplink state|Uplink errors|Uplink M-cast|
-----------+-----------+-----------+------------+-------------+-------------|
192.168.0.2|DES-3200-26|3d 12:03:05|25: up      |0            |24560        |
           |           |           |26:up       |11           |113          |
-----------+-----------+-----------+------------+-------------+-------------|
192.168.0.2|DES-3200-52|1d 04:00:15|49: up      |10           |5497812      |
           |           |           |50:up       |1133         |3145         |
           |           |           |51:down     |0            |0            |
           |           |           |52:down     |0            |0            |
-----------+-----------+-----------+------------+-------------+-------------|

Худший ответ:
Вообще-то, Prettytable понимает multirow

data = [
    {'ip':'192.168.0.2', 'model':'DES-3200-26', 'uptime': '3d 12:03:05', 'uplink state': '25: up\n26: up', 'uplink err': '0\n11', 'uplink mcast': '24560\n113'},
    {'ip':'192.168.0.3', 'model':'DES-3200-52', 'uptime': '1d 04:00:15', 'uplink state': '49: up\n50: up\n51: down\n52: down', 'uplink err': '10\n1133\n0\n0', 'uplink mcast': '5497812\n3145\n0\n0'},
]

def set_data_for_PrettyTable(data):
    field_names = data[0].keys()
    rows = []
    for row in data:
        rows.append(row.values())
    return field_names, rows

field_names, rows = set_data_for_PrettyTable(data)

table = PrettyTable()
table.hrules = 1
table.field_names = field_names
table.add_rows(rows)
table.align["uplink state"] = "l"
print(table)

+-------------+-------------+-------------+--------------+------------+--------------+
|      ip     |    model    |    uptime   | uplink state | uplink err | uplink mcast |
+-------------+-------------+-------------+--------------+------------+--------------+
| 192.168.0.2 | DES-3200-26 | 3d 12:03:05 | 25: up       |     0      |    24560     |
|             |             |             | 26: up       |     11     |     113      |
+-------------+-------------+-------------+--------------+------------+--------------+
| 192.168.0.3 | DES-3200-52 | 1d 04:00:15 | 49: up       |     10     |   5497812    |
|             |             |             | 50: up       |    1133    |     3145     |
|             |             |             | 51: down     |     0      |      0       |
|             |             |             | 52: down     |     0      |      0       |
+-------------+-------------+-------------+--------------+------------+--------------+