Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,36 @@ jobs:
# workspace copy is already installed, so what gets executed is the working tree and
# not whatever is on main, which is the whole point of running them in a pull request.
- run: uv run nbcheck run
# Same cells again, on this leg's interpreter, saved as one small JSON file per
# notebook. The versions job below is what reads them. Recording is separate from
# `nbcheck run` on purpose: that step stops at the first cell that raises, and this
# one keeps going, so a lesson that breaks cannot hide every version difference in
# the lessons after it.
- run: uv run nbversion record --into build/versions
- uses: actions/upload-artifact@v4
with:
name: versions-${{ matrix.python }}
path: build/versions
retention-days: 1

# Both legs of the notebooks matrix ran the same cells on different interpreters. This
# job is the one that compares the two and fails if a cell's output depends on which
# Python the reader has and the lesson does not say so.
versions:
runs-on: ubuntu-latest
needs: notebooks
steps:
- uses: actions/checkout@v5
- uses: astral-sh/setup-uv@v6
with:
enable-cache: true
- run: uv sync --all-packages
- uses: actions/download-artifact@v5
with:
pattern: versions-*
merge-multiple: true
path: build/versions
- run: uv run nbversion compare build/versions/3.15 build/versions/3.14

blueprints:
runs-on: ubuntu-latest
Expand Down
2 changes: 2 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ lesson.code(

Say what the reader is looking at and what the other version does instead. "This differs on 3.14" is not a note, it is an apology. Add `quiet=True` when a paragraph near the top of the lesson already explains a difference that then turns up in a dozen cells, so the same sentence is not repeated under every one of them.

There is a second keyword for the other kind of cell. `varies=` is for output that depends on the reader's machine rather than on the version: which flags their interpreter was configured with, how many files are in their standard library, how deep the C stack goes before it runs out. It reads exactly the same to a reader and the check treats it differently. A `differs` note is a claim two recordings can test, so it fails when it stops being true. A `varies` note is not, because whether two runs agree about a machine difference depends on which two machines made them, and a CI box where both interpreters came from the same builder would delete a note that is still right for somebody on a framework install. So `varies` is reported and never fails. Do not reach for it to silence a real version difference.

When the differing cell is the lesson's central observation, the note is not enough. Either the lesson gets a short section explaining both versions, because the difference is itself worth teaching, or the example changes to one that behaves the same on both. Which of the two depends on whether the difference is interesting. `LOAD_COMMON_CONSTANT` is interesting and gets explained. A line number inside `asyncio` is not, and the cell should stop printing it.

## Definition of done for a lesson
Expand Down
68 changes: 51 additions & 17 deletions lessons/t01-one-line-seven-stages/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
image somebody finds later.
"""

from nbbuild import Lesson
from nbbuild import BANNER, TRAILING_NONE, Lesson
from nbdiagram import Diagrams

lesson = Lesson("t01-one-line-seven-stages", "t01")
Expand Down Expand Up @@ -76,14 +76,20 @@
## Which interpreter is this

Nearly every fact in this lesson is a fact about one particular build of CPython. Instruction names change between releases and so do the sizes of things, so every lesson here starts by saying out loud which binary is about to produce the output you are reading.

If the banner says 3.14, which is what Colab installs today, nearly all of this lesson is the same and a handful of cells are not. The differences below all come from one change. On 3.15 the implicit `return None` at the end of a module is a `LOAD_COMMON_CONSTANT`, and `None` is not in the constant table at all. On 3.14 it is an ordinary `LOAD_CONST`, and `None` is in the table. That is one more constant and two fewer bytes of bytecode everywhere those get printed. Stage 6 comes back to it and asks your build directly.
""")


lesson.code("""
lesson.code(
"""
import pyxray

pyxray.show()
""")
""",
differs=BANNER,
quiet=True,
)


lesson.md("""
Expand Down Expand Up @@ -247,9 +253,13 @@
""")


lesson.code("""
lesson.code(
"""
print(compiler.what_the_optimizer_did(result))
""")
""",
differs=TRAILING_NONE,
quiet=True,
)


