Automatic grading framework for student code in Google Colab and Jupyter notebooks
Project description
notebook-cell-tester
Automatic grading framework for student code in Google Colab and Jupyter notebooks.
Instructors drop a single test cell below the student's code cell. The framework reads the previous cell's source from IPython history, executes it, runs the configured tests, and renders a color-coded HTML results table — no extra setup required from students.
Table of contents
- Installation
- Quick start
- How it works
- Test types
- return — function return value
- output — exact stdout match
- exception — expected exception
- regex / not_regex — source code pattern
- variable — value with validator
- type_check — isinstance check
- partial_output — fuzzy match
- regex_output — stdout regex
- contains_output — stdout substring
- multiline_output — order-independent lines
- Sections
- TestCase field reference
- Results display
- Common patterns
- License
Installation
pip install notebook-cell-tester
Colab one-liner — paste this at the top of the test cell if the package is not pre-installed in the environment:
!pip install -q notebook-cell-tester
Quick start
Place this cell immediately after the student's code cell in the notebook:
from notebook_cell_tester import ColabTestFramework, TestCase
tester = ColabTestFramework()
tests = [
TestCase(
name="add_numbers returns correct sum",
test_type="return",
function_name="add_numbers",
inputs=[3, 4],
expected=7,
description="add_numbers(3, 4) should return 7",
),
TestCase(
name="Prints greeting",
test_type="output",
stdin_input="Alice",
expected="Hello, Alice!",
),
]
tester.run_tests(tests)
tester.display_results()
Running this cell produces a color-coded HTML table showing each test's pass/fail status, expected vs. actual values, and (when something crashes) a collapsible traceback that students can expand.
How it works
ColabTestFramework()is instantiated in the test cell — the cell below the student's code.run_tests(tests)callsload_last_cell(), which readsIn[-2]from the IPython namespace to get the second-to-last cell (the student's code, not the test cell).- Each
TestCaseis dispatched to the appropriate internal method based ontest_type. display_results()renders an HTML table with pass/fail badges, result details, and collapsible error tracebacks.
Tests that only inspect the IPython namespace (variable, type_check on a variable) run even when the student hasn't executed their cell yet.
Test types
Summary
test_type |
Checks | Key fields |
|---|---|---|
return |
Function return value | function_name, inputs, expected, tolerance |
output |
Exact printed output | stdin_input, expected, optional function_name |
exception |
Function raises expected exception | function_name, inputs, expected |
regex |
Source code matches pattern | pattern |
not_regex |
Source code does NOT match pattern | pattern |
variable |
Variable satisfies validator | variable_name, validator |
type_check |
Value is correct type | function_name or variable_name, expected |
partial_output |
Output similarity ≥ threshold (Levenshtein) | expected, similarity_threshold |
regex_output |
Printed output matches regex | pattern |
contains_output |
Printed output contains substring | expected |
multiline_output |
Every expected line appears in output | expected |
return — function return value
Calls function_name(*inputs) and checks that the return value equals expected.
TestCase(
name="Square of 5",
test_type="return",
function_name="square",
inputs=[5],
expected=25,
)
Use tolerance for floating-point comparisons (abs(result - expected) <= tolerance):
TestCase(
name="Average of [1, 2, 3]",
test_type="return",
function_name="average",
inputs=[[1, 2, 3]],
expected=2.0,
tolerance=1e-9,
)
output — exact stdout match
Captures everything printed to stdout and compares it to expected (after stripping leading/trailing whitespace).
# Test the whole cell — no function_name needed
TestCase(
name="Greeting output",
test_type="output",
stdin_input="Alice",
expected="Hello, Alice!",
)
# Test a specific function's printed output
TestCase(
name="print_square(4)",
test_type="output",
function_name="print_square",
inputs=[4],
expected="16",
)
Supply stdin_input to simulate one or more input() calls. Separate multiple lines with \n.
exception — expected exception
Passes when function_name(*inputs) raises exactly the expected exception type. Fails if it returns normally or raises a different exception.
TestCase(
name="Division by zero raises ValueError",
test_type="exception",
function_name="safe_divide",
inputs=[10, 0],
expected=ValueError,
)
regex / not_regex — source code pattern
Searches the student's source code for a Python regex. Useful for requiring or forbidding specific language constructs without running the code.
# Require a for loop
TestCase(
name="Uses a for loop",
test_type="regex",
pattern=r"\bfor\b",
description="Your solution must use a for loop.",
)
# Forbid the built-in sum()
TestCase(
name="Does not call sum()",
test_type="not_regex",
pattern=r"\bsum\s*\(",
error_message="Implement the summation manually — do not use sum().",
)
Both types use re.MULTILINE | re.DOTALL flags.
variable — value with validator
Reads a variable from the IPython namespace and passes it to a validator callable. The test passes when validator(value) returns True.
TestCase(
name="score is between 0 and 100",
test_type="variable",
variable_name="score",
validator=lambda v: 0 <= v <= 100,
error_message="score = {value}, but it must be between 0 and 100.",
)
Use {value} as a placeholder in error_message — it is replaced with the actual variable value when the test fails.
type_check — isinstance check
Passes when isinstance(value, expected) is True. Works with a function's return value or a named variable.
# Check a function's return type
TestCase(
name="get_scores() returns a list",
test_type="type_check",
function_name="get_scores",
inputs=[],
expected=list,
)
# Check a variable's type
TestCase(
name="result is a float",
test_type="type_check",
variable_name="result",
expected=float,
)
# Accept multiple types using a tuple
TestCase(
name="index is int or float",
test_type="type_check",
variable_name="index",
expected=(int, float),
)
partial_output — fuzzy match
Passes when the Levenshtein similarity between the actual and expected output is ≥ similarity_threshold. Useful when minor formatting differences (extra spaces, punctuation) should still receive credit.
TestCase(
name="Greeting (fuzzy match)",
test_type="partial_output",
stdin_input="Bob",
expected="Hello, Bob! Welcome.",
similarity_threshold=0.85,
description="At least 85% similar to the expected greeting.",
)
similarity_threshold must be a float in (0.0, 1.0]. A value of 1.0 requires an exact match; 0.8 allows up to ~20% edit distance.
regex_output — stdout regex
Passes when re.search(pattern, output) finds a match in the printed output. Good for checking output format without requiring an exact string.
TestCase(
name="Output contains a decimal number",
test_type="regex_output",
pattern=r"\d+\.\d+",
error_message="Expected a decimal number in the output.",
)
# With a specific function
TestCase(
name="format_price() output includes currency symbol",
test_type="regex_output",
function_name="format_price",
inputs=[9.99],
pattern=r"\$\d+\.\d{2}",
)
contains_output — stdout substring
Passes when expected is a substring of the printed output (case-sensitive).
TestCase(
name="Output mentions 'Error'",
test_type="contains_output",
expected="Error",
description="The program must print an error message.",
)
multiline_output — order-independent lines
Splits expected on newlines and verifies that every non-empty line appears somewhere in the output. Order does not matter — useful when students may print items in any sequence.
TestCase(
name="Prints name and age",
test_type="multiline_output",
expected="Name: Alice\nAge: 30",
description="Both lines must appear in the output (any order).",
)
Sections
Use TestSection to group tests into named blocks, each rendered as a separate table with its own header. Import it alongside ColabTestFramework and TestCase, then call run_sections() instead of run_tests().
from notebook_cell_tester import ColabTestFramework, TestCase, TestSection
tester = ColabTestFramework()
tester.run_sections([
TestSection("Part 1: Code structure", [
TestCase(
name="Uses a for loop",
test_type="regex",
pattern=r"\bfor\b",
description="Your solution must iterate with a for loop.",
),
TestCase(
name="Does not use sum()",
test_type="not_regex",
pattern=r"\bsum\s*\(",
error_message="Implement the summation manually — do not call sum().",
),
]),
TestSection("Part 2: Correctness", [
TestCase(
name="total([1, 2, 3]) == 6",
test_type="return",
function_name="total",
inputs=[[1, 2, 3]],
expected=6,
),
TestCase(
name="total([]) == 0",
test_type="return",
function_name="total",
inputs=[[]],
expected=0,
),
]),
])
tester.display_results()
display_results() automatically detects that sections were used and renders a named header bar above each group showing the section title and its own n/total passed count, followed by an overall summary banner at the top.
run_tests() with a flat list continues to work exactly as before — sections are opt-in.
TestCase field reference
| Field | Type | Required for | Notes |
|---|---|---|---|
name |
str |
all | Display name shown in the results table |
test_type |
str |
all | One of the test type strings above |
function_name |
str |
function tests, type_check (one of) |
Looked up from get_ipython().user_ns |
variable_name |
str |
variable, type_check (one of) |
Looked up from get_ipython().user_ns |
inputs |
list |
function tests | Positional arguments; defaults to [] |
stdin_input |
str |
output tests | Simulates input() calls; lines separated by \n |
expected |
Any |
most tests | Return value, output string, exception type, or target type |
similarity_threshold |
float |
partial_output |
Must be in (0.0, 1.0] |
tolerance |
float |
return (optional) |
Passes when abs(got - expected) <= tolerance; must be >= 0 |
validator |
Callable |
variable |
lambda value: bool |
pattern |
str |
regex, not_regex, regex_output |
Python regex string |
description |
str |
optional on all | Shown as an italic subtitle under the test name |
error_message |
str |
optional on all | Custom failure message; use {value} placeholder in variable tests |
success_message |
str |
optional on all | Custom message shown when the test passes; replaces the default pass message |
Results display
display_results() renders an HTML table inside the notebook:
- Summary banner — gradient bar showing
passed / total (%)with a congratulatory message when all tests pass. - Section headers (when using
run_sections()) — a purple gradient bar above each group showing the section name and its ownn/total passedcount. - Status column — green
✓ PASSor red✗ FAILbadge per test row. - Test column — test name, with
descriptionshown as a small italic subtitle when set. - Details column — human-readable message (expected vs. actual values, similarity percentages, missing lines, etc.).
- Collapsible traceback — when an unhandled exception occurs during execution, a
▶ Show technical detailsdisclosure element hides the raw traceback by default, keeping the table readable for beginners.
Common patterns
Test an entire cell (no function)
Omit function_name. The framework re-executes the student's full cell source in an isolated namespace with stdin redirected.
TestCase(
name="Reads two numbers and prints their sum",
test_type="output",
stdin_input="3\n7",
expected="10",
)
Combine structural and functional tests
tests = [
# 1. Check the algorithm used
TestCase(
name="Uses recursion",
test_type="regex",
pattern=r"def\s+factorial.*?\n(?:.*\n)*?.*\bfactorial\s*\(",
description="factorial() must call itself recursively.",
),
# 2. Check correctness
TestCase(
name="factorial(5) == 120",
test_type="return",
function_name="factorial",
inputs=[5],
expected=120,
),
# 3. Check error handling
TestCase(
name="Raises ValueError for negative input",
test_type="exception",
function_name="factorial",
inputs=[-1],
expected=ValueError,
),
]
Check a variable's value and type together
tests = [
TestCase(
name="result is a float",
test_type="type_check",
variable_name="result",
expected=float,
),
TestCase(
name="result is positive",
test_type="variable",
variable_name="result",
validator=lambda v: v > 0,
error_message="result = {value}, but it must be greater than 0.",
),
]
Custom pass and fail messages
Use success_message to replace the default technical pass message with something more encouraging, and error_message to give students a clear hint on failure:
TestCase(
name="factorial(5) == 120",
test_type="return",
function_name="factorial",
inputs=[5],
expected=120,
success_message="Correct! Your factorial function works perfectly.",
error_message="Check your base case and recursive step.",
)
Flexible output with partial_output
When students may phrase output slightly differently, use partial_output to award credit for near-correct answers:
TestCase(
name="Farewell message",
test_type="partial_output",
stdin_input="Maria",
expected="Goodbye, Maria! See you soon.",
similarity_threshold=0.80,
description="Your farewell message must be at least 80% similar to the expected output.",
)
License
MIT — see LICENSE.
Project details
Release history Release notifications | RSS feed
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 notebook_cell_tester-1.5.1.tar.gz.
File metadata
- Download URL: notebook_cell_tester-1.5.1.tar.gz
- Upload date:
- Size: 24.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3bb55cd6a8a973509cf5604edd2280ee735814a75572ba6977708d5dca0cb2fd
|
|
| MD5 |
679969f1e7f0426fa548b0463020b9a8
|
|
| BLAKE2b-256 |
500f88556fbed8ab4b9262d9997ef458d376073a20e190848e0afb37d4ef9da0
|
Provenance
The following attestation bundles were made for notebook_cell_tester-1.5.1.tar.gz:
Publisher:
publish.yml on EnriqueVilchezL/notebook_cell_tester
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
notebook_cell_tester-1.5.1.tar.gz -
Subject digest:
3bb55cd6a8a973509cf5604edd2280ee735814a75572ba6977708d5dca0cb2fd - Sigstore transparency entry: 1768027480
- Sigstore integration time:
-
Permalink:
EnriqueVilchezL/notebook_cell_tester@b5f3a58d9f06a9f3cec3e0d564f24a58b6044c44 -
Branch / Tag:
refs/tags/v1.5.1 - Owner: https://github.com/EnriqueVilchezL
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@b5f3a58d9f06a9f3cec3e0d564f24a58b6044c44 -
Trigger Event:
release
-
Statement type:
File details
Details for the file notebook_cell_tester-1.5.1-py3-none-any.whl.
File metadata
- Download URL: notebook_cell_tester-1.5.1-py3-none-any.whl
- Upload date:
- Size: 20.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
cfc5c259e4250f5ee732205dbf508a473817b483a9fe4078f55422d2efd44851
|
|
| MD5 |
bee43716ff689233e3b38d69d89cfc67
|
|
| BLAKE2b-256 |
dfb797fbc3487b7660c4a9595420140597834ef0716cf92da10c24955b04d867
|
Provenance
The following attestation bundles were made for notebook_cell_tester-1.5.1-py3-none-any.whl:
Publisher:
publish.yml on EnriqueVilchezL/notebook_cell_tester
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
notebook_cell_tester-1.5.1-py3-none-any.whl -
Subject digest:
cfc5c259e4250f5ee732205dbf508a473817b483a9fe4078f55422d2efd44851 - Sigstore transparency entry: 1768027601
- Sigstore integration time:
-
Permalink:
EnriqueVilchezL/notebook_cell_tester@b5f3a58d9f06a9f3cec3e0d564f24a58b6044c44 -
Branch / Tag:
refs/tags/v1.5.1 - Owner: https://github.com/EnriqueVilchezL
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@b5f3a58d9f06a9f3cec3e0d564f24a58b6044c44 -
Trigger Event:
release
-
Statement type: