Sort a list of objects by multiple attributes in Python

By James L.

When working with data stored in a list of objects, you often need to sort them based on multiple criteria.

In this blog post, we will discuss the following topics:

Sorting using sorted() function

In Python, the sorted() function is used to sort the elements of an iterable (such as lists, tuples, or strings). It creates a new sorted list without modifying the original list.

To sort a list of objects by multiple attributes, you can pass a tuple of key functions to the key parameter of the sorted() function.

For example, imagine you have a list of objects with employee information like names, ages, and salaries. You want to sort the list of objects first by age (ascending) and then by salary (descending). Here’s the code:

class Employee:
    def __init__(self, name, age, salary):
        self.name = name
        self.age = age
        self.salary = salary


# List of Employee objects
employees = [
    Employee("Alice", 30, 5000),
    Employee("Bob", 25, 6000),
    Employee("Charlie", 35, 4500),
    Employee("David", 25, 7000)
]

# Sorting the list of employees first by age (ascending) and then by salary (descending)
sorted_employees = sorted(employees, key=lambda x: (x.age, -x.salary))

# Printing the sorted list
for emp in sorted_employees:
    print(f"Name: {emp.name}, Age: {emp.age}, Salary: {emp.salary}")

Output:

Name: David, Age: 25, Salary: 7000
Name: Bob, Age: 25, Salary: 6000
Name: Alice, Age: 30, Salary: 5000
Name: Charlie, Age: 35, Salary: 4500

In this example, the key parameter of the sorted() function is set to a lambda function that returns a tuple(x.age, -x.salary). This means the sorting is first done by age in ascending order and then by salary in descending order (the negative sign is used to achieve descending order).

Sorting using sort() method

The sort() method in Python is very similar to the sorted() function, but it directly sorts the list in place rather than returning a new sorted list.

Similarly, you can pass a tuple of key functions to the key parameter of the sort() method.

Here’s an example:

class Employee:
    def __init__(self, name, age, salary):
        self.name = name
        self.age = age
        self.salary = salary


# List of Employee objects
employees = [
    Employee("Alice", 30, 5000),
    Employee("Bob", 25, 6000),
    Employee("Charlie", 35, 4500),
    Employee("David", 25, 7000)
]

# Sorting the list of employees first by age (ascending) and then by salary (descending)
employees.sort(key=lambda x: (x.age, -x.salary))

# Printing the sorted list
for emp in employees:
    print(f"Name: {emp.name}, Age: {emp.age}, Salary: {emp.salary}")

Output:

Name: David, Age: 25, Salary: 7000
Name: Bob, Age: 25, Salary: 6000
Name: Alice, Age: 30, Salary: 5000
Name: Charlie, Age: 35, Salary: 4500

In this example, the key parameter of the sort() method is set to a lambda function that returns a tuple(x.age, -x.salary). This means the sorting is first done by age in ascending order and then by salary in descending order (the negative sign is used to achieve descending order).

Sorting in reverse

You can sort a list of objects by multiple attributes in descending order by setting the reverse parameter of the sorted() method to True.

Here’s an example:

class Employee:
    def __init__(self, name, age, salary):
        self.name = name
        self.age = age
        self.salary = salary


# List of Employee objects
employees = [
    Employee("Alice", 30, 5000),
    Employee("Bob", 25, 6000),
    Employee("Charlie", 35, 4500),
    Employee("David", 25, 7000)
]

# Sorting the list of employees first by age (descending) and then by salary (descending)
sorted_employees = sorted(
    employees, key=lambda x: (x.age, x.salary), reverse=True)

# Printing the sorted list
for emp in sorted_employees:
    print(f"Name: {emp.name}, Age: {emp.age}, Salary: {emp.salary}")

In this example, the list of Employee objects is sorted based on their attributes (age) and (salary) in descending order using the sorted() function and a custom key function.

Similarly, you can set the reverse parameter of the sort() function to True.

employees.sort(key=lambda x: (x.age, x.salary), reverse=True)

Sorting using attrgetter()

You can use the attrgetter() function instead of a lambda function to efficiently access specific elements within an object based on their attributes.

Here’s an example:

from operator import attrgetter


class Employee:
    def __init__(self, name, age, salary):
        self.name = name
        self.age = age
        self.salary = salary


# List of Employee objects
employees = [
    Employee("Alice", 30, 5000),
    Employee("Bob", 25, 9000),
    Employee("Charlie", 35, 4500),
    Employee("David", 25, 7000)
]

# Sorting the list of employees first by age (ascending) and then by salary (ascending)
sorted_employees = sorted(
    employees, key=attrgetter('age', 'salary'))

