# An intro programming course — units, worked examples and sorting you can watch

Recipe No. 28, Labs and workshops. From The know.sh Cookbook: https://know.sh/cookbook/coding-course

Put your intro course in one place, mistakes included. Each unit is a document: a section per concept with a worked example and a trace, a section per exercise, and every mistake your classes make filed as a *Bug*. Beside the sorting unit sits a visualiser that counts every comparison and swap as the bars move.

- For: a teacher running a first programming course in Python
- You get: a Python course: tested examples, Bug sections and sorting to watch
- Time: an evening to draft, two to run it all
- Made with: Shelf, Document, Section, Your AI assistant, Elements, Public link

## How it works

1. **Give your assistant one unit at a time.** Hand over the slides and exercise files for one unit. Ask for a section per concept with a worked example and a trace, and each model answer in its own section.
2. **Run every example yourself.** Copy each code block into your own editor and run it on the input the section gives. Where the output and the trace disagree, fix the section.
3. **Ask for a sorting visualiser, and test it.** Ask for a widget element: sixteen bars, four algorithms, **Step** and **Play**. Selection sort on sixteen bars must make 120 comparisons every time, whatever the order.
4. **File the mistakes as Bugs.** After each lesson, ask your assistant to add each mistake as a *Bug* section titled with what the student saw: *My list came back as None*. Share each unit without the model answers.

## Try this prompt

Your assistant, connected to know.sh (Claude, ChatGPT or a local model):

> Write a widget element for section 3 of Unit 5 in my know.sh library: a sorting visualiser with sixteen bars from a fixed seed, and bubble, insertion, selection and merge sort on the same bars. Add Step, Play, Reset and a speed slider, and running counts of comparisons and swaps. Use only the page’s colours, and tell me the counts to expect so I can check it.

Your assistant, connected to know.sh (Claude, ChatGPT or a local model):

> Using know.sh, make a shelf called Intro to programming and a document called “Unit 5 — Searching and sorting” from my slides and exercise files. One Insight section per concept, with the idea in two sentences, a worked example in a Python 3.12 code block and a line-by-line trace; one Question section per exercise; each model answer in its own Evidence section.

## Elements in the specimens

Written as your assistant writes them through the know.sh MCP server. Each reads as plain words until you turn elements on (Account → Elements).

Widget, specimen 1:

