Introduction
In Jupyter, "generate markdown programmatically" can mean two different things. You might want to render markdown as the output of a code cell, or you might want to create actual markdown cells in the notebook file itself. Those are related workflows, but they use different APIs.
Rendering Markdown from a Code Cell
If the goal is to display markdown during notebook execution, use IPython.display.Markdown with display. This keeps the logic in Python while still producing rich notebook output.
1from IPython.display import Markdown, display
2
3summary = "Model accuracy improved from 0.81 to 0.89."
4items = ["cleaned missing values", "scaled numeric features", "added class weights"]
5
6markdown_text = "## Training Summary
7
8"
9markdown_text += summary + "
10
11"
12markdown_text += "### Steps
13"
14markdown_text += "
15".join(f"- {item}" for item in items)
16
17display(Markdown(markdown_text))
This is the best choice for dynamic reports inside a running notebook. The markdown appears as rendered output under the code cell.
Generating Tables and Metrics Dynamically
A common pattern is to compute values first and then format them into markdown.
1from IPython.display import Markdown, display
2
3metrics = {
4 "precision": 0.91,
5 "recall": 0.87,
6 "f1": 0.89,
7}
8
9rows = "
10".join(f"| {name} | {value:.2f} |" for name, value in metrics.items())
11text = """
12## Evaluation
13
14| Metric | Value |
15|---|---:| """ + rows display(Markdown(text)) ``` That approach is simple, readable, and works well for notebook-based reporting. ## Creating Real Markdown Cells in the Notebook File Rendered output is not the same as a markdown cell. If you need to insert or modify notebook cells programmatically, use `nbformat` and edit the notebook JSON structure. ```python import nbformat from nbformat.v4 import new_markdown_cell path = "report.ipynb" nb = nbformat.read(path, as_version=4) cell = new_markdown_cell("## Generated Notes This section was inserted by a script.") nb.cells.insert(1, cell) nbformat.write(nb, path) ``` This changes the notebook document itself. It is useful for pre-processing notebooks, report generation pipelines, or tools that assemble notebooks from templates. ## Choose the Right Workflow Use rendered markdown output when the content is ephemeral and belongs to one execution run. Use notebook-cell generation when the markdown should become part of the saved notebook structure. You can also combine them. For example, a reporting pipeline may generate a notebook with markdown cells ahead of time and then use `display(Markdown(...))` for execution-specific summaries. ## Template-Driven Markdown For longer reports, build markdown with templates instead of concatenating many small strings by hand. Even Python’s built-in f-strings are enough for many notebooks. ```python from IPython.display import Markdown name = "Customer churn" count = 1245 rate = 0.173 report = f""" ## Dataset Report - Dataset: {name} - Rows: {count} - Churn rate: {rate:.1%} """ Markdown(report) ``` This keeps the notebook readable while still making the output dynamic. ## Common Pitfalls - Expecting `display(Markdown(...))` to create a new markdown cell is a common misunderstanding. It only creates rendered output. - Building large markdown strings with many manual concatenations becomes hard to maintain. - Forgetting to escape special markdown characters can break tables or headings. - Editing notebook JSON without `nbformat` is error-prone because notebooks have a structured schema. - Mixing static narrative and generated output without a clear layout can make notebooks harder to read. ## Summary - In Jupyter, rendered markdown output and markdown cells are different things. - Use `display(Markdown(...))` to show dynamic markdown during execution. - Use `nbformat` when you need to insert or modify actual markdown cells. - Build larger markdown sections from templates for readability. - Pick the workflow that matches whether the content is temporary output or persistent notebook structure.