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.
Calling
list.sort()sorts your list right where it sits and explicitly returnsNone. Never try to assign its result.The built-in
sorted()creates a fresh, sorted copy and leaves your original sequence completely untouched.Both methods let you pass custom
key=logic and areverse=Trueflag whenever you need them.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
# 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
# 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
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
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
from operator import itemgetter
sorted(students, key=itemgetter("score"))
sorted(students, key=itemgetter("score", "name")) # multi-fieldWorking 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
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 secondThat 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
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
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 lastSkipping 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
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
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
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.