Almost every Python developer hits this exact gotcha early on: you call list.sort(), assign it to a new variable, print it out, and stare at an unexpected None. Don't sweat it - here is a quick breakdown of what is actually happening behind the scenes and how to dodge it next time.

  1. Calling list.sort() sorts your list right where it sits and explicitly returns None. Never try to assign its result.

  2. The built-in sorted() creates a fresh, sorted copy and leaves your original sequence completely untouched.

  3. Both methods let you pass custom key= logic and a reverse=True flag whenever you need them.

  4. Python’s sorting algorithm is strictly stable: anytime two items compare equal, they keep their original relative positions.

The Classic Gotcha: sort() Returns None

Python

python11 lines
# Wrong
numbers = [3, 1, 4, 1, 5]
sorted_numbers = numbers.sort()  # sorted_numbers is None
print(sorted_numbers)  # None

# Correct
numbers.sort()
print(numbers)  # [1, 1, 3, 4, 5]

# Or use sorted() if you need a new list
sorted_numbers = sorted(numbers)

The method list.sort() rearranges elements in place and returns None entirely by design - it's Python's way of reminding you that the original data was altered. Whenever you want to store the sorted outcome directly into a variable, reach for sorted() instead.

list.sort() vs sorted(): How to Choose

Under the hood, both rely on Timsort (which caps out at O(n log n) even in the worst scenarios) and take the exact same key= and reverse= arguments. The real trade-off comes down to whether you want to mutate your existing data or keep things flexible.

list.sort() sorted()
Return value None A brand-new sorted list
Modifies original Yes No
Compatible types Only lists Any iterable
Memory usage Leaner (in-place) Allocates a fresh copy

Pick list.sort() when you are totally fine discarding the prior order and want to avoid unnecessary memory overhead. Opt for sorted() whenever you need to preserve the source collection intact, or when you are sorting things like tuples, sets, strings, and generator outputs.

Python

python7 lines
# sorted() works on any iterable
print(sorted((3, 1, 2)))        # tuple: [1, 2, 3]
print(sorted({3, 1, 2}))        # set: [1, 2, 3]
print(sorted("cba"))            # string: ['a', 'b', 'c']

# list.sort() is list-only
(3, 1, 2).sort()  # AttributeError: 'tuple' object has no attribute 'sort'

Custom Ordering with the key Parameter

The key parameter takes any function or callable you want. Python runs it exactly once per item to generate an internal sorting score, ordering everything based on those scores rather than raw element comparisons.

Python

python13 lines
words = ["banana", "apple", "kiwi", "cherry"]

# Sort by string length
words.sort(key=len)
print(words)  # ['kiwi', 'apple', 'banana', 'cherry']

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

# Sort by last character
sorted(words, key=lambda x: x[-1])

Sorting Lists of Dictionaries

You will run into this pattern constantly when handling JSON payloads or database records. You can point the key function at whichever dictionary field you care about:

Python

python14 lines
students = [
    {"name": "Alice", "score": 88},
    {"name": "Bob", "score": 95},
    {"name": "Charlie", "score": 72},
]

# Sort by score ascending
by_score = sorted(students, key=lambda x: x["score"])

# Sort by score descending
by_score_desc = sorted(students, key=lambda x: x["score"], reverse=True)

# Sort by name alphabetically
by_name = sorted(students, key=lambda x: x["name"])

If you are just pulling a dictionary value directly, grabbing operator.itemgetter tends to read cleaner and runs a bit faster than an inline lambda:

Python

python4 lines
from operator import itemgetter

sorted(students, key=itemgetter("score"))
sorted(students, key=itemgetter("score", "name"))  # multi-field

Working with Custom Class Instances

Whenever you need to sort your own custom objects, an inline lambda or operator.attrgetter makes it simple to target specific instance attributes:

Python

python15 lines
from operator import attrgetter

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

students = [Student("Alice", 88), Student("Bob", 95), Student("Charlie", 72)]

# Sort by score
students.sort(key=attrgetter("score"))

