Repository: astanin/python-tabulate

Issue:
Improve error reporting when tabular_data is a dict of non-iterable values
I am trying to tabulate a counter from collections counter
``` python
print(tabulate(counter))
```

this approach gives an error message:
```
 File "/Users/wf/Library/Python/3.8/lib/python/site-packages/tabulate.py", line 1528, in tabulate
    list_of_lists, headers = _normalize_tabular_data(
  File "/Users/wf/Library/Python/3.8/lib/python/site-packages/tabulate.py", line 1089, in _normalize_tabular_data
    izip_longest(*tabular_data.values())
TypeError: 'int' object is not iterable
```

**What else should i do - or would this be a feature request?**

Discussion:
@WolfgangFahl: This is what it did so far to get my result - unfortunately floatfmt doesn't seem to work:
```python
def showCounter(self,counter,title,headers=["#","key","count","%"],tablefmt='pretty',limit=50):
        '''
        show the counter contents
        '''
        bins=len(counter.keys())
        limit=min(bins,limit)
        total=sum(counter.values())
        table=[("total",bins,total)]
        for i,bintuple in enumerate(counter.most_common(limit)):
            key,count=bintuple
            table.append((i+1,key,count,count/total*100.0))
        print(title)
        print(tabulate(table,headers=headers,tablefmt=tablefmt,floatfmt=".2f"))
```@ivan-radic: Hi,

I'd like to second this feature request.
It would be great to have built-in support for `collections` `Counter`s, instead of writing my own wrapper function.

Best regards,
Ivan@patrickdeanbrown: Hi @WolfgangFahl,

When ```tablefmt='pretty'``` is set, tabulate appears to be treating each column as if it were a string, prior to applying `floatfmt`. So no float formatting is being applied, even though the floatfmt parameter is set. As a workaround, you can either not specify tablefmt, or you can apply the format builtin when calculating your data.

```python
table.append((i + 1, key, count, format(count / total * 100.0, "0.2f")))
```

As for directly using a Counter object with tabulate, you can call the Counter.items() method, and it should work.

```python
from collections import Counter
counter = Counter(['a', 'a', 'a', 'b', 'b', 'c', 'c', 'c', 'c', 'c', 'c', 'c', ])
print(tabulate(counter.items()))

# Expected output:
#
# -  -
# a  3
# b  2
# c  7
# -  -
```
  @WolfgangFahl: @patrickdeanbrown  thank you for looking into this. Your workarounds are nice but do not solve the problem/fulfill the need of a simpler interface that this issue is requesting.@astanin: Tabulate can process an iterable (list) of dictionaries. It should work with a list of `Counter`s too:

```pycon
>>> c = Counter(a = 3, b = 4)
>>> print(tabulate([dict(c)], headers="keys"))
  a    b
---  ---
  3    4
>>> print(tabulate([c], headers="keys"))
  a    b
---  ---
  3    4
```

Using `counter.items()` is the intended way to print similar data as rows.

```pycon
>>> print(tabulate(c.items()))
-  -
a  3
b  4
-  -
```@astanin: But the error message can be improved. The library should have suggested what data shape it expects.

Solution patch:
diff --git a/.circleci/config.yml b/.circleci/config.yml
deleted file mode 100644
index ac9c525..0000000
--- a/.circleci/config.yml
+++ /dev/null
@@ -1,60 +0,0 @@
-# Python CircleCI 2.0 configuration file
-#
-# Check https://circleci.com/docs/2.0/language-python/ for more details
-#
-version: 2
-jobs:
-  build:
-    docker:
-      # specify the version you desire here
-      # use `-browsers` prefix for selenium tests, e.g. `3.6.1-browsers`
-      - image: circleci/python:3.8
-
-      # Specify service dependencies here if necessary
-      # CircleCI maintains a library of pre-built images
-      # documented at https://circleci.com/docs/2.0/circleci-images/
-      # - image: circleci/postgres:9.4
-
-    working_directory: ~/repo
-
-    steps:
-      - checkout
-
-      # Download and cache dependencies
-      - restore_cache:
-          keys:
-          - v1-dependencies-{{ checksum ".circleci/requirements.txt" }}
-          # fallback to using the latest cache if no exact match is found
-          - v1-dependencies-
-
-      - run:
-          name: install dependencies
-          command: |
-            python3 -m venv venv
-            . venv/bin/activate
-            pip install -r .circleci/requirements.txt
-
-      - save_cache:
-          paths:
-            - ./venv
-          key: v1-dependencies-{{ checksum ".circleci/requirements.txt" }}
-
-      - run:
-          name: build wheel
-          command: |
-            . venv/bin/activate
-            python -m build -nwx .
-
-      - store_artifacts:
-          path: dist
-          destination: dist
-
-      - run:
-          name: run tests
-          command: |
-            . venv/bin/activate
-            tox -e py38-extra
-
-      - store_artifacts:
-          path: test-reports
-          destination: test-reports
diff --git a/.circleci/requirements.txt b/.circleci/requirements.txt
deleted file mode 100644
index ea5e80c..0000000
--- a/.circleci/requirements.txt
+++ /dev/null
@@ -1,10 +0,0 @@
-pytest
-tox
-numpy
-pandas
-wcwidth
-setuptools
-pip
-build
-wheel
-setuptools_scm
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
new file mode 100644
index 0000000..7e7c906
--- /dev/null
+++ b/.github/workflows/lint.yml
@@ -0,0 +1,26 @@
+name: lint
+
+on:
+  - push
+  - pull_request
+
+jobs:
+  build:
+    runs-on: ubuntu-latest
+    strategy:
+      matrix:
+        python-version: ['3.12']
+
+    steps:
+    - uses: actions/checkout@v4
+    - name: Set up Python ${{ matrix.python-version }}
+      uses: actions/setup-python@v4
+      with:
+        python-version: ${{ matrix.python-version }}
+    - name: Install dependencies
+      run: |
+        python -m pip install --upgrade pip
+        python -m pip install tox tox-gh
+    - name: Run linters
+      run: |
+        tox -e lint
diff --git a/.github/workflows/tabulate.yml b/.github/workflows/tabulate.yml
new file mode 100644
index 0000000..9adabb7
--- /dev/null
+++ b/.github/workflows/tabulate.yml
@@ -0,0 +1,26 @@
+name: python-tabulate
+
+on:
+  - push
+  - pull_request
+
+jobs:
+  build:
+    runs-on: ubuntu-latest
+    strategy:
+      matrix:
+        python-version: ['3.9', '3.10', '3.11', '3.12']
+
+    steps:
+    - uses: actions/checkout@v4
+    - name: Set up Python ${{ matrix.python-version }}
+      uses: actions/setup-python@v4
+      with:
+        python-version: ${{ matrix.python-version }}
+    - name: Install dependencies
+      run: |
+        python -m pip install --upgrade pip
+        python -m pip install pytest numpy pandas
+    - name: Run tests
+      run: |
+        pytest -v --doctest-modules --ignore benchmark.py
diff --git a/HOWTOPUBLISH b/HOWTOPUBLISH
index 5be16ed..795fc73 100644
--- a/HOWTOPUBLISH
+++ b/HOWTOPUBLISH
@@ -1,7 +1,7 @@
 # update contributors and CHANGELOG in README
 # tag version release
 python3 benchmark.py  # then update README