# Printing the sorted list
for emp in sorted_employees:
    print(f"Name: {emp.name}, Age: {emp.age}, Salary: {emp.salary}")

Output:

Name: David, Age: 25, Salary: 7000
Name: Bob, Age: 25, Salary: 9000
Name: Alice, Age: 30, Salary: 5000
Name: Charlie, Age: 35, Salary: 4500

Similarly, for the sort() method, use the following code:

employees.sort(key=attrgetter('age', 'salary'))

Case-insensitive sorting

By default, in Python, uppercase letters come before lowercase ones in alphabetical sorting. For example, ‘Banana’ would appear before ‘apple’ in alphabetical sorting, as the uppercase ‘B’ in ‘Banana’ precedes the lowercase ‘a’ in ‘apple’.

To handle case-insensitivity properly, you can either use, lower(), casefold(), or locale.strxfrm() methods.

Using lower():

In Python, the lower() method returns a new string with all alphabetic characters converted to lowercase. However, it may not handle Unicode characters.

Here’s an example that sorts a list of Student objects first by subject (alphabetically) and then by marks (descending):

class Student:
    def __init__(self, name, subject, marks):
        self.name = name
        self.subject = subject
        self.marks = marks


# List of Student objects
students = [
    Student("David", "physics", 70),
    Student("Charlie", "Chemistry", 45),
    Student("Alice", "Physics", 50),
    Student("Bob", "chemistry", 60)
]

# Sorting the list of students first by subject (alphabetically) and then by marks (descending)
sorted_students = sorted(students, key=lambda x: (x.subject.lower(), -x.marks))

# Printing the sorted list
for student in sorted_students:
    print(
        f"Name: {student.name}, Subject: {student.subject}, Marks: {student.marks}")

Output:

Name: Bob, Subject: chemistry, Marks: 60
Name: Charlie, Subject: Chemistry, Marks: 45
Name: David, Subject: physics, Marks: 70
Name: Alice, Subject: Physics, Marks: 50

Similarly, for the sort() method:

class Student:
    def __init__(self, name, subject, marks):
        self.name = name
        self.subject = subject
        self.marks = marks


# List of Student objects
students = [
    Student("David", "physics", 70),
    Student("Charlie", "Chemistry", 45),
    Student("Alice", "Physics", 50),
    Student("Bob", "chemistry", 60)
]

# Sorting the list of students first by subject (alphabetically) and then by marks (descending)
students.sort(key=lambda x: (x.subject.lower(), -x.marks))

# Printing the sorted list
for student in students:
    print(
        f"Name: {student.name}, Subject: {student.subject}, Marks: {student.marks}")

Using casefold():

In Python, the casefold() method returns a new string with all alphabetic characters converted to lowercase. However, it’s more aggressive in converting characters to their lowercase forms and is recommended for case-insensitive comparisons, especially when dealing with Unicode characters.

Here’s the same example, utilizing casefold():

class Student:
    def __init__(self, name, subject, marks):
        self.name = name
        self.subject = subject
        self.marks = marks


# List of Student objects
students = [
    Student("David", "physics", 70),
    Student("Charlie", "Chemistry", 45),
    Student("Alice", "Physics", 50),
    Student("Bob", "chemistry", 60)
]

# Sorting the list of students first by subject (alphabetically) and then by marks (descending)
sorted_students = sorted(students, key=lambda x: (
    x.subject.casefold(), -x.marks))

# Printing the sorted list
for student in sorted_students:
    print(
        f"Name: {student.name}, Subject: {student.subject}, Marks: {student.marks}")

Output:

Name: Bob, Subject: chemistry, Marks: 60
Name: Charlie, Subject: Chemistry, Marks: 45
Name: David, Subject: physics, Marks: 70
Name: Alice, Subject: Physics, Marks: 50

Similarly, for the sort() method:

class Student:
    def __init__(self, name, subject, marks):
        self.name = name
        self.subject = subject
        self.marks = marks


# List of Student objects
students = [
    Student("David", "physics", 70),
    Student("Charlie", "Chemistry", 45),
    Student("Alice", "Physics", 50),
    Student("Bob", "chemistry", 60)
]

# Sorting the list of students first by subject (alphabetically) and then by marks (descending)
students.sort(key=lambda x: (x.subject.casefold(), -x.marks))

# Printing the sorted list
for student in students:
    print(
        f"Name: {student.name}, Subject: {student.subject}, Marks: {student.marks}")

Using locale.strxfrm():

locale.strxfrm() is another method to handle case-insensitive sorting that relies on the current locale’s collation order. It may be more suitable for case-insensitive sorting in some specific locales.

Here’s the example with locale.strxfrm():

import locale
from functools import cmp_to_key


class Student:
    def __init__(self, name, subject, marks):
        self.name = name
        self.subject = subject
        self.marks = marks


# List of Student objects
students = [
    Student("David", "physics", 70),
    Student("Charlie", "Chemistry", 45),
    Student("Alice", "Physics", 50),
    Student("Bob", "chemistry", 60)
]

# Setting the locale to the user's default setting
locale.setlocale(locale.LC_ALL, '')

# Sorting the list of students first by subject (alphabetically) and then by marks (descending)
sorted_students = sorted(students, key=cmp_to_key(
    lambda x, y: locale.strxfrm(x.subject) - locale.strxfrm(y.subject) or y.marks - x.marks))

# Printing the sorted list
for student in sorted_students:
    print(
        f"Name: {student.name}, Subject: {student.subject}, Marks: {student.marks}")

Output:

Name: Bob, Subject: chemistry, Marks: 60
Name: Charlie, Subject: Chemistry, Marks: 45
Name: David, Subject: physics, Marks: 70
Name: Alice, Subject: Physics, Marks: 50

Error handling

Here are some common errors you may encounter when sorting list of objects by multiple attributes in Python and how to handle them (handling error is similar for both sorted() and sort()):

TypeError:

This error occurs if you try to sort a list of objects by attribute of different data types that cannot be compared (e.g., integers and strings).

Check the data types before sorting and convert them if necessary.

Here’s an example that prints the error message if the comparison fails due to incompatible data types.

class Employee:
    def __init__(self, name, age, salary):
        self.name = name
        self.age = age
        self.salary = salary


# List of Employee objects
employees = [
    Employee("Alice", "Thirty", 5000),
    Employee("Bob", 25, 6000),
    Employee("Charlie", 35, 4500),
    Employee("David", 25, 7000)
]

try:
    # Sorting the list of employees first by age (ascending) and then by salary (descending)
    sorted_employees = sorted(employees, key=lambda x: (x.age, -x.salary))

    # Printing the sorted list
    for emp in sorted_employees:
        print(f"Name: {emp.name}, Age: {emp.age}, Salary: {emp.salary}")
except TypeError:
    print("Error: Unable to compare due to incompatible types")

Output:

Error: Unable to compare due to incompatible types

In this example, the age of one of the employees is passed as a string (“Thirty”) instead of an integer. When the sorted() function tries to compare these ages during sorting, it encounters an error because it can’t directly compare an integer to a string. As a result, a TypeError is raised.

AttributeError:

This error occurs if you try to access an attribute that does not exist in one or more objects in the list.

Ensure that all objects in the list have the same attributes before sorting. You can handle missing attributes gracefully using error-handling constructs like try-except blocks.

Here’s an example:

class Employee:
    def __init__(self, name, age):
        self.name = name
        self.age = age


# List of Employee objects
employees = [
    Employee("Alice", 30),
    Employee("Bob", 25),
    Employee("Charlie", 35),
    Employee("David", 25)
]

try:
    # Sorting the list of employees first by age (ascending) and then by weight (ascending)
    sorted_employees = sorted(employees, key=lambda x: (x.age, x.weight))

    # Printing the sorted list
    for emp in sorted_employees:
        print(f"Name: {emp.name}, Age: {emp.age}, Weight: {emp.weight}")

except AttributeError:
    print("AttributeError: Attribute is missing")

Output:

AttributeError: Attribute is missing

In this example, the Employee class has two attributes name, and age. We try to sort the list of employees objects based on the attributes age and weight. But the Employee class doesn’t have the weight attribute. This will raise an AttributeError and print an error message indicating that an attribute is missing.

Conclusion

In this comprehensive guide, we explored various methods and considerations for sorting a list of objects by multiple attributes in Python. From using the built-in sorted() function and sort() method to handling case-insensitive sorting and different data types, you are now equipped to tackle various sorting challenges effectively.

Remember these key takeaways:

Choice of method:

Use sorted() for a new sorted list and sort() to modify the original list.
Use sort() if performance is a major concern.
attrgetter() is faster than lambda functions, especially for larger datasets.

Case-insensitive sorting:

Use lower() for simple ASCII text, casefold() for broader Unicode support, and locale.strxfrm for locale-specific sorting.

Error Handling:

Anticipate potential errors like type mismatches and missing attributes, and implement graceful error handling mechanisms.

By understanding these concepts and applying the techniques covered in this guide, you can confidently conquer list sorting tasks in your Python projects!

Happy coding!!! Living with code →