lesson.md(f"""
Expand All @@ -270,9 +280,13 @@
""")


lesson.code(r"""
lesson.code(
r"""
print(compiler.what_the_optimizer_did(compiler.stages("answer = six * 7\n")))
""")
""",
differs=TRAILING_NONE,
quiet=True,
)


lesson.md(f"""
Expand All @@ -282,7 +296,8 @@
""")


lesson.code("""
lesson.code(
"""
import dis

print("co_consts ", result.code.co_consts)
Expand All @@ -291,7 +306,10 @@
print("co_code ", len(result.code.co_code), "bytes")
print()
dis.dis(result.code)
""")
""",
differs="On 3.14 co_consts is (6, None) rather than (6,) and the bytecode is 10 bytes rather than 12, because None still needs a table entry there.",
quiet=True,
)


lesson.md(f"""
Expand All @@ -305,14 +323,18 @@
""")


lesson.code("""
lesson.code(
"""
loaded = {i.argval for i in dis.get_instructions(result.code) if i.opname == "LOAD_CONST"}
inline = [str(i) for i in result.optimized if "SMALL_INT" in i.opname]

print("constants the code object carries:", result.code.co_consts)
print("constants any instruction loads: ", loaded or "none")
print("where the 42 actually lives: ", inline)
""")
""",
differs="On 3.14 one instruction does load a constant, the None at the end, so the middle line says {None} rather than none. The point of the cell survives: nothing loads the 6.",
quiet=True,
)


lesson.md(f"""
Expand All @@ -322,13 +344,17 @@
""")


lesson.code("""
lesson.code(
"""
print("python ", sys.version.split()[0])
print("last instructions ", [str(item) for item in result.optimized[-2:]])
print("co_consts ", result.code.co_consts)
print("bytecode ", len(result.code.co_code), "bytes")
print("None is a constant", None in result.code.co_consts)
""")
""",
differs="This cell is here to differ. On 3.14 the last line says True and on 3.15 it says False, which is the whole paragraph above turned into output.",
quiet=True,
)


lesson.md(f"""
Expand All @@ -353,9 +379,13 @@
""")


lesson.code("""
lesson.code(
"""
print(result.summary())
""")
""",
differs="On 3.14 the last number is 10 bytes rather than 12. Everything to the left of it is the same.",
quiet=True,
)


lesson.md("""
Expand All @@ -382,14 +412,18 @@
""")


lesson.code(r"""
lesson.code(
r"""
MINE = "x = 2 ** 10\n"

mine = compiler.stages(MINE)
print(mine.summary())
print()
print(compiler.what_the_optimizer_did(mine))
""")
""",
differs=TRAILING_NONE,
quiet=True,
)


