nbgrader_jupyterquiz.grader package
Grader subpackage — nbgrader preprocessor and quiz authoring utilities.
Submodules
nbgrader_jupyterquiz.grader._review module
Static HTML review rendering for graded quizzes.
The rendered output is scoped, self-contained HTML + inline CSS —
suitable for display in any browser without a running Jupyter server
or kernel. Emitted by
display_review()
from within the auto-generated hidden-tests block; nbgrader
generate_feedback propagates it into the per-student feedback HTML.
- nbgrader_jupyterquiz.grader._review.fmt_pts(value: float) str[source]
Format a point value for display, collapsing binary-float noise.
Rounds to three decimal places and strips trailing zeros so that accumulation artefacts such as
0.3 + 0.3 + 0.4 == 0.9999999999999999don’t bleed into the visible review text. Three decimals preserve common fractional weights exactly — halves (0.5), quarters (0.25), eighths (0.125), tenths (0.1) — which a two-decimal rounding would mangle (round(0.125, 2) == 0.12under banker’s rounding).Parameters
- valuefloat
Point value to format.
Returns
- str
Canonical short textual form, e.g.
"1","0.5","0.125".
- nbgrader_jupyterquiz.grader._review.render_review_html(result: QuizResult) str[source]
Render a full quiz review as a scoped HTML string.
Parameters
- resultQuizResult
Grading outcome to render.
Returns
- str
Complete
<style>+<div>block ready forIPython.display.HTML.
nbgrader_jupyterquiz.grader._scoring module
Per-question-type grading helpers for autograde.
Each grade_* function takes a parsed question dict plus the
student’s recorded response payload and returns True iff the
response satisfies that question’s correctness criteria. Grading is
all-or-nothing per question — partial credit at the quiz level falls
out of summing per-question outcomes in
QuizResult.
- nbgrader_jupyterquiz.grader._scoring.expected_answer(question: dict[str, Any]) Any[source]
Derive a human-readable representation of a question’s expected answer.
Parameters
- questiondict
Question dict as produced by
parse.
Returns
- list or None
A list of expected answer texts (choice/string) or values/ranges (numeric).
Nonefor unsupported question types.
- nbgrader_jupyterquiz.grader._scoring.grade_many_choice(question: dict[str, Any], recorded: dict[str, Any]) bool[source]
Grade a many-choice question (set equality).
Parameters
- questiondict
Question dict with its answer list.
- recordeddict
Student’s recorded response with a
selectedlist.
Returns
- bool
True iff the set of selected answers equals the set of correct answers.
- nbgrader_jupyterquiz.grader._scoring.grade_multiple_choice(question: dict[str, Any], recorded: dict[str, Any]) bool[source]
Grade a single-choice question.
Parameters
- questiondict
Question dict with its answer list.
- recordeddict
Student’s recorded response with a
selectedstring.
Returns
- bool
True iff
selectedmatches acorrect: trueanswer.
- nbgrader_jupyterquiz.grader._scoring.grade_numeric(question: dict[str, Any], recorded: dict[str, Any]) bool[source]
Grade a numeric question (value or range, optional precision).
Parameters
- questiondict
Question dict with its answer list (values and/or ranges).
- recordeddict
Student’s recorded response with a
parsednumeric field.
Returns
- bool
True iff
parsedmatches acorrectvalue (at the configured precision) or falls inside acorrectrange.
- nbgrader_jupyterquiz.grader._scoring.grade_string(question: dict[str, Any], recorded: dict[str, Any]) bool[source]
Grade a string question (exact or fuzzy match).
Parameters
- questiondict
Question dict with its answer list; each answer may set
match_caseandfuzzy_threshold.- recordeddict
Student’s recorded response with a
valuestring.
Returns
- bool
True iff
valuematches any correct answer (honouringmatch_caseandfuzzy_thresholdusing Levenshtein similarity).
nbgrader_jupyterquiz.grader.autograde module
Autograder helper for graded quizzes (v0.4.0+).
Reads the responses.json sidecar file written by the JS recorder
and grades the recorded responses against the quiz definition.
Intended to be called from the auto-generated hidden-tests block of a
graded-quiz cell — see
CreateQuiz.
Usage (inside an autograder cell):
from nbgrader_jupyterquiz import grade_quiz
result = grade_quiz("quiz-1-autograded", questions=[...])
result.display_review()
_result.score # bare expression → nbgrader partial credit
The sidecar path is resolved relative to the current working
directory, which nbgrader autograde sets to the assignment directory.
Per-question grading is all-or-nothing; partial credit at the quiz
level falls out of summing QuestionResult.earned across the
quiz’s questions.
- exception nbgrader_jupyterquiz.grader.autograde.GradeQuizError[source]
Bases:
ExceptionRaised when the helper cannot locate or interpret required inputs.
- class nbgrader_jupyterquiz.grader.autograde.QuestionResult(qnum: int, question: dict[str, Any], recorded: Any, correct: bool)[source]
Bases:
objectOutcome of grading a single question.
- correct: bool
- property earned: float
Compute the points earned on this question (all-or-nothing per question).
Returns
- float
self.pointsif correct, else 0.
- property expected: Any
Return a human-readable representation of the expected answer.
Returns
- Any
List of expected answer texts or numeric values/ranges.
- property points: float
Return the maximum points for this question (defaults to 1).
Returns
- float
Positive point weight. Integer values (from
{N}) come through asint; fractional values ({0.5}) asfloat.
- qnum: int
- question: dict[str, Any]
- property question_type: str
Return the canonical type string of the underlying question.
Returns
- str
"multiple_choice","many_choice","numeric","string", or"unknown".
- recorded: Any
- class nbgrader_jupyterquiz.grader.autograde.QuizResult(grade_id: str, details: list[QuestionResult])[source]
Bases:
objectOutcome of grading every question in a quiz region.
- details: list[QuestionResult]
- display_review() None[source]
Emit an HTML review of the quiz into the current cell output.
Intended to be called from the auto-generated hidden-tests block of a graded-quiz cell. When
nbgrader generate_feedbackconverts the autograded notebook to HTML, the review appears inline with the score so students can see which answers were correct, which they picked, and which they missed.The output is pure static HTML with scoped inline CSS — it renders correctly in any browser without a running Jupyter server or kernel.
- grade_id: str
- property max_score: float
Compute the sum of per-question point values.
Returns
- float
Total points this quiz can yield.
- property passed: bool
Return True when every question in the quiz was answered correctly.
Returns
- bool
True iff
score == max_score.
- nbgrader_jupyterquiz.grader.autograde.grade_quiz(grade_id: str, *, questions: list[dict[str, Any]] | None = None, notebook_path: str | Path | None = None) QuizResult[source]
Grade the quiz identified by
grade_idagainst recorded responses.Parameters
- grade_idstr
The sidecar key under which student responses are recorded. The auto-generated test cells use
"<task_grade_id>-autograded".- questionslist of dict, optional
Answer key — the list of question dicts (as produced by the parser) for this quiz. When provided, the notebook is not read. This is the path taken by auto-generated test cells (see
CreateQuiz), which embed the answer key directly in their### BEGIN HIDDEN TESTSblock.- notebook_pathstr or Path, optional
Path to the notebook containing the quiz task cell, for the fallback case where
questionsisNone. Defaults to the only.ipynbfile in the current working directory.
Returns
- QuizResult
Grading result with per-question details. If the sidecar is missing or the quiz has no recorded responses, every question is reported as incorrect (score 0/max).
nbgrader_jupyterquiz.grader.encode module
Encode quiz questions for embedding in notebooks.
nbgrader_jupyterquiz.grader.parse module
Parse quiz question source from notebook cell markdown.
- exception nbgrader_jupyterquiz.grader.parse.ParseError[source]
Bases:
ExceptionRaised when quiz source cannot be parsed.
- class nbgrader_jupyterquiz.grader.parse.Quiz(options: dict[str, ~typing.Any], questions: list[dict[str, ~typing.Any]], warnings: list[str] = <factory>)[source]
Bases:
objectA parsed quiz with options, a list of question dicts, and parse-time warnings.
warningscollects non-fatal issues the parser spotted (e.g. anMCquestion with 0 or 1 correct answers). Fatal issues raiseParseErrorinstead. Callers such as theCreateQuizpreprocessor surface these throughnbgrader’s UI logger.- options: dict[str, Any]
- questions: list[dict[str, Any]]
- warnings: list[str]
- nbgrader_jupyterquiz.grader.parse.find_quiz_regions(source: str, begin_quiz_delimiter: str = '#### Quiz', end_quiz_delimiter: str = '#### End Quiz') tuple[list[tuple[str, list[str]]], list[str]][source]
Extract regions within quiz delimiters.
Parameters
- sourcestr
Full source text of the markdown cell.
- begin_quiz_delimiterstr, optional
Marker that opens a quiz region.
- end_quiz_delimiterstr, optional
Marker that closes a quiz region.
Returns
- quizzeslist[tuple[str, list[str]]]
Each entry is
(options_header, quiz_lines).- remaining_lineslist[str]
Lines that fall outside any quiz region.
- nbgrader_jupyterquiz.grader.parse.line_to_mc_answer(line: str) dict[str, Any][source]
Parse a multiple/many-choice answer line.
Parameters
- linestr
Answer line starting with
+(correct) or-(incorrect).
Returns
- dict
Answer dict with
correct,answer, and optional fields.
- nbgrader_jupyterquiz.grader.parse.line_to_numeric_answer(line: str) dict[str, Any][source]
Parse a numeric answer line.
Parameters
- linestr
Answer line starting with
+(correct) or-(incorrect).
Returns
- dict
Answer dict with
correct,type, and value/range/feedback.
- nbgrader_jupyterquiz.grader.parse.line_to_question(line: str) dict[str, Any][source]
Parse a question line into a partial question dict (without answers).
Parameters
- linestr
Question line starting with
*.
Returns
- dict
Partial question dict with
type,question, and optional fields.
- nbgrader_jupyterquiz.grader.parse.parse_cell(source: str, begin_quiz_delimiter: str = '#### Quiz', end_quiz_delimiter: str = '#### End Quiz') tuple[list[Quiz], list[str]][source]
Parse quiz regions from a notebook cell source string.
Parameters
- sourcestr
Full source text of the markdown cell.
- begin_quiz_delimiterstr, optional
Marker that opens a quiz region.
- end_quiz_delimiterstr, optional
Marker that closes a quiz region.
Returns
- quizzeslist[Quiz]
Parsed Quiz objects. Non-fatal parse-time warnings are attached to each quiz’s
warningsfield.- cell_contentslist[str]
Remaining cell lines with quiz regions removed.
- nbgrader_jupyterquiz.grader.parse.parse_line(line: str, **components: tuple[str, str, Any]) dict[str, Any][source]
Parse delimited components from a line and typecast them.
Parameters
- linestr
Text to parse.
- **componentstuple[str, str, Any]
Each keyword is a component name mapped to a
(left_delim, right_delim, typecast)triple.
Returns
- dict
Parsed components.
Raises
- ParseError
If a duplicate component is found, the line ends with an unparsable segment, or a delimited field is not closed.
- nbgrader_jupyterquiz.grader.parse.parse_question(lines: list[str]) dict[str, Any][source]
Parse a question block into a question dict.
Parameters
- lineslist[str]
First line is the question line; remaining lines are answer lines.
Returns
- dict
Question dict matching the jupyterquiz schema.
- nbgrader_jupyterquiz.grader.parse.parse_quiz_options(header: str) dict[str, Any][source]
Parse quiz options from the header line following the begin delimiter.
Parameters
- headerstr
Text on the same line as the begin delimiter, after the delimiter itself. Expected format: space-separated
key=valuepairs. Boolean values aretrueorfalse(case-insensitive).filenametakes a string value. Unrecognised keys are ignored.
Returns
- dict
Quiz options dict with keys
encoded,inline,hidden,filename,hide_correctness,graded. Omitted keys retain their defaults.hide_correctness=truepropagateshide: trueto every MC / many-choice answer so the display hides correctness feedback and shows a neutral Selected / Deselected state instead. DefaultNone— the preprocessor treatsNoneas “off unless the host cell is graded” andTrue/Falseas explicit opt-in/opt-out.graded=falseopts a single quiz out of auto-grading inside a task cell — the generated cell is a plaindisplay_quiz(...)code cell with no nbgrader metadata, no hidden tests, and correctness feedback visible. DefaultNone— the preprocessor treatsNoneas “graded iff the host task cell has agrade_idandauto_generate_testsis on”.
- nbgrader_jupyterquiz.grader.parse.redact_answer_key(questions: list[dict[str, Any]]) list[dict[str, Any]][source]
Return a deep copy of
questionswith answer-key fields stripped.The release notebook embeds question JSON into a hidden span the student’s browser loads. Without redaction, the student can read the answer key out of the DOM. Stripping the matching fields makes
hide_correctnessmode actually withhold the key, not just the visual feedback.The redaction is keyed off question
type:multiple_choice/many_choice: dropcorrectfrom each answer. Keepanswer,code,feedback,hide.numeric: dropvalue,range,correctfrom each answer. Keepfeedbackandtype=defaultentries so fall-through “Incorrect, try again” feedback still works.string: replaceanswerswith an empty list. String questions are server-graded; the JS path only runs in self-check mode (no hide-correctness), so an empty list is sufficient when this function is called.
Per-answer
feedbackstrings are intentionally preserved — they are pedagogically valuable, and instructors who want them hidden can omit them from the question source.Parameters
- questionslist[dict]
The full parsed-question list (typically
Quiz.questions). Not mutated.
Returns
- list[dict]
Deep copy of
questionswith answer-key fields removed. Safe to serialise into the release notebook’s display JSON.
- nbgrader_jupyterquiz.grader.parse.split_questions(quiz_source: list[str]) list[list[str]][source]
Split lines of a quiz region into individual question blocks.
Each returned question is a list of logical lines — one for the question itself, followed by one per answer. A logical line may span multiple physical source lines when its content extends across them, joined with
\n. Continuation is driven by markdown-list indentation: a physical line is a continuation of the active logical line iff its first-non-whitespace column is strictly greater than the opener’s column (0for questions,2for answers). Code blocks delimited by triple-backticks are an exception — physical lines inside an open code block are appended regardless of indentation, matching markdown’s fenced-code semantics.Parameters
- quiz_sourcelist[str]
Physical lines within a quiz delimiter region.
Returns
- list[list[str]]
Each inner list contains the question logical line followed by its answer logical lines. Each logical line is a single
str(possibly containing\n).
Raises
- ParseError
If a logical line ends with an unclosed delimited field — i.e. indentation drops back to the opener’s level (or beyond) while the field is still expecting its closer.
nbgrader_jupyterquiz.grader.preprocessor module
Nbgrader preprocessor that converts markdown quiz regions into interactive quizzes.
- class nbgrader_jupyterquiz.grader.preprocessor.CreateQuiz(*args: t.Any, **kwargs: t.Any)[source]
Bases:
NbGraderPreprocessorConvert markdown quiz regions to interactive jupyterquiz cells.
Register in
nbgrader_config.pyat the front of theGenerateAssignmentpreprocessor list so that auto-generated autograder cells are picked up bySaveCells(into the gradebook) andClearHiddenTests(to strip the grading body from the release):c.GenerateAssignment.preprocessors.insert(0, "nbgrader_jupyterquiz.CreateQuiz")
Appending instead of inserting (
.append(...)) will cause autograde to fail because our cells wouldn’t be in the gradebook and their checksums wouldn’t match the release.- auto_generate_tests
Whether to auto-generate autograder-cell content when the host task cell carries an nbgrader
grade_id.When
True(the default), each generateddisplay_quizcode cell also becomes the nbgrader-tracked graded cell: its source carries a### BEGIN HIDDEN TESTSblock that embeds the answer key and callsgrade_quiz()— the block is stripped from the release byClearHiddenTestsand restored at autograde time byOverwriteCells. A bare_result.scoreexpression at the end of the cell feeds nbgrader’s partial-credit scoring (utils.determine_grade). The task cell’s ownpointsfield is left untouched so it can still be manually graded alongside the auto-graded quiz score.Disable to opt out of auto-grading and keep
display_quizas a plain (non-graded) code cell; in that mode instructors author their own autograded test cells.
- begin_quiz_delimiter
The delimiter marking the beginning of quiz source.
- end_quiz_delimiter
The delimiter marking the end of quiz source.
- enforce_metadata
Whether to raise an error if cells containing quiz regions are not marked as ‘Manually Graded Task’ cells. Only disable this if you are using nbgrader generate_assignment without the full grading pipeline.
- name = ''
- preprocess(nb: NotebookNode, resources: ResourcesDict) tuple[NotebookNode, ResourcesDict][source]
Process all cells in the notebook, expanding quiz regions.
Parameters
- nbNotebookNode
Source notebook.
- resourcesResourcesDict
Nbgrader resources dict (provides
unique_key).
Returns
- NotebookNode, ResourcesDict
Modified notebook with quiz cells appended after each quiz region.
- preprocess_cell(cell: NotebookNode, resources: ResourcesDict, _index: int) tuple[NotebookNode, ResourcesDict][source]
No-op — all processing happens in
preprocess().Parameters
- cellNotebookNode
Current cell.
- resourcesResourcesDict
Nbgrader resources dict.
- _indexint
Cell index (unused).
Returns
- NotebookNode, ResourcesDict
Cell unchanged.
- quiz_cell_counter = count(0)
nbgrader_jupyterquiz.grader.validate module
Validate quiz questions against JSON Schema.
- class nbgrader_jupyterquiz.grader.validate.Schema(*values)[source]
Bases:
EnumJSON Schema definitions for jupyterquiz question types.
- MC = {'$id': 'https://github.com/jmshea/jupyterquiz/mc_schema.json', '$schema': 'https://json-schema.org/draft/2020-12/schema', 'description': 'Schema for Multiple or Many Choice Questions in JupyterQuiz', 'properties': {'answers': {'items': {'properties': {'answer': {'type': 'string'}, 'answer_cols': {'type': 'number'}, 'correct': {'type': 'boolean'}, 'feedback': {'type': 'string'}, 'hide': {'type': 'boolean'}}, 'required': ['answer', 'correct'], 'type': 'object'}, 'type': 'array'}, 'code': {'type': 'string'}, 'points': {'exclusiveMinimum': 0, 'type': 'number'}, 'question': {'type': 'string'}, 'type': {'pattern': 'multiple_choice|many_choice', 'type': 'string'}}, 'required': ['type', 'question', 'answers'], 'title': 'JupyterQuiz Multiple or Many Choice Quiz', 'type': 'object'}
- NUM = {'$id': 'https://github.com/jmshea/jupyterquiz/num_schema.json', '$schema': 'https://json-schema.org/draft/2020-12/schema', 'description': 'Schema for Numeric Questions in JupyterQuiz', 'properties': {'answers': {'items': {'anyOf': [{'properties': {'correct': {'type': 'boolean'}, 'feedback': {'type': 'string'}, 'value': {'type': 'number'}}, 'required': ['value', 'correct'], 'type': 'object'}, {'properties': {'correct': {'type': 'boolean'}, 'feedback': {'type': 'string'}, 'range': {'maxItems': 2, 'minItems': 2, 'type': 'array'}}, 'required': ['range', 'correct'], 'type': 'object'}, {'properties': {'feedback': {'type': 'string'}, 'type': {'pattern': 'default', 'type': 'string'}}, 'required': ['type', 'feedback'], 'type': 'object'}]}, 'type': 'array'}, 'hide': {'type': 'boolean'}, 'points': {'exclusiveMinimum': 0, 'type': 'number'}, 'precision': {'type': 'integer'}, 'question': {'type': 'string'}, 'type': {'pattern': 'numeric', 'type': 'string'}}, 'title': 'JupyterQuiz Numeric Question', 'type': 'object'}
- STR = {'$id': 'https://github.com/jmshea/jupyterquiz/str_schema.json', '$schema': 'https://json-schema.org/draft/2020-12/schema', 'description': 'Schema for String (free-text) Questions in JupyterQuiz', 'properties': {'answers': {'items': {'anyOf': [{'properties': {'answer': {'type': 'string'}, 'correct': {'type': 'boolean'}, 'feedback': {'type': 'string'}, 'fuzzy_threshold': {'maximum': 1, 'minimum': 0, 'type': 'number'}, 'match_case': {'type': 'boolean'}}, 'required': ['answer', 'correct'], 'type': 'object'}, {'properties': {'feedback': {'type': 'string'}, 'type': {'pattern': 'default', 'type': 'string'}}, 'required': ['type', 'feedback'], 'type': 'object'}]}, 'type': 'array'}, 'hide': {'type': 'boolean'}, 'points': {'exclusiveMinimum': 0, 'type': 'number'}, 'question': {'type': 'string'}, 'type': {'pattern': 'string', 'type': 'string'}}, 'required': ['type', 'question', 'answers'], 'title': 'JupyterQuiz String Question', 'type': 'object'}
- nbgrader_jupyterquiz.grader.validate.validate_question(question: dict[str, Any]) None[source]
Validate a question dict against the appropriate JSON Schema.
Parameters
- questiondict
Parsed question dictionary with at least a
'type'key.
Raises
- jsonschema.exceptions.ValidationError
If the question does not conform to its schema.