While map(), filter(), zip(), and enumerate() are explicitly designed as functional tools, Python’s built-in reversed() and sorted() functions also adhere to core functional programming principles. They are pure functions that operate on iterables, produce new sequences without modifying the originals, and promote a declarative style of programming. Understanding their functional characteristics is key to using them effectively and idiomatically.

The Functional Nature of reversed() and sorted()

Both reversed() and sorted() are non-destructive. This is their most critical functional trait. Unlike the list.sort() method, which mutates the list in-place and returns None, these functions take an iterable as input and return a brand new object containing the reversed or sorted data. This aligns with the functional programming tenet of avoiding side effects, making code more predictable, easier to reason about, and safer to use within larger expressions.

# Demonstrating non-destructive behavior
original_list = [3, 1, 4, 1, 5]

# sorted() returns a new list
new_sorted_list = sorted(original_list)
print("Original:", original_list)  # Output: Original: [3, 1, 4, 1, 5]
print("Sorted:  ", new_sorted_list) # Output: Sorted:   [1, 1, 3, 4, 5]

# reversed() returns a new iterator (often converted to a list)
new_reversed_iterator = reversed(original_list)
new_reversed_list = list(new_reversed_iterator)
print("Reversed:", new_reversed_list) # Output: Reversed: [5, 1, 4, 1, 3]
print("Original:", original_list)    # Output: Original: [3, 1, 4, 1, 5]

Return Types: List vs. Iterator

A crucial distinction exists in what these functions return. sorted() always returns a new list, regardless of the input iterable type. This is because it needs to gather all elements into a contiguous structure to perform the sorting algorithm.

Conversely, reversed() returns a lazy iterator (specifically, a list_reverseiterator object when given a list). It does not create a new list immediately; it merely provides a way to access the original sequence’s elements in reverse order. This is memory-efficient, especially for large sequences, as it doesn’t require copying all elements. However, if you need a persistent reversed sequence (to use multiple times or index into it), you must materialize the iterator into a list() or tuple().

my_string = "hello"
reversed_iterator = reversed(my_string)
print(reversed_iterator)  # Output: <reversed object at 0x...>
# To get a string back, join the iterator. To get a list, pass it to list().
reversed_string = ''.join(reversed_iterator)
print(reversed_string)    # Output: olleh

# sorted() always gives a list, so we join it to get a sorted string.
sorted_string = ''.join(sorted(my_string))
print(sorted_string)      # Output: ehllo

Custom Sorting and Reversing with key and reverse

Both functions accept a key function, a hallmark of functional design. The key parameter allows you to transform each element for the purposes of comparison without altering the actual data being returned. This is far cleaner and more efficient than the outdated DSU (Decorate-Sort-Undecorate) pattern.

The reverse parameter is a simple boolean to control ascending vs. descending order.

students = [("Alice", 88), ("Bob", 95), ("Charlie", 72)]
# Sort by grade (index 1) in descending order
sorted_by_grade = sorted(students, key=lambda x: x[1], reverse=True)
print(sorted_by_grade) # Output: [('Bob', 95), ('Alice', 88), ('Charlie', 72)]

# Reverse based on the length of the name
reversed_by_name_length = sorted(students, key=lambda x: len(x[0]), reverse=True)
print(reversed_by_name_length) # Output: [('Charlie', 72), ('Alice', 88), ('Bob', 95)]

Best Practices and Common Pitfalls

  1. Immutability: Always remember that the original iterable is left unchanged. This is a feature, not a bug, but it can be a pitfall if you mistakenly assume a mutation happened (my_list = sorted(my_list) is correct; sorted(my_list) on its own is not).

  2. Efficiency for reversed(): Since reversed() returns an iterator, calling it on the same sequence multiple times will create new iterators. If you need to use the reversed order more than once, it’s more efficient to materialize it into a list once and reuse that list.

  3. Sorting Heterogeneous Data: While sorted() can technically order heterogenous data (e.g., [1, 'a']), it will raise a TypeError if the elements cannot be compared (e.g., [1, 'a', {}]). Ensure your data is homogeneous or that your key function returns comparable types.

  4. Stability: The sorted() function guarantees a stable sort. This means that when multiple elements have the same key, their original order is preserved. This is incredibly important for performing complex, multi-criteria sorts by chaining multiple sorted() calls with different keys, starting with the least significant criterion.

# Stable sort allows for multi-level sorting
data = [("apple", 2), ("banana", 1), ("orange", 2), ("pear", 1)]
# First, sort by name (secondary criterion)
by_name = sorted(data, key=lambda x: x[0])
# Then, sort by number (primary criterion). The original order of names is preserved for equal numbers.
by_number_then_name = sorted(by_name, key=lambda x: x[1])
print(by_number_then_name)
# Output: [('banana', 1), ('pear', 1), ('apple', 2), ('orange', 2)]