Python Sorting: sorted() and list.sort() Guide

Python provides two built-in sorting mechanisms that serve different purposes. The `sorted()` function is a built-in that works on any iterable and returns a new sorted list. The `list.sort()` method...

Key Insights

  • sorted() returns a new sorted list from any iterable without modifying the original, while list.sort() sorts in-place and returns None—choose based on whether you need to preserve the original data.
  • Both methods accept identical key and reverse parameters for custom sorting logic, and Python’s sort is stable, meaning equal elements maintain their relative order.
  • Use sorted() for general-purpose sorting and readability; use list.sort() when memory efficiency matters and you don’t need the original list.

Understanding the Core Difference

Python provides two built-in sorting mechanisms that serve different purposes. The sorted() function is a built-in that works on any iterable and returns a new sorted list. The list.sort() method belongs exclusively to list objects and modifies them in-place.

Here’s the fundamental difference in action:

# sorted() - returns new list, original unchanged
original = [3, 1, 4, 1, 5]
result = sorted(original)
print(f"Original: {original}")  # [3, 1, 4, 1, 5]
print(f"Result: {result}")      # [1, 1, 3, 4, 5]

# list.sort() - modifies in-place, returns None
numbers = [3, 1, 4, 1, 5]
return_value = numbers.sort()
print(f"Numbers: {numbers}")         # [1, 1, 3, 4, 5]
print(f"Return value: {return_value}")  # None
Feature sorted() list.sort()
Return value New sorted list None
Modifies original No Yes
Works on Any iterable Lists only
Memory usage Creates new list In-place (efficient)

Using sorted() - The Functional Approach

The sorted() function is your go-to for most sorting scenarios. Its syntax is straightforward: sorted(iterable, key=None, reverse=False). The power of sorted() lies in its versatility—it accepts any iterable type.

# Sorting different iterable types
numbers = sorted([5, 2, 8, 1])
print(numbers)  # [1, 2, 5, 8]

# Works on tuples
tuple_sorted = sorted((5, 2, 8, 1))
print(tuple_sorted)  # [1, 2, 5, 8] - returns list, not tuple

# Sorting strings (by character)
chars = sorted("python")
print(chars)  # ['h', 'n', 'o', 'p', 't', 'y']

# Sorting dictionary keys
data = {'z': 1, 'a': 2, 'm': 3}
sorted_keys = sorted(data)
print(sorted_keys)  # ['a', 'm', 'z']

# Sorting dictionary by values
sorted_by_value = sorted(data.items(), key=lambda x: x[1])
print(sorted_by_value)  # [('z', 1), ('a', 2), ('m', 3)]

The functional nature of sorted() makes it ideal for data pipelines and situations where immutability matters:

original_scores = [85, 92, 78, 95]
top_scores = sorted(original_scores, reverse=True)[:3]

# Original preserved for other uses
average = sum(original_scores) / len(original_scores)
print(f"Top 3: {top_scores}")
print(f"Average: {average}")

Using list.sort() - The In-Place Method

The list.sort() method has identical parameters to sorted() but operates directly on the list object. It returns None, which is Python’s way of signaling that the operation modifies the object in-place.

# Proper usage
scores = [85, 92, 78, 95, 88]
scores.sort()
print(scores)  # [78, 85, 88, 92, 95]

# Common mistake - assigning the return value
results = scores.sort()  # results is None!
print(results)  # None

Use list.sort() when you’re working with large datasets and don’t need the original order, or when you’re explicitly managing state:

class LeaderBoard:
    def __init__(self):
        self.scores = []
    
    def add_score(self, score):
        self.scores.append(score)
        self.scores.sort(reverse=True)  # Keep sorted
    
    def top_10(self):
        return self.scores[:10]

Key Parameters: key and reverse

Both sorted() and list.sort() support the key parameter, which accepts a function that extracts a comparison key from each element. This is where sorting becomes powerful.

# Sort strings by length
words = ["python", "is", "awesome", "for", "sorting"]
by_length = sorted(words, key=len)
print(by_length)  # ['is', 'for', 'python', 'awesome', 'sorting']

# Case-insensitive string sorting
names = ["alice", "Bob", "Charlie", "david"]
sorted_names = sorted(names, key=str.lower)
print(sorted_names)  # ['alice', 'Bob', 'Charlie', 'david']

# Reverse sorting
numbers = [3, 1, 4, 1, 5, 9, 2, 6]
descending = sorted(numbers, reverse=True)
print(descending)  # [9, 6, 5, 4, 3, 2, 1, 1]

Sorting complex data structures is where key truly shines:

# Sorting list of dictionaries
employees = [
    {"name": "Alice", "salary": 70000, "age": 30},
    {"name": "Bob", "salary": 85000, "age": 25},
    {"name": "Charlie", "salary": 70000, "age": 35}
]

