FreeCampus Python

Sorting Collections Without Losing Meaning

Sort values and complete records by explicit policies while distinguishing mutation, new results, reversal, key functions, type boundaries, stability, and source preservation.
python-foundations collections-iteration sorting ordering
Open in Colab
  • Level: Python Foundations · Unit 3
  • Estimated time: 3–4.5 hours
  • You will learn: Sort scalar values and related record fields by an explicit, stable policy without accidentally replacing a list with None or destroying source order.
  • Practice in: Google Colab, JupyterLab, or a local editor

1. Rank complete records, not disconnected fields

A tournament result connects a pilot, score, and completion time:

results = [
    {"pilot": "Nova", "score": 91, "seconds": 74},
    {"pilot": "Mira", "score": 96, "seconds": 83},
    {"pilot": "Sol", "score": 91, "seconds": 68},
]

Sorting a separate score list would lose which pilot earned each score. The unit of movement must be the complete record. The sort policy then chooses which field determines order.

This lesson answers:

  • Does the operation mutate the list or create a new list?
  • Is reversal the same as sorting in descending order?
  • How does a key function turn a domain rule into a comparable value?
  • What happens when values are not mutually comparable?
  • How does stable sorting preserve earlier order among equal keys?

2. sorted() creates a new list

The built-in sorted() accepts any iterable and returns a list:

scores = [91, 74, 96, 83]
ranked_scores = sorted(scores)

print(scores)
print(ranked_scores)

scores remains in source order. ranked_scores is [74, 83, 91, 96].

The input does not need to be a list:

tuple_result = sorted((3, 1, 2))
set_result = sorted({"vault", "dock", "garden"})
dict_result = sorted({"Nova": 91, "Mira": 96})

print(type(tuple_result).__name__)
print(set_result)
print(dict_result)

All three results are lists. Direct dictionary traversal supplies keys, so sorted(dictionary) sorts keys.

3. .sort() changes one list and returns None

scores = [91, 74, 96, 83]
result = scores.sort()

print(scores)
print(result)

The existing list changes and result is None. This makes a common bug clear:

scores = [91, 74, 96]
scores = scores.sort()

print(scores)

The useful list name now refers to None. The sort already happened in place before assignment discarded the reference.

Choose from the state requirement:

  • use sorted(source) when source order remains evidence or both versions matter;
  • use working.sort() when the working list itself should adopt the new order.

Copy before an intentional in-place sort

source_scores = [91, 74, 96, 83]
working_scores = source_scores.copy()
working_scores.sort()

assert source_scores == [91, 74, 96, 83]
assert working_scores == [74, 83, 91, 96]

This separates preservation from mutation visibly.

4. Descending sort and reversal answer different questions

Use reverse=True to sort in descending order:

scores = [91, 74, 96, 83]
highest_first = sorted(scores, reverse=True)

assert highest_first == [96, 91, 83, 74]

.reverse() merely flips the current list positions:

arrival_scores = [91, 74, 96, 83]
result = arrival_scores.reverse()

assert arrival_scores == [83, 96, 74, 91]
assert result is None

The result is not score-ranked; it is reverse arrival order.

reversed() supplies values in reverse traversal order without changing the source:

route = ["dock", "garden", "vault"]
backtrack = list(reversed(route))

assert backtrack == ["vault", "garden", "dock"]
assert route == ["dock", "garden", "vault"]

reversed(route) itself is a reverse iterator, not a list. Convert it only when the program needs to retain all reversed values as a list.

Checkpoint: mutation and return values

5. Strings follow Unicode ordering unless you choose another policy

Default string comparison is case-sensitive and based on Unicode code points:

pilots = ["nova", "Mira", "sol", "Ada"]
default_order = sorted(pilots)

print(default_order)

Uppercase spellings can appear before lowercase ones. That behavior is deterministic but may not match a human-facing name policy.

The key argument asks Python to calculate a comparison key for each value:

name_order = sorted(pilots, key=str.casefold)
print(name_order)

str.casefold is passed without parentheses. Python calls it once per string for comparison and still returns the original spellings in the result.

assert name_order == ["Ada", "Mira", "nova", "sol"]

