Learning Goals
3 minBy the end of this lesson you can:
- Maintain a top-N scoreboard with
append,sortand a slice. - Sort ascending when a lower number is better (fewer tries = better score).
- Use
enumerate()to print a numbered leaderboard (1., 2., 3.).
Warm-Up
5 minQuick recall — predict the output. From PY-L1-17 you know .sort() and from PY-L1-18 you know list slicing.
scores = [10, 4, 7] scores.append(2) scores.sort() print(scores) print(scores[:3])
Show the answer
[2, 4, 7, 10] [2, 4, 7]
append(2) stuck it on the end → [10, 4, 7, 2]. .sort() orders ascending → [2, 4, 7, 10]. [:3] keeps the first three. That's the whole top-3 trick — append the new score, sort, slice. Three lines of Python; one tiny scoreboard.
Unlike most games, in Number Guesser a low score (few tries) is good. So ascending sort puts the best at the top. If we wanted highest-points-first, we'd use .sort(reverse=True) — but we don't today.
New Concept · Top-3 Scoreboard
14 minThe state — one list, owned by main
hi_scores = [] # holds at most 3 scores
One list, kept in the main program. We'll pass it into helper functions just like the inventory list in PY-L1-40.
Add a score, keep only the best 3
def add_score(hi_scores, new_score): hi_scores.append(new_score) hi_scores.sort() # ascending — best first del hi_scores[3:] # drop everything from index 3 onwards
Three steps: append, sort, trim. del hi_scores[3:] removes positions 3, 4, 5... so the list never grows past 3 items. If only one score exists, the slice [3:] is empty and nothing happens.
Why not hi_scores = hi_scores[:3]?
That would rebind the local hi_scores to a new list — but the main program's list wouldn't see the change. By using del hi_scores[3:] we modify the existing list in place, which the caller sees. Same passing-by-reference principle from PY-L1-31 and PY-L1-40.
Pretty print with enumerate
enumerate(list) gives you the index and the item together as you loop:
def print_scoreboard(hi_scores): print() print("===== HI-SCORE (fewer tries = better) =====") if len(hi_scores) == 0: print("(no scores yet — be the first!)") return for i, score in enumerate(hi_scores, start=1): print(i, ". ", score, "tries") print("=" * 44)
enumerate(hi_scores, start=1) yields (1, first_score), (2, second_score), etc. The start=1 just makes the numbers 1, 2, 3 instead of 0, 1, 2. Nicer for a leaderboard.
Hook it into the game
# main loop sketch hi_scores = [] while True: print_scoreboard(hi_scores) low, high, cap = pick_difficulty() won, used = play_round(low, high, cap) if won: add_score(hi_scores, used) ...
Print the board before each round (so the player sees the current top). After a win, register the score. Losses don't count — that's a design choice; you could keep them too and label them differently.
Top-N tables are always append, sort, trim. The direction of sort depends on whether high or low is better. The trim slice depends on N. Everything else stays the same.
Worked Example · Number Guesser v3
14 minThe full file
Update guesser.py to v3:
Code
# guesser.py — Number Guesser v3 (hi-scores) import random def pick_difficulty(): print() print("Difficulty: easy / medium / hard / impossible") while True: c = input("Pick: ").strip().lower() if c == "easy": return 1, 50, 10 if c == "medium": return 1, 100, 7 if c == "hard": return 1, 200, 8 if c == "impossible": return 1, 500, 9 print("Type easy / medium / hard / impossible.") def play_round(low, high, cap): secret = random.randint(low, high) tries = 0 print("I picked a number between", low, "and", high, ". You have", cap, "tries.") while tries < cap: raw = input("Guess: ").strip() if not raw.isdigit(): print("Whole numbers only.") continue guess = int(raw) if guess < low or guess > high: print("Out of range.") continue tries = tries + 1 if guess == secret: print("🎉 Correct in", tries, "out of", cap, "tries.") return True, tries elif guess < secret: print("⬆️ Higher.") else: print("⬇️ Lower.") print("💀 Out of tries. The number was", secret) return False, tries def add_score(hi_scores, new_score): hi_scores.append(new_score) hi_scores.sort() del hi_scores[3:] def print_scoreboard(hi_scores): print() print("=" * 44) print(" HI-SCORE (fewer tries = better)") print("=" * 44) if len(hi_scores) == 0: print("(no scores yet — be the first!)") else: for i, score in enumerate(hi_scores, start=1): print(" ", i, ". ", score, "tries") print("=" * 44) # main program print("=" * 44) print(" Number Guesser — v3") print("=" * 44) hi_scores = [] while True: print_scoreboard(hi_scores) low, high, cap = pick_difficulty() won, used = play_round(low, high, cap) if won: was_top = len(hi_scores) < 3 or used < hi_scores[-1] add_score(hi_scores, used) if was_top: print("📈 New entry on the hi-score table!") again = input("Play again? (y/n) ").strip().lower() if again != "y": print("\nFinal board:") print_scoreboard(hi_scores) print("Goodbye!") break
Sample run · three rounds, two wins, board fills up
============================================
Number Guesser — v3
============================================
============================================
HI-SCORE (fewer tries = better)
============================================
(no scores yet — be the first!)
============================================
Difficulty: easy / medium / hard / impossible
Pick: easy
I picked a number between 1 and 50 . You have 10 tries.
... player wins in 5 ...
🎉 Correct in 5 out of 10 tries.
📈 New entry on the hi-score table!
============================================
HI-SCORE (fewer tries = better)
============================================
1 . 5 tries
============================================
(another round, player wins in 8)
📈 New entry on the hi-score table!
============================================
HI-SCORE (fewer tries = better)
============================================
1 . 5 tries
2 . 8 tries
============================================
... etc ...Three things to notice
- The "new entry?" check happens BEFORE
add_score. We compare the new score against the current 3rd place (hi_scores[-1]) before adding. After adding, the new score might itself be the 3rd place, so the check wouldn't work the same. - Losses don't count. The
if won:guards the whole score-saving block. You could store losses with a special marker if you wanted a fuller history. - Difficulty is not tracked. A 5-try win on Easy is on the same board as a 5-try win on Hard. That's a deliberate simplification — the capstone will let you choose to track per-difficulty boards or not.
A complete tunable mini-game with menu, multiple difficulties, win/lose states, a top-3 leaderboard that survives across rounds, and a replay loop. About 80 lines of Python. Every idea was Level 1. This is the warm-up for the capstone — you'll polish it into something portfolio-worthy next lesson.
Try It Yourself
13 minThree tasks. Each one bolts on a tiny piece of the scoreboard.
Add a hi_scores = [] before your main loop. After every win in play_round (or in main after capturing the return), append the score. Print the raw list after each round to confirm it's filling up.
Hint
hi_scores = [] while True: low, high, cap = pick_difficulty() won, used = play_round(low, high, cap) if won: hi_scores.append(used) print("Raw scores:", hi_scores) ...
Play three rounds. The list should grow each time. We'll sort and trim in the next task.
Move the append into a function add_score(hi_scores, new_score). Inside, append, then sort(), then del hi_scores[3:]. Print the list after every win.
Hint
def add_score(hi_scores, new_score): hi_scores.append(new_score) hi_scores.sort() del hi_scores[3:]
Play five rounds. The list should never grow past 3 — and the three values should always be in ascending order, with the best (lowest) score first.
enumerate (stretch)Write print_scoreboard(hi_scores) exactly as in the worked example. Call it before every round. The first call should print "(no scores yet — be the first!)".
Hint
def print_scoreboard(hi_scores): print() print("=== HI-SCORE ===") if len(hi_scores) == 0: print("(empty)") return for i, score in enumerate(hi_scores, start=1): print(i, ".", score, "tries")
enumerate(list, start=1) is the secret sauce for human-friendly numbering. Without start=1 you'd get 0., 1., 2. — fine for programmers, weird for humans.
Mini-Challenge · Wei's Inflated Scoreboard
8 minWei's leaderboard grows forever — by round 10 the board has 10 scores. Find the bug.
# wei_scores.py — buggy
def add_score(scores, new_score):
scores.append(new_score)
sorted(scores) # bug 1
scores = scores[:3] # bug 2
scores = []
add_score(scores, 7)
add_score(scores, 4)
add_score(scores, 10)
add_score(scores, 2)
print(scores) # expected [2, 4, 7], actual [7, 4, 10, 2]- Bug 1.
sorted(scores)returns a new sorted list but doesn't changescores. We threw the result away. Usescores.sort()— the method modifies the list in place. - Bug 2.
scores = scores[:3]rebinds the local name to a new list — the caller's list is untouched. Usedel scores[3:]to actually trim the existing list.
Show the fix
def add_score(scores, new_score): scores.append(new_score) scores.sort() # in-place del scores[3:] # in-place
Both bugs are the same trap from a different angle. Python has two flavours of operation: ones that mutate the existing object (list.sort(), del list[i:], list.append(x)), and ones that return a new object (sorted(list), list[i:], list + [x]). When you want changes visible to the caller, mutate. When you want a fresh copy, use the return-new form and assign it.
Recap
3 minA top-N scoreboard is three list moves: append the new score, sort (ascending if lower is better), and trim with del list[N:]. The list lives in the main program and gets passed to helpers like add_score and print_scoreboard. enumerate with start=1 makes the leaderboard print with friendly 1, 2, 3 numbering. The game now remembers across rounds — your best three runs survive each play-again loop. Tomorrow is the capstone — we polish all three Number Guesser parts plus a recap of every Level 1 idea.
Vocabulary Card
- top-N scoreboard
- A list that holds the N best scores, ordered. Updated with append-sort-trim.
- in-place vs return-new
list.sort()mutates;sorted(list)returns a new list.del list[i:]mutates;list[:i]returns a new list. Knowing which is which is half of list mastery.enumerate(seq, start=1)- A built-in that pairs each item with its position.
start=1makes the numbers human-friendly. - persistent state
- Data that survives across function calls or game rounds. Today:
hi_scores. In Level 2: data that survives across program runs (files). - ascending vs descending sort
- Default
.sort()is ascending (smallest first). For descending, passreverse=True.
Homework
4 minPolish guesser.py into the warm-up for the capstone.
- Make the leaderboard show top-5 instead of top-3. Hint: change
del hi_scores[3:]todel hi_scores[5:]. One number, no other code change. - Track the player's name. At the start of the program, ask for it. When recording a score, store it as a 2-item list:
[used, name]. Then sort — Python sorts lists by their first item by default, so the ordering still works. Print the leaderboard as1. <tries> tries — <name>. - Bonus: also track losses. Add a separate
lossescounter in the main program. After every round (win or lose), bump it accordingly. Print "Wins: X, Losses: Y" in the scoreboard header.
Bring guesser.py — capstone time tomorrow.
Sample · top-5 with names & W/L
# guesser.py — v3.1 (top-5 + names + W/L) TOP_N = 5 def add_score(scores, used, name): scores.append([used, name]) scores.sort() # sorts by first item (used) ascending del scores[TOP_N:] def print_scoreboard(scores, wins, losses): print() print("=" * 44) print(" HI-SCORE (Wins:", wins, " Losses:", losses, ")") print("=" * 44) if not scores: print("(no scores yet — be the first!)") else: for i, entry in enumerate(scores, start=1): used, name = entry[0], entry[1] print(" ", i, ". ", used, "tries —", name) print("=" * 44) # main import random print("Welcome to Number Guesser Deluxe!") player = input("Your name: ").strip() or "Anonymous" hi_scores = [] wins = 0 losses = 0 while True: print_scoreboard(hi_scores, wins, losses) low, high, cap = pick_difficulty() won, used = play_round(low, high, cap) if won: wins = wins + 1 add_score(hi_scores, used, player) else: losses = losses + 1 if input("Play again? (y/n) ").strip().lower() != "y": print("Goodbye, " + player + "!") break
Two slick bits to notice. (1) Storing each entry as [used, name] means Python sorts by tries automatically — list comparison goes element by element, and the first element wins. (2) input(...).strip() or "Anonymous" uses the "empty string is falsy" trick to fall back to a default when the player just hits Enter. Both moves come back in Level 2 web forms and dictionaries.