Scientific Python Lectures: the Makefile, the strict type checker and the release numbers that do not line up
Tutorial material on the scientific Python ecosystem
At a glance
- What is it?
- The lecture material behind lectures.scientific-python.org is built by a short Makefile whose recipes disagree with the paths its own type checker excludes, while the release list runs 2022.1 after 2024.1. Anyone teaching from it should build it locally instead of trusting the tags.
- Who is it for?
- Take the text, build it yourself, and treat the release list as a naming scheme rather than a chronology, because the highest version number is not the most recently published build and the documentation half of the environment is unpinned.
- Can I use it commercially?
- Check first. The repository uses a licence we do not classify automatically, so read its LICENSE file before any commercial use.
- Is it still maintained?
- Yes. The repository last received commits 4 days ago.
- What is it written in?
- Mainly Python, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 3, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The only always-on build target guards itself with a bash builtin
The entire book build is one Makefile, and its one always-run target protects itself with a shell builtin. Two lines of configuration sit at the top:
PYTHON ?= python
PIP_INSTALL_CMD ?= $(PYTHON) -m pip installWhat is missing is SHELL. Make runs each recipe with /bin/sh, and the guard below leans on compgen, which is a bash builtin:
html:
# Check for ipynb files in source (should all be text - .md or .Rmd).
if compgen -G "*.ipynb" 2> /dev/null; then (echo "ipynb files" && exit 1); fi
jupyter-book build -W .Where the recipe shell is not bash, the test cannot report a match and the build proceeds regardless of what the comment above it warns about. The guard also stops at the repository root even though the lectures live under intro/, guide/, advanced/ and packages/. The build command carries -W, which turns warnings into errors, so a deprecation notice from Sphinx or any extension ends the whole book rather than printing a note into the log. Overriding PYTHON redirects pip and the notebook scripts, but not the builder, which is invoked by name and not through $(PYTHON).
make clean deletes recursively while the notebook guard only looks at the root
clean is the most destructive recipe here and it confirms nothing. It depends on rm-ipynb, which is a recursive find piped straight into rm:
clean: rm-ipynb
rm -rf _build
-find . -name ".ipynb_checkpoints" -exec rm -rf {} \;
-find . -name "joblib" -exec rm -rf {} \;
rm-ipynb:
find . -name "*.ipynb" -exec rm {} \;The asymmetry with the build guard is the substance of it: the guard asks whether a notebook sits in the root, the deletion walks the entire repository. A notebook left in intro/numpy/ therefore passes the html target untouched and is still removed by clean, while one dropped in the root fails the build and is then removed by the same target that failed. Nothing prompts, nothing is listed, and no dry run exists. A clone that carries personal notes in a .ipynb file, or a scratch directory named joblib, loses both to a single clean.
Five requirement files at the root, one of them installed by any target
The root carries five requirement files: requirements.txt, build_requirements.txt, dev_requirements.txt, test_requirements.txt and jl-build-requirements.txt. The Makefile names exactly one of them, jl-build-requirements.txt, inside the jl target. Nothing references the other three, so test runs pytest . with no recipe ahead of it that installs test_requirements.txt, and the only visible pytest constraint lives in the file whose own first line claims a different audience:
# Requirements for notebooks / Binderhub
numpy==2.4.2
scipy==1.17.0
matplotlib==3.10.8
pandas==3.0
scikit-learn==1.8.0
scikit-image==0.26
sympy==1.14.0
statsmodels==0.14.6
seaborn==0.13.2
pytest>=9.0That file mixes three policies in one list. The numeric stack is pinned exactly, the test tools carry lower bounds, and the documentation toolchain is bare: sphinx, sphinx-copybutton, requests, jupytext and myst_nb appear with no version at all. A notebook environment rebuilt from this list today is therefore not the one the book was last rendered against, and the part of the toolchain that decides how the pages look is the part that floats.
2022.1 was published after 2024.1
Releases in this repository are named by year, and the dates do not follow the names. Three are listed: 2024.1 dated 2024-04-26, 2022.1 dated 2025-04-29, and 2020.1-beta dated 2020-04-01. The middle entry was published a year after the highest numbered one. Sorting releases by name hands you the oldest build; sorting by date hands you a lower version than the entry above it. Every release title carries the same Release prefix, and the 2022.1 title ends with a space after the number, which matters for anything matching titles rather than tag names. The practical consequence is narrow and real: the newest tag is not the most recently published artifact, so a course that cites the 2024.1 release as current is citing a name, not a date.
Strict mypy, relaxed by hand and carved out path by path
The type checking starts strict and is then walked back. [tool.mypy] sets strict = true, warn_unreachable = true and three extra error codes, then switches two checks off again with allow_untyped_defs and allow_untyped_calls under the comment that these can be made more strict over time. The exclude block that follows removes roughly two dozen paths, whole directories included:
exclude = '''
(?x)(
^build/
| ^pyximages/
| conf\.py$
| .*/setup.*\.py$
| .*/auto_examples/
| _scripts/examples2nb.py$
| _scripts/post_parser.py$Two comments attribute exclusions to upstream bugs rather than local style: one points at a numpy issue for the markov chain solution in intro/numpy/solutions, the other at a matplotlib issue for the bar chart. The second of those comments spells the path intro/matplotliub while the pattern it annotates matches intro/matplotlib, so the note points at a directory name that is not in the tree. numpy_shared/test_cos_doubles.py also appears twice in the list. One comment at the top asks that this file be kept in sync with .pre-commit-config.yaml, which means the two lists drift together or not at all, with no tool enforcing it.
compare_optimizers.py is excluded at a path the Makefile never visits
One helper script is named at two different locations. The Makefile reaches it through advanced/mathematical_optimization/helper, while the mypy exclusion names the same file one level deeper, with an examples directory the recipe does not have:
compare-optimizers:
( cd advanced/mathematical_optimization/helper && \
python compare_optimizers.py )Whichever spelling is current, the other one is stale. The recipe also drops the interpreter override that the rest of the file respects: it calls python directly, where every other Python invocation goes through $(PYTHON), so make PYTHON=python3 compare-optimizers still runs whatever python resolves to on the shell path. The same chapter is otherwise excluded wholesale, since plot_gradient_descent.py sits under advanced/mathematical_optimization/examples, which means the teaching scripts of that section are opted out of checking rather than their imports.
Publishing is the build plus a force push to the pages branch
There is no separate deploy step. web depends on html and jl, and github depends on web, so publishing always pays for a full Jupyter Lite build first. That target installs jl-build-requirements.txt, deletes and recreates _build/jl, copies only the data and images directories into it, hands the directory to _scripts/process_notebooks.py, and then runs the lite builder with its output set inside the directory the first target wrote:
jl:
# Jupyter-lite files for book build.
$(PIP_INSTALL_CMD) -r jl-build-requirements.txt
rm -rf $(JL_DIR)
mkdir $(JL_DIR)
cp -r data images $(JL_DIR)
$(PYTHON) _scripts/process_notebooks.py $(JL_DIR)
$(PYTHON) -m jupyter lite build \
--contents $(JL_DIR) \
--output-dir $(BUILD_DIR)/interact \
--lite-dir $(JL_DIR)Since BUILD_DIR is _build/html, both builds end up in one folder, and the last line of the file, ghp-import -n _build/html -p -f, pushes and force replaces the pages branch with whatever is in it. Anything a previous build left behind in that folder travels with the push, and the only recipe that clears the directory is clean.
Editorial conclusion
Take the text, build it yourself, and treat the release list as a naming scheme rather than a chronology, because the highest version number is not the most recently published build and the documentation half of the environment is unpinned. Two operational facts matter before you touch the build: make clean deletes recursively without asking, including any directory named joblib anywhere in the clone, and make github is not a deploy step you can call on its own, since it forces a whole Jupyter Lite build and then replaces the pages branch with whatever sits in the output directory. On licensing, the repository license field reads NOASSERTION while LICENSE.md sits in the root and the reuse note describes the material as coming with no strings attached for your own teaching; decide on that wording rather than on the metadata. On contributing, changes are invited but reviewed and edited by the original authors and the editors, so fork and keep your edits if you need them to land.
Frequently asked questions
Is the Scientific Python Lectures repository still being worked on?
The main branch recorded a push on 2026-10-01. The release list is older and unordered by date: 2024.1 published 2024-04-26, 2022.1 published 2025-04-29, and 2020.1-beta published 2020-04-01. The README also asks contributors to send changes back for review by the original authors and editors.
Which license governs reuse of the Scientific Python Lectures material?
The repository license field reads NOASSERTION, yet LICENSE.md sits at the root. The reuse note in the README says the material comes with no strings attached and invites reuse and modification for your own teaching purposes, so the wording to rely on is that file rather than the metadata field.
What does building Scientific Python Lectures from source require?
There is no single install command. The Makefile html target calls jupyter-book build -W . after checking for stray notebooks, the jl target installs jl-build-requirements.txt with $(PIP_INSTALL_CMD) and then runs jupyter lite build, and four other requirement files at the root are referenced by no target at all.
Can I teach from Scientific Python Lectures without sending changes upstream?
Yes for your own teaching. The reuse note invites reuse and modification for teaching purposes and describes the material as carrying no strings attached. It also asks that improvements be contributed back, where changes are reviewed and edited by the original authors and the editors, so a fork is the way to keep your own edits.
What does make clean remove in the Scientific Python Lectures checkout?
It depends on rm-ipynb, which runs find . -name for notebooks and deletes them, then it removes _build entirely, every .ipynb_checkpoints directory, and every directory named joblib under the tree. No prompt is shown and there is no dry run, so a single clean reaches files outside the build output.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/scipy-lectures-scientific-python-lectures)