This is a preview of a function used as a value. Unit 5 explains callables and callbacks fully. For now, read key=str.casefold as “compare the casefolded form of each original string.”

Other built-ins can express simple policies:

words = ["star", "observatory", "map", "vault"]
by_length = sorted(words, key=len)

offsets = [-12, 4, -3, 9]
by_distance_from_zero = sorted(offsets, key=abs)

print(by_length)
print(by_distance_from_zero)

6. Mixed incomparable types should fail visibly

Python does not invent an ordering between unrelated types:

mixed = [3, "12", 7]
print(sorted(mixed))

This raises TypeError because integer and string ordering is undefined. Do not silently sort everything by str unless lexicographic display text is genuinely the required policy:

display_order = sorted(mixed, key=str)
print(display_order)

The key strings are "3", "12", and "7", so this is not numeric order. A better data boundary often converts all inputs to one intentional type before storing them.

Missing values need an explicit policy too:

scores = [91, None, 74]
print(sorted(scores))

The error asks you to decide whether None belongs first, last, or outside the ranked data. Sorting cannot infer domain meaning.

7. Sort records by fields with itemgetter

operator.itemgetter() creates a key callable that retrieves dictionary fields:

from operator import itemgetter

results = [
    {"pilot": "Nova", "score": 91, "seconds": 74},
    {"pilot": "Mira", "score": 96, "seconds": 83},
    {"pilot": "Sol", "score": 91, "seconds": 68},
]

by_score = sorted(results, key=itemgetter("score"), reverse=True)

for record in by_score:
    print(record["pilot"], record["score"])

Complete dictionaries move together; no pilot becomes disconnected from a score or time.

itemgetter() can retrieve several fields as a tuple key:

alphabetical_score_groups = sorted(
    results,
    key=itemgetter("score", "pilot"),
)

Python compares the score first and uses pilot only when scores are equal. The complete tuple key is ascending unless reverse=True reverses all of it.

An optional lambda spelling is common in Python code:

by_time = sorted(results, key=lambda record: record["seconds"])

Read this as a tiny unnamed function that receives one record and returns its seconds. You do not need to write lambdas in this unit; itemgetter() and built-in functions cover the lessons. Unit 5 teaches function values and lambdas in context.

Checkpoint: key selection and type boundaries

8. Stable sorting preserves earlier decisions among ties

Python’s sort is stable: records with equal keys retain their relative order from the input.

from operator import itemgetter

results = [
    {"pilot": "Nova", "score": 91, "arrival": 1},
    {"pilot": "Mira", "score": 96, "arrival": 2},
    {"pilot": "Sol", "score": 91, "arrival": 3},
    {"pilot": "Ivo", "score": 91, "arrival": 4},
]

ranked = sorted(results, key=itemgetter("score"), reverse=True)

assert [record["pilot"] for record in ranked] == [
    "Mira",
    "Nova",
    "Sol",
    "Ivo",
]

Nova, Sol, and Ivo all have score 91, so their arrival order remains intact. The list comprehension in this assertion is supplied only to inspect names; Unit 4 teaches comprehensions. An equivalent beginner-readable inspection is:

ranked_names = []

for record in ranked:
    ranked_names.append(record["pilot"])

assert ranked_names == ["Mira", "Nova", "Sol", "Ivo"]

Use stable passes for mixed directions

Suppose higher scores rank first, but equal scores use fewer seconds first. One tuple key with reverse=True would reverse both score and seconds, making slower times win ties. Stable passes express the mixed directions:

results = [
    {"pilot": "Nova", "score": 91, "seconds": 74},
    {"pilot": "Mira", "score": 96, "seconds": 83},
    {"pilot": "Sol", "score": 91, "seconds": 68},
    {"pilot": "Ivo", "score": 91, "seconds": 74},
]

ranked = sorted(results, key=itemgetter("seconds"))
ranked = sorted(ranked, key=itemgetter("score"), reverse=True)

ranked_names = []
for record in ranked:
    ranked_names.append(record["pilot"])

assert ranked_names == ["Mira", "Sol", "Nova", "Ivo"]

Apply the less important rule first (seconds ascending), then the most important rule (score descending). Stability preserves time order inside equal-score groups, and Nova remains ahead of Ivo because both key fields tie and Nova appeared first.