```element widget
{
  "title": "Four ways to sort sixteen bars",
  "caption": "Bubble, insertion, selection and merge sort on the same sixteen numbers, one comparison or one move per step, with running counts. Insertion sort here swaps neighbours rather than shifting them; merge sort is top-down and copies into a second list, so it counts writes, not swaps, and the bars show the merged part followed by what is left of each half. Time, memory and the computer’s caches are not modelled.",
  "height": 545,
  "html": "<div class=\"sorter\" tabindex=\"-1\">\n  <div class=\"algos\" role=\"group\" aria-label=\"Algorithm\">\n    <button type=\"button\" data-algo=\"bubble\" aria-pressed=\"true\">Bubble</button>\n    <button type=\"button\" data-algo=\"insertion\" aria-pressed=\"false\">Insertion</button>\n    <button type=\"button\" data-algo=\"selection\" aria-pressed=\"false\">Selection</button>\n    <button type=\"button\" data-algo=\"merge\" aria-pressed=\"false\">Merge</button>\n  </div>\n  <div class=\"stage\">\n    <div class=\"bars\" id=\"bars\" role=\"img\" aria-label=\"Bars\"></div>\n  </div>\n  <p class=\"legend\" aria-hidden=\"true\"><span><i class=\"key k-cmp\"></i>Compared</span><span><i class=\"key k-mov\"></i><span id=\"movName\">Swapped</span></span><span><i class=\"key k-done\"></i>In place</span><span><i class=\"key k-rng\"></i>Working on</span></p>\n  <dl class=\"counts\">\n    <div><dt>Comparisons</dt><dd id=\"cOut\">0</dd></div>\n    <div><dt id=\"sName\">Swaps</dt><dd id=\"sOut\">0</dd></div>\n    <div><dt>Step</dt><dd id=\"kOut\">0</dd></div>\n  </dl>\n  <p class=\"say\" id=\"say\" aria-live=\"polite\"></p>\n  <div class=\"controls\">\n    <button type=\"button\" id=\"step\">Step</button>\n    <button type=\"button\" id=\"play\" aria-pressed=\"false\">Play</button>\n    <button type=\"button\" id=\"reset\" class=\"quiet\">Reset</button>\n    <button type=\"button\" id=\"shuffle\" class=\"quiet\">New order</button>\n    <label class=\"speed\" for=\"speed\">Speed <input id=\"speed\" type=\"range\" min=\"1\" max=\"5\" step=\"1\" value=\"3\"> <output id=\"speedOut\" for=\"speed\">4 steps a second</output></label>\n  </div>\n</div>",
  "css": ".sorter { outline: none; }\n.algos { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 6px; }\n.algos button, .controls .quiet { background: var(--paper); color: var(--ink); border: 1px solid var(--ink); }\n.algos button { padding: 8px 4px; min-height: 40px; }\n.algos button[aria-pressed='true'] { background: var(--ink); color: var(--paper); }\n.stage { margin-top: 14px; border-bottom: 1px solid var(--ink); }\n.bars { display: grid; grid-template-columns: repeat(16, minmax(0, 1fr)); grid-template-rows: 170px auto 10px; column-gap: 3px; }\n.bar { grid-row: 1; align-self: end; box-sizing: border-box; background: var(--wash); border: 1px solid var(--ink-3); border-bottom: 0; }\n.bar.done { background: var(--ink-3); border-color: var(--ink-3); }\n.bar.cmp { background: var(--ink); border-color: var(--ink); }\n.bar.mov { border-color: var(--ink); background: repeating-linear-gradient(135deg, var(--ink) 0 3px, var(--paper) 3px 6px); }\n.val { grid-row: 2; padding-top: 4px; text-align: center; font: 400 11px/1 var(--sans); color: var(--ink-3); font-variant-numeric: tabular-nums; }\n.val.on { color: var(--ink); font-weight: 600; }\n.rng { grid-row: 3; align-self: start; height: 6px; margin-top: 3px; border: 1px solid var(--ink-2); border-top: 0; }\n.legend { display: flex; flex-wrap: wrap; gap: 4px 16px; margin: 10px 0 0; font: 400 12px/1.4 var(--sans); color: var(--ink-2); }\n.legend > span { display: inline-flex; align-items: center; gap: 6px; }\n.key { display: inline-block; width: 12px; height: 12px; box-sizing: border-box; }\n.k-cmp { background: var(--ink); }\n.k-mov { border: 1px solid var(--ink); background: repeating-linear-gradient(135deg, var(--ink) 0 2px, var(--paper) 2px 4px); }\n.k-done { background: var(--ink-3); }\n.k-rng { height: 6px; border: 1px solid var(--ink-2); border-top: 0; }\n.counts { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 0 18px; margin: 12px 0 0; border-top: 1px solid var(--ink); }\n.counts div { padding: 7px 0; border-bottom: 1px solid var(--rule); }\n.counts dt { font: 400 12.5px/1.3 var(--sans); color: var(--ink-3); }\n.counts dd { margin: 2px 0 0; font: 400 22px/1.2 var(--serif); font-variant-numeric: tabular-nums; }\n.say { min-height: 3em; margin: 10px 0 0; font: italic 400 16px/1.45 var(--serif); }\n.controls { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--rule); }\n.controls button { min-height: 40px; min-width: 64px; }\n.speed { display: flex; align-items: center; gap: 8px; flex: 1 1 200px; }\n.speed input { flex: 1; min-width: 80px; }\n.speed output { min-width: 8.5em; font: 400 13px var(--sans); color: var(--ink-2); font-variant-numeric: tabular-nums; }\nbutton:focus-visible, input:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }\n@media (max-width: 420px) {\n  .algos { grid-template-columns: repeat(2, minmax(0, 1fr)); }\n  .bars { column-gap: 2px; grid-template-rows: 150px auto 10px; }\n  .val { font-size: 10px; }\n  .controls button { flex: 1 1 40%; }\n}",
  "js": "(function () {\n  var root = document.querySelector('.sorter');\n  var $ = function (id) { return document.getElementById(id); };\n  var N = 16, LOW = 4, HIGH = 48;\n  var SPEEDS = [1, 2, 4, 8, 16];\n  var seed = 20260927, order = [], steps = [], at = 0, algo = 'bubble';\n  var playing = false, raf = 0, lastT = 0;\n  var barsEl = $('bars'), bars = [], vals = [], rng;\n\n  // A seeded Park-Miller generator, so the page opens on the same order every time.\n  function nextOrder() {\n    var pool = [], out = [];\n    for (var v = LOW; v <= HIGH; v++) pool.push(v);\n    for (var i = 0; i < N; i++) {\n      seed = (seed * 16807) % 2147483647;\n      out.push(pool.splice(seed % pool.length, 1)[0]);\n    }\n    return out;\n  }\n\n  function inversions(a) {\n    var n = 0;\n    for (var i = 0; i < a.length; i++) for (var j = i + 1; j < a.length; j++) if (a[i] > a[j]) n++;\n    return n;\n  }\n\n  // Runs the algorithm to the end and records every comparison and every move.\n  function trace(kind, input) {\n    var a = input.slice(), out = [], c = 0, s = 0, done = [], range = null, i, j;\n    for (i = 0; i < N; i++) done.push(false);\n    function push(o) {\n      out.push({ a: a.slice(), cmp: o.cmp || [], mov: o.mov || [], rng: range ? range.slice() : null, done: done.slice(), c: c, s: s, msg: o.msg });\n    }\n    function swap(x, y) { var t = a[x]; a[x] = a[y]; a[y] = t; s++; }\n    function allDone() { for (var k = 0; k < N; k++) done[k] = true; range = null; }\n    var inv = inversions(a);\n    push({ msg: 'The starting order: ' + N + ' bars, ' + inv + ' pairs out of order. Press Step or Play.' });\n\n    if (kind === 'bubble') {\n      var end = N - 1, pass = 1;\n      while (end > 0) {\n        range = [0, end];\n        var last = 0;\n        for (i = 0; i < end; i++) {\n          c++;\n          var out1 = a[i] > a[i + 1];\n          push({ cmp: [i, i + 1], msg: 'Pass ' + pass + ': compare ' + a[i] + ' and ' + a[i + 1] + (out1 ? '. Out of order.' : '. In order; leave them.') });\n          if (out1) { swap(i, i + 1); last = i; push({ mov: [i, i + 1], msg: 'Swap them: ' + a[i + 1] + ' moves right.' }); }\n        }\n        for (var k = last + 1; k <= end; k++) done[k] = true;\n        end = last;\n        if (end === 0) done[0] = true;\n        push({ msg: end > 0 ? 'End of pass ' + pass + '. Nothing after the last swap moves again, so those bars are in place.' : 'End of pass ' + pass + ': no swaps left to make.' });\n        pass++;\n      }\n      allDone();\n      push({ msg: 'Sorted in ' + c + ' comparisons and ' + s + ' swaps. Each swap put one out-of-order pair right, and the start had ' + inv + '.' });\n    }\n\n    if (kind === 'insertion') {\n      for (i = 1; i < N; i++) {\n        range = [0, i];\n        push({ msg: 'Take ' + a[i] + ' and walk it left into the sorted part.' });\n        for (j = i; j > 0; j--) {\n          c++;\n          var gt = a[j - 1] > a[j];\n          push({ cmp: [j - 1, j], msg: 'Compare ' + a[j - 1] + ' and ' + a[j] + (gt ? ': ' + a[j] + ' is smaller, so it moves left.' : ': in order, so ' + a[j] + ' stops here.') });\n          if (!gt) break;\n          swap(j - 1, j);\n          push({ mov: [j - 1, j], msg: 'Swap them.' });\n        }\n      }\n      allDone();\n      push({ msg: 'Sorted in ' + c + ' comparisons and ' + s + ' swaps. The swaps equal the ' + inv + ' out-of-order pairs; the comparisons are at most one more per bar.' });\n    }\n\n    if (kind === 'selection') {\n      for (i = 0; i < N - 1; i++) {\n        range = [i, N - 1];\n        var m = i;\n        for (j = i + 1; j < N; j++) {\n          c++;\n          var smaller = a[j] < a[m];\n          push({ cmp: [m, j], msg: 'Compare ' + a[j] + ' with the smallest so far, ' + a[m] + (smaller ? ': a new smallest.' : '.') });\n          if (smaller) m = j;\n        }\n        if (m !== i) {\n          swap(i, m); done[i] = true;\n          push({ mov: [i, m], msg: 'Swap the smallest left, ' + a[i] + ', into position ' + (i + 1) + '.' });\n        } else {\n          done[i] = true;\n          push({ msg: a[i] + ' is already the smallest left, so no swap.' });\n        }\n      }\n      allDone();\n      push({ msg: 'Sorted in ' + c + ' comparisons and ' + s + ' swaps. Selection sort always makes 120 comparisons on 16 bars, and never more than 15 swaps.' });\n    }\n\n    if (kind === 'merge') {\n      var list = function (x) { return x.join(', '); };\n      var merge = function (lo, hi) {\n        if (lo >= hi) return;\n        var mid = Math.floor((lo + hi) / 2);\n        merge(lo, mid);\n        merge(mid + 1, hi);\n        range = [lo, hi];\n        var L = a.slice(lo, mid + 1), R = a.slice(mid + 1, hi + 1), p = 0, q = 0, k = lo;\n        push({ msg: 'Merge ' + list(L) + ' with ' + list(R) + '.' });\n        while (p < L.length && q < R.length) {\n          c++;\n          var rh = k + (L.length - p);\n          push({ cmp: [k, rh], msg: 'Compare the fronts of the two halves, ' + L[p] + ' and ' + R[q] + '.' });\n          if (L[p] <= R[q]) { p++; }\n          else { a.splice(rh, 1); a.splice(k, 0, R[q]); q++; }\n          s++;\n          push({ mov: [k], msg: 'Write ' + a[k] + ', the smaller, next.' });\n          k++;\n        }\n        if (k <= hi) {\n          var rest = [];\n          for (var t = k; t <= hi; t++) rest.push(t);\n          s += rest.length;\n          push({ mov: rest, msg: 'One half is used up; write the other ' + rest.length + ' as they stand.' });\n        }\n      };\n      merge(0, N - 1);\n      allDone();\n      push({ msg: 'Sorted in ' + c + ' comparisons and ' + s + ' writes: four rounds of merging, 16 writes each. No swaps: merge sort copies into a second list.' });\n    }\n    return out;\n  }\n\n  function build() {\n    barsEl.textContent = '';\n    bars = []; vals = [];\n    for (var i = 0; i < N; i++) {\n      var b = document.createElement('div'); b.className = 'bar'; b.style.gridColumn = String(i + 1);\n      var v = document.createElement('div'); v.className = 'val'; v.style.gridColumn = String(i + 1);\n      barsEl.appendChild(b); barsEl.appendChild(v);\n      bars.push(b); vals.push(v);\n    }\n    rng = document.createElement('div'); rng.className = 'rng';\n    barsEl.appendChild(rng);\n  }\n\n  function render() {\n    var st = steps[at];\n    for (var i = 0; i < N; i++) {\n      var on = st.cmp.indexOf(i) >= 0, mv = st.mov.indexOf(i) >= 0;\n      bars[i].style.height = Math.round(st.a[i] / HIGH * 100) + '%';\n      bars[i].className = 'bar' + (st.done[i] ? ' done' : '') + (on ? ' cmp' : '') + (mv ? ' mov' : '');\n      vals[i].textContent = st.a[i];\n      vals[i].className = 'val' + (on || mv ? ' on' : '');\n    }\n    if (st.rng) { rng.style.display = ''; rng.style.gridColumn = (st.rng[0] + 1) + ' / ' + (st.rng[1] + 2); }\n    else rng.style.display = 'none';\n    barsEl.setAttribute('aria-label', 'Bars from left to right: ' + st.a.join(', '));\n    $('cOut').textContent = st.c;\n    $('sOut').textContent = st.s;\n    $('kOut').textContent = at + ' of ' + (steps.length - 1);\n    $('say').textContent = st.msg;\n    var merge = algo === 'merge';\n    $('sName').textContent = merge ? 'Writes' : 'Swaps';\n    $('movName').textContent = merge ? 'Written' : 'Swapped';\n    $('step').disabled = at >= steps.length - 1;\n  }\n\n  function load() {\n    stop();\n    steps = trace(algo, order);\n    at = 0;\n    render();\n  }\n\n  function advance() {\n    if (at < steps.length - 1) { at++; render(); }\n  }\n\n  function loop(t) {\n    if (!playing) return;\n    var interval = 1000 / SPEEDS[Number($('speed').value) - 1];\n    if (!lastT) lastT = t;\n    if (t - lastT >= interval) { lastT = t; advance(); }\n    if (at >= steps.length - 1) { stop(); return; }\n    raf = requestAnimationFrame(loop);\n  }\n\n  function play() {\n    if (at >= steps.length - 1) { at = 0; render(); }\n    playing = true; lastT = 0;\n    $('play').textContent = 'Pause'; $('play').setAttribute('aria-pressed', 'true');\n    $('say').setAttribute('aria-live', 'off');\n    raf = requestAnimationFrame(loop);\n  }\n\n  function stop() {\n    playing = false;\n    if (raf) cancelAnimationFrame(raf);\n    raf = 0;\n    $('play').textContent = 'Play'; $('play').setAttribute('aria-pressed', 'false');\n    $('say').setAttribute('aria-live', 'polite');\n  }\n\n  function speedText() {\n    var n = SPEEDS[Number($('speed').value) - 1];\n    var t = n + (n === 1 ? ' step a second' : ' steps a second');\n    $('speedOut').textContent = t;\n    $('speed').setAttribute('aria-valuetext', t);\n  }\n\n  root.querySelectorAll('[data-algo]').forEach(function (b) {\n    b.addEventListener('click', function () {\n      algo = b.getAttribute('data-algo');\n      root.querySelectorAll('[data-algo]').forEach(function (x) { x.setAttribute('aria-pressed', String(x === b)); });\n      load();\n    });\n  });\n  $('step').addEventListener('click', function () { stop(); advance(); });\n  $('play').addEventListener('click', function () { if (playing) stop(); else play(); });\n  $('reset').addEventListener('click', load);\n  $('shuffle').addEventListener('click', function () { order = nextOrder(); load(); });\n  $('speed').addEventListener('input', speedText);\n\n  build();\n  order = nextOrder();\n  speedText();\n  load();\n})();"
}
```

