My colleague Axel Hecht showed me something I didn't know about sorting in Python.

In Python you can sort with a tuple. It's best illustrated with a simple example:


>>> items = [(1, 'B'), (1, 'A'), (2, 'A'), (0, 'B'), (0, 'a')]
>>> sorted(items)
[(0, 'B'), (0, 'a'), (1, 'A'), (1, 'B'), (2, 'A')]

By default the sort and the sorted built-in function notices that the items are tuples so it sorts on the first element first and on the second element second.

However, notice how you get (0, 'B') appearing before (0, 'a'). That's because upper case comes before lower case characters. However, suppose you wanted to apply some "humanization" on that and sort case insensitively. You might try:


>>> sorted(items, key=str.lower)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: descriptor 'lower' requires a 'str' object but received a 'tuple'

which is an error we deserve because this won't work for the first part of each tuple.

We could try to write a lambda function (e.g. sorted(items, key=lambda x: x.lower() if isinstance(x, str) else x)) but that's not going to work because you'll only ever get to apply that to the first item.

Without further ado, here's how you do it. A lambda function that returns a tuple:


>>> sorted(items, key=lambda x: (x[0], x[1].lower()))
[(0, 'a'), (0, 'B'), (1, 'A'), (1, 'B'), (2, 'A')]

And there you have it! Thanks for sharing Axel!

As a bonus item for people still reading...
I'm sure you know that you can reverse a sort order simply by passing in sorted(items, reverse=True, ...) but what if you want to have different directions depend on the key that you're sorting on.

Using the technique of a lambda function that returns a tuple, here's how we sort a slightly more advanced structure:


>>> peeps = [{'name': 'Bill', 'salary': 1000}, {'name': 'Bill', 'salary': 500}, {'name': 'Ted', 'salary': 500}]

And now, sort with a lambda function returning a tuple:


>>> sorted(peeps, key=lambda x: (x['name'], x['salary']))
[{'salary': 500, 'name': 'Bill'}, {'salary': 1000, 'name': 'Bill'}, {'salary': 500, 'name': 'Ted'}]

Makes sense, right? Bill comes before Ted and 500 comes before 1000. But how do you sort it like that on the name but reverse on the salary? Simple, negate it:


>>> sorted(peeps, key=lambda x: (x['name'], -x['salary']))
[{'salary': 1000, 'name': 'Bill'}, {'salary': 500, 'name': 'Bill'}, {'salary': 500, 'name': 'Ted'}]

UPDATE

Webucator has made a video explaining this blog post as a video.

Thanks Nat!

Comments

Post your own comment
Fred

Nice technique! Small nit-pick: "... which is an error we deserve because this won't work for the first part of each tuple." That's not accurate. It doesn't fail because str.lower can't be applied to the first part of the tuple. It fails because it can't be applied to the tuple (the whole thing, which is an object in its own right). The "key" function gets passed every item in the iterable you're sorting, and str.lower can't take tuples. It does *not* get passed the contents inside the tuple one by one.

Saim Fadhley

I've seen this used in combination with the heapq module - you can maintain a sorted list of stuff by just putting tuples like this into the heapq. The neat thing is that unlike sorting the list of tuples it can maintain the sorted condition as an invariant property.

areader

also if you want to reverse the whole thing, there is a 'reverse' parameter in sorted:
sorted(peeps, key=lambda x: (x['name'], x['salary']), reverse=True)

and BTW did i mention that i love python for things like this?

Davina

In Python 2.x you can also use "lambda (x, y): (x, y.lower())", which is a function excepting a tuple, that will be unpacked before the function body executes

ëRiC

Nice! I've used itemgetter for the key before! But yea If I just set up the tuple correctly I can go without that!! :] thx!

Siddarth

This helped a lot , Thanks!!

Anonymous

nice!

Caoyuan

What if I want to sort by name descending and by salary ascending?

dude

2020 going strong

Your email will never ever be published.

Related posts