Python Sorting Mechanisms Practice Problems & Exercises
Practice: Sorting Mechanisms
← Back to lessonGiven a list of integers, return a new sorted list in ascending order. The original list must remain unchanged.
Constraints:
- Do not mutate the input list
- Return a new list object
Solution
def sort_safely(numbers):
return sorted(numbers)
# Test
original = [5, 2, 8, 1, 9]
result = sort_safely(original)
print(f"Original: {original}")
print(f"Sorted: {result}")
print(f"Original unchanged: {original == [5, 2, 8, 1, 9]}")
Key insight: sorted() always returns a new list. list.sort() returns None and modifies the list in place. Writing data = data.sort() is a classic Python bug that sets data to None.
def sort_safely(numbers):
"""Return a new sorted list without modifying the original.
Args:
numbers: list of integers
Returns:
A new list with elements sorted ascending
"""
# TODO: return a sorted copy — do NOT modify 'numbers'
passExpected Output
Original: [5, 2, 8, 1, 9]
Sorted: [1, 2, 5, 8, 9]
Original unchanged: TrueHints
Hint 1: list.sort() modifies the list in place and returns None — that is not what you want here.
Hint 2: sorted() returns a new list and leaves the original untouched.
Sort a list of strings alphabetically without regard to upper/lower case. The original casing of each string must be preserved in the output.
Example: Key insight: The ["Date", "apple", "cherry", "Banana"] becomes ["apple", "Banana", "cherry", "Date"]Solution
key function transforms each element for comparison only. The actual elements in the output retain their original form. str.lower is a built-in method reference — no need for a lambda.
def sort_case_insensitive(words):
"""Sort strings alphabetically, ignoring case.
Args:
words: list of strings
Returns:
New list sorted case-insensitively (original case preserved)
"""
# TODO: sort ignoring case — 'apple' and 'Apple' are treated the same
passExpected Output
['apple', 'Banana', 'cherry', 'Date']Hints
Hint 1: Default string sort uses Unicode code points — uppercase letters (A=65) sort before lowercase (a=97).
Hint 2: Use the key parameter with str.lower to compare lowercase versions while preserving original case.
Given a dictionary mapping names to scores, return a new dictionary sorted by score. Support both ascending and descending order.
Example: Key insight: sort_dict_by_value(scores, descending=True) returns highest scores first.Solution
d.items() yields (key, value) tuples. lambda x: x[1] extracts the value for comparison. dict() on a list of tuples preserves insertion order in Python 3.7+.
def sort_dict_by_value(d, descending=False):
"""Sort a dictionary by its values and return an ordered dict.
Args:
d: dictionary with string keys and numeric values
descending: if True, sort highest value first
Returns:
A new dict sorted by value
"""
# TODO: sort dict items by value, return a new dict
passExpected Output
Ascending: {'dave': 72, 'bob': 80, 'carol': 88, 'alice': 95}
Descending: {'alice': 95, 'carol': 88, 'bob': 80, 'dave': 72}Hints
Hint 1: Use d.items() to get (key, value) pairs, then sorted() with a key function that extracts the value.
Hint 2: Pass the sorted list of tuples to dict() to reconstruct an ordered dictionary (Python 3.7+ preserves insertion order).
Sort a list of integers that may contain None values. Python 3 raises TypeError if you try to sort a list containing None alongside integers. Handle this gracefully by placing None values at the end or beginning based on the nones_last parameter.
Example: Key insight: The tuple key [3, None, 1, None, 5, 2] with nones_last=True becomes [1, 2, 3, 5, None, None]Solution
(x is None, value) works because Python compares tuples element by element. False (0) sorts before True (1), so (False, 3) comes before (True, 0), pushing None to the end. Flip the boolean to push None to the front.
def sort_with_nones(data, nones_last=True):
"""Sort a list of integers that may contain None values.
Args:
data: list of integers and None values
nones_last: if True, put None at end; if False, put None at start
Returns:
New sorted list with None values placed as specified
"""
# TODO: sort handling None without raising TypeError
passExpected Output
Nones last: [1, 2, 3, 5, None, None]
Nones first: [None, None, 1, 2, 3, 5]Hints
Hint 1: Python 3 raises TypeError when comparing None to int. You need a key function that handles None.
Hint 2: Use a tuple key: (is_none_flag, actual_value). Since False (0) sorts before True (1), setting the flag to (x is None) pushes None to the end.
Sort a list of employee dictionaries by three criteria simultaneously:
- Department — ascending (alphabetical)
- Salary — descending (highest first within each department)
- Name — ascending (alphabetical for ties)
Use a single sort call with a tuple key. Key insight: Negating a numeric field (Solution
-e["salary"]) reverses its sort direction within a tuple key. This only works for numeric types. For non-numeric descending sorts, you need the multi-pass stable sort approach.
def sort_employees(employees):
"""Sort employees by department (asc), salary (desc), name (asc).
Args:
employees: list of dicts with 'name', 'dept', 'salary' keys
Returns:
New sorted list of employee dicts
"""
# TODO: single sort with tuple key
passExpected Output
Engineering 110000 Carol
Engineering 95000 Alice
Engineering 95000 Eve
Marketing 80000 Bob
Marketing 80000 DaveHints
Hint 1: Use a tuple key: (dept, -salary, name). Negating salary makes higher salaries sort first.
Hint 2: This works because Python compares tuples lexicographically — first by dept, then by -salary for ties, then by name.
Sort a list of records by category (ascending) and then by priority (descending within each category). You must use two separate sort passes to demonstrate that Python's sort stability makes multi-pass sorting correct. Do NOT use a tuple key.
Input:
records = [
{"item": "item_a", "category": "A", "priority": 1},
{"item": "item_b", "category": "B", "priority": 2},
{"item": "item_c", "category": "A", "priority": 3},
{"item": "item_d", "category": "B", "priority": 2},
]
Notice that Key insight: The order of sort passes matters. Sort secondary key first, primary key last. Because stable sort preserves relative order among equal elements, the secondary ordering survives the primary sort. item_b and item_d have equal priority — stable sort preserves their original relative order.Solution
item_b stays before item_d (same priority, original order preserved).
def stable_multi_sort(records):
"""Sort records by category (asc), then by priority (desc) using
two separate stable sort passes (NOT a tuple key).
Args:
records: list of dicts with 'item', 'category', 'priority'
Returns:
Sorted list demonstrating stable sort behavior
"""
# TODO: two-pass stable sort — which key do you sort FIRST?
passExpected Output
A 3 item_c
A 1 item_a
B 2 item_b
B 2 item_dHints
Hint 1: For multi-pass stable sorting, sort by the SECONDARY criterion first, then by the PRIMARY criterion last.
Hint 2: The last sort is the primary grouping. Because Python sort is stable, the secondary ordering is preserved within each primary group.
Sort a list of strings first by length (shortest first), then alphabetically among strings of the same length.
Example: ["to", "be", "or", "not", "that", "the", "art", "go"]
Strings of length 2: Key insight: Tuple keys are compared lexicographically. be, go, or, to (sorted alphabetically). Strings of length 3: art, not, the. Strings of length 4: that.Solution
(2, 'be') comes before (2, 'go') because the first elements are equal and 'be' < 'go'. (2, 'to') comes before (3, 'art') because 2 < 3 regardless of the second element.
def sort_strings_complex(words):
"""Sort strings by length (shortest first), then alphabetically for ties.
Args:
words: list of strings
Returns:
New sorted list
"""
# TODO: sort by length first, then alphabetically
passExpected Output
['be', 'go', 'to', 'art', 'not', 'or', 'that', 'the']
Wait — check the expected output carefully!Hints
Hint 1: Use a tuple key: (len(word), word) — Python will sort by length first, then by string value for ties.
Hint 2: Remember that string comparison is lexicographic — 'be' comes before 'go' comes before 'to'.
Implement the bubble sort algorithm from scratch. Include the early-exit optimization: if a pass completes without any swaps, the list is already sorted and you can stop.
This is O(n^2) worst case but O(n) best case (already sorted) with the optimization. Key insight: The early-exit optimization makes bubble sort O(n) on already-sorted input, similar to Timsort's run detection. Without the optimization, bubble sort always runs O(n^2). In practice, Python's built-in Timsort is dramatically faster for all inputs — this is an educational exercise, not a production pattern.Solution
def bubble_sort(arr):
"""Sort a list in-place using the bubble sort algorithm.
- Compare adjacent elements and swap if out of order
- Repeat passes until no swaps occur
- Optimize: after each pass, the last element is in its final position
Args:
arr: list of comparable elements (modified in-place)
Returns:
None (sorts in-place, like list.sort())
"""
# TODO: implement bubble sort with early-exit optimization
passExpected Output
[1, 1, 2, 3, 4, 5, 6, 9]Hints
Hint 1: Outer loop runs n-1 times. Inner loop compares arr[j] with arr[j+1] and swaps if arr[j] > arr[j+1].
Hint 2: Track whether any swap happened in the current pass. If no swaps occurred, the list is sorted — break early.
Hint 3: After pass i, the last i+1 elements are already in their final positions, so the inner loop can shrink.
Implement insertion sort with support for sorting a subarray (by specifying left and right indices). This is exactly how Timsort handles short runs — it uses insertion sort on small segments before merging them.
Why this matters: Timsort extends short runs to Key insight: Insertion sort is O(n^2) worst case but O(n) on nearly-sorted data, and it has very low constant overhead. Timsort uses it for runs shorter than minrun length using insertion sort because insertion sort has very low overhead for small arrays and is cache-friendly.Solution
minrun (typically 32-64 elements) because the overhead of merge sort is not worth it for small arrays. The subarray support (left/right parameters) mirrors how Timsort invokes it internally.
def insertion_sort(arr, left=0, right=None):
"""Sort a subarray in-place using insertion sort.
This is the same algorithm Timsort uses for small runs.
Args:
arr: list of comparable elements
left: start index of subarray (inclusive)
right: end index of subarray (inclusive). Defaults to last element.
Returns:
None (sorts in-place)
"""
# TODO: implement insertion sort on arr[left..right]
passExpected Output
Full sort: [1, 1, 2, 3, 4, 5, 6, 9]
Partial sort (indices 2-5): [3, 1, 1, 4, 5, 9, 2, 6]Hints
Hint 1: For each element from left+1 to right, save the current value as 'key'. Shift all larger elements one position right, then insert the key.
Hint 2: The inner while loop moves backward from the current position, shifting elements right until it finds the correct insertion point.
Hint 3: The right parameter defaults to len(arr)-1 if not provided. This allows sorting a subarray, which is exactly what Timsort does for short runs.
Build a competitive programming leaderboard ranker. This combines multiple sorting techniques from the lesson: tuple keys, negation for descending, None handling, and stability.
Test data:
entries = [
{"user": "alice", "solved": 3, "penalty": 240, "last_sub": 180},
{"user": "bob", "solved": 3, "penalty": 240, "last_sub": 120},
{"user": "carol", "solved": 5, "penalty": 180, "last_sub": 300},
{"user": "dave", "solved": 0, "penalty": None, "last_sub": 0},
{"user": "eve", "solved": 3, "penalty": 200, "last_sub": 150},
{"user": "frank", "solved": 0, "penalty": None, "last_sub": 0},
]
Solution
def rank_leaderboard(entries):
def sort_key(e):
no_solutions = (e["solved"] == 0)
penalty_val = e["penalty"] if e["penalty"] is not None else 0
return (
-e["solved"],
(no_solutions, penalty_val),
e["last_sub"],
e["user"],
)
ranked = sorted(entries, key=sort_key)
results = []
for i, e in enumerate(ranked, 1):
penalty_str = str(e["penalty"]) if e["penalty"] is not None else "N/A"
results.append((i, e["user"], e["solved"], penalty_str))
return results
# Test
entries = [
{"user": "alice", "solved": 3, "penalty": 240, "last_sub": 180},
{"user": "bob", "solved": 3, "penalty": 240, "last_sub": 120},
{"user": "carol", "solved": 5, "penalty": 180, "last_sub": 300},
{"user": "dave", "solved": 0, "penalty": None, "last_sub": 0},
{"user": "eve", "solved": 3, "penalty": 200, "last_sub": 150},
{"user": "frank", "solved": 0, "penalty": None, "last_sub": 0},
]
for rank, user, solved, penalty in rank_leaderboard(entries):
print(f"{rank}. {user:7s} - {solved} solved, penalty {penalty}")
Key insight: The tuple (no_solutions, penalty_val) is the critical trick. Since True > False in Python, (True, 0) sorts after (False, any_number), pushing zero-solve users to the bottom. This avoids the TypeError from comparing None directly.
def rank_leaderboard(entries):
"""Rank competitive programming contestants.
Ranking criteria (in order of priority):
1. Problems solved — descending (more is better)
2. Penalty time — ascending (less is better)
3. Last submission time — ascending (earlier is better)
4. Username — ascending (alphabetical tiebreak)
Users with 0 problems solved have penalty=None.
They sort to the bottom regardless of other fields.
Args:
entries: list of dicts with 'user', 'solved', 'penalty', 'last_sub'
Returns:
List of (rank, user, solved, penalty) tuples
"""
# TODO: build the sort key that handles all 4 criteria + None
passExpected Output
1. carol - 5 solved, penalty 180
2. eve - 3 solved, penalty 200
3. bob - 3 solved, penalty 240
4. alice - 3 solved, penalty 240
5. dave - 0 solved, penalty N/A
6. frank - 0 solved, penalty N/AHints
Hint 1: Negate 'solved' to sort descending. Use a tuple (no_solutions_flag, penalty_or_zero) to push None-penalty users to the bottom.
Hint 2: The key function returns a tuple: (-solved, (solved==0, penalty or 0), last_sub, user). Since True > False, users with solved==0 sort after everyone else.
Hint 3: Users with equal 'solved' and 'penalty' are differentiated by 'last_sub' (earlier is better), then by 'user' (alphabetical).
Implement merge sort from scratch with a focus on stability. Then write a function that proves your implementation is stable by tagging elements with their original indices and verifying the ordering is preserved after sorting.
Why this matters: Stability is not automatic — it depends on the merge step always choosing the LEFT element when values are equal. An unstable merge sort takes from the right on ties, breaking relative order.
Test data: Key insight: The [5, 3, 2, 5, 2, 8, 3, 1] — contains three pairs of duplicates (5, 2, 3).Solution
<= comparison in the merge step is the entire difference between a stable and unstable merge sort. Using < (strictly less than) would take from the right subarray on ties, breaking stability. Timsort's merge operation uses this same <= principle, which is why Python's sort is guaranteed stable.
def merge_sort_stable(arr):
"""Sort a list using merge sort, preserving stability.
Stability rule: when two elements are equal, the one that
appeared first in the original array must appear first in
the result.
Args:
arr: list of (value, original_index) tuples
Returns:
New sorted list of (value, original_index) tuples
"""
# TODO: implement merge sort
# TODO: in the merge step, when left[i] == right[j],
# take from LEFT to preserve stability
pass
def prove_stability(data):
"""Demonstrate that your merge sort is stable.
Tag each element with its original index, sort, then verify
that equal elements retained their original relative order.
Args:
data: list of integers (may contain duplicates)
Returns:
(sorted_values, is_stable) tuple
"""
# TODO: tag elements, sort with merge_sort_stable, verify order
passExpected Output
Sorted: [1, 2, 2, 3, 3, 5, 5, 8]
Stable: True
Detail: value=2 at original indices [2, 4] — order preserved
Detail: value=3 at original indices [1, 6] — order preserved
Detail: value=5 at original indices [0, 3] — order preservedHints
Hint 1: Merge sort: split the array in half, recursively sort each half, then merge. Base case: arrays of length 0 or 1 are already sorted.
Hint 2: The merge step is where stability lives. When comparing left[i] and right[j], if they are equal, always take from the LEFT subarray first. This preserves original order.
Hint 3: For prove_stability: tag each element as (value, original_index), sort, then check that for each group of equal values, the original indices are in ascending order.
