Writing Technical Books in 2026: Tools, Workflows, and the Case for Executable Code
A practitioner comparison of Quarto, Jupyter Book, Pandoc, Typst, and publisher toolchains for writing technical books with testable code examples and multi-format output.
I recently set up a Typst pipeline for my resume and started evaluating what I’d use if I were writing a technical book. The requirements seem simple: write in Markdown, include runnable code snippets, generate PDF and HTML and EPUB from one source, and know immediately when a library upgrade breaks an example. Turns out the tooling landscape is more fragmented than I expected.
Here’s what I found after evaluating every serious option. If you want to skip ahead to the working code, check out the quarto-book-template on GitHub and the follow-up post on snippet extraction and diagrams.
The Core Problem
Technical books rot. You publish with pandas==2.1, a year later readers install pandas==3.0, and half the examples throw deprecation warnings or outright fail. The traditional answer is a separate GitHub repo with CI tests, but that creates drift between the tested code and what’s printed in the book.
The ideal workflow: code lives in the book, executes during build, and a library upgrade that breaks an example fails the build before you publish.
The Landscape at a Glance
| Tool | Markdown | Executable Code | HTML | EPUB | Code Testing on Build | |
|---|---|---|---|---|---|---|
| Quarto | Yes | Python, R, Julia, Bash | Typst or LaTeX | Yes | Yes | Yes |
| Jupyter Book | .ipynb or .md | Python-first | LaTeX only | Yes | Yes | Yes |
| Pandoc | Yes | No | LaTeX | Yes | Yes | No |
| Typst | Typst markup | No | Native | Experimental | No | No |
| Sphinx | reStructuredText | No (natively) | LaTeX | Yes | Yes | No |
| AsciiDoc | AsciiDoc | No | Asciidoctor-pdf | Yes | Yes | No |
| Leanpub | Markua | No | Hosted | Hosted | Hosted | No |
Only two tools execute code as part of the build: Quarto and Jupyter Book.
Choosing the Right Tool
flowchart TD
A[Writing a technical book?] --> B{Does your code<br/>need to execute<br/>during build?}
B -->|Yes| C{Primary<br/>language?}
B -->|No| D{Want maximum<br/>control?}
C -->|Python / multi-language| E[Quarto]
C -->|Python + heavy notebooks| F[Jupyter Book]
D -->|Yes| G[Pandoc + Markdown]
D -->|No| H{Going with<br/>a publisher?}
H -->|Yes| I[AsciiDoc + code repo]
H -->|No| J[Leanpub]
Quarto: The New Default
Quarto is what you get when Posit (the RStudio people) decided R Markdown should work for every language. You write .qmd files — standard Markdown with fenced code blocks that actually run.
## Loading the Data
```{python}
import pandas as pd
df = pd.read_csv("sales.csv")
df.head()
```
When you run quarto render, every code block executes, output is captured (stdout, plots, tables), and the result is embedded in the final document. If pandas changed its API and df.head() now throws an error, the build fails.
What makes Quarto stand out
Multi-format from one source. A single _quarto.yml configuration generates PDF, HTML website, EPUB, and Word:
project:
type: book
book:
title: "Building Data Platforms"
chapters:
- index.qmd
- chapters/ingestion.qmd
- chapters/processing.qmd
- chapters/serving.qmd
format:
html:
theme: cosmo
typst-pdf:
documentclass: book
epub:
cover-image: cover.png
Typst PDF backend. As of Quarto 1.9 (March 2026), you can use Typst instead of LaTeX for PDF generation. This matters because LaTeX compilation is slow and its dependency tree is enormous. Typst compiles in milliseconds with a single 30MB binary.
Native book features. Cross-references (@fig-pipeline, @tbl-metrics, @sec-architecture), citations via BibTeX, automatic chapter numbering, figure captions, callout blocks — all built in, no plugins.
Language-agnostic. Python, R, Julia, and Bash code blocks all execute natively. You can mix languages in a single chapter.
The tradeoff
For books with heavy computation (ML training, database setup, infrastructure provisioning), every quarto render re-executes everything. Quarto has freeze and cache mechanisms to skip unchanged chapters, but if your examples take minutes to run, the feedback loop slows down.
Jupyter Book: The Notebook-Native Option
Jupyter Book takes the opposite approach — start from Jupyter notebooks and build a book around them. If your content is already notebooks or heavily visual (plots, dataframes, interactive widgets), this is a natural fit.
Where it works well
- Data science and ML books where output is visual
- Workshop materials and course content
- Anything already written as notebooks
Where it falls short
- PDF output is weak — goes through LaTeX with less control than Quarto
- No Typst backend — stuck with LaTeX for PDF
- Git diffs are painful — notebooks are JSON, so diffs are unreadable
- Prose-heavy chapters are awkward — notebooks aren’t great for long-form writing
- Development pace — the Executable Books Project moves slower than Posit’s backing of Quarto
If you’re starting fresh, Quarto can consume .ipynb files anyway, so there’s no lock-in advantage to choosing Jupyter Book.
Pandoc + Markdown: Maximum Control, Manual Assembly
Pandoc is the Swiss Army knife of document conversion. Write plain Markdown, run pandoc to get PDF, EPUB, HTML, Word, or a dozen other formats. Thorsten Ball used this approach to write Writing an Interpreter in Go and Writing a Compiler in Go — both well-regarded technical books.
The workflow:
chapters/*.md → pandoc → PDF (via LaTeX), EPUB, HTML
Pros
- Plain Markdown — no special syntax, works in any editor
- Total control — custom LaTeX templates, custom CSS, custom EPUB styling
- Git-friendly — pure text files, clean diffs
- Battle-tested — Pandoc has been around since 2006
Cons
- No executable code — you paste output manually and maintain a separate test repo
- LaTeX for PDF — slow compilation, dependency hell
- Manual cross-references — Pandoc supports them but they’re more verbose than Quarto’s
- No built-in book structure — you write a Makefile or script to stitch chapters together
This is the right choice if you want maximum control and are comfortable maintaining code examples separately. Several self-published authors use this approach successfully, often paired with Amazon KDP for distribution.
Typst: Fast PDF, No Code Execution
Typst is a modern typesetting system that replaces LaTeX with a simpler syntax and millisecond compilation. I use it for my resume and it’s excellent for layout-heavy documents.
For books, Typst works as a PDF backend (via Quarto) rather than a standalone authoring tool. You wouldn’t write a technical book directly in Typst because:
- No Markdown — Typst has its own markup language
- No executable code blocks
- No built-in book structure (chapters, cross-references, citations)
- No HTML or EPUB output (HTML export is still experimental)
Use Typst through Quarto for fast PDF generation, not as your primary authoring tool.
Publisher Toolchains
If you’re writing for a publisher, they dictate the toolchain:
| Publisher | Required Format | Notes |
|---|---|---|
| O’Reilly | AsciiDoc or Word via Atlas | Strong editorial, wide distribution |
| Manning | AsciiDoc or Word via LiveBook | ”In Action” series, early access program |
| Pragmatic Bookshelf | Custom markup | Developer-focused, strong editorial |
| Packt | Word or Google Docs | High volume, faster publishing |
The universal pattern with publishers: book text and code live in separate repositories. The publisher’s toolchain handles formatting and distribution. You maintain a GitHub repo with all code examples and CI tests. Code snippets in the book are extracted from or validated against the tested repo.
Gergely Orosz’s experience writing The Software Engineer’s Guidebook is a good read on what the publisher relationship actually looks like in practice.
The “Tested Repo” Pattern
Regardless of writing tool, professional technical authors converge on this pattern:
book-repo/
chapters/ # Prose (.qmd, .md, .adoc)
code/ # Actual runnable project
ch01/
ch02/
tests/ # CI tests for every example
scripts/
extract-snippets.py # Pulls code into chapters
.github/workflows/
test.yml # Runs all code on library upgrades
The code repo has CI. When Dependabot or Renovate opens a PR bumping a dependency, tests run against every code example. If something breaks, you fix the code, re-extract snippets, and re-render.
Quarto collapses this pattern — code lives in the book and executes during render. That’s its value proposition. But for books where examples require infrastructure (databases, cloud services, Docker), the separated pattern is more practical because you can’t spin up a Kubernetes cluster during quarto render.
Snippet Extraction: Literate Programming for Technical Books
The tested repo pattern raises a practical question: how do you get the code into the book? Copy-paste is error-prone and drifts from the source. The more disciplined approach is snippet extraction — you mark regions of your tested source code with named tags, and a build script pulls them into the manuscript automatically.
How it works
In your tested source code, you mark snippet boundaries:
# code/ch03/pipeline.py
def build_pipeline():
# <<< snippet: kafka-producer-setup >>>
producer = KafkaProducer(
bootstrap_servers=['localhost:9092'],
value_serializer=lambda v: json.dumps(v).encode('utf-8')
)
# <<< /snippet: kafka-producer-setup >>>
# <<< snippet: kafka-send-message >>>
producer.send('events', {'type': 'trade', 'price': 142.50})
producer.flush()
# <<< /snippet: kafka-send-message >>>
In your Markdown chapter, you reference the snippet by name:
## Setting Up the Producer
<!-- include: code/ch03/pipeline.py#kafka-producer-setup -->
The producer serializes values as JSON before sending to the `events` topic.
<!-- include: code/ch03/pipeline.py#kafka-send-message -->
A build script (often a simple Python script or a Pandoc filter) resolves the includes before Pandoc renders the final output. The reader sees clean code blocks. You never copy-paste.
What you gain
The appeal is clear:
- Code is always tested. CI runs against the actual source files. If
KafkaProducerchanges its API, the test fails — and you know exactly which snippet needs updating. - Single source of truth. The code in the book is extracted from real, working files. No drift between what’s printed and what runs.
- Full project context. The source files are complete, runnable programs — not fragments. A reader who clones the repo can run
python code/ch03/pipeline.pyand see it work. - Language-agnostic. Works with any language, any build tool, any publisher format.
Where it breaks down
This pattern has real costs that scale with book size:
Reordering chapters breaks narrative context. Snippet names are stable across moves, but the prose around them isn’t. “As we saw in Chapter 3” becomes wrong when Chapter 3 moves to Chapter 5. Every reorganization requires a manual pass through all narrative references.
Refactoring code cascades into the manuscript. Rename a function, split a class, restructure a module — now you need to update snippet markers, include references, and every piece of prose that mentions the old name. The code repo and book repo are loosely coupled in theory but tightly coupled in practice.
Cross-snippet dependencies are invisible. Chapter 5’s snippet assumes a variable from Chapter 3’s snippet exists. The extraction tool doesn’t know this — it just pulls text. If you reorder or remove the Chapter 3 snippet, Chapter 5’s code still extracts fine but no longer makes sense.
Dead snippets accumulate silently. Remove an include from the book, forget to clean up the markers in the source — no error, just cruft. Over time you lose track of which snippets are still referenced. You can write a linter for this, but now you’re maintaining tooling.
Context is lost in extraction. The reader sees 8 lines pulled from a 200-line file. Where do the imports come from? What class is this method in? You end up adding prose to explain the surrounding context that would have been self-evident in a complete listing — or you add more snippets, which increases the coupling surface.
flowchart TD
A[Source Code<br/>with snippet markers] --> B[CI Tests]
A --> C[Extraction Script]
C --> D[Markdown with<br/>resolved snippets]
D --> E[Pandoc]
E --> F[PDF]
E --> G[HTML]
E --> H[EPUB]
B -->|Tests pass| I[Code is correct]
B -->|Tests fail| J[Fix code,<br/>update snippets]
J --> A
K[Dependency upgrade] --> B
When snippet extraction is the right choice
Despite the overhead, this pattern wins in specific scenarios:
- Infrastructure-heavy examples. Your code needs Kafka, Postgres, Redis, or a Kubernetes cluster to run. You can’t execute this during a book render — it needs a proper CI environment with Docker Compose or similar.
- Large application examples. The book walks through building a complete application across chapters. Snippets are views into a cohesive codebase, not isolated fragments.
- Publisher-mandated formats. O’Reilly and Manning require AsciiDoc or Word. You can’t use Quarto’s output directly, so the snippet extraction approach with Pandoc or AsciiDoc tooling is the pragmatic choice.
- Performance-sensitive examples. ML training, data processing benchmarks, load tests — anything that takes minutes to run shouldn’t execute on every render.
Tooling options
You don’t need to build the extraction from scratch:
- Pandoc filters — write a Lua or Python filter that resolves
<!-- include: ... -->directives during Pandoc conversion - mdBook preprocessors — if using mdBook, custom preprocessors can handle snippet inclusion
- Quarto includes — Quarto supports
{{< include file.py >}}natively, and you can combine this witheval: falsefor code that shouldn’t execute during render - Custom scripts — a 50-line Python script with regex matching on your snippet markers is often sufficient
The hybrid approach: Quarto + selective extraction
The most pragmatic architecture combines both patterns:
## Simple, self-contained example (Quarto executes this)
```{python}
df = pd.DataFrame({'price': [100, 102, 98, 105]})
df['returns'] = df['price'].pct_change()
df.head()
```
## Infrastructure example (extracted from tested repo, not executed)
```{python}
#| eval: false
{{< include code/ch03/pipeline.py#kafka-producer-setup >}}
```
Simple examples execute inline and their output is captured. Infrastructure-heavy examples are included from the tested repo with eval: false — they render as code blocks but don’t execute during quarto render. CI tests in the code repo validate the infrastructure examples separately.
This gives you the best of both worlds: inline execution where it’s practical, and tested extraction where it’s necessary. The tradeoff is two testing mechanisms (Quarto render + CI tests), but that’s a reasonable cost for a book that has both lightweight and infrastructure-heavy examples.
Leanpub: Zero-Ops Publishing
Leanpub deserves a mention for self-publishing. You write in Markua (their Markdown variant), push to a GitHub repo, and Leanpub builds PDF/EPUB/HTML. They handle sales, distribution, and incremental publishing — readers who buy early get every update.
- Pros: No build toolchain to maintain, handles payments, “publish early, publish often” model
- Cons: No executable code, 20% royalty cut, vendor lock-in on formatting, less control over output
Good for getting a book out quickly. Less good for the “testable code” requirement.
Decision Framework
| If you want… | Use |
|---|---|
| Executable code + multi-format output | Quarto |
| Notebook-heavy data science book | Quarto or Jupyter Book |
| Maximum control, comfortable with LaTeX | Pandoc + Markdown |
| Publisher deal (O’Reilly, Manning) | AsciiDoc + separate code repo |
| Self-publish with zero ops | Leanpub |
| Fast PDF from Quarto | Quarto with Typst backend |
| Resume or CV | Typst directly |
My Recommendation
For a new technical book in 2026 with Python code examples:
- Quarto with Typst PDF backend for authoring and multi-format output
- GitHub Actions running
quarto renderon every push — if code breaks, the build fails - Dependabot/Renovate for dependency upgrades — PRs trigger a full render, catching breakage immediately
- Optionally Leanpub for distribution if self-publishing, or pitch to O’Reilly/Manning if you want the publisher route (you’ll need to port to AsciiDoc, but the content transfers)
The workflow becomes:
flowchart LR
A[Edit .qmd] --> B[git push]
B --> C[GitHub Action]
C --> D[quarto render]
D --> E{All code<br/>executes?}
E -->|Pass| F[Generate PDF<br/>HTML + EPUB]
E -->|Fail| G[Build fails<br/>fix code]
F --> H[Publish<br/>new version]
G --> A
I[Renovate PR<br/>bumps dependency] --> C
This is CI/CD for books. The same engineering practices we apply to production code — version control, automated testing, continuous delivery — applied to technical writing.
Further Reading
- Quarto documentation
- Quarto 1.9 — Typst Books
- How to Write Your First Technical Book — freeCodeCamp overview of the process
- The Tools I Use To Write Books — Thorsten Ball on the Pandoc workflow
- Self-publishing with Pandoc and LaTeX
- Self-publishing with reStructuredText and Sphinx
- Four Years Writing a Tech Book — Gergely Orosz on the publisher experience
- Writing a Technical Book for Manning — Hacker News discussion