Python 3: Sorting a List of Objects or Dictionaries by Multiple Keys Bidirectionally

Basic Sorting


# Sort a list of dictionary objects by a key - case sensitive
from operator import itemgetter
mylist = sorted(mylist, key=itemgetter('name'))

# Sort a list of dictionary objects by a key - case insensitive 
mylist = sorted(mylist, key=lambda k: k['name'].lower())

The above methods for sorting a list of dictionary objects is very common and suitable for sorting lists containing dictionaries or objects. sorted also allows your key function to return a tuple of values if you wish to sort your list by more than one key like so:

Sorting by Multiple Keys


# Sort by multiple keys - case sensitive
from operator import itemgetter
mylist = sorted(mylist, key=itemgetter('name', 'age'))

# Sort by multiple keys - case insensitive
mylist = sorted(mylist, key=lambda k: (k['name'].lower(), k['age']))

Bidirectional Sorting with Numeric Values

Sorting by multiple keys bidirectionally is simple when dealing with numeric values.


from functools import cmp_to_key
from operator import itemgetter, methodcaller

def cmp(a, b):
    return (a > b) - (a < b)


def multikeysort(items, columns, functions={}, getter=itemgetter):
    """Sort a list of dictionary objects or objects by multiple keys bidirectionally.
    Keyword Arguments:
    items -- A list of dictionary objects or objects
    columns -- A list of column names to sort by. Use -column to sort in descending order
    functions -- A Dictionary of Column Name -> Functions to normalize or process each column value
    getter -- Default "getter" if column function does not exist
              operator.itemgetter for Dictionaries
              operator.attrgetter for Objects
    """
    comparers = []
    for col in columns:
        column = col[1:] if col.startswith('-') else col
        if not column in functions:
            functions[column] = getter(column)
        comparers.append((functions[column], 1 if column == col else -1))

    def comparer(left, right):
        for func, polarity in comparers:
            result = cmp(func(left), func(right))
            if result:
                return polarity * result
        else:
            return 0

    return sorted(items, key=cmp_to_key(comparer))


def compose(inner_func, *outer_funcs):
    """Compose multiple unary functions together into a single unary function"""
    if not outer_funcs:
        return inner_func
    outer_func = compose(*outer_funcs)
    return lambda *args, **kwargs: outer_func(inner_func(*args, **kwargs))

And here are a few examples of them in use:


# We will use a list of dictionary objects representing students, their classes and their current grade
students = [
    {'name': 'Paul Allen', 'class': 'Science', 'grade': 'A'},
    {'name': 'paul allen', 'class': 'Math', 'grade': 'C'},
    {'name': 'Bob Lewis', 'class': 'Science', 'grade': 'D'},
    {'name': 'Bob Lewis', 'class': 'math', 'grade': 'b'},
    {'name': 'bob Lewis', 'class': 'History', 'grade': 'f'},
]

# Sort by class and grade descending, case sensitive
ex1 = multikeysort(students, ['class', '-grade'])

# Sort by class and grade descending, case insensitive
# Create a unary function that will get the column value and then call the .lower() method
get_class = compose(itemgetter('class'), methodcaller('lower'))
get_grade = compose(itemgetter('grade'), methodcaller('lower'))

ex2 = multikeysort(students, ['class', '-grade'],
                   {'class': get_class, 'grade': get_grade})