# Sort by multiple attributes: score desc, then name asc
students.sort(key=attrgetter("name"))   # secondary sort first
students.sort(key=attrgetter("score"), reverse=True)  # primary sort second

That multi-step sort works reliably thanks to stability: when the primary sort kicks in, any matching elements retain whatever arrangement your earlier secondary sort gave them.

Multi-Field Sorting in a Single Pass

Instead of doing multiple sorting passes, a cleaner trick is packing your comparison values into a tuple inside the key function. Python evaluates tuples left-to-right, index-by-index:

Python

python9 lines
students = [
    {"name": "Alice", "grade": "A", "score": 95},
    {"name": "Bob", "grade": "A", "score": 88},
    {"name": "Charlie", "grade": "B", "score": 95},
]

# Sort by grade ascending, then score descending
# Negate score to reverse just that field
result = sorted(students, key=lambda x: (x["grade"], -x["score"]))

Because -95 < -88, the entry ("A", -95) neatly surfaces before ("A", -88). Keep in mind this quick minus-sign inversion only works for numerical values; if you are juggling mixed string directions, stick to the two-pass approach shown earlier.

Gracefully Handling Missing Dictionary Keys

If you are sorting through messy real-world data where some records might be missing the target key altogether, swap brackets for .get() with a safe fallback:

Python

python10 lines
data = [
    {"name": "Alice", "score": 90},
    {"name": "Bob"},  # no score
    {"name": "Charlie", "score": 75},
]

# float("inf") ensures missing scores sort to the end
# Without a fallback, x["score"] raises KeyError on the Bob entry
sorted(data, key=lambda x: x.get("score", float("inf")))
# Result: Alice (90), Charlie (75) sorted ascending, Bob last

Skipping that fallback value causes Python to immediately throw a KeyError the second it encounters a record without that key.

Understanding Sort Stability

Python's sorting mechanics are guaranteed to be stable. That means if two items have identical sorting keys, their original positions relative to each other will never get scrambled. This comes in handy anytime you want ties resolved naturally by the order data was entered.

Python

python6 lines
data = [("Alice", 2), ("Bob", 1), ("Charlie", 2), ("Dave", 1)]

# Sort by score only
sorted(data, key=lambda x: x[1])
# [('Bob', 1), ('Dave', 1), ('Alice', 2), ('Charlie', 2)]
# Bob comes before Dave (original order), Alice before Charlie (original order)

Because stability is baked in, chaining multiple sorting passes works predictably without accidentally wiping out the ordering work you established in earlier steps.

Reversing the Order

Both sorting options take a boolean reverse=True flag when you need descending order:

Python

python6 lines
numbers = [3, 1, 4, 1, 5, 9]

numbers.sort(reverse=True)
print(numbers)  # [9, 5, 4, 3, 1, 1]

desc = sorted(numbers, reverse=True)

Just be aware that using reverse=True alongside a tuple key will flip the sorting direction for every single item inside that tuple. If you only want to invert one specific field, selectively negate numeric values individually instead.

Performance: When Does It Actually Matter?

In everyday scripts, picking between sort() and sorted() mostly boils down to readability. You only really need to obsess over micro-optimizations once you are processing serious volume:

  • list.sort() avoids churning through memory to allocate a brand-new list, which pays off if you are sorting bulky arrays inside tight loops.

  • Timsort detects and leverages existing order, meaning nearly-sorted inputs finish way faster than the worst-case O(n log n) runtime.

  • If your goal is just pulling the top handful of records, heapq.nlargest(n, iterable, key=...) avoids sorting the entire collection and runs noticeably faster than slicing.

Python

python4 lines
import heapq

scores = [88, 95, 72, 91, 83]
top3 = heapq.nlargest(3, scores)  # [95, 91, 88], faster than sorted()[-3:]

If you ever need to verify your environment setup before running these, verifying your Python installation only takes a quick terminal check. All of these core sorting behaviors have stayed rock-solid and consistent ever since Python 3.0.

For heavier data processing pipelines, keeping your packages tidy inside isolated conda environments makes it straightforward to test across different Python setups without cluttering your system packages.