-tox -e py37-extra,py38-extra,py39-extra,py310-extra
+tox -e py38-extra,py39-extra,py310-extra,py311-extra,py312-extra
 python3 -m build -nswx .
 twine upload --repository-url https://test.pypi.org/legacy/ dist/*
 twine upload dist/*
diff --git a/README.md b/README.md
index d64b99a..b596cf5 100644
--- a/README.md
+++ b/README.md
@@ -53,7 +53,7 @@ pip install tabulate
 Build status
 ------------
 
-[![Build status](https://circleci.com/gh/astanin/python-tabulate.svg?style=svg)](https://circleci.com/gh/astanin/python-tabulate/tree/master) [![Build status](https://ci.appveyor.com/api/projects/status/8745yksvvol7h3d7/branch/master?svg=true)](https://ci.appveyor.com/project/astanin/python-tabulate/branch/master)
+[![python-tabulate](https://github.com/astanin/python-tabulate/actions/workflows/tabulate.yml/badge.svg)](https://github.com/astanin/python-tabulate/actions/workflows/tabulate.yml) [![Build status](https://ci.appveyor.com/api/projects/status/8745yksvvol7h3d7/branch/master?svg=true)](https://ci.appveyor.com/project/astanin/python-tabulate/branch/master)
 
 Library usage
 -------------
@@ -81,7 +81,7 @@ The following tabular data types are supported:
 -   list of lists or another iterable of iterables
 -   list or another iterable of dicts (keys as columns)
 -   dict of iterables (keys as columns)
--   list of dataclasses (Python 3.7+ only, field names as columns)
+-   list of dataclasses (field names as columns)
 -   two-dimensional NumPy array
 -   NumPy record arrays (names as columns)
 -   pandas.DataFrame
@@ -121,10 +121,22 @@ dictionaries or named tuples:
 ```pycon
 >>> print(tabulate({"Name": ["Alice", "Bob"],
 ...                 "Age": [24, 19]}, headers="keys"))
-  Age  Name
------  ------
-   24  Alice
-   19  Bob
+Name      Age
+------  -----
+Alice      24
+Bob        19
+```
+
+When data is a list of dictionaries, a dictionary can be passed as `headers`
+to replace the keys with other column labels:
+
+```pycon
+>>> print(tabulate([{1: "Alice", 2: 24}, {1: "Bob", 2: 19}], 
+...                headers={1: "Name", 2: "Age"}))
+Name      Age
+------  -----
+Alice      24
+Bob        19
 ```
 
 ### Row Indices
@@ -666,18 +678,31 @@ Ver2  19.2
 
 ### Custom column alignment
 
-`tabulate` allows a custom column alignment to override the above. The
-`colalign` argument can be a list or a tuple of `stralign` named
-arguments. Possible column alignments are: `right`, `center`, `left`,
-`decimal` (only for numbers), and `None` (to disable alignment).
-Omitting an alignment uses the default. For example:
+`tabulate` allows a custom column alignment to override the smart alignment described above.
+Use `colglobalalign` to define a global setting. Possible alignments are: `right`, `center`, `left`, `decimal` (only for numbers).
+Furthermore, you can define `colalign` for column-specific alignment as a list or a tuple. Possible values are `global` (keeps global setting), `right`, `center`, `left`, `decimal` (only for numbers), `None` (to disable alignment). Missing alignments are treated as `global`.
 
 ```pycon
->>> print(tabulate([["one", "two"], ["three", "four"]], colalign=("right",))
------  ----
-  one  two
-three  four
------  ----
+>>> print(tabulate([[1,2,3,4],[111,222,333,444]], colglobalalign='center', colalign = ('global','left','right')))
+---  ---  ---  ---
+ 1   2      3   4
+111  222  333  444
+---  ---  ---  ---
+```
+
+### Custom header alignment
+
+Headers' alignment can be defined separately from columns'. Like for columns, you can use:
+- `headersglobalalign` to define a header-specific global alignment setting. Possible values are `right`, `center`, `left`, `None` (to follow column alignment),
+- `headersalign` list or tuple to further specify header-wise alignment. Possible values are `global` (keeps global setting), `same` (follow column alignment), `right`, `center`, `left`, `None` (to disable alignment). Missing alignments are treated as `global`.
+
+```pycon
+>>> print(tabulate([[1,2,3,4,5,6],[111,222,333,444,555,666]], colglobalalign = 'center', colalign = ('left',), headers = ['h','e','a','d','e','r'], headersglobalalign = 'right', headersalign = ('same','same','left','global','center')))
+
+h     e   a      d   e     r
+---  ---  ---  ---  ---  ---
+1     2    3    4    5    6
+111  222  333  444  555  666
 ```
 
 ### Number formatting
@@ -1061,14 +1086,14 @@ To run tests on all supported Python versions, make sure all Python
 interpreters, `pytest` and `tox` are installed, then run `tox` in the root
 of the project source tree.
 
-On Linux `tox` expects to find executables like `python3.7`, `python3.8` etc.
-On Windows it looks for `C:\Python37\python.exe`, `C:\Python38\python.exe` etc. respectively.
+On Linux `tox` expects to find executables like `python3.11`, `python3.12` etc.
+On Windows it looks for `C:\Python311\python.exe`, `C:\Python312\python.exe` etc. respectively.
 
 One way to install all the required versions of the Python interpreter is to use [pyenv](https://github.com/pyenv/pyenv).
 All versions can then be easily installed with something like:
 
-     pyenv install 3.7.12
-     pyenv install 3.8.12
+     pyenv install 3.11.7
+     pyenv install 3.12.1
      ...
 
 Don't forget to change your `PATH` so that `tox` knows how to find all the installed versions. Something like
@@ -1076,10 +1101,10 @@ Don't forget to change your `PATH` so that `tox` knows how to find all the insta
      export PATH="${PATH}:${HOME}/.pyenv/shims"
 
 To test only some Python environments, use `-e` option. For example, to
-test only against Python 3.7 and Python 3.10, run:
+test only against Python 3.11 and Python 3.12, run:
 
 ```shell
-tox -e py37,py310
+tox -e py311,py312
 ```
 
 in the root of the project source tree.
@@ -1087,7 +1112,7 @@ in the root of the project source tree.
 To enable NumPy and Pandas tests, run:
 
 ```shell
-tox -e py37-extra,py310-extra
+tox -e py311-extra,py312-extra
 ```
 
 (this may take a long time the first time, because NumPy and Pandas will
@@ -1123,5 +1148,5 @@ Bart Broere, Vilhelm Prytz, Alexander Gažo, Hugo van Kemenade,
 jamescooke, Matt Warner, Jérôme Provensal, Kevin Deldycke,
 Kian-Meng Ang, Kevin Patterson, Shodhan Save, cleoold, KOLANICH,
 Vijaya Krishna Kasula, Furcy Pin, Christian Fibich, Shaun Duncan,
-Dimitri Papadopoulos.
+Dimitri Papadopoulos, Élie Goudout.
 
diff --git a/appveyor.yml b/appveyor.yml
index 4eb2dd8..12bd772 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -8,14 +8,14 @@ environment:
     # The list here is complete (excluding Python 2.6, which
     # isn't covered by this document) at the time of writing.
 
-    - PYTHON: "C:\\Python37"
-    - PYTHON: "C:\\Python38"
-    - PYTHON: "C:\\Python39"
-    - PYTHON: "C:\\Python37-x64"
-    - PYTHON: "C:\\Python38-x64"
+    #- PYTHON: "C:\\Python39"
+    #- PYTHON: "C:\\Python310"
+    #- PYTHON: "C:\\Python311"
+    #- PYTHON: "C:\\Python312"
     - PYTHON: "C:\\Python39-x64"
     - PYTHON: "C:\\Python310-x64"
     - PYTHON: "C:\\Python311-x64"
+    - PYTHON: "C:\\Python312-x64"
 
 install:
   # Newer setuptools is needed for proper support of pyproject.toml
@@ -29,9 +29,6 @@ build: off
 
 test_script:
   # Put your test command here.
-  # If you don't need to build C extensions on 64-bit Python 3.3 or 3.4,
-  # you can remove "build.cmd" from the front of the command, as it's
-  # only needed to support those cases.
   # Note that you must use the environment variable %PYTHON% to refer to
   # the interpreter you're using - Appveyor does not do anything special
   # to put the Python version you want to use on PATH.
@@ -40,9 +37,7 @@ test_script:
 
 after_test:
   # This step builds your wheels.
-  # Again, you only need build.cmd if you're building C extensions for
-  # 64-bit Python 3.3/3.4. And you need to use %PYTHON% to get the correct
-  # interpreter
+  # Again, you need to use %PYTHON% to get the correct interpreter
   #- "build.cmd %PYTHON%\\python.exe setup.py bdist_wheel"
   - "%PYTHON%\\python.exe -m build -nswx ."
 
diff --git a/pyproject.toml b/pyproject.toml
index 5a8c1fd..8cb6216 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -13,14 +13,13 @@ classifiers = [
     "License :: OSI Approved :: MIT License",
     "Operating System :: OS Independent",
     "Programming Language :: Python :: 3",
-    "Programming Language :: Python :: 3.7",
-    "Programming Language :: Python :: 3.8",
     "Programming Language :: Python :: 3.9",
     "Programming Language :: Python :: 3.10",
     "Programming Language :: Python :: 3.11",
+    "Programming Language :: Python :: 3.12",
     "Topic :: Software Development :: Libraries",
 ]
-requires-python = ">=3.7"
+requires-python = ">=3.9"
 dynamic = ["version"]
 
 [project.urls]
diff --git a/tabulate/__init__.py b/tabulate/__init__.py
index 3b1a1e1..8f21abe 100644
--- a/tabulate/__init__.py
+++ b/tabulate/__init__.py
@@ -1,5 +1,6 @@
 """Pretty-print tabular data."""
 
+import warnings
 from collections import namedtuple
 from collections.abc import Iterable, Sized
 from html import escape as htmlescape
@@ -10,6 +11,7 @@
 import math
 import textwrap
 import dataclasses
+import sys
 
 try:
     import wcwidth  # optional wide-character (CJK) support
@@ -101,12 +103,17 @@ def _is_file(f):
 )
 
 
+def _is_separating_line_value(value):
+    return type(value) is str and value.strip() == SEPARATING_LINE
+
+
 def _is_separating_line(row):
     row_type = type(row)
     is_sl = (row_type == list or row_type == str) and (
-        (len(row) >= 1 and row[0] == SEPARATING_LINE)
-        or (len(row) >= 2 and row[1] == SEPARATING_LINE)
+        (len(row) >= 1 and _is_separating_line_value(row[0]))
+        or (len(row) >= 2 and _is_separating_line_value(row[1]))
     )
+
     return is_sl
 
 
@@ -133,6 +140,30 @@ def _pipe_line_with_colons(colwidths, colaligns):
     return "|" + "|".join(segments) + "|"
 
 
+def _grid_segment_with_colons(colwidth, align):
+    """Return a segment of a horizontal line with optional colons which indicate
+    column's alignment in a grid table."""
+    width = colwidth
+    if align == "right":
+        return ("=" * (width - 1)) + ":"
+    elif align == "center":
+        return ":" + ("=" * (width - 2)) + ":"
+    elif align == "left":
+        return ":" + ("=" * (width - 1))
+    else:
+        return "=" * width
+
+
+def _grid_line_with_colons(colwidths, colaligns):
+    """Return a horizontal line with optional colons to indicate column's alignment
+    in a grid table."""
+    if not colaligns:
+        colaligns = [""] * len(colwidths)
+    segments = [_grid_segment_with_colons(w, a) for a, w in zip(colaligns, colwidths)]
+    return "+" + "+".join(segments) + "+"
+
+
+
 def _mediawiki_row_with_attrs(separator, cell_values, colwidths, colaligns):
     alignment = {
         "left": "",
@@ -223,7 +254,7 @@ def make_header_line(is_header, colwidths, colaligns):
             colwidths, [alignment[colalign] for colalign in colaligns]
         )
         asciidoc_column_specifiers = [
-            "{:d}{}".format(width, align) for width, align in asciidoc_alignments
+            f"{width:d}{align}" for width, align in asciidoc_alignments
         ]
         header_list = ['cols="' + (",".join(asciidoc_column_specifiers)) + '"']
 
@@ -399,6 +430,16 @@ def escape_empty(val):
         padding=1,
         with_header_hide=None,
     ),
+    "colon_grid": TableFormat(
+        lineabove=Line("+", "-", "+", "+"),
+        linebelowheader=_grid_line_with_colons,
+        linebetweenrows=Line("+", "-", "+", "+"),
+        linebelow=Line("+", "-", "+", "+"),
+        headerrow=DataRow("|", "|", "|"),
+        datarow=DataRow("|", "|", "|"),
+        padding=1,
+        with_header_hide=None,
+    ),
     "outline": TableFormat(
         lineabove=Line("+", "-", "+", "+"),
         linebelowheader=Line("+", "=", "+", "+"),
@@ -692,6 +733,7 @@ def escape_empty(val):
     "mixed_grid": "mixed_grid",
     "double_grid": "double_grid",
     "fancy_grid": "fancy_grid",
+    "colon_grid": "colon_grid",
     "pipe": "pipe",
     "orgtbl": "orgtbl",
     "jira": "jira",
@@ -1228,6 +1270,17 @@ def _format(val, valtype, floatfmt, intfmt, missingval="", has_invisible=True):
     if valtype is str:
         return f"{val}"
     elif valtype is int:
+        if isinstance(val, str):
+            val_striped = val.encode('unicode_escape').decode('utf-8')
+            colored = re.search(r'(\\[xX]+[0-9a-fA-F]+\[\d+[mM]+)([0-9.]+)(\\.*)$', val_striped)
+            if colored:
+                total_groups = len(colored.groups())
+                if total_groups == 3:
+                    digits = colored.group(2)
+                    if digits.isdigit():
+                        val_new = colored.group(1) + format(int(digits), intfmt) + colored.group(3)
+                        val = val_new.encode('utf-8').decode('unicode_escape')
+            intfmt = ""
         return format(val, intfmt)
     elif valtype is bytes:
         try:
@@ -1270,7 +1323,7 @@ def _align_header(
 
 
 def _remove_separating_lines(rows):
-    if type(rows) == list:
+    if isinstance(rows, list):
         separating_lines = []
         sans_rows = []
         for index, row in enumerate(rows):
@@ -1296,7 +1349,7 @@ def _prepend_row_index(rows, index):
     if isinstance(index, Sized) and len(index) != len(rows):
         raise ValueError(
             "index must be as long as the number of data rows: "
-            + "len(index)={} len(rows)={}".format(len(index), len(rows))
+            + f"len(index)={len(index)} len(rows)={len(rows)}"
         )
     sans_rows, separating_lines = _remove_separating_lines(rows)
     new_rows = []
@@ -1318,7 +1371,8 @@ def _bool(val):
 
 
 def _normalize_tabular_data(tabular_data, headers, showindex="default"):
-    """Transform a supported data type to a list of lists, and a list of headers.
+    """Transform a supported data type to a list of lists, and a list of headers,
+    with headers padding.
 
     Supported tabular data types:
 
@@ -1330,7 +1384,7 @@ def _normalize_tabular_data(tabular_data, headers, showindex="default"):
 
     * list of OrderedDicts (usually used with headers="keys")
 
-    * list of dataclasses (Python 3.7+ only, usually used with headers="keys")
+    * list of dataclasses (usually used with headers="keys")
 
     * 2D NumPy arrays
 
@@ -1352,20 +1406,27 @@ def _normalize_tabular_data(tabular_data, headers, showindex="default"):
 
     try:
         bool(headers)
-        is_headers2bool_broken = False  # noqa
     except ValueError:  # numpy.ndarray, pandas.core.index.Index, ...
-        is_headers2bool_broken = True  # noqa
         headers = list(headers)
 
+    err_msg = (
+        "\n\nTo build a table python-tabulate requires two-dimensional data "
+        "like a list of lists or similar."
+        "\nDid you forget a pair of extra [] or ',' in ()?"
+    )
     index = None
     if hasattr(tabular_data, "keys") and hasattr(tabular_data, "values"):
         # dict-like and pandas.DataFrame?
         if hasattr(tabular_data.values, "__call__"):
             # likely a conventional dict
             keys = tabular_data.keys()
-            rows = list(
-                izip_longest(*tabular_data.values())
-            )  # columns have to be transposed
+            try:
+                rows = list(
+                    izip_longest(*tabular_data.values())
+                )  # columns have to be transposed
+            except TypeError:  # not iterable
+                raise TypeError(err_msg)
+
         elif hasattr(tabular_data, "index"):
             # values is a property, has .index => it's likely a pandas.DataFrame (pandas 0.11.0)
             keys = list(tabular_data)
@@ -1388,7 +1449,10 @@ def _normalize_tabular_data(tabular_data, headers, showindex="default"):
             headers = list(map(str, keys))  # headers should be strings
 
     else:  # it's a usual iterable of iterables, or a NumPy array, or an iterable of dataclasses
-        rows = list(tabular_data)
+        try:
+            rows = list(tabular_data)
+        except TypeError:  # not iterable
+            raise TypeError(err_msg)
 
         if headers == "keys" and not rows:
             # an empty table (issue #81)
@@ -1456,7 +1520,7 @@ def _normalize_tabular_data(tabular_data, headers, showindex="default"):
             and len(rows) > 0
             and dataclasses.is_dataclass(rows[0])
         ):
-            # Python 3.7+'s dataclass
+            # Python's dataclass
             field_names = [field.name for field in dataclasses.fields(rows[0])]
             if headers == "keys":
                 headers = field_names
@@ -1498,13 +1562,12 @@ def _normalize_tabular_data(tabular_data, headers, showindex="default"):
         pass
 
     # pad with empty headers for initial columns if necessary
+    headers_pad = 0
     if headers and len(rows) > 0:
-        nhs = len(headers)
-        ncols = len(rows[0])
-        if nhs < ncols:
-            headers = [""] * (ncols - nhs) + headers
+        headers_pad = max(0, len(rows[0]) - len(headers))
+        headers = [""] * headers_pad + headers
 
-    return rows, headers
+    return rows, headers, headers_pad
 
 
 def _wrap_text_to_colwidths(list_of_lists, colwidths, numparses=True):
@@ -1580,8 +1643,11 @@ def tabulate(
     missingval=_DEFAULT_MISSINGVAL,
     showindex="default",
     disable_numparse=False,
+    colglobalalign=None,
     colalign=None,
     maxcolwidths=None,
+    headersglobalalign=None,
+    headersalign=None,
     rowalign=None,
     maxheadercolwidths=None,
 ):
@@ -1597,7 +1663,7 @@ def tabulate(
     The first required argument (`tabular_data`) can be a
     list-of-lists (or another iterable of iterables), a list of named
     tuples, a dictionary of iterables, an iterable of dictionaries,
-    an iterable of dataclasses (Python 3.7+), a two-dimensional NumPy array,
+    an iterable of dataclasses, a two-dimensional NumPy array,
     NumPy record array, or a Pandas' dataframe.
 
 
@@ -1636,8 +1702,8 @@ def tabulate(
     -  -  --
 
 
-    Column alignment
-    ----------------
+    Column and Headers alignment
+    ----------------------------
 
     `tabulate` tries to detect column types automatically, and aligns
     the values properly. By default it aligns decimal points of the
@@ -1646,6 +1712,23 @@ def tabulate(
     (`numalign`, `stralign`) are: "right", "center", "left", "decimal"
     (only for `numalign`), and None (to disable alignment).
 
+    `colglobalalign` allows for global alignment of columns, before any
+        specific override from `colalign`. Possible values are: None
+        (defaults according to coltype), "right", "center", "decimal",
+        "left".
+    `colalign` allows for column-wise override starting from left-most
+        column. Possible values are: "global" (no override), "right",
+        "center", "decimal", "left".
+    `headersglobalalign` allows for global headers alignment, before any
+        specific override from `headersalign`. Possible values are: None
+        (follow columns alignment), "right", "center", "left".
+    `headersalign` allows for header-wise override starting from left-most
+        given header. Possible values are: "global" (no override), "same"
+        (follow column alignment), "right", "center", "left".
+
+    Note on intended behaviour: If there is no `tabular_data`, any column
+        alignment argument is ignored. Hence, in this case, header
+        alignment cannot be inferred from column alignment.
 
     Table formats
     -------------
@@ -1802,6 +1885,30 @@ def tabulate(
     │ eggs      │  451      │
     ╘═══════════╧═══════════╛
 
+    "colon_grid" is similar to "grid" but uses colons only to define
+    columnwise content alignment, with no whitespace padding:
+
+    >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]],
+    ...                ["strings", "numbers"], "colon_grid"))
+    +-----------+-----------+
+    | strings   | numbers   |
+    +:==========+:==========+
+    | spam      | 41.9999   |
+    +-----------+-----------+
+    | eggs      | 451       |
+    +-----------+-----------+
+
+    >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]],
+    ...                ["strings", "numbers"], "colon_grid",
+    ...                colalign=["right", "left"]))
+    +-----------+-----------+
+    | strings   | numbers   |
+    +==========:+:==========+
+    | spam      | 41.9999   |
+    +-----------+-----------+
+    | eggs      | 451       |
+    +-----------+-----------+
+
     "outline" is the same as the "grid" format but doesn't draw lines between rows:
 
     >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]],
@@ -2065,7 +2172,7 @@ def tabulate(
     if tabular_data is None:
         tabular_data = []
 
-    list_of_lists, headers = _normalize_tabular_data(
+    list_of_lists, headers, headers_pad = _normalize_tabular_data(
         tabular_data, headers, showindex=showindex
     )
     list_of_lists, separating_lines = _remove_separating_lines(list_of_lists)
@@ -2118,6 +2225,13 @@ def tabulate(
         numalign = "decimal" if numalign == _DEFAULT_ALIGN else numalign
         stralign = "left" if stralign == _DEFAULT_ALIGN else stralign
 
+    # 'colon_grid' uses colons in the line beneath the header to represent a column's
+    # alignment instead of literally aligning the text differently. Hence,
+    # left alignment of the data in the text output is enforced.
+    if tablefmt == "colon_grid":
+        colglobalalign = "left"
+        headersglobalalign = "left"
+
     # optimization: look for ANSI control codes once,
     # enable smart width functions only if a control code is found
     #
@@ -2181,30 +2295,72 @@ def tabulate(
     ]
 
     # align columns
-    aligns = [numalign if ct in [int, float] else stralign for ct in coltypes]
+    # first set global alignment
+    if colglobalalign is not None:  # if global alignment provided
+        aligns = [colglobalalign] * len(cols)
+    else:  # default
+        aligns = [numalign if ct in [int, float] else stralign for ct in coltypes]
+    # then specific alignments
     if colalign is not None:
         assert isinstance(colalign, Iterable)
+        if isinstance(colalign, str):
+            warnings.warn(
+                f"As a string, `colalign` is interpreted as {[c for c in colalign]}. "
+                f'Did you mean `colglobalalign = "{colalign}"` or `colalign = ("{colalign}",)`?',
+                stacklevel=2,
+            )
         for idx, align in enumerate(colalign):
-            aligns[idx] = align
+            if not idx < len(aligns):
+                break
+            elif align != "global":
+                aligns[idx] = align
     minwidths = (
         [width_fn(h) + min_padding for h in headers] if headers else [0] * len(cols)
     )
+    aligns_copy = aligns.copy()
+    # Reset alignments in copy of alignments list to "left" for 'colon_grid' format,
+    # which enforces left alignment in the text output of the data.
+    if tablefmt == "colon_grid":
+        aligns_copy = ["left"] * len(cols)
     cols = [
         _align_column(c, a, minw, has_invisible, enable_widechars, is_multiline)
-        for c, a, minw in zip(cols, aligns, minwidths)
+        for c, a, minw in zip(cols, aligns_copy, minwidths)
     ]
 
+    aligns_headers = None
     if headers:
         # align headers and add headers
         t_cols = cols or [[""]] * len(headers)
-        t_aligns = aligns or [stralign] * len(headers)
+        # first set global alignment
+        if headersglobalalign is not None:  # if global alignment provided
+            aligns_headers = [headersglobalalign] * len(t_cols)
+        else:  # default
+            aligns_headers = aligns or [stralign] * len(headers)
+        # then specific header alignments
+        if headersalign is not None:
+            assert isinstance(headersalign, Iterable)
+            if isinstance(headersalign, str):
+                warnings.warn(
+                    f"As a string, `headersalign` is interpreted as {[c for c in headersalign]}. "
+                    f'Did you mean `headersglobalalign = "{headersalign}"` '
+                    f'or `headersalign = ("{headersalign}",)`?',
+                    stacklevel=2,
+                )
+            for idx, align in enumerate(headersalign):
+                hidx = headers_pad + idx
+                if not hidx < len(aligns_headers):
+                    break
+                elif align == "same" and hidx < len(aligns):  # same as column align
+                    aligns_headers[hidx] = aligns[hidx]
+                elif align != "global":
+                    aligns_headers[hidx] = align
         minwidths = [
             max(minw, max(width_fn(cl) for cl in c))
             for minw, c in zip(minwidths, t_cols)
         ]
         headers = [
             _align_header(h, a, minw, width_fn(h), is_multiline, width_fn)
-            for h, a, minw in zip(headers, t_aligns, minwidths)
+            for h, a, minw in zip(headers, aligns_headers, minwidths)
         ]
         rows = list(zip(*cols))
     else:
@@ -2219,7 +2375,14 @@ def tabulate(
     _reinsert_separating_lines(rows, separating_lines)
 
     return _format_table(
-        tablefmt, headers, rows, minwidths, aligns, is_multiline, rowaligns=rowaligns
+        tablefmt,
+        headers,
+        aligns_headers,
+        rows,
+        minwidths,
+        aligns,
+        is_multiline,
+        rowaligns=rowaligns,
     )
 
 
@@ -2350,7 +2513,9 @@ def str(self):
         return self
 
 
-def _format_table(fmt, headers, rows, colwidths, colaligns, is_multiline, rowaligns):
+def _format_table(
+    fmt, headers, headersaligns, rows, colwidths, colaligns, is_multiline, rowaligns
+):
     """Produce a plain-text representation of the table."""
     lines = []
     hidden = fmt.with_header_hide if (headers and fmt.with_header_hide) else []
@@ -2372,7 +2537,7 @@ def _format_table(fmt, headers, rows, colwidths, colaligns, is_multiline, rowali
         _append_line(lines, padded_widths, colaligns, fmt.lineabove)
 
     if padded_headers:
-        append_row(lines, padded_headers, padded_widths, colaligns, headerrow)
+        append_row(lines, padded_headers, padded_widths, headersaligns, headerrow)
         if fmt.linebelowheader and "linebelowheader" not in hidden:
             _append_line(lines, padded_widths, colaligns, fmt.linebelowheader)
 
@@ -2646,8 +2811,6 @@ def _main():
                               (default: simple)
     """
     import getopt
-    import sys
-    import textwrap
 
     usage = textwrap.dedent(_main.__doc__)
     try:
@@ -2690,7 +2853,7 @@ def _main():
             print(usage)
             sys.exit(0)
     files = [sys.stdin] if not args else args
-    with (sys.stdout if outfile == "-" else open(outfile, "w")) as out:
+    with sys.stdout if outfile == "-" else open(outfile, "w") as out:
         for f in files:
             if f == "-":
                 f = sys.stdin
diff --git a/tox.ini b/tox.ini
index c6260d2..a7fe513 100644
--- a/tox.ini
+++ b/tox.ini
@@ -8,9 +8,16 @@
 # for testing and it is disabled by default.
 
 [tox]
-envlist = lint, py{37, 38, 39, 310, 311}
+envlist = lint, py{38, 39, 310, 311, 312}
 isolated_build = True
 
+[gh]
+python =
+    3.9: py39-extra
+    3.10: py310-extra
+    3.11: py311-extra
+    3.12: py312-extra
+
 [testenv]
 commands = pytest -v --doctest-modules --ignore benchmark.py {posargs}
 deps =
@@ -25,21 +32,6 @@ commands = python -m pre_commit run -a
 deps =
     pre-commit
 
-[testenv:py37]
-basepython = python3.7
-commands = pytest -v --doctest-modules --ignore benchmark.py {posargs}
-deps =
-    pytest
-
-[testenv:py37-extra]
-basepython = python3.7
-commands = pytest -v --doctest-modules --ignore benchmark.py {posargs}
-deps =
-    pytest
-    numpy
-    pandas
-    wcwidth
-
 [testenv:py38]
 basepython = python3.8
 commands = pytest -v --doctest-modules --ignore benchmark.py {posargs}
@@ -105,6 +97,21 @@ deps =
     pandas
     wcwidth
 
+[testenv:py312]
+basepython = python3.12
+commands = pytest -v --doctest-modules --ignore benchmark.py {posargs}
+deps =
+    pytest
+
+[testenv:py312-extra]
+basepython = python3.12
+setenv = PYTHONDEVMODE = 1
+commands = pytest -v --doctest-modules --ignore benchmark.py {posargs}
+deps =
+    pytest
+    numpy
+    pandas
+    wcwidth
 
 [flake8]
 max-complexity = 22


Test patch:
diff --git a/test/common.py b/test/common.py
index d95e84f..ca5a069 100644
--- a/test/common.py
+++ b/test/common.py
@@ -1,13 +1,12 @@
 import pytest  # noqa
 from pytest import skip, raises  # noqa
-
+import warnings
 
 def assert_equal(expected, result):
     print("Expected:\n%s\n" % expected)
     print("Got:\n%s\n" % result)
     assert expected == result
 
-
 def assert_in(result, expected_set):
     nums = range(1, len(expected_set) + 1)
     for i, expected in zip(nums, expected_set):
@@ -27,3 +26,18 @@ def rows_to_pipe_table_str(rows):
         lines.append(line)
 
     return "\n".join(lines)
+
+
+def check_warnings(func_args_kwargs, *, num=None, category=None, contain=None):
+    func, args, kwargs = func_args_kwargs
+    with warnings.catch_warnings(record=True) as W:
+        # Causes all warnings to always be triggered inside here.
+        warnings.simplefilter("always")
+        func(*args, **kwargs)
+        # Checks
+        if num is not None:
+            assert len(W) == num
+        if category is not None:
+            assert all([issubclass(w.category, category) for w in W])
+        if contain is not None:
+            assert all([contain in str(w.message) for w in W])
diff --git a/test/test_api.py b/test/test_api.py
index 046d752..e658e82 100644
--- a/test/test_api.py
+++ b/test/test_api.py
@@ -48,8 +48,11 @@ def test_tabulate_signature():
         ("missingval", ""),
         ("showindex", "default"),
         ("disable_numparse", False),
+        ("colglobalalign", None),
         ("colalign", None),
         ("maxcolwidths", None),
+        ("headersglobalalign", None),
+        ("headersalign", None),
         ("rowalign", None),
         ("maxheadercolwidths", None),
     ]
diff --git a/test/test_cli.py b/test/test_cli.py
index ce85f19..e71572d 100644
--- a/test/test_cli.py
+++ b/test/test_cli.py
@@ -2,7 +2,6 @@
 
 """
 
-
 import os
 import sys
 
diff --git a/test/test_input.py b/test/test_input.py
index a178bd9..721d03a 100644
--- a/test/test_input.py
+++ b/test/test_input.py
@@ -522,7 +522,7 @@ def test_py37orlater_list_of_dataclasses_headers():
 
 def test_list_bytes():
     "Input: a list of bytes. (issue #192)"
-    lb = [["你好".encode("utf-8")], ["你好"]]
+    lb = [["你好".encode()], ["你好"]]
     expected = "\n".join(
         ["bytes", "---------------------------", r"b'\xe4\xbd\xa0\xe5\xa5\xbd'", "你好"]
     )
diff --git a/test/test_internal.py b/test/test_internal.py
index 64e1d12..e7564d3 100644
--- a/test/test_internal.py
+++ b/test/test_internal.py
@@ -182,7 +182,12 @@ def test_wrap_text_wide_chars():
 
     rows = [["청자청자청자청자청자", "약간 감싸면 더 잘 보일 수있는 다소 긴 설명입니다"]]
     widths = [5, 20]
-    expected = [["청자\n청자\n청자\n청자\n청자", "약간 감싸면 더 잘\n보일 수있는 다소 긴\n설명입니다"]]
+    expected = [
+        [
+            "청자\n청자\n청자\n청자\n청자",
+            "약간 감싸면 더 잘\n보일 수있는 다소 긴\n설명입니다",
+        ]
+    ]
     result = T._wrap_text_to_colwidths(rows, widths)
 
     assert_equal(expected, result)
diff --git a/test/test_output.py b/test/test_output.py
index 9043aed..ed565a8 100644
--- a/test/test_output.py
+++ b/test/test_output.py
@@ -1,7 +1,8 @@
 """Test output of the various forms of tabular data."""
+from pytest import mark
 
 import tabulate as tabulate_module
-from common import assert_equal, raises, skip
+from common import assert_equal, raises, skip, check_warnings
 from tabulate import tabulate, simple_separated_format, SEPARATING_LINE
 
 # _test_table shows
@@ -136,7 +137,10 @@ def test_plain_maxcolwidth_autowraps_wide_chars():
 
     table = [
         ["hdr", "fold"],
-        ["1", "약간 감싸면 더 잘 보일 수있는 다소 긴 설명입니다 설명입니다 설명입니다 설명입니다 설명"],
+        [
+            "1",
+            "약간 감싸면 더 잘 보일 수있는 다소 긴 설명입니다 설명입니다 설명입니다 설명입니다 설명",
+        ],
     ]
     expected = "\n".join(
         [
@@ -1414,6 +1418,97 @@ def test_fancy_grid_multiline_row_align():
     assert_equal(expected, result)
 
 
+def test_colon_grid():
+    "Output: colon_grid with two columns aligned left and center"
+    expected = "\n".join(
+        [
+            "+------+------+",
+            "| H1   | H2   |",
+            "+=====:+:====:+",
+            "| 3    | 4    |",
+            "+------+------+",
+        ]
+    )
+    result = tabulate([[3, 4]], headers=("H1", "H2"), tablefmt="colon_grid", colalign=["right", "center"])
+    assert_equal(expected, result)
+
+
+def test_colon_grid_wide_characters():
+    "Output: colon_grid with wide chars in header"
+    try:
+        import wcwidth  # noqa
+    except ImportError:
+        skip("test_colon_grid_wide_characters is skipped")
+    headers = list(_test_table_headers)
+    headers[1] = "配列"
+    expected = "\n".join(
+        [
+            "+-----------+---------+",
+            "| strings   | 配列    |",
+            "+:==========+========:+",
+            "| spam      | 41.9999 |",
+            "+-----------+---------+",
+            "| eggs      | 451     |",
+            "+-----------+---------+",
+        ]
+    )
+    result = tabulate(_test_table, headers, tablefmt="colon_grid", colalign=["left", "right"])
+    assert_equal(expected, result)
+
+
+def test_colon_grid_headerless():
+    "Output: colon_grid without headers"
+    expected = "\n".join(
+        [
+            "+------+---------+",
+            "| spam | 41.9999 |",
+            "+------+---------+",
+            "| eggs | 451     |",
+            "+------+---------+",
+        ]
+    )
+    result = tabulate(_test_table, tablefmt="colon_grid")
+    assert_equal(expected, result)
+
+
+def test_colon_grid_multiline():
+    "Output: colon_grid with multiline cells"
+    table = [["Data\n5", "33\n3"]]
+    headers = ["H1\n1", "H2\n2"]
+    expected = "\n".join(
+        [
+            "+------+------+",
+            "| H1   | H2   |",
+            "| 1    | 2    |",
+            "+:=====+:=====+",
+            "| Data | 33   |",
+            "| 5    | 3    |",
+            "+------+------+",
+        ]
+    )
+    result = tabulate(table, headers, tablefmt="colon_grid")
+    assert_equal(expected, result)
+
+
+def test_colon_grid_with_empty_cells():
+    table = [["A", ""], ["", "B"]]
+    headers = ["H1", "H2"]
+    alignments = ["center", "right"]
+    expected = "\n".join(
+        [
+            "+------+------+",
+            "| H1   | H2   |",
+            "+:====:+=====:+",
+            "| A    |      |",
+            "+------+------+",
+            "|      | B    |",
+            "+------+------+",
+        ]
+    )
+    result = tabulate(table, headers, tablefmt="colon_grid", colalign=alignments)
+    assert_equal(expected, result)
+
+
 def test_outline():
     "Output: outline with headers"
     expected = "\n".join(
@@ -2638,6 +2733,44 @@ def test_intfmt():
     assert_equal(expected, result)
 
 
+def test_intfmt_with_string_as_integer():
+    "Output: integer format"
+    result = tabulate([[82642], ["1500"], [2463]], intfmt=",", tablefmt="plain")
+    expected = "82,642\n  1500\n 2,463"
+    assert_equal(expected, result)
+
+
+@mark.skip(reason="It detects all values as floats but there are strings and integers.")
+def test_intfmt_with_string_with_floats():
+    "Output: integer format"
+    result = tabulate([[82000.38], ["1500.47"], ["2463"], [92165]], intfmt=",", tablefmt="plain")
+    expected = "82000.4\n 1500.47\n 2463\n92,165"
+    assert_equal(expected, result)
+
+
+def test_intfmt_with_colors():
+    "Regression: Align ANSI-colored values as if they were colorless."
+    colortable = [
+        ("\x1b[33mabc\x1b[0m", 42, "\x1b[31m42\x1b[0m"),
+        ("\x1b[35mdef\x1b[0m", 987654321, "\x1b[32m987654321\x1b[0m"),
+    ]
+    colorheaders = ("test", "\x1b[34mtest\x1b[0m", "test")
+    formatted = tabulate(colortable, colorheaders, "grid", intfmt=",")
+    expected = "\n".join(
+        [
+            "+--------+-------------+-------------+",
+            "| test   |        \x1b[34mtest\x1b[0m |        test |",
+            "+========+=============+=============+",
+            "| \x1b[33mabc\x1b[0m    |          42 |          \x1b[31m42\x1b[0m |",
+            "+--------+-------------+-------------+",
+            "| \x1b[35mdef\x1b[0m    | 987,654,321 | \x1b[32m987,654,321\x1b[0m |",
+            "+--------+-------------+-------------+",
+        ]
+    )
+    print(f"expected: {expected!r}\n\ngot:      {formatted!r}\n")
+    assert_equal(expected, formatted)
+
+
 def test_empty_data_with_headers():
     "Output: table with empty data and headers as firstrow"
     expected = ""
@@ -2681,6 +2814,72 @@ def test_colalign_multi_with_sep_line():
     assert_equal(expected, result)
 
 
+def test_column_global_and_specific_alignment():
+    """Test `colglobalalign` and `"global"` parameter for `colalign`."""
+    table = [[1, 2, 3, 4], [111, 222, 333, 444]]
+    colglobalalign = "center"
+    colalign = ("global", "left", "right")
+    result = tabulate(table, colglobalalign=colglobalalign, colalign=colalign)
+    expected = "\n".join(
+        [
+            "---  ---  ---  ---",
+            " 1   2      3   4",
+            "111  222  333  444",
+            "---  ---  ---  ---",
+        ]
+    )
+    assert_equal(expected, result)
+
+
+def test_headers_global_and_specific_alignment():
+    """Test `headersglobalalign` and `headersalign`."""
+    table = [[1, 2, 3, 4, 5, 6], [111, 222, 333, 444, 555, 666]]
+    colglobalalign = "center"
+    colalign = ("left",)
+    headers = ["h", "e", "a", "d", "e", "r"]
+    headersglobalalign = "right"
+    headersalign = ("same", "same", "left", "global", "center")
+    result = tabulate(
+        table,
+        headers=headers,
+        colglobalalign=colglobalalign,
+        colalign=colalign,
+        headersglobalalign=headersglobalalign,
+        headersalign=headersalign,
+    )
+    expected = "\n".join(
+        [
+            "h     e   a      d   e     r",
+            "---  ---  ---  ---  ---  ---",
+            "1     2    3    4    5    6",
+            "111  222  333  444  555  666",
+        ]
+    )
+    assert_equal(expected, result)
+
+
+def test_colalign_or_headersalign_too_long():
+    """Test `colalign` and `headersalign` too long."""
+    table = [[1, 2], [111, 222]]
+    colalign = ("global", "left", "center")
+    headers = ["h"]
+    headersalign = ("center", "right", "same")
+    result = tabulate(
+        table, headers=headers, colalign=colalign, headersalign=headersalign
+    )
+    expected = "\n".join(["      h", "---  ---", "  1  2", "111  222"])
+    assert_equal(expected, result)
+
+
+def test_warning_when_colalign_or_headersalign_is_string():
+    """Test user warnings when `colalign` or `headersalign` is a string."""
+    table = [[1, "bar"]]
+    opt = {"colalign": "center", "headers": ["foo", "2"], "headersalign": "center"}
+    check_warnings(
+        (tabulate, [table], opt), num=2, category=UserWarning, contain="As a string"
+    )
+
+
 def test_float_conversions():
     "Output: float format parsed"
     test_headers = ["str", "bad_float", "just_float", "with_inf", "with_nan", "neg_inf"]
@@ -2881,6 +3080,27 @@ def test_list_of_lists_with_index_with_sep_line():
     assert_equal(expected, result)
 
 
+def test_with_padded_columns_with_sep_line():
+    table = [
+        ["1", "one"],  # "1" as a str on purpose
+        [1_000, "one K"],
+        SEPARATING_LINE,
+        [1_000_000, "one M"],
+    ]
+    expected = "\n".join(
+        [
+            "+---------+-------+",
+            "|       1 | one   |",
+            "|    1000 | one K |",
+            "|---------+-------|",
+            "| 1000000 | one M |",
+            "+---------+-------+",
+        ]
+    )
+    result = tabulate(table, tablefmt="psql")
+    assert_equal(expected, result)
+
+
 def test_list_of_lists_with_supplied_index():
     "Output: a table with a supplied index"
     dd = zip(*[list(range(3)), list(range(101, 104))])
diff --git a/test/test_textwrapper.py b/test/test_textwrapper.py
index f3070b1..b315a5c 100644
--- a/test/test_textwrapper.py
+++ b/test/test_textwrapper.py
@@ -1,6 +1,4 @@
 """Discretely test functionality of our custom TextWrapper"""
-from __future__ import unicode_literals
-
 import datetime
 
 from tabulate import _CustomTextWrap as CTW, tabulate
@@ -144,7 +142,8 @@ def test_wrap_color_in_single_line():
 
 def test_wrap_color_line_splillover():
     """TextWrapper: Wrap a line - preserve internal color tags and wrap them to
-    other lines when required, requires adding the colors tags to other lines as appropriate"""
+    other lines when required, requires adding the colors tags to other lines as appropriate
+    """
     # This has both a text color and a background color
     data = "This is a \033[31mtest string for testing TextWrap\033[0m with colors"