Table, specimen 2:

```element table
{
  "caption": "The unit’s sorting algorithms, side by side",
  "columns": [
    {
      "label": "Algorithm",
      "align": "left"
    },
    {
      "label": "Best",
      "align": "left"
    },
    {
      "label": "Average",
      "align": "left"
    },
    {
      "label": "Worst",
      "align": "left"
    },
    {
      "label": "Extra memory",
      "align": "left"
    },
    {
      "label": "Stable",
      "align": "left"
    }
  ],
  "rows": [
    [
      "Bubble sort, stopping early",
      "O(n)",
      "O(n²)",
      "O(n²)",
      "O(1)",
      "Yes"
    ],
    [
      "Insertion sort",
      "O(n)",
      "O(n²)",
      "O(n²)",
      "O(1)",
      "Yes"
    ],
    [
      "Selection sort",
      "O(n²)",
      "O(n²)",
      "O(n²)",
      "O(1)",
      "No"
    ],
    [
      "Merge sort",
      "O(n log n)",
      "O(n log n)",
      "O(n log n)",
      "O(n)",
      "Yes"
    ],
    [
      "Python’s sorted()",
      "O(n)",
      "O(n log n)",
      "O(n log n)",
      "O(n)",
      "Yes"
    ]
  ],
  "pick": 4,
  "notes": "Comparisons made on a list of *n* items. Bubble sort reaches O(n) only if it stops after a pass with no swaps; insertion sort does on a list already in order. Selection sort as usually written can swap equal items out of their order, so it is not stable. Python’s `sorted()` is Timsort, a merge sort that finds runs already in order and extends short ones by insertion: marked as the one to use outside this unit."
}
```