lesson.md("""
Expand Down
52 changes: 43 additions & 9 deletions lessons/t01-one-line-seven-stages/t01.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -69,14 +69,20 @@
"source": [
"## Which interpreter is this\n",
"\n",
"Nearly every fact in this lesson is a fact about one particular build of CPython. Instruction names change between releases and so do the sizes of things, so every lesson here starts by saying out loud which binary is about to produce the output you are reading."
"Nearly every fact in this lesson is a fact about one particular build of CPython. Instruction names change between releases and so do the sizes of things, so every lesson here starts by saying out loud which binary is about to produce the output you are reading.\n",
"\n",
"If the banner says 3.14, which is what Colab installs today, nearly all of this lesson is the same and a handful of cells are not. The differences below all come from one change. On 3.15 the implicit `return None` at the end of a module is a `LOAD_COMMON_CONSTANT`, and `None` is not in the constant table at all. On 3.14 it is an ordinary `LOAD_CONST`, and `None` is in the table. That is one more constant and two fewer bytes of bytecode everywhere those get printed. Stage 6 comes back to it and asks your build directly."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "t01-06",
"metadata": {},
"metadata": {
"cpython_internals": {
"differs": "This prints the interpreter you are on, so it is different for everybody."
}
},
"outputs": [],
"source": [
"import pyxray\n",
Expand Down Expand Up @@ -325,7 +331,11 @@
"cell_type": "code",
"execution_count": null,
"id": "t01-27",
"metadata": {},
"metadata": {
"cpython_internals": {
"differs": "On 3.14 the implicit return None at the end is a LOAD_CONST and None sits in co_consts, so you get one more constant and two fewer bytes of bytecode than the text says."
}
},
"outputs": [],
"source": [
"print(compiler.what_the_optimizer_did(result))"
Expand Down Expand Up @@ -359,7 +369,11 @@
"cell_type": "code",
"execution_count": null,
"id": "t01-30",
"metadata": {},
"metadata": {
"cpython_internals": {
"differs": "On 3.14 the implicit return None at the end is a LOAD_CONST and None sits in co_consts, so you get one more constant and two fewer bytes of bytecode than the text says."
}
},
"outputs": [],
"source": [
"print(compiler.what_the_optimizer_did(compiler.stages(\"answer = six * 7\\n\")))"
Expand All @@ -379,7 +393,11 @@
"cell_type": "code",
"execution_count": null,
"id": "t01-32",
"metadata": {},
"metadata": {
"cpython_internals": {
"differs": "On 3.14 co_consts is (6, None) rather than (6,) and the bytecode is 10 bytes rather than 12, because None still needs a table entry there."
}
},
"outputs": [],
"source": [
"import dis\n",
Expand Down Expand Up @@ -410,7 +428,11 @@
"cell_type": "code",
"execution_count": null,
"id": "t01-34",
"metadata": {},
"metadata": {
"cpython_internals": {
"differs": "On 3.14 one instruction does load a constant, the None at the end, so the middle line says {None} rather than none. The point of the cell survives: nothing loads the 6."
}
},
"outputs": [],
"source": [
"loaded = {i.argval for i in dis.get_instructions(result.code) if i.opname == \"LOAD_CONST\"}\n",
Expand All @@ -435,7 +457,11 @@
"cell_type": "code",
"execution_count": null,
"id": "t01-36",
"metadata": {},
"metadata": {
"cpython_internals": {
"differs": "This cell is here to differ. On 3.14 the last line says True and on 3.15 it says False, which is the whole paragraph above turned into output."
}
},
"outputs": [],
"source": [
"print(\"python \", sys.version.split()[0])\n",
Expand Down Expand Up @@ -482,7 +508,11 @@
"cell_type": "code",
"execution_count": null,
"id": "t01-40",
"metadata": {},
"metadata": {
"cpython_internals": {
"differs": "On 3.14 the last number is 10 bytes rather than 12. Everything to the left of it is the same."
}
},
"outputs": [],
"source": [
"print(result.summary())"
Expand Down Expand Up @@ -522,7 +552,11 @@
"cell_type": "code",
"execution_count": null,
"id": "t01-43",
"metadata": {},
"metadata": {
"cpython_internals": {
"differs": "On 3.14 the implicit return None at the end is a LOAD_CONST and None sits in co_consts, so you get one more constant and two fewer bytes of bytecode than the text says."
}
},
"outputs": [],
"source": [
"MINE = \"x = 2 ** 10\\n\"\n",
Expand Down
10 changes: 7 additions & 3 deletions lessons/t02-text-becomes-tokens/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
notebook full of broken images.
"""

from nbbuild import Lesson
from nbbuild import BANNER, Lesson
from nbdiagram import Diagrams

lesson = Lesson("t02-text-becomes-tokens", "t02")
Expand Down Expand Up @@ -92,11 +92,15 @@
""")


lesson.code("""
lesson.code(
"""
import pyxray

pyxray.show()
""")
""",
differs=BANNER,
quiet=True,
)


lesson.md(f"""
Expand Down
6 changes: 5 additions & 1 deletion lessons/t02-text-becomes-tokens/t02.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,11 @@
"cell_type": "code",
"execution_count": null,
"id": "t02-06",
"metadata": {},
"metadata": {
"cpython_internals": {
"differs": "This prints the interpreter you are on, so it is different for everybody."
}
},
"outputs": [],
"source": [
"import pyxray\n",
Expand Down
Loading
Loading