Generate exercise versions of Jupyter notebooks
Project description
ipynb-scrubber
Generate exercise versions of Jupyter notebooks by clearing solution cells and removing instructor-only content.
[!NOTE] This is a project made to satisfy a need on some personal projects. The behaivor has been tested to work for these projects but will not be supported for other uses.
Issues will be reviewed if opened, and any legitimate bugs will be fixed, but new features or ideas will likely be rejected unless accompanied by a working pull request with comprehensive tests.
Thanks for understanding.
Features
- Clear solution cells: Replace cell contents with placeholder text while preserving structure
- Save notes: Collect code cell contents before clearing and save to a separate Markdown file for instructor reference with bidirectional linking
- Custom replacement text: Use cell-specific text instead of default placeholder
- All cell types supported: Works with code, markdown, and raw cells
- Remove cells entirely: Omit instructor-only cells from the output
- Multiple syntax options: Use cell tags or cell-type-appropriate comment syntax
- Preserve structure: Maintain notebook structure and metadata
- Clear all outputs: Remove all cell outputs and execution counts for a clean slate
- Project-wide processing: Process multiple notebooks with a single command using a TOML config file
- Flexible CLI: Unix-style stdin/stdout for single files, or config-based batch processing for projects
Installation
Install with a python package manager like pip or uv:
pip install ipynb-scrubber
Usage
The tool provides two commands for different workflows:
Single Notebook: scrub-notebook
Process a single notebook via stdin/stdout (Unix-style):
ipynb-scrubber scrub-notebook < input.ipynb > output.ipynb
Options
--clear-tag TAG: Tag marking cells to clear (default:scrub-clear)--clear-text TEXT: Replacement text for cleared cells where unspecified (default:# TODO: Implement this)--omit-tag TAG: Tag marking cells to omit entirely (default:scrub-omit)
Examples
Using default settings:
ipynb-scrubber scrub-notebook < lecture.ipynb > exercise.ipynb
Using custom tags:
ipynb-scrubber scrub-notebook \
--clear-tag solution \
--omit-tag private \
< lecture.ipynb > exercise.ipynb
Using custom placeholder text:
ipynb-scrubber scrub-notebook \
--clear-text "# YOUR CODE HERE" \
< lecture.ipynb > exercise.ipynb
Project-Wide: scrub-project
Process multiple notebooks using a configuration file:
ipynb-scrubber scrub-project
The command searches for configuration in the following order, starting from the current directory and moving upward:
.ipynb-scrubber.toml(standalone config file)pyproject.tomlwith[tool.ipynb-scrubber]section
This means you can run the command from any subdirectory of your project.
Configuration File Formats
Option 1: Standalone .ipynb-scrubber.toml
Create a .ipynb-scrubber.toml file with global options and file entries:
# Global options (optional - these are defaults)
[options]
clear-tag = "scrub-clear"
clear-text = "# TODO: Implement this"
omit-tag = "scrub-omit"
# File entries (required - at least one)
[[files]]
input = "lectures/lesson1.ipynb"
output = "exercises/lesson1.ipynb"
[[files]]
input = "lectures/lesson2.ipynb"
output = "exercises/lesson2.ipynb"
clear-text = "# YOUR CODE HERE" # Override global option
[[files]]
input = "lectures/lesson3.ipynb"
output = "exercises/lesson3.ipynb"
clear-tag = "solution" # Custom tag for this file
omit-tag = "instructor"
Each file entry supports:
input(required): Path to source notebookoutput(required): Path where scrubbed notebook will be writtenclear-tag(optional): Override global clear tagclear-text(optional): Override global clear textomit-tag(optional): Override global omit tag
Option 2: Using pyproject.toml
Add configuration to your existing pyproject.toml under
[tool.ipynb-scrubber]:
# Global options (optional - these are defaults)
[tool.ipynb-scrubber.options]
clear-tag = "scrub-clear"
clear-text = "# TODO: Implement this"
omit-tag = "scrub-omit"
# File entries (required - at least one)
[[tool.ipynb-scrubber.files]]
input = "lectures/lesson1.ipynb"
output = "exercises/lesson1.ipynb"
[[tool.ipynb-scrubber.files]]
input = "lectures/lesson2.ipynb"
output = "exercises/lesson2.ipynb"
clear-text = "# YOUR CODE HERE"
This is convenient if you're already using pyproject.toml for your Python
project. The tool will automatically find and use this configuration.
Custom Config File
Specify a different config file location (bypasses automatic discovery):
ipynb-scrubber scrub-project --config-file path/to/config.toml
Marking Cells
There are two ways to mark cells for processing:
1. Cell Tags (All Cell Types)
Add tags to cells using Jupyter's tag interface. This works for all cell types (code, markdown, raw):
- Add
scrub-cleartag to solution cells that should be cleared - Add
scrub-omittag to cells that should be removed entirely
Note: The scrub-note tag requires source-based syntax (see below) and only
works for code cells.
2. Source-Based Options (Code & Markdown)
Use cell-type-appropriate syntax for more control, including custom replacement text:
Code Cells - Quarto Options
#| scrub-clear
def secret_solution():
return 42
# Or with custom replacement text:
#| scrub-clear: # WRITE YOUR SOLUTION HERE
def another_solution():
return "hidden"
# To save to notes and clear (requires ID):
#| scrub-note: exercise-1
def solution_with_notes():
# This solution will be saved to the notes file
# and then cleared from the student version
return "answer"
# With custom replacement text:
#| scrub-note: exercise-2 | # YOUR SOLUTION HERE
def another_noted_solution():
return "more answers"
# To omit entirely:
#| scrub-omit
# This cell will be removed
print("Instructor only!")
Markdown Cells - HTML Comments
<!-- scrub-clear -->
## Answer
The solution is 42 because...
<!-- Or with custom replacement text: -->
<!-- scrub-clear: **Write your answer here** -->
## Another Question
This answer will be replaced.
<!-- To omit entirely: -->
<!-- scrub-omit -->
## Instructor Notes
These notes are only for the instructor.
Note: The scrub-note option is only available for code cells.
Raw Cells - Tags Only
Raw cells only support metadata tags to avoid format conflicts:
# Cell metadata: {"tags": ["scrub-clear"]}
$$\int_0^1 x^2 dx = \frac{1}{3}$$
# Cell metadata: {"tags": ["scrub-omit"]}
% This LaTeX comment will be omitted entirely
Custom Replacement Text
When using source-based options, you can specify custom text to replace the cleared content:
#| scrub-clear: Your custom text(code cells)<!-- scrub-clear: Your custom text -->(markdown cells)- Empty text:
#| scrub-clear:(results in empty cell)
If no custom text is provided, the default --clear-text value is used.
Notes Files
Code cells only - Cells marked with the scrub-note tag will have their
content saved to a separate Markdown file before being cleared from the
student version. This creates bidirectional linking between the exercise and
solutions.
Required format:
#| scrub-note: note-id
#| scrub-note: note-id | custom replacement text
The note-id is required and should be a human-readable identifier (e.g.,
exercise-1, question-2a). When the cell is cleared, a reference comment
is automatically added:
# TODO: Implement this
# (See notes: exercise-1)
This creates a clear link from the exercise notebook to the notes file.
Behavior by command:
scrub-notebook: If note cells are found but no--notes-fileis specified, a warning is issued but processing continuesscrub-project: If note cells are found but nonotes-fileis specified in the config, processing fails with an error
Notes file format:
The notes file is generated in Markdown format with human-readable IDs:
# Notebook Notes
This file contains the original content of cells marked for note-taking.
## exercise-1
\```python
def secret_solution():
return 42
\```
## question-2a
\```python
def another_solution():
return "answer"
\```
---
*Generated by ipynb-scrubber*
Usage examples:
# scrub-notebook with notes
ipynb-scrubber scrub-notebook --notes-file solutions.md < lecture.ipynb > exercise.ipynb
# scrub-project with notes in config
# .ipynb-scrubber.toml:
# [[files]]
# input = "lecture.ipynb"
# output = "exercise.ipynb"
# notes-file = "solutions.md"
Example
Input Notebook
Code Cell 1 (no tags):
# Instructions - this will remain unchanged
print("Exercise: implement the functions below")
Code Cell 2 (Quarto option with custom text):
#| scrub-clear: # TODO: Write your add function here
def add(a, b):
return a + b
result = add(1, 2)
print(f"Result: {result}")
Markdown Cell 3 (HTML comment):
<!-- scrub-clear: **Write your explanation here** -->
## Solution Explanation
The add function works by using the + operator...
Code Cell 4 (cell tag - will be omitted):
# Cell has metadata: {"tags": ["scrub-omit"]}
# This cell will be removed entirely
assert add(1, 2) == 3
print("Tests pass!")
Output Notebook
Code Cell 1 (unchanged):
# Instructions - this will remain unchanged
print("Exercise: implement the functions below")
Code Cell 2 (cleared with custom text):
# TODO: Write your add function here
Markdown Cell 3 (cleared with custom text):
**Write your explanation here**
Code Cell 4 (omitted entirely)
Behavior
- All cell outputs are cleared: Every cell has its output and execution count removed
- Tagged cells are processed:
- Cells with the clear tag have their source code replaced with placeholder text
- Cells with the omit tag are removed entirely from the output
- Notebook metadata: An
exercise_versionflag is added to the notebook metadata - Error handling: Invalid notebooks produce helpful error messages
License
Apache License 2.0
Contributing
Contributions are welcome! Please feel free to submit a Pull Request, but note that comprehensive test coverage and clear justification for why the request should be considered (keeping in mind new features increase the maintenance burden) must be included.
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file ipynb_scrubber-0.4.0.tar.gz.
File metadata
- Download URL: ipynb_scrubber-0.4.0.tar.gz
- Upload date:
- Size: 42.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6f6e98522aa93f0b449019176f2ba7b4e786963fcc4df9d47e029ca0eccf2d8b
|
|
| MD5 |
779865c1f4eb9fd00aaf307315728278
|
|
| BLAKE2b-256 |
50de08f894f6ef510b917637d41f43c38cc4bd2a4197cfc8b3a584c237c8e0e2
|
Provenance
The following attestation bundles were made for ipynb_scrubber-0.4.0.tar.gz:
Publisher:
release.yml on jkeifer/ipynb-scrubber
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
ipynb_scrubber-0.4.0.tar.gz -
Subject digest:
6f6e98522aa93f0b449019176f2ba7b4e786963fcc4df9d47e029ca0eccf2d8b - Sigstore transparency entry: 641758535
- Sigstore integration time:
-
Permalink:
jkeifer/ipynb-scrubber@efb84a72d7dd7290b53f4517b6d8212d6ba4a086 -
Branch / Tag:
refs/tags/v0.4.0 - Owner: https://github.com/jkeifer
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@efb84a72d7dd7290b53f4517b6d8212d6ba4a086 -
Trigger Event:
release
-
Statement type:
File details
Details for the file ipynb_scrubber-0.4.0-py3-none-any.whl.
File metadata
- Download URL: ipynb_scrubber-0.4.0-py3-none-any.whl
- Upload date:
- Size: 18.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
47b9aa1e6ca524c223a85034bda232f819ff7787c5d302052b2657622d03ea0b
|
|
| MD5 |
5b3998fa10a2e07d56e84c87ad1a1b49
|
|
| BLAKE2b-256 |
2bf6eff871e81dfc3ec582f2f8b17e78873eab1dfaf76f8c754edcd046d63b30
|
Provenance
The following attestation bundles were made for ipynb_scrubber-0.4.0-py3-none-any.whl:
Publisher:
release.yml on jkeifer/ipynb-scrubber
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
ipynb_scrubber-0.4.0-py3-none-any.whl -
Subject digest:
47b9aa1e6ca524c223a85034bda232f819ff7787c5d302052b2657622d03ea0b - Sigstore transparency entry: 641758540
- Sigstore integration time:
-
Permalink:
jkeifer/ipynb-scrubber@efb84a72d7dd7290b53f4517b6d8212d6ba4a086 -
Branch / Tag:
refs/tags/v0.4.0 - Owner: https://github.com/jkeifer
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@efb84a72d7dd7290b53f4517b6d8212d6ba4a086 -
Trigger Event:
release
-
Statement type: