Configuration

nbprint supports three complementary ways to configure a report:

  1. Notebook-first: point nbprint at an existing .ipynb and drive sectioning, styling, and page layout from cell tags, cell metadata, and a small %%nbprint magic.

  2. YAML-first: describe the full document in YAML (via pydantic models, composed with lerna and omegaconf).

  3. Hybrid: start from a notebook and add YAML overlays that target cells by index, tag, section, or cell type without rewriting the notebook.

        graph TB
    subgraph Inputs
        nb("notebook<br>(.ipynb)")
        yml("YAML config<br>(.yaml)")
        ovl("overlays<br>(YAML or<br>notebook metadata)")
    end

    nbct[/nbprint<br>template/]
    pjs[/paged.js<br>layout engine/]
    nbc{nbconvert}
    o@{ shape: doc, label: "HTML / PDF" }

    nb e1@-->nbc
    yml e2@-->nbc
    ovl -.->|merged at ingest| nbc
    nbct --> nbc
    pjs --- nbct
    nbc e3@--> o
    e1@{animate: true}
    e2@{animate: true}
    e3@{animate: true}
    

Notebook-First Workflow

Point nbprint at a .ipynb and it is treated as a full configuration input. All notebook metadata under notebook.metadata.nbprint becomes configuration; cell metadata and tags drive routing, styling, and visibility.

nbprint path/to/report.ipynb

Worked examples live in examples/:

Section routing from cell tags

By default every ingested cell lands in middlematter. To route cells to named sections, tag them:

// cell.metadata
{
  "tags": ["nbprint:section:covermatter"]
}

The tag form is nbprint:section:<name> where <name> is any of the 13 section names (see Structured Sections below).

Section routing from cell metadata

Equivalent to a tag, but takes priority:

// cell.metadata.nbprint
{
  "section": "frontmatter"
}

Notebook-level page and output config

Embed a full page/output spec directly in the notebook. No YAML is required:

// notebook.metadata.nbprint
{
  "page": {
    "_target_": "nbprint.PageGlobal",
    "size": "Letter",
    "orientation": "portrait"
  },
  "outputs": {
    "_target_": "nbprint.NBConvertOutputs",
    "target": "pdf"
  }
}

YAML or CLI overrides always take precedence over notebook-embedded values, so you can use a notebook’s defaults and override on the command line.

NBPrintCell runtime API

Inside a running notebook you can express cell-level intent through a Python object. Metadata is persisted via a hidden display() output with a custom MIME type; nbprint picks it up at ingestion time.

from nbprint import NBPrintCell, Style, Font

NBPrintCell(section="covermatter", css=":scope { text-align: center; }")

with NBPrintCell(style=Style(font=Font(size=24))):
    display(Markdown("# Cover Page"))

%%nbprint cell magic

A lighter alternative for quick one-liners:

%%nbprint section=frontmatter css=":scope { font-size: 18px; }"
display(Markdown("# Introduction"))

Explicit cell metadata takes precedence over runtime metadata, which takes precedence over the magic, so a hand-edited value in cell.metadata.nbprint always wins.

YAML-First Workflow

YAML is the most expressive way to describe a report: every field of every pydantic model is reachable, and the lerna config system makes it easy to share pieces across reports.

A minimal example:

---
debug: false
outputs:
  _target_: nbprint.NBConvertOutputs
  root: ./outputs
  target: html

content:
  - _target_: nbprint.ContentMarkdown
    content: |
      # A Generic Report
      ## A Subtitle
    css: ":scope { text-align: center; }"

  - _target_: nbprint.ContentPageBreak
  - _target_: nbprint.ContentTableOfContents
  - _target_: nbprint.ContentPageBreak

  - _target_: nbprint.ContentFlexRowLayout
    sizes: [1, 1]
    content:
      - _target_: nbprint.ContentFlexColumnLayout
        content:
          - _target_: nbprint.ContentMarkdown
            content: "# Left column"
      - _target_: nbprint.ContentFlexColumnLayout
        content:
          - _target_: nbprint.ContentMarkdown
            content: "# Right column"

Run it:

nbprint examples/basic.yaml

Common content types include ContentMarkdown, ContentCode, ContentPageBreak, ContentTableOfContents, and the layout containers ContentFlexRowLayout / ContentFlexColumnLayout / ContentInlineLayout.

Output formats & nbconvert configuration

The outputs block (NBConvertOutputs) controls how the generated notebook is executed and converted. Common fields:

Field

Default

Description

target

html

Output format: ipynb, html, webhtml, webpdf

execute

true

Execute the notebook before converting

timeout

600

Per-cell execution timeout (seconds)

template

nbprint

nbconvert template name

Passing arbitrary nbconvert / traitlets options

Anything configurable on an nbconvert exporter, preprocessor, or the app itself is reachable through nbconvert_config, so nbprint doesn’t need a field for each option. Entries map 1:1 onto nbconvert’s --Class.trait=value CLI options and are applied to the conversion (exporter) pass.

For example, WebPDFExporter.page_render_timeout controls how long (in ms) the headless browser waits for JavaScript output to settle before capturing the PDF. This helps with notebooks that have heavy interactive charts:

outputs:
  _target_: nbprint.NBConvertOutputs
  target: webpdf
  nbconvert_config:
    # flat dotted keys ...
    WebPDFExporter.page_render_timeout: 5000
    # ... or nested namespaces — both are equivalent
    TemplateExporter:
      exclude_input: true

The nested form maps directly onto a hydra/lerna CLI override:

nbprint examples/basic.yaml \
  ++outputs.target=webpdf \
  ++outputs.nbconvert_config.WebPDFExporter.page_render_timeout=5000

Scalars pass through as-is, booleans render as True/False, and lists are JSON-encoded so container traits round-trip through the CLI.

Traits that nbprint already manages (NbConvertApp.export_format (to), TemplateExporter.template_name (template), NbConvertApp.output_base (output), ExecutePreprocessor.enabled (execute), and ExecutePreprocessor.timeout) are rejected in nbconvert_config with an error pointing at the dedicated field (target, template, naming/root, execute, timeout). Set those via their own fields instead.

Structured Sections

For book-style documents, nbprint supports 13 ordered sections grouped into 6 logical groups:

Group

Sections

Description

Prematter

prematter

Hidden content executed before report construction

Covermatter

covermatter

Cover page content

Frontmatter

title, copyright, dedication, table_of_contents, frontmatter

Title page, legal, dedication, TOC, preface

Middlematter

middlematter, middlematter_separators

Main body and chapter dividers

Endmatter

appendix, index, endmatter

Supplementary material, index, bibliography

Rearmatter

rearmatter

Back cover content

Instead of a flat list, content: can be keyed by section name. Sections are rendered in document order; unused sections are simply empty.

content:
  covermatter:
    - _target_: nbprint.ContentMarkdown
      content: "# My Report"
      css: ":scope { text-align: center; }"

  table_of_contents:
    - _target_: nbprint.ContentTableOfContents

  middlematter:
    - _target_: nbprint.ContentMarkdown
      content: "# Section One"

  endmatter:
    - _target_: nbprint.ContentMarkdown
      content: "# Bibliography"

Flat-list content: is still supported and is treated as middlematter.

Chapters (list-of-lists middlematter)

For multi-chapter documents, supply middlematter as a list of lists. Each sublist is a chapter: its first element is promoted to a chapter separator and the remaining elements form the chapter body. nbprint interleaves them in document order: [chapter1-title, *chapter1-body, chapter2-title, *chapter2-body, …].

content:
  middlematter:
    - # Chapter One — first item becomes the title page
      - _target_: nbprint.ContentMarkdown
        content: "# Chapter One"
      - _target_: nbprint.ContentMarkdown
        content: "Body of the first chapter…"

    - # Chapter Two
      - _target_: nbprint.ContentMarkdown
        content: "# Chapter Two"
      - _target_: nbprint.ContentMarkdown
        content: "Body of the second chapter…"

By default each separator renders as a standalone chapter title page, with a hard page break is inserted before and after it. To keep separators inline with their chapter body instead, disable the behavior:

content:
  separator_title_pages: false
  middlematter:
    - [...]

The promoted separators are also available directly under the middlematter_separators section if you prefer to populate them explicitly alongside a flat middlematter list.

See examples/chapters.yaml for a full working example.

Per-section page layout

Each section can carry its own page layout via PageGlobal.pages. This generates CSS @page <section> { ... } rules and maps cells to them via data-nbprint-section attributes:

page:
  _target_: nbprint.PageGlobal
  bottom_right:
    _target_: nbprint.PageRegion
    content:
      _target_: nbprint.PageNumber

  pages:
    covermatter:
      # no headers/footers on the cover
    frontmatter:
      counter_reset: true
      counter_style: lower-roman
    middlematter:
      counter_reset: true

See examples/sections.yaml for a full working example.

Section-level style defaults

Each section can carry a default Style that every cell in that section inherits. A cell’s own Style fields take precedence; unset fields fall back to the section default.

content:
  section_styles:
    covermatter:
      font: {size: 28}
      horizontal_alignment: center
    frontmatter:
      font: {family: "Georgia, serif"}

  covermatter:
    - _target_: nbprint.ContentMarkdown
      content: "# My Report"

Formatting Overlays

Overlays are rules that apply formatting to ingested notebook cells without modifying the notebook itself. There are three kinds.

Overlay — merge formatting onto matching cells

Matches cells by index, tag, cell type, or target section, and merges css / classname / attrs / style / ignore into the resulting Content:

overlays:
  - match: {index: 0}
    css: ":scope { text-align: center; }"

  - match: {tag: "chart"}
    style:
      border: {bottom: {width: 2, style: solid, color: "#333"}}

  - match: {cell_type: markdown}
    classname: "prose-body"

  - match: {section: covermatter}
    style:
      font: {size: 28}

Match fields combine with AND semantics, and an empty match matches every cell. Multiple overlays apply in list order; later overlays stack on top of earlier ones:

  • css is appended

  • classname is appended (accumulated as a list)

  • attrs is merged (overlay keys win on collision)

  • style is merged via Style.merge (overlay fields win)

  • ignore is set when provided

Overlays can also come from notebook metadata. YAML-declared overlays and notebook-declared overlays compose:

// notebook.metadata.nbprint
{
  "overlays": [
    {"match": {"tag": "emphasis"}, "css": ":scope { font-style: italic; }"}
  ]
}

LayoutOverlay — wrap contiguous cells in a flex container

A layout overlay matches the same way but wraps contiguous runs of matched cells (same section, consecutive notebook indices) in a ContentFlexRowLayout, ContentFlexColumnLayout, or ContentInlineLayout:

layout_overlays:
  # Any two adjacent "sidebyside"-tagged cells become a row
  - match: {tag: "sidebyside"}
    layout: row
    sizes: [1, 1]

  # Cells at notebook indices 3..5 (inclusive) get stacked in a column
  - index_range: [3, 5]
    layout: column

  # Wrapper itself can carry CSS / classname / style
  - match: {tag: "gallery"}
    layout: inline
    css: ":scope { gap: 8px; }"
    classname: gallery-wrap

Non-contiguous matches produce multiple wrappers. Both kinds of overlay can be combined: formatting overlays apply to the individual cells, and a layout overlay then wraps them.

PageBoxOverlay — wrap contiguous cells in a page box

Same matching rules, but the wrapper is a ContentPageBox: the matched run becomes one logical page, arranged by a named layout preset and followed by a page break. Select it with wrapper: page-box:

layout_overlays:
  # Every run of "pair"-tagged cells becomes a two-column page
  - match: {tag: "pair"}
    wrapper: page-box
    layout: columns-2
    gap: 1rem

  # A four-up dashboard page, in landscape
  - match: {tag: "dashboard"}
    wrapper: page-box
    layout: grid-2x2
    page_orientation: landscape

This is the multi-cell counterpart to NBPrintPage, which turns a single cell into a page box from inside the notebook. Reach for the overlay when a page is assembled from several cells.

Available fields mirror ContentPageBox: layout (any layout preset), fit, min_pages, gap, padding, align, justify, grid_template, page_size, page_orientation, page_margins, plus the wrapper formatting (css, classname, attrs, style) shared with LayoutOverlay. Overlay css is appended to the preset’s CSS rather than replacing it.

sizes is flex-only and is rejected on a page-box overlay; use a columns/grid preset, or grid_template for explicit tracks.

As with every overlay, these can live in notebook metadata instead of YAML:

// notebook.metadata.nbprint
{
  "layout_overlays": [
    {"match": {"tag": "pair"}, "wrapper": "page-box", "layout": "columns-2", "gap": "1rem"}
  ]
}

Footnotes

Wrap a note in <span class="nbprint-footnote"> and paged.js moves it to the foot of the page where it’s cited, leaving a superscript number in the text. Notes are numbered through the document.

In a markdown cell:

Revenue grew 12%<span class="nbprint-footnote">Unaudited, as of Q3.</span> year over year.

From a code cell, nbprint.footnote returns the same HTML, with the note text escaped:

from IPython.display import HTML
from nbprint import footnote

HTML(f"<p>Revenue grew 12%{footnote('Unaudited, as of Q3.')} year over year.</p>")

Without paged.js (pagedjs: false), the note stays in the text as a small parenthetical.

Lerna Config Composition

nbprint uses lerna (a Rust-powered, Hydra-compatible config framework) for YAML composition. Everything Hydra supports works identically, and lerna adds a few features that are particularly useful for reports.

Composing reports from parts

Pull in reusable pieces via a defaults: list:

# examples/hydra.yaml
defaults:
  - config: inline       # examples/hydra/config/inline.yaml
  - page: report         # examples/hydra/page/report.yaml
  - parameters: string1  # examples/hydra/parameters/string1.yaml
  - _self_

outputs:
  naming: "{{name}}"
  root: ./outputs

This lets a handful of reusable YAML files describe a large family of reports.

CLI list manipulation

lerna supports list operations directly from the CLI, which is useful for adding a single chart or section to an existing report without editing the YAML:

# Append a content item
nbprint examples/basic.yaml \
  'content=append({_target_: nbprint.ContentMarkdown, content: "# Added"})'

# Prepend
nbprint report.yaml 'content=prepend({_target_: nbprint.ContentPageBreak})'

# Insert at a specific index
nbprint report.yaml 'content=insert(0, {_target_: nbprint.ContentMarkdown, content: "Intro"})'

# Remove by index / value / clear
nbprint report.yaml 'content=remove_at(-1)'
nbprint report.yaml 'tags=remove_value(draft)'
nbprint report.yaml 'overlays=list_clear()'

Operation

Effect

key=append(v, ...)

Add item(s) to end

key=prepend(v, ...)

Add item(s) to beginning

key=insert(idx, v)

Insert at index

key=remove_at(idx)

Remove by index (negative allowed)

key=remove_value(v)

Remove first matching item

key=list_clear()

Clear all items

Quote the whole override for shell safety. Works in bash, zsh, fish, PowerShell, and cmd.

Sweeps and multirun

Generate a family of reports from one command:

nbprint report.yaml -m \
  parameters.model=choice(ridge,lasso,elasticnet) \
  parameters.alpha=interval(0.01,0.5)

Combined with outputs.naming: "{{name}}-{{parameters.model}}-{{parameters.alpha}}" this produces one cleanly named HTML/PDF per combination.

_patch_ — modify composed configs before CLI overrides

Pull in a vendored config and modify it:

defaults:
  - vendor/large_defaults
  - _self_
  - _patch_:
    - ~unwanted_key                 # delete a key
    - content=remove_value(stale)   # drop a content item
    - +overlays=append({match: {tag: draft}, ignore: true})  # add an overlay

Patches apply after composition but before CLI overrides, so CLI still wins. See the lerna README for the full _patch_ reference (scoped _patch_@package, _here_ / _global_ prefixes, nested accumulation, etc.).

Default overrides at the call site

nbprint’s CLI accepts default overrides without editing YAML:

# ++callable switches to the multirun model, nbprintx.parameters takes inline
# JSON or a file path, and execute=False skips execution
nbprint examples/basic.ipynb \
  ++callable=/nbprintx \
  +nbprint.outputs.naming='{{name}}-{{date}}-{{a}}' \
  +nbprintx.parameters=[{a: 1},{a: 2},{a: 3}] \
  ++nbprint.outputs.execute=False

Running a Report

nbprint examples/basic.yaml        # YAML-first
nbprint path/to/report.ipynb       # notebook-first
nbprint report.yaml notebook=foo.ipynb   # hybrid: YAML + notebook

The output lands in the directory set by outputs.root (default ./outputs), named per outputs.naming (default {{name}}-{{date}}). Both the generated notebook and the chosen render target (html, pdf, …) are written; the notebook is useful for inspection and further experimentation.

example notebook output example basic output

You can also download a rendered PDF of the basic example. The gallery has rendered HTML for every example.

ccflow Integration

nbprint is compatible with ccflow callable models. nbprint outputs like NBConvertOutputs inherit from ccflow.ResultsBase, and parameters inherit from ccflow.ContextBase.

nbprint-cc +nbprint.name=test +context=[]

ccflow workflows are exposed as multi-run models:

nbprint examples/basic.ipynb \
  ++callable=/nbprintx \
  +nbprint.outputs.naming='{{name}}-{{date}}-{{a}}' \
  +nbprintx.parameters=[{a: 1},{a: 2},{a: 3}] \
  ++nbprint.outputs.execute=False

Precedence Summary

When the same piece of configuration can come from multiple places, resolution order is:

  1. CLI overrides (always highest priority)

  2. YAML explicit values (config.yaml, _self_)

  3. YAML _patch_ directives (applied during composition)

  4. Notebook-level metadata (notebook.metadata.nbprint.{page,outputs,overlays,layout_overlays})

  5. Defaults from composed configs and pydantic model defaults

For section assignment specifically:

  1. cell.metadata.nbprint.section

  2. nbprint:section:<name> cell tag

  3. NBPrintCell(section=...) MIME metadata

  4. %%nbprint section=... magic

  5. Default: middlematter