# Sort by salary, then by age for ties
by_salary = sorted(employees, key=lambda e: (e["salary"], e["age"]))
for emp in by_salary:
    print(f"{emp['name']}: ${emp['salary']} (age {emp['age']})")
# Alice: $70000 (age 30)
# Charlie: $70000 (age 35)
# Bob: $85000 (age 25)

Advanced Sorting Techniques

For complex sorting scenarios, Python offers the operator module with itemgetter and attrgetter functions. These are often faster than lambda functions and more readable:

from operator import itemgetter, attrgetter

# Sorting tuples by specific index
students = [("Alice", 85), ("Bob", 92), ("Charlie", 78)]
by_grade = sorted(students, key=itemgetter(1), reverse=True)
print(by_grade)  # [('Bob', 92), ('Alice', 85), ('Charlie', 78)]

# Multiple sort keys with itemgetter
data = [("A", 2, 5), ("B", 1, 3), ("C", 2, 1)]
sorted_data = sorted(data, key=itemgetter(1, 2))
print(sorted_data)  # [('B', 1, 3), ('C', 2, 1), ('A', 2, 5)]

# Sorting objects by attribute
class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age
    
    def __repr__(self):
        return f"Person({self.name}, {self.age})"

people = [Person("Alice", 30), Person("Bob", 25), Person("Charlie", 30)]
by_age_name = sorted(people, key=attrgetter("age", "name"))
print(by_age_name)  # [Person(Bob, 25), Person(Alice, 30), Person(Charlie, 30)]

Python’s sort is stable, meaning elements that compare equal retain their original relative order. This enables elegant multi-level sorting:

# Sort by secondary criterion first, then primary
data = [("A", 2), ("B", 1), ("C", 2), ("D", 1)]
data.sort(key=itemgetter(0))  # Sort by letter first
data.sort(key=itemgetter(1))  # Then by number (stable!)
print(data)  # [('B', 1), ('D', 1), ('A', 2), ('C', 2)]

Performance Considerations

Both methods use Timsort, Python’s hybrid sorting algorithm with O(n log n) average complexity. The key difference is memory usage:

import timeit
import sys

# Memory comparison
large_list = list(range(100000, 0, -1))

# sorted() creates new list
sorted_result = sorted(large_list)
print(f"Original size: {sys.getsizeof(large_list)} bytes")
print(f"Sorted copy size: {sys.getsizeof(sorted_result)} bytes")

# Timing comparison
def test_sorted():
    data = list(range(10000, 0, -1))
    result = sorted(data)

def test_sort():
    data = list(range(10000, 0, -1))
    data.sort()

sorted_time = timeit.timeit(test_sorted, number=1000)
sort_time = timeit.timeit(test_sort, number=1000)

print(f"sorted(): {sorted_time:.4f}s")
print(f"list.sort(): {sort_time:.4f}s")
# list.sort() is typically slightly faster due to no list creation

Choose sorted() when:

  • You need to preserve the original data
  • Working with non-list iterables
  • Writing functional-style code
  • Clarity matters more than marginal performance gains

Choose list.sort() when:

  • You don’t need the original order
  • Working with very large lists where memory matters
  • Building stateful objects that maintain sorted data

Common Pitfalls and Solutions

The most frequent mistake is treating list.sort() like it returns a value:

# WRONG - result is None
numbers = [3, 1, 4]
result = numbers.sort()
print(result)  # None

# CORRECT
numbers = [3, 1, 4]
numbers.sort()
print(numbers)  # [1, 3, 4]

# OR use sorted()
numbers = [3, 1, 4]
result = sorted(numbers)
print(result)  # [1, 3, 4]

Another common issue is unintended mutation with shared references:

# Problem: shared reference
original = [3, 1, 4]
reference = original
reference.sort()
print(original)  # [1, 3, 4] - modified!

# Solution: use sorted() for independent copy
original = [3, 1, 4]
independent = sorted(original)
independent.append(99)
print(original)  # [3, 1, 4] - unchanged

When sorting custom objects, ensure your key function handles edge cases:

# Handling None values
data = [{"value": 5}, {"value": None}, {"value": 3}]

# Safe sorting with None handling
sorted_data = sorted(data, key=lambda x: (x["value"] is None, x["value"]))
print(sorted_data)  # None values go first

Understanding when to use sorted() versus list.sort() is fundamental to writing clean, efficient Python code. Default to sorted() for its versatility and safety, but reach for list.sort() when you need that extra performance edge with large datasets. Both methods are powerful tools that, when used correctly, make sorting in Python remarkably straightforward.

Liked this? There's more.

Every week: one practical technique, explained simply, with code you can use immediately.