codecov[bot]: # [Codecov](https://codecov.io/gh/pri22296/beautifultable/pull/77?src=pr&el=h1) Report
> Merging [#77](https://codecov.io/gh/pri22296/beautifultable/pull/77?src=pr&el=desc) into [master](https://codecov.io/gh/pri22296/beautifultable/commit/fa9b8750bf944a3164c47d1ec4c46374af55e682?src=pr&el=desc) will **increase** coverage by `0.54%`.
> The diff coverage is `66.66%`.

[![Impacted file tree graph](https://codecov.io/gh/pri22296/beautifultable/pull/77/graphs/tree.svg?width=650&token=JGEoYMIaay&height=150&src=pr)](https://codecov.io/gh/pri22296/beautifultable/pull/77?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master      #77      +/-   ##
==========================================
+ Coverage    78.7%   79.24%   +0.54%     
==========================================
  Files          12       12              
  Lines        1000     1002       +2     
  Branches      158      159       +1     
==========================================
+ Hits          787      794       +7     
+ Misses        158      152       -6     
- Partials       55       56       +1
```


| [Impacted Files](https://codecov.io/gh/pri22296/beautifultable/pull/77?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [beautifultable/beautifultable.py](https://codecov.io/gh/pri22296/beautifultable/pull/77/diff?src=pr&el=tree#diff-YmVhdXRpZnVsdGFibGUvYmVhdXRpZnVsdGFibGUucHk=) | `72.61% <66.66%> (+1.15%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/pri22296/beautifultable/pull/77?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/pri22296/beautifultable/pull/77?src=pr&el=footer). Last update [fa9b875...0dc3b0c](https://codecov.io/gh/pri22296/beautifultable/pull/77?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

diff --git a/.gitignore b/.gitignore
index e70f354..87e4a82 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,3 +6,4 @@ build/
 .tox/
 *.pyc
 .coverage
+.vscode/
diff --git a/beautifultable/beautifultable.py b/beautifultable/beautifultable.py
index dde020c..90ff9bd 100644
--- a/beautifultable/beautifultable.py
+++ b/beautifultable/beautifultable.py
@@ -800,12 +800,16 @@ class BeautifulTable(object):
             If `True` then table is sorted as if each comparison was reversed.
         """
         if isinstance(key, int):
-            index = key
+            key = operator.itemgetter(key)
         elif isinstance(key, basestring):
-            index = self.get_column_index(key)
+            key = operator.itemgetter(self.get_column_index(key))
+        elif callable(key):
+            pass
         else:
-            raise TypeError("'key' must either be 'int' or 'str'")
-        self._table.sort(key=operator.itemgetter(index), reverse=reverse)
+            raise TypeError(
+                "'key' must either be 'int' or 'str' or a 'callable'"
+            )
+        self._table.sort(key=key, reverse=reverse)
 
     def copy(self):
         """Return a shallow copy of the table.
@@ -1432,7 +1436,7 @@ class BeautifulTable(object):
 
         return "\n".join(string_)
 
-    def to_csv(self, file_name, delimiter=','):
+    def to_csv(self, file_name, delimiter=","):
         """Export table to CSV format.
 
         Parameters
@@ -1446,22 +1450,22 @@ class BeautifulTable(object):
 
         if not isinstance(file_name, str):
             raise ValueError(
-                (
-                    "Expected 'file_name' to be string, got {}"
-                ).format(type(file_name).__name__)
+                ("Expected 'file_name' to be string, got {}").format(
+                    type(file_name).__name__
+                )
             )
 
         try:
-            with open(file_name, mode='wt', newline='') as csv_file:
-                csv_writer = csv.writer(csv_file,
-                                        delimiter=delimiter,
-                                        quoting=csv.QUOTE_MINIMAL)
+            with open(file_name, mode="wt", newline="") as csv_file:
+                csv_writer = csv.writer(
+                    csv_file, delimiter=delimiter, quoting=csv.QUOTE_MINIMAL
+                )
                 csv_writer.writerow(self.column_headers)  # write header
                 csv_writer.writerows(self._table)  # write table
         except OSError:
             raise
 
-    def from_csv(self, file_name, delimiter=',', header_exists=True):
+    def from_csv(self, file_name, delimiter=",", header_exists=True):
         """Create table from CSV file.
 
         Parameters
@@ -1483,13 +1487,13 @@ class BeautifulTable(object):
 
         if not isinstance(file_name, str):
             raise ValueError(
-                (
-                    "Expected 'file_name' to be string, got {}"
-                ).format(type(file_name).__name__)
+                ("Expected 'file_name' to be string, got {}").format(
+                    type(file_name).__name__
+                )
             )
 
         try:
-            with open(file_name, mode='rt', newline='') as csv_file:
+            with open(file_name, mode="rt", newline="") as csv_file:
                 csv_file = csv.reader(csv_file, delimiter=delimiter)
 
                 if header_exists:

Allow sort based on an arbitrary callable
Currently we can sort the table based on a column. But we should allow the use of any callable as a key function like the sort method of list.
diff --git a/test.py b/test.py
index 0ad9017..2e8699f 100644
--- a/test.py
+++ b/test.py
@@ -25,6 +25,8 @@ class TableOperationsTestCase(unittest.TestCase):
         for item1, item2 in zip(iterable1, iterable2):
             self.assertEqual(item1, item2)
 
+    # Test for table operations
+
     def test_filter(self):
         new_table = self.table.filter(lambda x: x["rank"] > 1)
         self.assertEqual(len(self.table), 5)
@@ -36,6 +38,58 @@ class TableOperationsTestCase(unittest.TestCase):
         for row_t, row in zip(new_table, rows):
             self.compare_iterable(row_t, row)
 
+    def test_sort_by_index(self):
+        self.table.sort(0)
+        rows = [
+            ["Ethan", 2, "boy"],
+            ["Isabella", 1, "girl"],
+            ["Jacob", 1, "boy"],
+            ["Michael", 3, "boy"],
+            ["Sophia", 2, "girl"],
+        ]
+        for row_t, row in zip(self.table, rows):
+            self.compare_iterable(row_t, row)
+
+    def test_sort_by_index_reversed(self):
+        self.table.sort(0, reverse=True)
+        rows = [
+            ["Ethan", 2, "boy"],
+            ["Isabella", 1, "girl"],
+            ["Jacob", 1, "boy"],
+            ["Michael", 3, "boy"],
+            ["Sophia", 2, "girl"],
+        ]
+        for row_t, row in zip(self.table, reversed(rows)):
+            self.compare_iterable(row_t, row)
+
+    def test_sort_by_header(self):
+        self.table.sort("name")
+        rows = [
+            ["Ethan", 2, "boy"],
+            ["Isabella", 1, "girl"],
+            ["Jacob", 1, "boy"],
+            ["Michael", 3, "boy"],
+            ["Sophia", 2, "girl"],
+        ]
+        for row_t, row in zip(self.table, rows):
+            self.compare_iterable(row_t, row)
+
+    def test_sort_by_callable(self):
+        self.table.sort(lambda x: (x[1], x[0]))
+        rows = [
+            ["Isabella", 1, "girl"],
+            ["Jacob", 1, "boy"],
+            ["Ethan", 2, "boy"],
+            ["Sophia", 2, "girl"],
+            ["Michael", 3, "boy"],
+        ]
+        for row_t, row in zip(self.table, rows):
+            self.compare_iterable(row_t, row)
+
+    def test_sort_raises_exception(self):
+        with self.assertRaises(TypeError):
+            self.table.sort(None)
+
     # Tests for column operations
 
     def test_column_count(self):
@@ -530,26 +584,26 @@ class TableOperationsTestCase(unittest.TestCase):
 
     def test_csv_export(self):
         # Create csv files in path.
-        self.table.to_csv('beautiful_table.csv')
-        self.table.to_csv('./docs/beautiful_table.csv')
+        self.table.to_csv("beautiful_table.csv")
+        self.table.to_csv("./docs/beautiful_table.csv")
 
         with self.assertRaises(ValueError):
             self.table.to_csv(1)
 
         # Check if csv files exist.
-        self.assertTrue(os.path.exists('beautiful_table.csv'))
-        self.assertTrue(os.path.exists('./docs/beautiful_table.csv'))
+        self.assertTrue(os.path.exists("beautiful_table.csv"))
+        self.assertTrue(os.path.exists("./docs/beautiful_table.csv"))
 
         # Teardown step.
-        os.remove('beautiful_table.csv')
-        os.remove('./docs/beautiful_table.csv')
+        os.remove("beautiful_table.csv")
+        os.remove("./docs/beautiful_table.csv")
 
     def test_csv_import(self):
         # Export table as CSV file and import it back.
-        self.table.to_csv('beautiful_table.csv')
+        self.table.to_csv("beautiful_table.csv")
 
         test_table = BeautifulTable()
-        test_table.from_csv('beautiful_table.csv')
+        test_table.from_csv("beautiful_table.csv")
 
         with self.assertRaises(ValueError):
             self.table.from_csv(1)
@@ -561,11 +615,11 @@ class TableOperationsTestCase(unittest.TestCase):
         #     self.assertEqual(self.table[index], test_table[index])
 
         test_table = BeautifulTable()
-        test_table.from_csv('beautiful_table.csv', header_exists=False)
+        test_table.from_csv("beautiful_table.csv", header_exists=False)
         self.assertEqual(len(self.table), len(test_table) - 1)
 
         # Teardown step.
-        os.remove('beautiful_table.csv')
+        os.remove("beautiful_table.csv")
 
 
 if __name__ == "__main__":

pip install -e .
parse_log_pytest
pytest
wcwidth pytest
3.6
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
attrs @ file:///opt/conda/conda-bld/attrs_1642510447205/work
-e git+https://github.com/pri22296/beautifultable.git@ed329595a1fd0dfaaf4d8d6ff75a36e514b07331#egg=beautifultable
certifi==2021.5.30
importlib-metadata @ file:///tmp/build/80754af9/importlib-metadata_1631916693255/work
iniconfig @ file:///home/linux1/recipes/ci/iniconfig_1610983019677/work
more-itertools @ file:///tmp/build/80754af9/more-itertools_1637733554872/work
packaging @ file:///tmp/build/80754af9/packaging_1637314298585/work
pluggy @ file:///tmp/build/80754af9/pluggy_1615976315926/work
py @ file:///opt/conda/conda-bld/py_1644396412707/work
pyparsing @ file:///tmp/build/80754af9/pyparsing_1635766073266/work
pytest==6.2.4
toml @ file:///tmp/build/80754af9/toml_1616166611790/work
typing_extensions @ file:///opt/conda/conda-bld/typing_extensions_1647553014482/work
wcwidth==0.2.13
zipp @ file:///tmp/build/80754af9/zipp_1633618647012/work

name: beautifultable
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - attrs=21.4.0=pyhd3eb1b0_0
  - ca-certificates=2025.2.25=h06a4308_0
  - certifi=2021.5.30=py36h06a4308_0
  - importlib-metadata=4.8.1=py36h06a4308_0
  - importlib_metadata=4.8.1=hd3eb1b0_0
  - iniconfig=1.1.1=pyhd3eb1b0_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.3=he6710b0_2
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - more-itertools=8.12.0=pyhd3eb1b0_0
  - ncurses=6.4=h6a678d5_0
  - openssl=1.1.1w=h7f8727e_0
  - packaging=21.3=pyhd3eb1b0_0
  - pip=21.2.2=py36h06a4308_0
  - pluggy=0.13.1=py36h06a4308_0
  - py=1.11.0=pyhd3eb1b0_0
  - pyparsing=3.0.4=pyhd3eb1b0_0
  - pytest=6.2.4=py36h06a4308_2
  - python=3.6.13=h12debd9_1
  - readline=8.2=h5eee18b_0
  - setuptools=58.0.4=py36h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - toml=0.10.2=pyhd3eb1b0_0
  - typing_extensions=4.1.1=pyh06a4308_0
  - wheel=0.37.1=pyhd3eb1b0_0
  - xz=5.6.4=h5eee18b_1
  - zipp=3.6.0=pyhd3eb1b0_0
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - wcwidth==0.2.13
prefix: /opt/conda/envs/beautifultable

test.py::TableOperationsTestCase::test_sort_by_callable
test.py::TableOperationsTestCase::test_access_column_by_header test.py::TableOperationsTestCase::test_access_row_by_index test.py::TableOperationsTestCase::test_access_row_element_by_header test.py::TableOperationsTestCase::test_access_row_element_by_index test.py::TableOperationsTestCase::test_align_all test.py::TableOperationsTestCase::test_ansi_ellipsis test.py::TableOperationsTestCase::test_ansi_ellipsis_mb test.py::TableOperationsTestCase::test_ansi_sequences test.py::TableOperationsTestCase::test_ansi_strip test.py::TableOperationsTestCase::test_ansi_strip_mb test.py::TableOperationsTestCase::test_ansi_wrap test.py::TableOperationsTestCase::test_ansi_wrap_mb test.py::TableOperationsTestCase::test_append_column test.py::TableOperationsTestCase::test_append_row test.py::TableOperationsTestCase::test_column_count test.py::TableOperationsTestCase::test_contains test.py::TableOperationsTestCase::test_csv_export test.py::TableOperationsTestCase::test_csv_import test.py::TableOperationsTestCase::test_delitem_int test.py::TableOperationsTestCase::test_delitem_slice test.py::TableOperationsTestCase::test_delitem_str test.py::TableOperationsTestCase::test_eastasian_characters test.py::TableOperationsTestCase::test_empty_header test.py::TableOperationsTestCase::test_empty_table_by_column test.py::TableOperationsTestCase::test_empty_table_by_row test.py::TableOperationsTestCase::test_filter test.py::TableOperationsTestCase::test_get_column_header test.py::TableOperationsTestCase::test_get_column_index test.py::TableOperationsTestCase::test_get_string test.py::TableOperationsTestCase::test_getitem_slice test.py::TableOperationsTestCase::test_insert_column test.py::TableOperationsTestCase::test_insert_row test.py::TableOperationsTestCase::test_left_align test.py::TableOperationsTestCase::test_mixed_align test.py::TableOperationsTestCase::test_newline test.py::TableOperationsTestCase::test_newline_multiple_columns test.py::TableOperationsTestCase::test_pop_column_by_header test.py::TableOperationsTestCase::test_pop_column_by_position test.py::TableOperationsTestCase::test_pop_row test.py::TableOperationsTestCase::test_right_align test.py::TableOperationsTestCase::test_row_count test.py::TableOperationsTestCase::test_serialno test.py::TableOperationsTestCase::test_setitem_int test.py::TableOperationsTestCase::test_setitem_slice test.py::TableOperationsTestCase::test_setitem_str test.py::TableOperationsTestCase::test_signmode_plus test.py::TableOperationsTestCase::test_sort_by_header test.py::TableOperationsTestCase::test_sort_by_index test.py::TableOperationsTestCase::test_sort_by_index_reversed test.py::TableOperationsTestCase::test_sort_raises_exception test.py::TableOperationsTestCase::test_table_auto_width test.py::TableOperationsTestCase::test_table_width_zero test.py::TableOperationsTestCase::test_update_column test.py::TableOperationsTestCase::test_update_row test.py::TableOperationsTestCase::test_update_row_slice test.py::TableOperationsTestCase::test_wep_ellipsis test.py::TableOperationsTestCase::test_wep_strip test.py::TableOperationsTestCase::test_wep_wrap
.gitignore beautifultable/beautifultable.py
.gitignore beautifultable/beautifultable.py