Sorting is one of those problems every programmer eventually implements from scratch at least once, and then almost never implements from scratch again — because once I understood how Python’s built-in sorting actually works, I realized it’s almost always the better choice over hand-rolled sorting logic. In this guide, I’ll walk through both approaches: using Python’s built-in sorting tools the right way, and implementing a couple of classic sorting algorithms manually to understand what’s actually happening underneath, including their time complexity.
Reading a List of Numbers from the User
Before sorting anything, I need to get the numbers into a list. Here’s the pattern I use most often:
numbers_input = input("Enter numbers separated by spaces: ")
numbers = [int(n) for n in numbers_input.split()]
print("Original list:", numbers)
Example run:
Enter numbers separated by spaces: 9 3 7 1 5
Original list: [9, 3, 7, 1, 5]
.split() breaks the input string into individual pieces on whitespace, and the list comprehension converts each piece into an integer.
Method 1: Using Python’s Built-in sorted() Function
This is the approach I use in almost all real-world code, because it’s fast, well-tested, and requires no manual implementation:
numbers = [9, 3, 7, 1, 5]
sorted_numbers = sorted(numbers)
print("Ascending order:", sorted_numbers)
print("Original list unchanged:", numbers)
Output:
Ascending order: [1, 3, 5, 7, 9]
Original list unchanged: [9, 3, 7, 1, 5]
sorted() is a built-in function that returns a new sorted list, leaving the original untouched. This is important — if I want to sort a list in place instead, without creating a new one, I use the list’s own .sort() method:
numbers = [9, 3, 7, 1, 5]
numbers.sort()
print("Sorted in place:", numbers)
Output:
Sorted in place: [1, 3, 5, 7, 9]
Both sorted() and .sort() accept a reverse=True argument if I want descending order instead:
numbers = [9, 3, 7, 1, 5]
print(sorted(numbers, reverse=True))
# Output: [9, 7, 5, 3, 1]
A Complete Interactive Sorting Program
def sort_numbers():
numbers_input = input("Enter numbers separated by spaces: ")
try:
numbers = [int(n) for n in numbers_input.split()]
except ValueError:
print("Please enter valid whole numbers only.")
return
ascending = sorted(numbers)
print("Numbers in ascending order:", ascending)
sort_numbers()
Example run:
Enter numbers separated by spaces: 42 8 15 4 23 16
Numbers in ascending order: [4, 8, 15, 16, 23, 42]
How Python’s Built-in Sort Actually Works Internally
Python’s sorted() and .sort() both use an algorithm called Timsort, developed by Tim Peters specifically for Python (and later adopted by other languages, including Java for object arrays). Timsort is a hybrid sorting algorithm derived from merge sort and insertion sort, specifically designed to perform extremely well on real-world data, which is often partially ordered rather than completely random.
Key facts about Timsort that I find genuinely useful to know:
- Its worst-case time complexity is O(n log n), the same as merge sort.
- Its best-case time complexity is O(n), when the input is already sorted or nearly sorted — Timsort detects existing “runs” of ordered data and takes advantage of them, which pure merge sort doesn’t do as efficiently.
- It’s a stable sort, meaning elements that compare as equal retain their original relative order — this matters a lot when sorting complex objects by one attribute while wanting to preserve order on ties.
people = [("Ali", 25), ("Sara", 30), ("John", 25)]
sorted_people = sorted(people, key=lambda p: p[1])
print(sorted_people)
# Output: [('Ali', 25), ('John', 25), ('Sara', 30)]
# Ali still appears before John because stability preserves their original relative order
Implementing Bubble Sort Manually (For Understanding)
Even though I never use this in real production code, implementing a basic sorting algorithm by hand was genuinely useful for understanding what “sorting” actually costs computationally. Bubble sort repeatedly steps through the list, comparing adjacent elements and swapping them if they’re in the wrong order:
def bubble_sort(arr):
numbers = arr.copy()
n = len(numbers)
for i in range(n):
swapped = False
for j in range(0, n - i - 1):
if numbers[j] > numbers[j + 1]:
numbers[j], numbers[j + 1] = numbers[j + 1], numbers[j]
swapped = True
if not swapped:
break # Already sorted -- stop early
return numbers
numbers = [9, 3, 7, 1, 5]
print(bubble_sort(numbers))
# Output: [1, 3, 5, 7, 9]
Bubble sort has a worst-case and average-case time complexity of O(n²), because for each of the n elements, it potentially scans through nearly the entire remaining list again. For small lists this is unnoticeable, but it becomes dramatically slower than Timsort as the list size grows — this is exactly why I never use it beyond educational purposes.
Implementing Selection Sort Manually
Another classic algorithm — selection sort repeatedly finds the minimum element from the unsorted portion of the list and moves it to the front:
def selection_sort(arr):
numbers = arr.copy()
n = len(numbers)
for i in range(n):
min_index = i
for j in range(i + 1, n):
if numbers[j] < numbers[min_index]:
min_index = j
numbers[i], numbers[min_index] = numbers[min_index], numbers[i]
return numbers
numbers = [9, 3, 7, 1, 5]
print(selection_sort(numbers))
# Output: [1, 3, 5, 7, 9]
Selection sort is also O(n²) in all cases, but unlike bubble sort, it doesn’t benefit from early termination on already-sorted data — it always performs the same number of comparisons regardless of the input’s initial order.
Comparing Performance: Built-in Sort vs Manual Implementations
import random
import time
data = [random.randint(0, 100000) for _ in range(5000)]
start = time.perf_counter()
sorted(data)
print("Built-in sorted():", time.perf_counter() - start)
start = time.perf_counter()
bubble_sort(data)
print("Bubble sort:", time.perf_counter() - start)
When I’ve run comparisons like this myself, the built-in sorted() function is consistently and dramatically faster — often by two or three orders of magnitude on lists of a few thousand elements — because Timsort is implemented in optimized C code within CPython, while my hand-written Python loops carry the overhead of the Python bytecode interpreter for every single comparison and swap.
Sorting with a Custom Key
Real-world sorting often isn’t as simple as plain ascending numbers — I frequently need to sort by some derived value. The key parameter handles this cleanly:
words = ["banana", "kiwi", "apple", "fig"]
sorted_by_length = sorted(words, key=len)
print(sorted_by_length)
# Output: ['fig', 'kiwi', 'apple', 'banana']
numbers = [-9, 3, -7, 1, 5]
sorted_by_absolute = sorted(numbers, key=abs)
print(sorted_by_absolute)
# Output: [1, 3, 5, -7, -9]
Common Mistakes I’ve Made
- Confusing
sorted()and.sort()— using.sort()when I actually needed the original list preserved, or trying to assign the result of.sort()to a variable (.sort()returnsNone, not the sorted list). - Reimplementing sorting manually in production code, not realizing Python’s built-in Timsort is both faster and more thoroughly tested than anything I’d write myself.
- Forgetting
int()conversion when reading numbers frominput(), causing lexicographic (string-based) sorting instead of numeric sorting —"10" < "9"isTrueas strings, which surprises people the first time they see it. - Using bubble sort or selection sort on large real datasets, not accounting for how badly O(n²) algorithms scale compared to O(n log n).
Real-World Applications
I use sorted() with a custom key constantly in real projects: sorting a list of file paths by modification date, sorting API results by price or rating, sorting log entries chronologically, or sorting user records alphabetically by last name. Understanding sorting algorithm complexity also matters directly in technical interviews and in situations where I’m evaluating whether a third-party library’s sorting approach is appropriate for very large datasets.
Frequently Asked Questions
What’s the difference between sorted() and .sort()? sorted() returns a new sorted list and leaves the original unchanged; .sort() sorts the list in place and returns None.
What sorting algorithm does Python use internally? Timsort, a hybrid of merge sort and insertion sort, with O(n log n) worst-case and O(n) best-case time complexity.
Should I ever write my own sorting algorithm in real projects? Generally no — Python’s built-in sorted() is faster, more reliable, and more thoroughly tested than a hand-written implementation; manual sorting algorithms are primarily valuable for learning and interviews.
How do I sort numbers entered as strings correctly? Convert each entry to an integer or float with int() or float() before sorting — sorting raw strings compares them lexicographically, not numerically.
Summary
Sorting a list of numbers in Python is, in practice, a one-line task thanks to the built-in sorted() function and Timsort’s excellent real-world performance. But understanding how algorithms like bubble sort and selection sort work by hand — and why they’re O(n²) compared to Timsort’s O(n log n) — gives real insight into why “just use the built-in function” isn’t just convenient advice, it’s actually the objectively better engineering choice in nearly every practical situation.