Choosing Between Jupyter Notebooks and Python Scripts in DataSpell: A Decision Guide
DataSpell treats Jupyter notebooks and Python scripts with #%% cell markers as equals. This guide compares execution models, Git diffs, collaboration, CI/CD, and remote kernels—then shows how to validate your choice with concrete steps.
10 Jul 2026, 20:43 UTC

The Decision: Notebook or Script for Interactive Data Science in DataSpell
DataSpell 2024.x treats .ipynb (Jupyter notebook) and .py files with # %% cell markers as first-class citizens. Both render inline outputs, support cell execution, and share the Variable Explorer and DataFrame viewers. The choice is not about capability—it’s about how your team handles version control, collaboration, CI/CD, and environment reproducibility. This guide lays out the constraints, compares the formats, and shows how to validate the workflow you pick.
Constraints That Shape the Choice
- Version control hygiene: Git diffs and merge conflict resolution differ drastically between JSON-based notebooks and plain-text scripts.
- Collaboration surface: Who needs to read or run the work—DataSpell users only, or also GitHub reviewers, Colab users, JupyterHub teams?
- CI/CD integration: Does your pipeline run pytest on scripts, or do you need papermill/nbmake for notebook execution?
- Kernel magics and extensions: Reliance on
%matplotlib,%%timeit, or IPython-specific magics. - Licensing: DataSpell 2024.1+ requires a paid license for commercial use after the 30-day trial; no Community Edition exists.
Compact Comparison
| Dimension | .ipynb (Jupyter Notebook) |
.py#%% (Python Script with Cell Markers) |
|---|---|---|
| Execution model | Persistent Jupyter kernel (local/remote); state survives across cells until restart | Per-cell or whole-file via Python interpreter; optional "Run in Console" for REPL-style state |
| Git diff quality | JSON-heavy; visual diff viewer in DataSpell 2023.2+, but merges remain hard | Clean line-oriented diffs; standard Git tooling works |
| Portability | Renders natively on GitHub, GitLab, Colab, JupyterHub, nbviewer | Cell UI requires DataSpell, VS Code (Jupyter ext), or PyCharm Pro; plain Python runs anywhere |
| CI/CD integration | Needs nbconvert, jupytext, papermill, or nbmake to export/execute in pipelines | Direct integration with pytest, black, ruff, mypy, standard packaging |
| Kernel magics | Full IPython magic support (%load_ext, %%timeit, etc.) |
Magics work only if an IPython kernel is attached; plain Python ignores them |
| File size & performance | Bloats with embedded outputs; >50 MB notebooks slow to open/save | Lightweight; outputs exist only in IDE session |
| Debugging | Cell-level breakpoints via Jupyter kernel protocol | Standard Python debugger; breakpoints persist, require "Run with Debug" per cell/file |
| Environment metadata | Embeds kernel spec (name, env) in notebook metadata | Relies on project interpreter; document separately (requirements.txt, poetry.lock, conda env.yml) |
Trade-offs in Practice
Execution Model and State
A notebook’s kernel holds variable state across cells—restarting the kernel clears everything. A .py#%% file runs each cell in a fresh interpreter unless you use the "Run in Console" action (right-click cell → Run in Console), which attaches to a persistent Python Console tool window. For exploratory work where you repeatedly mutate large DataFrames, the notebook kernel avoids reloading data. For reproducible runs, the script’s stateless default is safer.
Version Control and Merge Conflicts
DataSpell’s visual notebook diff (since 2023.2) shows side-by-side rendered cells, but merging conflicting JSON structures often requires manual editing. Scripts diff like any Python file. If your team does frequent branching and merging, .py#%% reduces friction. A hybrid approach uses Jupytext to keep a paired .ipynb and .py#%% in sync—commit both, review the script diff, and let the notebook serve collaborators.
Collaboration and Review
If reviewers read code on GitHub/GitLab, .ipynb renders instantly. .py#%% shows as plain Python with # %% comments; the cell structure is invisible. For external stakeholders, export the notebook via File → Export → Notebook or run jupytext --to notebook in CI.
Packaging, Linting, and Testing
Scripts plug directly into the Python toolchain. Add test functions in a # %% cell marked # %% [pytest] (convention) and run pytest on the file. Notebooks require nbmake or papermill to execute as tests, and nbconvert --to script or jupytext to lint with ruff/black. The extra steps are manageable but add pipeline complexity.
Remote Kernels
Both formats attach to remote Jupyter servers (SSH, Docker, WSL, Kubernetes) via Settings → Languages & Frameworks → Jupyter → Jupyter Servers → Add. The notebook uses the remote kernel’s environment natively. A script can run locally while reading remote data via SQLAlchemy, requests, or cloud SDKs—no kernel attachment needed for data access.
Performance at Scale
Notebooks over 50 MB (many output cells, large embedded images) open and save noticeably slower. Clear outputs via Cell → Clear All Outputs before committing. Scripts stay small; the IDE only holds outputs in memory during the session.
Concrete Validation: Set Up and Compare Both Workflows
Run these steps in DataSpell 2024.x (verify via Help → About) to see the differences firsthand. You need a DataSpell license (trial or paid) and Git installed.
1. Create a Paired Project
# Terminal (project root)
mkdir dataspell-format-demo && cd dataspell-format-demo
git init
# Create a virtual environment (optional but recommended)
python -m venv .venv && source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install pandas matplotlib jupytext pytest ruff
# Open in DataSpell: File → Open → select folder
Permissions: Write access to project directory. Risk: None—creates local files only.
2. Build a Notebook and Convert to Script
- In DataSpell: File → New → Jupyter Notebook → name
analysis.ipynb. - Add cells:
# %% [markdown] # Load data # %% import pandas as pd import matplotlib.pyplot as plt df = pd.DataFrame({'x': range(100), 'y': [i**2 for i in range(100)]}) df.head() # %% plt.plot(df['x'], df['y']) plt.title('Quadratic growth') plt.show() - Run each cell via gutter icons. Observe inline plot and Variable Explorer.
- Right-click
analysis.ipynb→ Convert to Python Script. DataSpell createsanalysis.pywith# %%and# %% [markdown]markers. Outputs are stripped—conversion is one-way for outputs.
Check: Open analysis.py; verify cell markers and markdown comments. Run cells via gutter icons; they execute in the Python Console (prompt appears in tool window).
3. Compare Git Diffs
# Terminal
git add analysis.ipynb analysis.py
git commit -m "Initial notebook and script"
# Modify both: change plot title to 'Quadratic growth (updated)'
git diff analysis.ipynb # JSON diff—hard to read
git diff analysis.py # Clean unified diff
Expected: Script diff shows a single line change; notebook diff shows JSON structure changes. In DataSpell, Git → Show Diff on the notebook opens the visual diff viewer—useful for review, not for merging.
4. Test CI Integration
Create test_analysis.py (plain test file) and test_notebook.ipynb (notebook with a test cell).
# test_analysis.py
def test_dataframe_shape():
import pandas as pd
df = pd.DataFrame({'x': range(100), 'y': [i**2 for i in range(100)]})
assert df.shape == (100, 2)
# test_notebook.ipynb (create via File → New → Jupyter Notebook)
# %%
import pandas as pd
df = pd.DataFrame({'x': range(100), 'y': [i**2 for i in range(100)]})
# %% [pytest]
assert df.shape == (100, 2)
Run locally:
pytest test_analysis.py -v # passes
nbmake test_notebook.ipynb # requires nbmake installed; passes
In CI (GitHub Actions example), the script path is trivial; the notebook path needs pip install nbmake and nbmake test_notebook.ipynb.
5. Attach a Remote Kernel (Optional)
- Start a remote Jupyter server (e.g.,
jupyter lab --no-browser --port=8888 --ip=0.0.0.0on a VM or Docker). - In DataSpell: Settings → Languages & Frameworks → Jupyter → Jupyter Servers → Add → enter URL with token.
- Open
analysis.ipynb; click kernel picker (top-right) → select remote server. - Run a cell; output comes from remote kernel.
- Open
analysis.py; same kernel picker works. Cells execute on the remote kernel.
Verification: Variable Explorer shows remote kernel’s variables. Risk: Network latency; ensure SSH tunnel or VPN if server is not publicly exposed.
Limitations and Cautions
- Cell marker detection: DataSpell only recognizes
# %%or# %% [markdown]. Markers like# In[1]:or%%timeare ignored. Agree on a team standard. - Conversion losses:
.ipynb → .py#%%strips outputs and widget state. Converting back does not restore them. Treat conversion as one-way for outputs. - Magics in scripts: IPython magics (
%matplotlib inline,%%timeit) work only when an IPython kernel is attached. In plain Python execution they raiseSyntaxError. Useimport matplotlib; matplotlib.use('inline')andtimeitmodule instead. - Environment reproducibility: Notebooks embed kernel metadata; scripts do not. Always commit
requirements.txt,poetry.lock, orconda env.ymlalongside scripts. - Licensing: Confirm your organization’s DataSpell license status before standardizing on DataSpell-specific workflows (e.g., the unified notebook/script UI, visual diff viewer).
Practical Verification Checklist
- Open DataSpell → File → New → Jupyter Notebook / Python File; add
# %%cells; run via gutter icons; confirm Variable Explorer and inline plots appear. - Right-click a
.ipynb→ Convert to Python Script; inspect generated.pyfor markers. - Initialize Git repo; commit both formats; use Git → Show Diff to compare diff quality; simulate a merge conflict.
- Configure a remote Jupyter server (Settings → Jupyter Servers → Add); attach both file types; run cells and verify kernel connection.
- Run
pyteston a.py#%%file with test functions; runnbmakeorpapermillon a.ipynbto validate CI integration. - Check DataSpell version via Help → About; confirm 2024.1+ for latest notebook diff viewer and remote kernel improvements.
Decision Summary
Choose .ipynb when: notebooks are reviewed on GitHub/GitLab/Colab, you rely heavily on IPython magics, or your pipeline already uses papermill/nbmake. Choose .py#%% when: Git diff/merge hygiene matters, you want direct pytest/ruff/mypy integration, files exceed 50 MB, or the team standardizes on plain Python tooling. For mixed environments, adopt Jupytext to maintain a synchronized pair—commit both, review the script, publish the notebook.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.