Sort dictionary items, not just keys

score_by_pilot = {"Nova": 91, "Mira": 96, "Sol": 91}
score_pairs = sorted(score_by_pilot.items(), key=itemgetter(1), reverse=True)

print(score_pairs)

Each item is (key, value), so itemgetter(1) selects the score. Equal scores retain dictionary insertion order. The source dictionary remains unchanged.

NoteDo not sort more often than the output requires

Sorting performs more work than one traversal. Sort once when the program needs an ordered view, retain the result while it remains valid, and avoid sorting the same unchanged collection inside another traversal. Unit 7 will give this comparison a formal vocabulary.

Checkpoint: stable record ordering

9. Produce the tournament leaderboard

Preserve the submitted result order and build two ranked views.

from operator import itemgetter

source_results = [
    {"pilot": "Nova", "score": 91, "seconds": 74},
    {"pilot": "Mira", "score": 96, "seconds": 83},
    {"pilot": "Sol", "score": 91, "seconds": 68},
    {"pilot": "Ivo", "score": 91, "seconds": 74},
]

alphabetical = None
ranked = None
leaderboard_lines = []

Requirements:

  1. create an alphabetical list by pilot name without case-sensitive surprises;
  2. create a ranked list where higher score wins, fewer seconds breaks a score tie, and original submission order breaks a complete tie;
  3. append one display line per ranked record with a one-based place; and
  4. leave the source list and every record unchanged.
assert [record["pilot"] for record in alphabetical] == [
    "Ivo",
    "Mira",
    "Nova",
    "Sol",
]
assert [record["pilot"] for record in ranked] == [
    "Mira",
    "Sol",
    "Nova",
    "Ivo",
]
assert leaderboard_lines == [
    "1. Mira — 96 points — 83s",
    "2. Sol — 91 points — 68s",
    "3. Nova — 91 points — 74s",
    "4. Ivo — 91 points — 74s",
]
assert source_results == [
    {"pilot": "Nova", "score": 91, "seconds": 74},
    {"pilot": "Mira", "score": 96, "seconds": 83},
    {"pilot": "Sol", "score": 91, "seconds": 68},
    {"pilot": "Ivo", "score": 91, "seconds": 74},
]

Boundary variation: append {"pilot": "ada", "score": 96, "seconds": 83} to the source. Predict its alphabetical position and how the complete leaderboard tie uses submission order. Rerun from the source cell rather than mutating an already ranked result.

Hint: separate the alphabetic and competition policies

itemgetter("pilot") is case-sensitive, so use an optional supplied lambda for the alphabetical view: key=lambda record: record["pilot"].casefold(). For mixed competition directions, sort by seconds ascending first and then by score descending. Enumerate the ranked records from one for display.

Show one complete solution after attempting the leaderboard
alphabetical = sorted(
    source_results,
    key=lambda record: record["pilot"].casefold(),
)

ranked = sorted(source_results, key=itemgetter("seconds"))
ranked = sorted(ranked, key=itemgetter("score"), reverse=True)

leaderboard_lines = []
for place, record in enumerate(ranked, start=1):
    line = (
        f"{place}. {record['pilot']} — "
        f"{record['score']} points — {record['seconds']}s"
    )
    leaderboard_lines.append(line)

Both sorted() calls create outer lists and move references to complete records; they do not mutate the source records. The stable second pass preserves the seconds rule and original order where the full ranking key ties.

10. Explain the ordering policy

  1. Why is items = items.sort() a bug even though the sort itself occurs?
  2. When is reverse traversal different from descending sorting?
  3. What does a key callable return, and what values appear in the final list?
  4. Why should mixed types or missing values trigger a policy decision?
  5. How do stable passes express one descending and one ascending field?

Key points

TipKey points
  • sorted() returns a new list; .sort() mutates a list and returns None.
  • reverse=True reverses sort order; .reverse() and reversed() reverse current traversal order without calculating rank.
  • A key callable produces the comparison value while complete original items move into the result.
  • Normalize or reject mixed data deliberately; sorting cannot invent domain rules.
  • Python sorting is stable, so equal-key items retain their earlier relative order.
  • Sort complete records to preserve relationships, and use stable passes for mixed ascending and descending field requirements.

References

Back to top