<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Blog on Posit Open Source</title>
    <link>https://opensource.posit.co/blog/</link>
    <description>Recent content in Blog on Posit Open Source</description>
    <generator>Hugo -- gohugo.io</generator>
    <language>en-us</language>
    <lastBuildDate>Wed, 15 Apr 2026 00:00:00 +0000</lastBuildDate>
    <atom:link href="https://opensource.posit.co/blog/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Introducing Great Docs: Beautiful Documentation for Python Packages</title>
      <link>https://opensource.posit.co/blog/2026-04-15_great-docs-introduction/</link>
      <pubDate>Wed, 15 Apr 2026 00:00:00 +0000</pubDate>
      <guid>https://opensource.posit.co/blog/2026-04-15_great-docs-introduction/</guid>
      <dc:creator>Rich Iannone</dc:creator><description><![CDATA[<p>When someone discovers your Python package, the first thing they see is the documentation site. That site should look good, feel cohesive, and reflect the identity of your project.</p>
<p>While building documentation sites for projects like 






<a href="https://posit-dev.github.io/great-tables/" target="_blank" rel="noopener">Great Tables</a>
 and 






<a href="https://posit-dev.github.io/pointblank/" target="_blank" rel="noopener">Pointblank</a>
, we learned just how much effort goes into making a site that looks distinctive and matches the character of each project: custom themes, tailored layouts, interactive features, and countless small design decisions. That experience taught me what a great documentation site needs, and I wanted to distill all of those learnings into a tool that gives every Python package a polished site from the start. That led me to build 






<a href="https://posit-dev.github.io/great-docs/" target="_blank" rel="noopener">Great Docs</a>
, a documentation generator that produces beautiful sites out of the box (but with simple options to customize the look and make it yours).</p>
<p>Great Docs is now part of the 






<a href="https://posit.co/" target="_blank" rel="noopener">Posit</a>
 open-source ecosystem, 






<a href="https://pypi.org/project/great-docs/" target="_blank" rel="noopener">available on PyPI</a>
, and at <code>v0.7</code> with seven releases since its initial soft launch.</p>
<p><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://opensource.posit.co/blog/2026-04-15_great-docs-introduction/assets/gd-homepage.png"
      alt="The Great Docs homepage with annotations highlighting the major site features: metadata sidebar, logo, navbar with gradient, light/dark mode toggle, keyboard shortcut reference, GitHub widget, and search." 
      loading="lazy"
    >
  </figure></div>
</p>
<h2 id="what-is-great-docs">What Is Great Docs?
</h2>
<p>Great Docs is a documentation site generator for Python packages. You point it at your project and it produces a documentation site with API reference, CLI reference, user guides, changelog, and landing page. It auto-discovers your package&rsquo;s public API, generates 






<a href="https://quarto.org" target="_blank" rel="noopener">Quarto</a>
-based pages, and renders the result into a static site.</p>
<p>The entire workflow involves just a few commands:</p>
<div class="code-block"><div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">great-docs init       <span class="c1"># one-time: auto-detect your package, write config</span>
</span></span><span class="line"><span class="cl">great-docs build      <span class="c1"># build (or rebuild) the site</span>
</span></span><span class="line"><span class="cl">great-docs preview    <span class="c1"># open it in your browser</span></span></span></code></pre></td></tr></table>
</div>
</div></div>
<p>There is no boilerplate to write, no templates to configure, and no theme to choose as the defaults produce a good-looking site on their own. If you want to go further, the <code>great-docs.yml</code> configuration file offers extensive customization: navbar gradients, content styles, announcement banners, author metadata, custom sections, and much more.</p>
<h2 id="why-another-documentation-generator">Why Another Documentation Generator?
</h2>
<p>There is no shortage of documentation tooling in the Python ecosystem, but after years of building and maintaining documentation sites for my own packages, a few pain points kept surfacing:</p>
<p><strong>Discovery is manual.</strong> Most tools require you to explicitly list every class, function, and method you want documented, which becomes tedious and error-prone as your package grows.</p>
<p><strong>The output looks dated.</strong> Many popular generators produce sites that feel like they belong to an earlier era of the web. Mobile responsiveness, dark mode, and modern typography are afterthoughts, if they are supported at all.</p>
<p><strong>LLMs cannot easily consume the output.</strong> Developers routinely paste documentation into AI assistants, and if your docs are not structured for machine consumption, the answers those assistants produce will be worse.</p>
<p><strong>Deployment is a separate project.</strong> Getting from a built site to a live GitHub Pages deployment often involves manually writing CI workflows.</p>
<p><strong>No quality tools.</strong> Most generators stop at rendering. If you want to check for broken links, catch spelling and grammar issues, or understand how your API has changed across versions, you are on your own with third-party tools and custom scripts.</p>
<p>Great Docs addresses all of these, and the rest of this post walks through the key features.</p>
<h2 id="auto-discovery-your-api-documented-automatically">Auto-Discovery: Your API, Documented Automatically
</h2>
<p>When you run <code>great-docs init</code>, the tool inspects your package and discovers its public API automatically. It finds classes, functions, dataclasses, protocols, enumerations, exceptions, type aliases, and more, using a combination of runtime introspection and static analysis via 






<a href="https://mkdocstrings.github.io/griffe/" target="_blank" rel="noopener">griffe</a>
. It detects your docstring style (NumPy, Google, or Sphinx) and writes a <code>great-docs.yml</code> configuration file that captures the full structure of your API.</p>
<p>The result is 13 distinct object-type categories: classes with many methods get their own dedicated sections, overloaded signatures are displayed correctly, and long signatures are formatted across multiple lines for readability (all without you having to enumerate a single export).</p>
<p>If you later add new functions or classes to your package, just run <code>great-docs init</code> again (or <code>great-docs build</code> with the <code>--scan</code> option) and the configuration updates to reflect your current API.</p>
<p><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://opensource.posit.co/blog/2026-04-15_great-docs-introduction/assets/api-reference-index.png"
      alt="A portion of an API reference index page generated by Great Docs, showing categorized Python objects with type badges and sidebar filtering." 
      loading="lazy"
    >
  </figure></div>
</p>
<h2 id="powered-by-quarto-and-quartodoc">Powered by Quarto and quartodoc
</h2>
<p>Great Docs is an evolution of 






<a href="https://machow.github.io/quartodoc/" target="_blank" rel="noopener">quartodoc</a>
, which pioneered the idea of generating Quarto-based API reference pages for Python packages. All of the excellent work quartodoc does for API documentation (docstring rendering, cross-references between symbols, etc.) carries forward into Great Docs. quartodoc will continue to be maintained as a standalone tool for projects that need focused API reference generation, while Great Docs builds on that foundation to add site-wide features like styling, interactive widgets, CLI reference, quality tools, and the LLM-friendly output described below.</p>
<p>Under the hood, Great Docs generates 






<a href="https://quarto.org" target="_blank" rel="noopener">Quarto</a>
 <code>.qmd</code> files and renders them into a static site. Quarto is a scientific and technical publishing system that supports executable code cells, rich cross-references, callout blocks, tabsets, and many other features useful for documentation, and by building on it Great Docs inherits all of these capabilities. User guides can include live code examples with rendered output, API reference pages get syntax highlighting via Pygments, and the entire site benefits from Quarto&rsquo;s rendering pipeline.</p>
<h2 id="styling-and-interactive-features">Styling and Interactive Features
</h2>
<p>Great Docs ships with a set of styles and interactive features that cover most of what you would want from a documentation site:</p>
<ul>
<li><strong>Dark mode toggle</strong> with system-preference detection and persistence across sessions</li>
<li><strong>Animated navbar gradients</strong> with eight preset themes (sky, peach, prism, lilac, mint, ocean, sunset, forest)</li>
<li><strong>Subtle content gradients</strong> that add visual warmth without distraction</li>
<li><strong>GitHub widget</strong> showing live star and fork counts</li>
<li><strong>Sidebar search</strong> for quickly filtering long API reference lists</li>
<li><strong>Smart sidebar wrapping</strong> that handles long class and method names gracefully</li>
<li><strong>Responsive design</strong> that works well on phones and tablets</li>
<li><strong>Copy Page widget</strong> for one-click copy-as-Markdown on reference pages</li>
<li><strong>Back-to-top button</strong> that appears after scrolling, with smooth animation and dark mode support</li>
<li><strong>Keyboard navigation</strong> with shortcuts for search (<code>s</code>), page browsing (<code>[</code>/<code>]</code>), dark mode (<code>d</code>), and a help overlay (<code>?</code>)</li>
<li><strong>Social cards</strong> with automatic Open Graph and Twitter Card meta tags for rich link previews</li>
<li><strong>Page tags</strong> for categorizing pages via YAML frontmatter, rendered as pill-shaped links above the title with an auto-generated tags index page</li>
<li><strong>Page status badges</strong> marking pages as <em>new</em>, <em>beta</em>, <em>deprecated</em>, or <em>experimental</em>, with color-coded badges below the title and compact icons in the sidebar</li>
<li><strong>Inline icons</strong> via the <code>{{&lt; icon name &gt;}}</code> shortcode, giving access to 1,900+ 






<a href="https://lucide.dev/" target="_blank" rel="noopener">Lucide</a>
 icons that scale with text and inherit color</li>
<li><strong>Navigation icons</strong> via 






<a href="https://lucide.dev/" target="_blank" rel="noopener">Lucide</a>
 for navbar and sidebar labels</li>
<li><strong>Internationalization</strong> with support for 23 languages via a single <code>site.language</code> config option</li>
<li><strong>JS Tooltips</strong> replacing native browser tooltips with styled, theme-aware popovers</li>
<li><strong>License feature badges</strong> showing permissions, conditions, and limitations as color-coded badge groups</li>
</ul>
<p>Dark mode and sidebar filtering are expected in any modern documentation site, keyboard shortcuts let you navigate without reaching for the mouse, and the navbar gradients give each project a distinct visual identity without requiring any design work from the package author.</p>
<h2 id="llm-friendly-by-default">LLM-Friendly by Default
</h2>
<p>Great Docs automatically generates <code>llms.txt</code> and <code>llms-full.txt</code> files alongside your documentation site: structured, plain-text representations of your entire documentation designed for consumption by large language models.</p>
<p>Every reference page also gets a parallel Markdown version, and a &ldquo;Copy Page&rdquo; widget lets users (or AI assistants) grab the content of any page in Markdown format with a single click. A &ldquo;View as Markdown&rdquo; option renders the plain-text version directly in the browser.</p>
<p>Great Docs also supports the 






<a href="https://agentskills.io/" target="_blank" rel="noopener">Agent Skills</a>
 specification. If your project contains a <code>SKILL.md</code> file, Great Docs automatically discovers it, publishes the skill at <code>/.well-known/skills/</code>, and serves it for agent discovery without any configuration. If you do not have a hand-written skill, Great Docs generates one for you. Either way, coding agents like Claude Code, GitHub Copilot, Cursor, and Codex can find and install your package&rsquo;s skill with a single command:</p>
<div class="code-block"><div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">npx skills add https://your-org.github.io/your-package/</span></span></code></pre></td></tr></table>
</div>
</div></div>
<p>You can also author a comprehensive, hand-written <code>SKILL.md</code> in your project and Great Docs will automatically discover and publish it. A detailed skill can include core concepts, step-by-step workflows, common error patterns with fixes, reference files for configuration and CLI commands, setup and build scripts, and a description of what the agent can and cannot do autonomously. Great Docs publishes the entire skill directory structure and renders it on a dedicated Skills page in your documentation site, with a visual layout of the skill&rsquo;s file tree, expandable sections for each companion file (references, scripts, assets), and install instructions for every major agent platform.</p>
<p><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://opensource.posit.co/blog/2026-04-15_great-docs-introduction/assets/skills-page.png"
      alt="The Skills page for the Great Tables documentation site, showing the skill file tree, companion files, and agent install instructions." 
      loading="lazy"
    >
  </figure></div>
</p>
<p>The practical effect is that when a developer asks an AI assistant &ldquo;how do I use the <code>build()</code> method?&rdquo;, the quality of the answer depends on the quality of the documentation the model has access to. And when a coding agent needs to configure, build, or troubleshoot your package&rsquo;s docs, a structured skill file gives it enough context to act autonomously. These features mean the documentation works for human readers, LLM chat contexts, and agentic workflows.</p>
<h2 id="cli-documentation">CLI Documentation
</h2>
<p>If your package has a 






<a href="https://click.palletsprojects.com/" target="_blank" rel="noopener">Click</a>
-based command-line interface, Great Docs can generate reference pages for every command automatically. It discovers your CLI commands, renders each one with its arguments, options, and help text, and adds them to the site with a dedicated sidebar. A reference switcher dropdown lets users toggle between the API Reference and CLI Reference views.</p>
<p>All you have to do is specify the CLI module in your configuration:</p>
<div class="code-block"><div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">cli</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">enabled</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">module</span><span class="p">:</span><span class="w"> </span><span class="l">my_package.cli</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">cli</span></span></span></code></pre></td></tr></table>
</div>
</div></div>
<p><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://opensource.posit.co/blog/2026-04-15_great-docs-introduction/assets/cli-page.png"
      alt="The CLI reference page for the great-docs build command, showing the command description, arguments, and options in a structured layout." 
      loading="lazy"
    >
  </figure></div>
</p>
<p>Click is the starting point, but the 






<a href="https://posit-dev.github.io/great-docs/roadmap.html" target="_blank" rel="noopener">roadmap</a>
 includes support for Typer, argparse, and Fire, with a plugin interface for additional frameworks. The plan is also to move beyond raw <code>--help</code> output toward richer, API-reference-style rendering: styled parameter tables with type annotations and defaults, auto-generated usage examples, cross-references between subcommands and parent groups, environment variable documentation, and full search integration so CLI commands are indexed alongside API symbols.</p>
<h2 id="user-guides-custom-sections-and-blogs">User Guides, Custom Sections, and Blogs
</h2>
<p>API reference alone is rarely sufficient. Developers need narrative documentation that explains concepts, walks through workflows, and provides context that docstrings cannot.</p>
<p>Great Docs supports a <code>user_guide/</code> directory where you place <code>.qmd</code> or <code>.md</code> files. You can also add hand-written HTML pages that are auto-discovered and integrated into the site with minimal transformation, which is useful for product landing pages, interactive demos, or any content that does not fit the <code>.qmd</code> workflow. These are automatically discovered and added to the site with their own navigation section. You can use numeric prefixes (e.g., <code>01-installation.qmd</code>, <code>02-quickstart.qmd</code>) to control ordering in the directory; Great Docs strips the prefixes from the generated URLs, so readers see clean paths like <code>/user_guide/installation.html</code>. Subdirectories are supported for hierarchical organization.</p>
<p>Beyond user guides, you can define arbitrary custom sections for recipes, tutorials, examples, or any other content grouping. And if you want a blog, Great Docs supports blog-type sections with chronological listings and Quarto&rsquo;s blog features built in.</p>
<div class="code-block"><div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">sections</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">title</span><span class="p">:</span><span class="w"> </span><span class="l">Recipes</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">dir</span><span class="p">:</span><span class="w"> </span><span class="l">recipes</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">navbar_after</span><span class="p">:</span><span class="w"> </span><span class="l">User Guide</span></span></span></code></pre></td></tr></table>
</div>
</div></div>
<h2 id="source-code-links">Source Code Links
</h2>
<p>Every documented class, method, and function gets an automatic link back to its source code on GitHub, with precise line-number ranges. This is powered by griffe&rsquo;s static analysis, so the links point to exactly the right lines, not just the file. The placement is configurable (in the usage section or next to the title), and you can specify the branch or tag to link against.</p>
<h2 id="one-command-deployment">One-Command Deployment
</h2>
<p>When your documentation is ready, deploying to GitHub Pages is a single command:</p>
<div class="code-block"><div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">great-docs setup-github-pages</span></span></code></pre></td></tr></table>
</div>
</div></div>
<p>This generates a GitHub Actions workflow file that builds your documentation and deploys it automatically on every push. The workflow is configurable (branch, Python version, docs directory), and once it is in place, your documentation stays up to date with every commit.</p>
<h2 id="quality-tools-built-in">Quality Tools Built In
</h2>
<p>Great Docs includes tooling for keeping your documentation healthy:</p>
<ul>
<li><strong><code>great-docs check</code></strong> audits your configuration against your actual exports, showing what is documented and what is missing</li>
<li><strong><code>great-docs check-links</code></strong> finds broken links across your documentation, with configurable timeouts and ignore patterns</li>
<li><strong><code>great-docs lint</code></strong> analyzes your public API for missing docstrings, broken cross-references, style mismatches, and unknown directives</li>
<li><strong><code>great-docs proofread</code></strong> runs local grammar and spelling checks powered by 






<a href="https://writewithharper.com/" target="_blank" rel="noopener">Harper</a>
, with a built-in technical dictionary and multiple output formats</li>
</ul>
<p>All of these produce machine-readable JSON output, making them suitable for CI integration.</p>
<h2 id="configuration">Configuration
</h2>
<p>The <code>great-docs.yml</code> configuration file is the single source of truth for your documentation site. The defaults are sensible enough that most projects need very little customization, but when you do want to adjust things the options are extensive: display names, author metadata with ORCID identifiers, funding and copyright information, announcement banners, homepage modes, theme settings, sidebar behavior, and more.</p>
<p>Here is what a configuration might look like for a well-established project:</p>
<div class="code-block"><div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span><span class="lnt">20
</span><span class="lnt">21
</span><span class="lnt">22
</span><span class="lnt">23
</span><span class="lnt">24
</span><span class="lnt">25
</span><span class="lnt">26
</span><span class="lnt">27
</span><span class="lnt">28
</span><span class="lnt">29
</span><span class="lnt">30
</span><span class="lnt">31
</span><span class="lnt">32
</span><span class="lnt">33
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">display_name</span><span class="p">:</span><span class="w"> </span><span class="l">My Package</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">announcement</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">content</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;Version 2.0 is here! Check out the changelog.&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">info</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">style</span><span class="p">:</span><span class="w"> </span><span class="l">lilac</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">dismissable</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">navbar_style</span><span class="p">:</span><span class="w"> </span><span class="l">sky</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">content_style</span><span class="p">:</span><span class="w"> </span><span class="l">lilac</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">authors</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Mara Rosario</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">role</span><span class="p">:</span><span class="w"> </span><span class="l">Maintainer</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">github</span><span class="p">:</span><span class="w"> </span><span class="l">mbrosario</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">orcid</span><span class="p">:</span><span class="w"> </span><span class="m">0000-0002-8471-3056</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">sections</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">title</span><span class="p">:</span><span class="w"> </span><span class="l">Tutorials</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">dir</span><span class="p">:</span><span class="w"> </span><span class="l">tutorials</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">navbar_after</span><span class="p">:</span><span class="w"> </span><span class="l">User Guide</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">cli</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">enabled</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">module</span><span class="p">:</span><span class="w"> </span><span class="l">my_package.cli</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">cli</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">reference</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">sections</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="nt">title</span><span class="p">:</span><span class="w"> </span><span class="l">Core</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">contents</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">MyApp, Config]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="nt">title</span><span class="p">:</span><span class="w"> </span><span class="l">Utilities</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">contents</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">parse, validate, format_output]</span></span></span></code></pre></td></tr></table>
</div>
</div></div>
<h2 id="the-iterative-workflow">The Iterative Workflow
</h2>
<p>In practice, using Great Docs looks like this:</p>
<ol>
<li>
<p><strong><code>great-docs init</code></strong>: Run once to scaffold your configuration. Review the generated <code>great-docs.yml</code> and make any adjustments (reorder sections, add authors, enable CLI docs, etc.).</p>
</li>
<li>
<p><strong>Edit <code>great-docs.yml</code></strong>: Customize display name, add announcement banners, configure sections, set navbar gradients. This file is committed to your repository.</p>
</li>
<li>
<p><strong><code>great-docs build</code></strong>: Rebuild whenever you want to see changes. This is fast and idempotent.</p>
</li>
<li>
<p><strong><code>great-docs preview</code></strong>: Open the built site locally to review.</p>
</li>
<li>
<p><strong>Iterate</strong>: Adjust configuration, write user guide pages, add recipes. Rebuild and preview.</p>
</li>
<li>
<p><strong><code>great-docs setup-github-pages</code></strong>: When you are ready to go live, set up automated deployment. From this point on, every push to your main branch publishes updated documentation.</p>
</li>
</ol>
<p>The <code>great-docs.yml</code> file and your <code>user_guide/</code> directory are the only things you commit. The entire <code>great-docs/</code> build directory is ephemeral and gitignored.</p>
<h2 id="whats-next">What&rsquo;s Next
</h2>
<p>Great Docs is under active development and has shipped seven releases (<code>v0.1</code> through <code>v0.7</code>) since its initial launch. The 






<a href="https://posit-dev.github.io/great-docs/roadmap.html" target="_blank" rel="noopener">roadmap</a>
 is quite ambitious. Near-term priorities include author attribution with GitHub-style avatars, reading time estimates, breadcrumb navigation, and an enhanced CLI reference supporting Typer, argparse, and Fire alongside Click. Further out, the roadmap covers multi-version documentation with a version selector, multi-language API documentation spanning Python, R, Rust, and JavaScript, interactive examples powered by Pyodide or JupyterLite, notebook galleries, instant SPA-like page navigation, and a plugin system for third-party extensions.</p>
<h2 id="get-started">Get Started
</h2>
<p>Great Docs is open source under the MIT license and available on 






<a href="https://pypi.org/project/great-docs/" target="_blank" rel="noopener">PyPI</a>
. To get started:</p>
<div class="code-block"><div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">pip install great-docs
</span></span><span class="line"><span class="cl"><span class="nb">cd</span> your-python-project
</span></span><span class="line"><span class="cl">great-docs init
</span></span><span class="line"><span class="cl">great-docs build
</span></span><span class="line"><span class="cl">great-docs preview</span></span></code></pre></td></tr></table>
</div>
</div></div>
<p>If you have feedback, feature ideas, or run into issues, please open an 






<a href="https://github.com/posit-dev/great-docs/issues" target="_blank" rel="noopener">issue on GitHub</a>
.</p>
]]></description>
      <enclosure url="https://opensource.posit.co/blog/2026-04-15_great-docs-introduction/assets/great-docs-logo.png" length="1599984" type="image/png" />
    </item>
    <item>
      <title>Chrome Headless Shell in Quarto</title>
      <link>https://opensource.posit.co/blog/2026-04-14_chrome-headless-shell/</link>
      <pubDate>Tue, 14 Apr 2026 00:00:00 +0000</pubDate>
      <guid>https://opensource.posit.co/blog/2026-04-14_chrome-headless-shell/</guid>
      <dc:creator>Christophe Dervieux</dc:creator><description><![CDATA[<p>Quarto uses a headless browser behind the scenes to render 






<a href="https://quarto.org/docs/authoring/diagrams.html" target="_blank" rel="noopener">Mermaid and Graphviz diagrams</a>
 to PNG for print formats like PDF and DOCX. Until now, this meant installing Puppeteer-bundled Chromium via <code>quarto install chromium</code> &mdash; a setup that worked, but came with some rough edges.</p>
<p>Starting with Quarto 1.9, <code>quarto install chromium</code> is deprecated. The recommended replacement is 






<a href="https://developer.chrome.com/blog/chrome-headless-shell" target="_blank" rel="noopener">Chrome Headless Shell</a>
, a lightweight, headless-only browser from Google&rsquo;s 






<a href="https://developer.chrome.com/blog/chrome-for-testing" target="_blank" rel="noopener">Chrome for Testing</a>
 infrastructure.</p>
<h2 id="why-the-switch">Why the switch?
</h2>
<p>Puppeteer-bundled Chromium served Quarto well, but it had limitations that kept coming up:</p>
<ul>
<li><strong>System dependencies in containers</strong>: The Puppeteer Chromium binary requires system libraries that aren&rsquo;t always present in minimal Docker images or WSL environments. This led to cryptic errors that were hard to debug.</li>
<li><strong>No arm64 Linux support</strong>: Puppeteer didn&rsquo;t distribute Chromium builds for arm64 Linux, leaving users without an easy install path.</li>
<li><strong>Large download size</strong>: The full Chromium bundle is significantly larger than what Quarto actually needs for headless rendering.</li>
</ul>
<p>Chrome Headless Shell solves all three. It&rsquo;s purpose-built for headless automation, has fewer system dependencies, ships arm64 Linux builds, and is smaller to download.</p>
<h2 id="installing-chrome-headless-shell">Installing Chrome Headless Shell
</h2>
<p>If you don&rsquo;t already have Chrome or Edge installed on your system, install Chrome Headless Shell:</p>
<div class="code-block code-with-filename" role="group" aria-labelledby="code-filename-0">
  <div class="code-with-filename-label" id="code-filename-0"><span class="font-mono text-sm">Terminal</span></div><div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">quarto install chrome-headless-shell</span></span></code></pre></td></tr></table>
</div>
</div></div>
<p>Quarto will automatically detect and use any existing Chrome or Edge browser on your system. Chrome Headless Shell is the recommended fallback for environments where a full browser isn&rsquo;t available &mdash; CI servers, Docker containers, and headless VMs.</p>
<h2 id="migrating-from-chromium">Migrating from Chromium
</h2>
<p>If you previously installed Chromium via <code>quarto install chromium</code>, the migration is straightforward:</p>
<div class="code-block code-with-filename" role="group" aria-labelledby="code-filename-1">
  <div class="code-with-filename-label" id="code-filename-1"><span class="font-mono text-sm">Terminal</span></div><div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">quarto uninstall chromium
</span></span><span class="line"><span class="cl">quarto install chrome-headless-shell</span></span></code></pre></td></tr></table>
</div>
</div></div>
<p>Running <code>quarto check install</code> will warn you if a legacy Chromium installation is detected and suggest the migration.</p>
<h3 id="ci-and-automation">CI and automation
</h3>
<p>If your CI pipeline uses <code>quarto install chromium --no-prompt</code>, it will continue to work &mdash; the command still installs a working headless browser, but now shows a deprecation warning. Updating your scripts to <code>quarto install chrome-headless-shell --no-prompt</code> avoids the warning and uses the new tool directly. In Quarto 1.10, <code>quarto install chromium</code> will transparently redirect to Chrome Headless Shell, so either command will produce the same result.</p>
<h2 id="whats-next">What&rsquo;s next
</h2>
<p>The transition away from Puppeteer Chromium is happening gradually. In Quarto 1.9, <code>quarto install chromium</code> shows a deprecation warning and <code>quarto check install</code> flags any legacy Chromium installation. In the upcoming Quarto 1.10, <code>quarto install chromium</code> will transparently redirect to Chrome Headless Shell, and installing Chrome Headless Shell will auto-remove any legacy Chromium.</p>
<p>If you&rsquo;re still using the old Chromium install, now is a good time to switch.</p>
<p>Learn more on the 


  
  
  





<a href="https://quarto.org/docs/authoring/diagrams.html#chrome-install" target="_blank" rel="noopener">Chrome Install</a>
 documentation page.</p>
<p>The Chromium icon in the 






<a href="thumbnail.png">listing and social card image</a>
 for this post is by <a href="https://icon-icons.com/authors/602-jeremiah" class="external">Jeremiah</a> via <a href="https://icon-icons.com/icon/chromium/104592" class="external">icon-icons.com</a>. License: <a href="https://creativecommons.org/licenses/by/4.0/" class="external">CC BY 4.0</a></p>
]]></description>
      <enclosure url="https://opensource.posit.co/blog/2026-04-14_chrome-headless-shell/thumbnail.png" length="11265" type="image/png" />
    </item>
    <item>
      <title>RAG with raghilda TRIVIAL</title>
      <link>https://opensource.posit.co/blog/2026-04-14_rag-with-raghilda/</link>
      <pubDate>Tue, 14 Apr 2026 00:00:00 +0000</pubDate>
      <guid>https://opensource.posit.co/blog/2026-04-14_rag-with-raghilda/</guid>
      <dc:creator>Daniel Falbel</dc:creator>
      <dc:creator>Tomasz Kalinowski</dc:creator><description><![CDATA[<p>We&rsquo;re happy to introduce Raghilda, a new Python package for building RAG (Retrieval-Augmented Generation) solutions.</p>
<p>RAG is a simple concept that comes up anytime you want to <em>retrieve</em> content for an LLM to improve or <em>augment</em> the <em>generated</em> output.</p>
<p><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://opensource.posit.co/blog/2026-04-14_rag-with-raghilda/rag-diagram.svg"
      alt="Flowchart comparing two pipelines: Without RAG shows User Query flowing directly to LLM to Response. With RAG shows User Query flowing through a Retrieve Relevant Documents step before reaching the LLM."  title="Without RAG, the LLM generates a response using only the user query. With RAG, relevant documents are retrieved and provided to the LLM before it generates a response." 
      loading="lazy"
    ><figcaption class="text-sm text-center text-gray-500">Without RAG, the LLM generates a response using only the user query. With RAG, relevant documents are retrieved and provided to the LLM before it generates a response.</figcaption>
  </figure></div>
</p>
<p>LLMs are great at reasoning and generating text, but their knowledge is frozen at training time.
They can&rsquo;t access private documents, recent information, or anything that wasn&rsquo;t in the training
data. When asked about these topics, they either refuse to answer or — worse — hallucinate
a confident-sounding response. RAG solves this by giving the model access to relevant
information at query time, without needing to retrain it.</p>
<p>In practice, most tools built on top of LLMs already do this. ChatGPT uses web search to
include recent news in its answers. Claude Code reads the codebase using tools like <code>grep</code>,
<code>list_files</code>, and <code>symbol_search</code> before generating code. RAG is what makes LLMs useful for
tasks that require specific, up-to-date, or private knowledge.</p>
<p>Modern LLMs support context windows of 100K tokens or more, which might seem like it makes
RAG unnecessary — just paste everything into the prompt. But this doesn&rsquo;t work well in
practice. LLMs suffer from &ldquo;lost in the middle&rdquo; (sometimes called &ldquo;context rot&rdquo;): they pay less
attention to information buried in the middle of a very long prompt, so relevant content gets
missed. On top of that, sending your entire knowledge base with every query is expensive, slow,
and most real-world document collections are too large to fit in a single context window anyway.
Long context and RAG are complementary — a larger window lets you include more retrieved
chunks, but retrieval is what gives you precision: the model sees 5 relevant paragraphs instead
of 500 irrelevant pages.</p>
<h2 id="raghilda">raghilda
</h2>
<p>Building a good retrieval system is the hard part of RAG.
raghilda is a Python framework designed to handle it. You
give it URLs or file paths, and it takes care of the rest.
The defaults are opinionated but transparent — every step is
exposed and replaceable. You can swap the chunker, change the
embedding provider, or write a custom ingestion function
without fighting the framework.</p>
<ul>
<li>
<p><strong>Document processing.</strong> Converting HTML pages, PDFs, and DOCX files into clean text is
surprisingly messy. raghilda handles this automatically, converting documents to Markdown.
For websites, <code>find_links()</code> crawls and discovers pages so you don&rsquo;t have to list them
by hand.</p>
</li>
<li>
<p><strong>Smart chunking.</strong> Naive fixed-size chunking can split a code block or paragraph in half.
raghilda&rsquo;s Markdown chunker splits text at semantic boundaries — headings, paragraphs,
sentences — and preserves the heading hierarchy so each chunk retains context about where
it came from.</p>
</li>
<li>
<p><strong>Multiple storage backends.</strong> raghilda supports DuckDB (local, zero-config), ChromaDB,
and OpenAI Vector Stores. The API is the same across backends, so you can start with a
local DuckDB file and move to a hosted solution later without rewriting your code.</p>
</li>
<li>
<p><strong>Hybrid retrieval.</strong> Pure vector search finds semantically similar content but misses exact
keyword matches. raghilda combines semantic search, BM25 keyword matching, and attribute
filters — so you can search by meaning, by keywords, and by metadata (e.g. source URL,
document type, or any custom attribute) all at once.</p>
</li>
</ul>
<h2 id="how-it-works">How it works
</h2>
<p>A retrieval system has two phases: <strong>ingestion</strong> — turning
your documents into a searchable store — and <strong>retrieval</strong> —
finding the right chunks given a query. raghilda exposes both
phases clearly, with each step exposed as an
individual call you can customize or replace.</p>
<p><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://opensource.posit.co/blog/2026-04-14_rag-with-raghilda/pipeline-diagram.svg"
      alt="Flowchart showing two phases: Ingestion flows from URLs/Files through Read as Markdown, Chunk, Embed, to Store. Retrieval fans a Query out to Semantic Search, BM25 Search, and Attribute Filters, which merge into Relevant Chunks."  title="raghilda&#39;s two phases: ingestion prepares your documents for search, retrieval finds the relevant chunks at query time." 
      loading="lazy"
    ><figcaption class="text-sm text-center text-gray-500">raghilda&rsquo;s two phases: ingestion prepares your documents for search, retrieval finds the relevant chunks at query time.</figcaption>
  </figure></div>
</p>
<p>Let&rsquo;s walk through a minimal example using a Wikipedia article about Princess Ragnhild
of Norway.</p>
<p>First, you <strong>create a store</strong> with an embedding provider. The store is where your chunks and
their vector embeddings will live:</p>
<div class="code-block"><div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span><span class="lnt">7
</span><span class="lnt">8
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">raghilda.store</span> <span class="kn">import</span> <span class="n">DuckDBStore</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">raghilda.embedding</span> <span class="kn">import</span> <span class="n">EmbeddingOpenAI</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">store</span> <span class="o">=</span> <span class="n">DuckDBStore</span><span class="o">.</span><span class="n">create</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">location</span><span class="o">=</span><span class="s2">&#34;ragnhild.db&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">embed</span><span class="o">=</span><span class="n">EmbeddingOpenAI</span><span class="p">(),</span>
</span></span><span class="line"><span class="cl">    <span class="n">overwrite</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span></span></span></code></pre></td></tr></table>
</div>
</div></div>
<p>Then you <strong>read</strong> the document and <strong>chunk</strong> it. <code>read_as_markdown()</code> converts the URL
to Markdown, and <code>MarkdownChunker</code> splits it into overlapping chunks at semantic boundaries:</p>
<div class="code-block"><div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">raghilda.read</span> <span class="kn">import</span> <span class="n">read_as_markdown</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">raghilda.chunker</span> <span class="kn">import</span> <span class="n">MarkdownChunker</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">doc</span> <span class="o">=</span> <span class="n">read_as_markdown</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;https://en.wikipedia.org/wiki/Princess_Ragnhild,_Mrs._Lorentzen&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># We intentionally use a small chunk size for display purposes.</span>
</span></span><span class="line"><span class="cl"><span class="c1"># In practice, chunk sizes of ~1600 characters are a good</span>
</span></span><span class="line"><span class="cl"><span class="c1"># compromise on size versus retrieval quality.</span>
</span></span><span class="line"><span class="cl"><span class="n">chunker</span> <span class="o">=</span> <span class="n">MarkdownChunker</span><span class="p">(</span><span class="n">chunk_size</span><span class="o">=</span><span class="mi">200</span><span class="p">,</span> <span class="n">target_overlap</span><span class="o">=</span><span class="mf">0.25</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">chunked</span> <span class="o">=</span> <span class="n">chunker</span><span class="o">.</span><span class="n">chunk</span><span class="p">(</span><span class="n">doc</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;</span><span class="si">{</span><span class="nb">len</span><span class="p">(</span><span class="n">chunked</span><span class="o">.</span><span class="n">chunks</span><span class="p">)</span><span class="si">}</span><span class="s2"> chunks&#34;</span><span class="p">)</span></span></span></code></pre></td></tr></table>
</div>
</div></div>
<div class="code-block"><pre tabindex="0"><code>185 chunks</code></pre></div>
<p>Finally, you <strong>upsert</strong> the chunked document into the store
and build the search indexes. Embedding is handled by the
store itself — since the embedding provider is configured at
creation time, all chunks in a store are guaranteed to use
consistent embeddings:</p>
<div class="code-block"><div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">store</span><span class="o">.</span><span class="n">upsert</span><span class="p">(</span><span class="n">chunked</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">store</span><span class="o">.</span><span class="n">build_index</span><span class="p">()</span></span></span></code></pre></td></tr></table>
</div>
</div></div>
<p>During <strong>retrieval</strong>, you query the store and get back the most relevant chunks. raghilda
runs semantic search and BM25 keyword search, then merges
the results:</p>
<div class="code-block"><div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">chunks</span> <span class="o">=</span> <span class="n">store</span><span class="o">.</span><span class="n">retrieve</span><span class="p">(</span><span class="s2">&#34;Did she move to Brazil?&#34;</span><span class="p">,</span> <span class="n">top_k</span><span class="o">=</span><span class="mi">2</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">for</span> <span class="n">chunk</span> <span class="ow">in</span> <span class="n">chunks</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="nb">print</span><span class="p">(</span><span class="n">chunk</span><span class="o">.</span><span class="n">text</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="nb">print</span><span class="p">(</span><span class="s2">&#34;---&#34;</span><span class="p">)</span></span></span></code></pre></td></tr></table>
</div>
</div></div>
<div class="code-block"><pre tabindex="0"><code>Lorentzen&#34;), a member of the Lorentzen family of shipping magnates.
In the same year, they moved to Brazil, where her husband was an
industrialist and a main owner of Aracruz Celulose. She lived in
Brazil until her death 59 years later.
---
to Rio de Janeiro, where her husband had substantial business
holdings. Their residence in Brazil was originally temporary, but
they
---</code></pre></div>
<blockquote>
<p><strong>Tip:</strong> In practice, you&rsquo;ll usually work with many documents at
once — just loop over your URLs or file paths and call
<code>upsert()</code> for each one.
See the 






<a href="https://posit-dev.github.io/raghilda/user-guide/getting-started.html" target="_blank" rel="noopener">Getting Started</a>
 guide for
a full walkthrough.</p>
</blockquote>
<h2 id="using-with-an-llm">Using with an LLM
</h2>
<p>A retrieval system on its own just returns text chunks. To
get actual answers, you connect it to an LLM. The simplest
way to do this is to register a search function as a tool
that the LLM can call when it needs information.</p>
<p>Here&rsquo;s an example using







<a href="https://posit-dev.github.io/chatlas/" target="_blank" rel="noopener">chatlas</a>
:</p>
<div class="code-block"><div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">chatlas</span> <span class="kn">import</span> <span class="n">ChatOpenAI</span>
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">json</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">search_ragnhild</span><span class="p">(</span><span class="n">query</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;&#34;&#34;Search for information about Princess Ragnhild.&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="n">chunks</span> <span class="o">=</span> <span class="n">store</span><span class="o">.</span><span class="n">retrieve</span><span class="p">(</span><span class="n">query</span><span class="p">,</span> <span class="n">top_k</span><span class="o">=</span><span class="mi">10</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">json</span><span class="o">.</span><span class="n">dumps</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="p">[{</span><span class="s2">&#34;text&#34;</span><span class="p">:</span> <span class="n">c</span><span class="o">.</span><span class="n">text</span><span class="p">,</span> <span class="s2">&#34;context&#34;</span><span class="p">:</span> <span class="n">c</span><span class="o">.</span><span class="n">context</span><span class="p">}</span> <span class="k">for</span> <span class="n">c</span> <span class="ow">in</span> <span class="n">chunks</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">    <span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">chat</span> <span class="o">=</span> <span class="n">ChatOpenAI</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">model</span><span class="o">=</span><span class="s2">&#34;gpt-4.1-mini&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">system_prompt</span><span class="o">=</span><span class="s2">&#34;Answer questions about Princess Ragnhild &#34;</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;using the search tool. Always search before answering.&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">chat</span><span class="o">.</span><span class="n">register_tool</span><span class="p">(</span><span class="n">search_ragnhild</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">_</span> <span class="o">=</span> <span class="n">chat</span><span class="o">.</span><span class="n">chat</span><span class="p">(</span><span class="s2">&#34;Which year did she move to Brazil?&#34;</span><span class="p">,</span> <span class="n">echo</span><span class="o">=</span><span class="s2">&#34;text&#34;</span><span class="p">)</span></span></span></code></pre></td></tr></table>
</div>
</div></div>
<div class="code-block"><pre tabindex="0"><code>Princess Ragnhild moved to Brazil in the same year she got married,
1953. Her marriage and the move to Brazil were connected, as her
husband was an industrialist and owner of business holdings in Brazil.
They settled in Rio de Janeiro and lived there until her death in 2012.</code></pre></div>
<p>Compare this with the same question without the search tool:</p>
<div class="code-block"><div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span><span class="lnt">7
</span><span class="lnt">8
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">chat_no_rag</span> <span class="o">=</span> <span class="n">ChatOpenAI</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">model</span><span class="o">=</span><span class="s2">&#34;gpt-4.1-mini&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">system_prompt</span><span class="o">=</span><span class="s2">&#34;Answer questions about Princess Ragnhild.&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">_</span> <span class="o">=</span> <span class="n">chat_no_rag</span><span class="o">.</span><span class="n">chat</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;Which year did she move to Brazil?&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">echo</span><span class="o">=</span><span class="s2">&#34;text&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span></span></span></code></pre></td></tr></table>
</div>
</div></div>
<div class="code-block"><pre tabindex="0"><code>Princess Ragnhild moved to Brazil in 1960.</code></pre></div>
<p>With the search tool, the LLM retrieves the relevant chunks
and grounds its answer in the actual document. Without it,
the model has to rely on its training data — and may
hallucinate or give a vague response.</p>
<h2 id="learn-more">Learn more
</h2>
<ul>
<li>






<a href="https://posit-dev.github.io/raghilda/user-guide/getting-started.html" target="_blank" rel="noopener">Getting Started</a>
 — full
walkthrough building a store from a documentation site</li>
<li>






<a href="https://github.com/posit-dev/raghilda/tree/main/examples" target="_blank" rel="noopener">Examples</a>

— complete scripts showing RAG workflows with chatlas,
ChromaDB, and more</li>
<li>






<a href="https://github.com/posit-dev/raghilda" target="_blank" rel="noopener">GitHub repository</a>

— source code, issues, and contributions</li>
</ul>
]]></description>
      <enclosure url="https://opensource.posit.co/blog/2026-04-14_rag-with-raghilda/thumbnail-wd.jpg" length="113156" type="image/jpeg" />
    </item>
    <item>
      <title>Structuring Reproducible Research Projects in R: A Workflow with renv, Quarto, and GitHub</title>
      <link>https://opensource.posit.co/blog/2026-04-13_reproducible-research-renv-quarto-github/</link>
      <pubDate>Mon, 13 Apr 2026 00:00:00 +0000</pubDate>
      <guid>https://opensource.posit.co/blog/2026-04-13_reproducible-research-renv-quarto-github/</guid>
      <dc:creator>Dianyi Yang</dc:creator><description><![CDATA[<p>Increasingly, academic disciplines, including the social sciences, are adopting data-science tools and calling for greater transparency and reproducibility in research. Many leading journals now require authors to share the data and code necessary to replicate published findings as a condition of publication.</p>
<p>Yet preparing replication materials can be daunting, especially for researchers new to data science. It is not uncommon for scholars to struggle to reproduce even their own results, due to issues such as disorganized code and data, software version mismatches, missing random seeds, or differences in operating systems and platforms. While some of these challenges are complex, many reproducibility problems stem from preventable organizational issues. Structuring code and data in a clear, consistent, and tool-friendly manner can significantly reduce these difficulties &mdash; and brings additional benefits at virtually no cost, including smoother collaboration and easier integration with AI tools.</p>
<figure>
<img src="https://opensource.posit.co/blog/2026-04-13_reproducible-research-renv-quarto-github/img/1.png" data-fig-alt="Comic: on Friday evening a programmer says &#39;I&#39;ll finish this on Monday&#39;; on Monday morning they stare at their code asking &#39;What does this mean?!&#39;" alt="Meme from r/ProgrammerHumor" />
<figcaption aria-hidden="true">Meme from r/ProgrammerHumor</figcaption>
</figure>
<p>In this post, I&rsquo;ll walk through the project structure I use in my own research. It has evolved over time and reflects what I currently consider best practice for reproducible academic work. Adopting a clear structure from the outset makes it significantly easier to reproduce results, onboard collaborators, and maintain projects over the long term.</p>
<p>The structure covers the full research lifecycle: from data cleaning and analysis to manuscript preparation and presentation. The template is built around R and Quarto (the successor to R Markdown), but the underlying principles translate easily to Python and other publishing workflows, including LaTeX. For convenience, I&rsquo;ve created a ready-to-use template on 






<a href="https://github.com/kv9898/academic-project-template" target="_blank" rel="noopener">GitHub</a>
 that you can clone and adapt for your own projects.</p>
<p>The project structure looks like this:</p>
<figure>
<img src="https://opensource.posit.co/blog/2026-04-13_reproducible-research-renv-quarto-github/img/2.png" data-fig-alt="File explorer showing the project directory tree: folders for _extensions, .quarto, code, data_processed, data_raw, extras, manuscript, outputs, renv, and slides, plus config files like _quarto.yml, .gitattributes, .gitignore, .Rprofile, README.md, and renv.lock" alt="Project structure" />
<figcaption aria-hidden="true">Project structure</figcaption>
</figure>
<p>I&rsquo;ll now break down each component in turn, explaining the purpose of the key folders and files, and the reasoning behind the design.</p>
<h2 id="git-and-github">Git and GitHub
</h2>
<p>One major threat to reproducibility is a file named <code>latest_final_v3_definitive.R</code>. While this naming convention feels natural during exploratory analysis&mdash;especially under deadline pressure&mdash;it quickly becomes impossible to reconstruct what actually changed, when, and why. Future-you (and your collaborators) will not be grateful.</p>
<p>Version control systems like Git solve this problem by recording a structured history of changes through <em>commits</em>. Instead of creating new files for every iteration, you preserve a single evolving project with a transparent timeline. This makes it easy to revisit earlier versions, understand how results evolved, and collaborate without overwriting each other&rsquo;s work.</p>
<p>GitHub builds on this by providing a shared platform for hosting repositories, reviewing changes, managing issues, and coordinating collaboration. It also makes it very clear who introduced a particular change&mdash;an accountability feature that tends to concentrate the mind when committing code.</p>
<p>In the example project structure, several files and folders are Git-related:</p>
<pre><code>.git/ (invisible folder created by Git)
.gitignore
*.gitattributes
*/
└── .gitignore
</code></pre>
<p>The <code>.git/</code> folder is created automatically when you initialize a repository. It contains the full version history and metadata of the project. You generally do not need to interact with it directly&mdash;just avoid modifying or deleting it.</p>
<p>The <code>.gitignore</code> file specifies which files and folders should be tracked or ignored by Git. Since all changes to tracked files are recorded in the project history, a useful rule of thumb is to track only what is necessary to reproduce your results. Files that can be regenerated from code&mdash;such as intermediate data, plots, tables, or compiled PDFs&mdash;are often better ignored to keep the repository clean and lightweight.</p>
<p>The <code>.gitattributes</code> file defines additional rules for how Git should handle specific file types. Compared with <code>.gitignore</code>, it appears less often in many repositories, but it is especially useful when you need file-type-specific behavior. In particular, large raw data files (e.g., large .csv files) and binary formats (such as .rds, .RData, or images) can significantly increase repository size if tracked directly. In these cases, Git Large File Storage (Git LFS) can be used to store the actual files separately, while keeping lightweight pointer files in the main repository history.</p>
<h2 id="data-directories">Data directories
</h2>
<p>I use two separate folders to store raw (<code>data_raw/</code>) and cleaned data (<code>data_processed/</code>). Keeping these distinct makes the workflow more transparent: the original data remains untouched, while processed data can be saved in a format that is fast to load during analysis.</p>
<p>This structure also aligns with common journal expectations that replication code should be able to reproduce results starting from the raw data. By preserving raw inputs and separating preprocessing steps, you make the transformation pipeline explicit rather than implicit.</p>
<pre><code>data_raw/
└── *.csv (tracked through Git LFS)

data_processed/
├── *.rds (not tracked in Git)
└── .gitignore
</code></pre>
<p>In the template, the example raw data file (<code>data_raw/Brexit.csv</code>) is tracked using Git LFS. While the file itself is neither large nor binary (unlike formats such as <code>.dta</code> or <code>.rds</code>), LFS is used here for demonstration purposes. In real-world research projects, raw datasets are often substantial in size, and setting up LFS early helps avoid repository bloat later on.</p>
<p>The <code>data_processed/</code> folder contains the cleaned dataset in <code>.rds</code> format, along with a <code>.gitignore</code> file that excludes all files in this folder (except the <code>.gitignore</code> file itself) from version control. This design encourages regeneration of cleaned data from raw inputs via code, rather than relying on previously saved intermediate objects. It also helps keep the repository lightweight.</p>
<p>The <code>.rds</code> format is used for several reasons. First, it is a native R format that preserves data types and attributes faithfully. Second, it encourages the use of RDS over <code>.RData</code>. Unlike <code>.RData</code>, which loads all stored objects into the global environment under their <em>original</em> names, <code>.rds</code> files require explicit assignment when loaded. This reduces the risk of unintentionally overwriting existing objects and promotes more transparent workflows.</p>
<p>Finally, the <code>.gitignore</code> file inside <code>data_processed/</code> serves an additional purpose: it ensures that the folder itself is tracked by Git. Since Git does not record empty directories, this placeholder guarantees that the directory exists when the project is cloned. This is important because saving cleaned data to a non-existent directory will otherwise result in an error in R.</p>
<p>For researchers working with <strong>confidential</strong> or <strong>restricted</strong> data, it is often helpful to separate sensitive materials from those that can be shared publicly. In some cases, this may mean maintaining a private repository during the research phase and preparing a separate public-facing repository upon publication.</p>
<p>Importantly, simply deleting confidential files before making a repository public is not sufficient. Git preserves the full commit history, meaning sensitive data may still be accessible in earlier revisions. Creating a fresh repository that contains only the materials intended for public release&mdash;such as replication code and non-sensitive data&mdash;helps prevent unintended data leakage.</p>
<p>This approach also makes it easier to curate a clean, well-documented version of the project specifically designed for replication and reuse.</p>
<h2 id="code-organization-and-outputs">Code organization and outputs
</h2>
<pre><code>code/
├── 1_data_cleaning.R
├── 2_descriptive.R
└── 3_main_analysis.R

outputs/
├── *.rds (not tracked in Git)
└── .gitignore
</code></pre>
<p>The <code>code/</code> folder contains all scripts related to data processing and analysis. In the template, these are written in R, but the same structure works equally well for Python scripts (<code>.py</code>), notebooks (<code>.ipynb</code>), or other programming languages.</p>
<p>A key principle is to prefix scripts with numbers and descriptive names that reflect the research workflow: data cleaning, descriptive analysis, and then inferential analysis. If the logical order of steps is unclear, following the sequence in which results appear in the manuscript is often a reliable guide. This makes the analytical pipeline explicit and allows others (and future-you) to reproduce results step-by-step.</p>
<p>Each script should include a header section with basic metadata about its role in the project. This may include the paper title, authors, purpose of the script, input files, and output files. Making inputs and outputs explicit helps clarify dependencies and encourages scripts that transform data rather than rely on objects lingering in the global environment.</p>
<p>Below is an example of an analysis script (<code>code/3_main_analysis.R</code>) I used in the template that follows these principles:</p>
<div class="code-block"><div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span><span class="lnt">20
</span><span class="lnt">21
</span><span class="lnt">22
</span><span class="lnt">23
</span><span class="lnt">24
</span><span class="lnt">25
</span><span class="lnt">26
</span><span class="lnt">27
</span><span class="lnt">28
</span><span class="lnt">29
</span><span class="lnt">30
</span><span class="lnt">31
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="c1">######## INFO ########</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># PROJECT</span>
</span></span><span class="line"><span class="cl"><span class="c1">## Paper: YOUR PAPER TITLE</span>
</span></span><span class="line"><span class="cl"><span class="c1">## Authors: YOUR NAME</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># R Script</span>
</span></span><span class="line"><span class="cl"><span class="c1">## Purpose: This script performs linear regression analysis.</span>
</span></span><span class="line"><span class="cl"><span class="c1">## Inputs: data_processed/Brexit.rds</span>
</span></span><span class="line"><span class="cl"><span class="c1">## Outputs: outputs/regression_table.rds</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Setup ----</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">tidyverse</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">here</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">modelsummary</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">i_am</span><span class="p">(</span><span class="s">&#34;code/3_main_analysis.R&#34;</span><span class="p">)</span> <span class="c1"># helps with relative paths</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Read in the cleaned data ----</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">brexit_data</span> <span class="o">&lt;-</span> <span class="nf">read_rds</span><span class="p">(</span><span class="nf">here</span><span class="p">(</span><span class="s">&#34;data_processed/Brexit.rds&#34;</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Main analysis ----</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">model</span> <span class="o">&lt;-</span> <span class="nf">lm</span><span class="p">(</span><span class="n">leave</span> <span class="o">~</span> <span class="n">turnout</span> <span class="o">+</span> <span class="n">income</span> <span class="o">+</span> <span class="n">noqual</span><span class="p">,</span> <span class="n">data</span> <span class="o">=</span> <span class="n">brexit_data</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Output the model summary ----</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">reg_table</span> <span class="o">&lt;-</span> <span class="nf">modelsummary</span><span class="p">(</span><span class="n">model</span><span class="p">,</span> <span class="n">stars</span> <span class="o">=</span> <span class="kc">TRUE</span><span class="p">,</span> <span class="n">output</span> <span class="o">=</span> <span class="s">&#34;latex&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nf">write_rds</span><span class="p">(</span><span class="n">reg_table</span><span class="p">,</span> <span class="nf">here</span><span class="p">(</span><span class="s">&#34;outputs/regression_table.rds&#34;</span><span class="p">))</span></span></span></code></pre></td></tr></table>
</div>
</div></div>
<p>Two additional tips are worth noting. First, the <code># SECTION ----</code> syntax creates collapsible sections and structured outlines in RStudio and the Positron IDE. This makes longer scripts significantly easier to navigate and encourages more intentional organization of code.</p>
<figure>
<img src="https://opensource.posit.co/blog/2026-04-13_reproducible-research-renv-quarto-github/img/3-pos.png" data-fig-alt="Positron IDE showing collapsible code sections created with the &#39;# Section ----&#39; syntax" alt="Collapsible sections in Positron" />
<figcaption aria-hidden="true">Collapsible sections in Positron</figcaption>
</figure>
<figure>
<img src="https://opensource.posit.co/blog/2026-04-13_reproducible-research-renv-quarto-github/img/3-rstudio.png" data-fig-alt="RStudio IDE showing collapsible code sections created with the &#39;# Section ----&#39; syntax" alt="Collapsible sections in RStudio" />
<figcaption aria-hidden="true">Collapsible sections in RStudio</figcaption>
</figure>
<p>Second, managing working directories is a seemingly basic task that is frequently mishandled&mdash;especially in collaborative projects. In RStudio, the recommended approach is to work within an <code>.Rproj</code> file, which defines a project root and ensures that relative paths behave consistently across machines. However, in many academic settings (including political science), this practice is not systematically taught. As a result, it is still common to see replication files from top journals that begin with something like <code>setwd(&quot;~/Path/To/Project&quot;)</code>, often commented out with the expectation that collaborators will manually adjust it.</p>
<p>This approach is fragile. It assumes a specific directory structure on every machine and introduces hidden dependencies on local file paths. Code that depends on <code>setwd()</code> is difficult to port, share, or automate.</p>
<p>Positron improves this situation by automatically setting the working directory to the folder opened in the IDE, encouraging a project-level workflow by default. However, many users carry over the habit of opening individual scripts rather than entire project directories.</p>
<p>The <code>here</code> package provides a robust solution that avoids reliance on the working directory altogether. By anchoring a script to the project root using <code>here::i_am()</code> and constructing paths with <code>here()</code>, file references become explicit and portable. This ensures that scripts run consistently across machines, IDEs, and collaboration environments&mdash;regardless of local directory structures.</p>
<p>The <code>outputs/</code> folder complements this approach by providing a dedicated location for all results generated by the code. This includes intermediate objects (e.g., fitted models) and final products (e.g., tables and figures). If intermediate artifacts become numerous, they can be stored in a separate <code>objects/</code> folder. The accompanying <code>.gitignore</code> file ensures that these generated files are not versioned, reinforcing the principle that results should be regenerated from code rather than preserved as static artifacts.</p>
<h2 id="virtual-environments-and-dependencies-renv">Virtual environments and dependencies (<code>renv</code>)
</h2>
<p>Many R users do not update their R or package versions regularly. In practice, the version in use is often determined by when the researcher first learned R&mdash;or when the machine was purchased, whichever is later. Deprecation warnings are politely ignored as long as the code continues to run.</p>
<p>The problem only becomes visible when code that worked perfectly on your old machine suddenly fails on a new machine&mdash;or worse, on a collaborator&rsquo;s machine. At that point, dependency management stops being an abstract concern and becomes a very practical one.</p>
<p>A common solution to this problem is the use of <strong>virtual environments</strong>, which capture the exact package versions used in a project&mdash;a concept long established in the Python ecosystem. In R, the <code>renv</code> package provides a convenient way to create and manage project-specific libraries. Package versions remain fixed within the project, independent of updates to the global R installation or differences across collaborators&rsquo; machines.</p>
<p>The state of the environment is recorded in the <code>renv.lock</code> file, which is committed to Git. This file serves as a snapshot of the project&rsquo;s dependency graph at a given point in time, including package versions and their sources. As a result, the same software environment can be reproduced on any machine with a simple call to <code>renv::restore()</code>.</p>
<p>Three key components related to <code>renv</code> appear in the template structure:</p>
<pre><code>renv/
.Rprofile
renv.lock
</code></pre>
<p>The <code>renv/</code> folder contains the project-specific package library. Most of its contents are not tracked in Git, since the environment can be regenerated from the information stored in <code>renv.lock</code>. Additionally, some packages include compiled C or C++ code that is platform-specific, meaning those installed binaries are not portable across operating systems.</p>
<p>The <code>.Rprofile</code> file includes a line that automatically activates the <code>renv</code> environment when the project is loaded, ensuring that the correct package versions are used without manual setup.</p>
<p>Automatic activation, however, only works when the project is <strong>opened as a whole</strong>&mdash;for example, by opening the <code>.Rproj</code> file or the project folder in Positron. If individual scripts are opened in isolation, the working directory will not be set to the project root at startup, and the project-level <code>.Rprofile</code> will not be executed. This is yet another reason to adopt a project-level workflow rather than treating scripts as standalone files.</p>
<h2 id="quarto-manuscripts-and-presentations">Quarto: manuscripts and presentations
</h2>
<p>The most visible stage of the research lifecycle is the dissemination of results&mdash;through manuscripts, presentations, and other public outputs. For quantitatively oriented researchers (which, if you have read this far, likely includes you), producing well-formatted documents that do justice to your carefully constructed tables and figures is essential.</p>
<p>Quarto is an open-source scientific and technical publishing system built on Pandoc. I use it for all of my manuscript writing and presentation slides, and I recommend it for researchers working in R, Python, or Julia. The template presented here is deliberately centered around Quarto, and in what follows I will briefly explain the reasoning behind that choice.</p>
<h3 id="why-quarto">Why Quarto?
</h3>
<p>What makes Quarto stand out is its ability to run R, Python, or Julia code directly within a document, seamlessly integrating analysis and writing. Tables, figures, and results can therefore be generated and updated automatically as the underlying code changes, ensuring that the manuscript always reflects the current state of the analysis.</p>
<p>Quarto is not a replacement for output formats such as HTML, Microsoft Word, LaTeX, or Typst. Rather, it acts as a <em>unifying</em> layer that can render the same <code>.qmd</code> source file into multiple formats simultaneously. This flexibility allows format decisions to be postponed and adapted to collaborators, institutions, or journal requirements.</p>
<p>Beyond manuscripts, Quarto also supports presentation formats and full websites. Learning a single tool therefore enables the production of academic papers, conference slides, and project or personal websites within a consistent workflow.</p>
<p>While these capabilities may sound familiar to experienced R Markdown users, I would still argue that Quarto is worth trying&mdash;even for those already comfortable with R Markdown&mdash;for four main reasons:</p>
<ol>
<li>
<p><strong>Language-agnostic support</strong>: Quarto is designed to work seamlessly with multiple programming languages (R, Python, Julia). A document can be executed using the native engine for each language&mdash;for example, a Python-only document runs through a Jupyter kernel without requiring R<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup>, which is more friendly to non-R users.</p>
</li>
<li>
<p><strong>Native support for extended features</strong>: Quarto includes built-in support for cross-referencing, citations, and advanced formatting without requiring additional packages or complex configurations. In contrast, R Markdown often relies on extensions such as <code>bookdown</code> to achieve similar functionality, which introduces additional dependencies. In practice, many students are taught only the basic R Markdown setup and may not be aware of these extensions. Quarto provides these features out of the box.</p>
</li>
<li>
<p><strong>More scannable code cell/chunk options and syntax</strong>: R Markdown users may be familiar with setting document-wide execution options inside a setup chunk using <code>knitr::opts_chunk$set(...)</code>, and specifying chunk options inline in a comma-separated format. While functional, this approach can become difficult to scan and maintain in larger documents.</p>
<div class="code-block"><div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span><span class="lnt">7
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-markdown" data-lang="markdown"><span class="line"><span class="cl">``<span class="sb">`{r}
</span></span></span><span class="line"><span class="cl"><span class="sb">knitr::opts_chunk$set(echo = FALSE, message = FALSE, warning = FALSE, error = FALSE)
</span></span></span><span class="line"><span class="cl"><span class="sb">`</span>`<span class="sb">`
</span></span></span><span class="line"><span class="cl"><span class="sb">
</span></span></span><span class="line"><span class="cl"><span class="sb">`</span>`<span class="sb">`{r barplot, fig.cap=&#39;Bar plot for y by x.&#39;, fig.height=3, fig.width=5}
</span></span></span><span class="line"><span class="cl"><span class="sb"># code for the bar plot
</span></span></span><span class="line"><span class="cl"><span class="sb">`</span>``</span></span></code></pre></td></tr></table>
</div>
</div></div>
<p>In Quarto, document-level execution options are defined declaratively in YAML, while cell-level options use a multi-line, command-style syntax. This makes both levels of configuration easier to scan, and typically easier to review in diffs and modify.</p>
<div class="code-block"><div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-markdown" data-lang="markdown"><span class="line"><span class="cl"><span class="s">```yaml
</span></span></span><span class="line"><span class="cl"><span class="nt">execute</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">echo</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">warning</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">error</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">message</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="s">```</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">``<span class="sb">`{r}
</span></span></span><span class="line"><span class="cl"><span class="sb">#| label: fig-barplot
</span></span></span><span class="line"><span class="cl"><span class="sb">#| fig-cap: &#39;Bar plot for y by x.&#39;
</span></span></span><span class="line"><span class="cl"><span class="sb">#| fig-height: 3
</span></span></span><span class="line"><span class="cl"><span class="sb">#| fig-width: 5
</span></span></span><span class="line"><span class="cl"><span class="sb"># code for the bar plot
</span></span></span><span class="line"><span class="cl"><span class="sb">`</span>``</span></span></code></pre></td></tr></table>
</div>
</div></div>
</li>
<li>
<p><strong>Centralized documentation</strong>: While R Markdown benefits from a large community and extensive online resources, its documentation is distributed across multiple sites<sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup>. Quarto, by contrast, maintains a single, comprehensive 






<a href="https://quarto.org" target="_blank" rel="noopener">documentation portal</a>
 that covers core usage and advanced features in one place, making it easier to navigate and learn systematically.</p>
</li>
</ol>
<h3 id="quarto-project-structure">Quarto project structure
</h3>
<p>The remaining components of the template are primarily related to Quarto. Below, I break down the key elements and explain the reasoning behind their organization.</p>
<pre><code>_extensions
.quarto (created by Quarto; not tracked in Git)
extras/
manuscript/
├── manuscript.pdf (not tracked in Git)
└── manuscript.qmd
slides/
├── slides.pdf (not tracked in Git)
└── slides.qmd
_quarto.yml
</code></pre>
<h3 id="configuration-file">Configuration file
</h3>
<p>The <code>_quarto.yml</code> file serves two main purposes.</p>
<p>First, it defines project-level execution behavior. In particular, the following setting ensures that code is executed relative to the project root, regardless of where individual .qmd files are located:</p>
<div class="code-block"><div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">project</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">execute-dir</span><span class="p">:</span><span class="w"> </span><span class="l">project</span></span></span></code></pre></td></tr></table>
</div>
</div></div>
<p>This provides an additional layer of protection for resolving relative paths correctly. Used together with the here package, it helps ensure that file paths behave consistently across different machines and execution contexts.</p>
<p>Second, <code>_quarto.yml</code> centralizes shared configuration options so they do not need to be repeated in each individual <code>.qmd</code> file. This reduces duplication, minimizes the risk of inconsistencies, and improves readability across documents.</p>
<p>In the template, shared options include the bibliography file and citation style:</p>
<div class="code-block"><div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">bibliography</span><span class="p">:</span><span class="w"> </span><span class="l">extras/references.bib</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">csl</span><span class="p">:</span><span class="w"> </span><span class="l">extras/apa.csl</span></span></span></code></pre></td></tr></table>
</div>
</div></div>
<p>Because these settings are defined at the project level, they apply automatically to both the manuscript and presentation slides.</p>
<h3 id="extras-bibliography-and-citation-styles">Extras: bibliography and citation styles
</h3>
<p>As mentioned above, the <code>extras/</code> folder contains the bibliography file (<code>references.bib</code>) and citation style file (<code>apa.csl</code>). These are referenced in the <code>_quarto.yml</code> configuration file, which means they are automatically available to all <code>.qmd</code> files in the project without needing to specify them individually.</p>
<pre><code>extras/
├── references.bib
└── apa.csl
</code></pre>
<p>Ideally, if you have other supplementary materials that are not part of the core code or data but are still relevant to the project (e.g., codebooks), they could also be stored in this folder. However, I have kept it focused on bibliography-related files for simplicity.</p>
<h4 id="bibliography-file">Bibliography file
</h4>
<p>The <code>references.bib</code> file is a standard BibTeX/BibLaTeX bibliography file familiar to LaTeX users. It contains structured reference entries, including fields such as author, title, journal, year, and other publication metadata.</p>
<p>Entries can be exported from reference managers such as Zotero or Mendeley, or generated directly within Quarto using the Visual Editor. Crucially, the bibliography file is kept separate from both the manuscript and the citation style. This separation allows the same reference database to be shared across manuscripts and slides, while making it easy to change formatting styles without modifying the source content.</p>
<h4 id="citation-style-file">Citation style file
</h4>
<p>The <code>apa.csl</code> file is a Citation Style Language (CSL) file that defines how citations and bibliography entries are formatted. It acts as a translation layer between the structured data in references.bib and the rendered output format.</p>
<p>In this template, the CSL file specifies APA style, which is common in the social sciences. However, switching styles is straightforward: replace the <code>apa.csl</code> file with another CSL file (e.g., Chicago, MLA, or a journal-specific style) and update the reference in <code>_quarto.yml</code>. No changes to the manuscript text are required.</p>
<p>To find a CSL file for a specific discipline or journal, you can browse the 






<a href="https://github.com/citation-style-language/styles" target="_blank" rel="noopener">CSL Style Repository</a>

, which contains thousands of maintained styles.</p>
<h3 id="manuscript">Manuscript
</h3>
<p>Dissemination of research findings is the ultimate goal of the research process, and the manuscript remains its primary vehicle.</p>
<pre><code>manuscript/
├── manuscript.pdf (not tracked in Git)
└── manuscript.qmd
_extensions/
└── kv9898/
    └── orcid/
</code></pre>
<p>The <code>manuscript/</code> folder contains the Quarto source file (<code>manuscript.qmd</code>) and the compiled PDF output (<code>manuscript.pdf</code>). As with other generated artifacts, the PDF is excluded from version control because it can always be regenerated from the source document.</p>
<p>The example <code>manuscript.qmd</code> provides a minimal template illustrating a typical academic structure, including numbered sections, figures, tables, cross-references, and citations. The template is intentionally simple but can be extended with additional formatting and structural elements as needed.</p>
<p>While Quarto&rsquo;s built-in PDF format supports core elements such as title, authors, date, and abstract, more specialized academic requirements&mdash;such as detailed affiliation formatting, ORCID display, keywords, custom headers, or journal-style front matter&mdash;often require additional customization.</p>
<figure>
<img src="https://opensource.posit.co/blog/2026-04-13_reproducible-research-renv-quarto-github/img/4.png" data-fig-alt="PDF output from the default Quarto template showing title, author, and abstract" alt="Default Quarto PDF template" />
<figcaption aria-hidden="true">Default Quarto PDF template</figcaption>
</figure>
<p>To address this, I created a 






<a href="https://github.com/kv9898/orcid" target="_blank" rel="noopener">custom extension</a>
 that builds on the default PDF format and adds features commonly required in academic manuscripts. The extension is included in the template under <code>_extensions/kv9898/orcid/</code>.</p>
<p>Packaging the manuscript format as a Quarto extension ensures that formatting logic is versioned and shared alongside the project, rather than maintained as ad hoc local tweaks. The extension is activated via the YAML front matter of manuscript.qmd:</p>
<div class="code-block"><div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">format</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="l">orcid-pdf:</span></span></span></code></pre></td></tr></table>
</div>
</div></div>
<p>The resulting PDF output more closely resembles a conventional academic paper, with structured author information, affiliations, ORCID identifiers, and keywords clearly presented. Because the template is implemented as a Quarto extension, it remains portable and reusable across projects, and can be further modified, particularly by those comfortable with LaTeX, to accommodate journal-specific formatting requirements.</p>
<figure>
<img src="https://opensource.posit.co/blog/2026-04-13_reproducible-research-renv-quarto-github/img/5.png" data-fig-alt="PDF output from the custom orcid extension template with structured affiliations, ORCID identifiers, and keywords" alt="Custom Quarto PDF template" />
<figcaption aria-hidden="true">Custom Quarto PDF template</figcaption>
</figure>
<h3 id="slides">Slides
</h3>
<p>Researchers often need to present their findings at conferences, seminars, or in teaching settings. Quarto supports multiple presentation formats, including Reveal.js, Beamer, and PowerPoint. In this template, I use <em>Beamer</em> to produce PDF slides, which are widely accepted in academic contexts and easy to share or print.</p>
<pre><code>slides/
├── slides.pdf (not tracked in Git)
└── slides.qmd
</code></pre>
<p>The <code>slides/</code> folder contains the Quarto source file (<code>slides.qmd</code>) and the compiled PDF output (<code>slides.pdf</code>). As with the manuscript, the PDF is treated as a generated artifact and excluded from version control.</p>
<p>The resulting slides are indistinguishable from those produced using a traditional LaTeX Beamer workflow. The key difference is that all content&mdash;including code, tables, and figures&mdash;can be generated directly from the same analytical pipeline.</p>
<figure>
<img src="https://opensource.posit.co/blog/2026-04-13_reproducible-research-renv-quarto-github/img/6.png" data-fig-alt="Beamer presentation slides generated from Quarto showing a title slide and a content slide with a table" alt="Quarto Beamer template" />
<figcaption aria-hidden="true">Quarto Beamer template</figcaption>
</figure>
<p>In this example, I deliberately reuse the same output objects (e.g., tables and figures) in both the manuscript and the slides. This guarantees consistency across formats and eliminates the risk of discrepancies between what appears in the paper and what is presented publicly.</p>
<p>The example slides also demonstrate practical considerations such as resizing tables and figures to fit slide layouts. They reference the same shared bibliography file used in the manuscript, ensuring consistent citation formatting across outputs. Although only a single reference is included in the example, the template is configured to allow references to span multiple frames, accommodating a realistic bibliography.</p>
<h2 id="readme-file">README file
</h2>
<pre><code>README.md
</code></pre>
<p>Following both GitHub conventions and academic best practice, every project should include a <code>README.md</code> file. This file serves as the primary entry point for others who wish to understand, reproduce, or build upon the research.</p>
<p>In the template, the README provides:</p>
<ul>
<li>A brief project description</li>
<li>Instructions for setting up the environment</li>
<li>Steps to reproduce the analysis, manuscript, and slides</li>
</ul>
<p>Additional placeholders are included for information such as the machine model and operating system used during development. While not always necessary, this metadata can be helpful when troubleshooting platform-specific issues, particularly for projects involving compiled dependencies. The README also notes the approximate time required to run the full analysis and render outputs, which helps set realistic expectations for replication.</p>
<p>Notably, the template does <em>not</em> include a <strong>LICENSE</strong> file by default. This is intentional. The appropriate license for academic code and data depends on disciplinary norms, institutional policies, journal requirements, and the researcher&rsquo;s intended level of openness. Common choices include MIT or GPL licenses for code, and Creative Commons licenses for data. In some cases, more restrictive or custom licenses may be appropriate. Researchers should select a license deliberately, ensuring it aligns with their sharing goals and complies with relevant policies.</p>
<h2 id="github-as-infrastructure--not-just-hosting">GitHub as infrastructure &mdash; not just hosting
</h2>
<p>Once a project is structured clearly and pushed to GitHub, it becomes more than a collection of files. It becomes <em>infrastructure</em>.</p>
<p>A well-organized repository makes <strong>collaboration</strong> dramatically smoother. Issues can serve as lightweight meeting minutes, evolving naturally into task lists. They can be assigned to specific contributors, grouped into milestones, and tracked over time. Pull requests and branching strategies help keep the main branch stable while allowing experimentation and iterative refinement. Code reviews become part of the workflow rather than an afterthought.</p>
<p>These practices, borrowed from software development, translate surprisingly well into academic collaboration. Instead of emailing attachments back and forth, collaborators work against a shared, versioned source of truth.</p>
<p>A clear project structure also makes modern AI tools significantly more useful. When your data, scripts, outputs, and manuscripts are logically organized, AI assistants in VS Code, Positron, or GitHub can reason about your project more effectively. They can trace how tables were generated, suggest improvements to analysis code, help refine writing based on the underlying results, or flag inconsistencies between figures and text. In other words, organization enables <em>context</em> &mdash; and context is what makes AI assistance meaningful rather than superficial.</p>
<p>There are also practical benefits. Once your work is version-controlled and backed up remotely, you no longer fear data loss due to a failed hard drive, a stolen laptop, or accidental overwrites. The repository itself becomes a durable record of the project&rsquo;s evolution.</p>
<p>Perhaps most importantly, a well-structured project reduces the <em>asymmetry of knowledge</em> among collaborators. Instead of each co-author being familiar with only one portion of the workflow, everyone can develop a <em>holistic understanding</em> of how the project fits together &mdash; from raw data to final manuscript. This makes feedback more constructive, collaboration more efficient, and the research process more transparent.</p>
<p>Reproducibility, then, is not merely about satisfying journal requirements. It is about building research projects that are resilient, collaborative, and adaptable &mdash; projects that scale not only across machines, but across people.</p>
<h2 id="conclusion">Conclusion
</h2>
<p>At its core, none of the tools discussed here&mdash;Git, renv, Quarto, or GitHub&mdash;are revolutionary on their own. What matters is how they are combined into a coherent project structure. Once that structure becomes habitual, reproducibility stops being an afterthought and becomes the default.</p>
<p>Adopting this workflow does not require perfect foresight or advanced technical expertise. It simply requires <em>deciding</em>, from the outset, that clarity, versioning, and regeneration will guide the project. The payoff is substantial: <strong>fewer replication headaches, smoother collaboration, better integration with modern tooling, and greater confidence in the durability of your work</strong>.</p>
<p>In the long run, a well-structured project is not just easier to reproduce&mdash;it is easier to think with.</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>When R and Python are combined within the same document, however, Quarto uses reticulate under the hood, similar to R Markdown.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:2">
<p>For example, see 






<a href="https://rmarkdown.rstudio.com/" target="_blank" rel="noopener">R Markdown official documentation</a>
 for the core features, and 






<a href="https://yihui.org/rmarkdown/" target="_blank" rel="noopener">Yihui&rsquo;s personal site</a>
 for more advanced features.&#160;<a href="#fnref:2" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]></description>
      <enclosure url="https://opensource.posit.co/blog/2026-04-13_reproducible-research-renv-quarto-github/img/3-pos.png" length="142508" type="image/png" />
    </item>
    <item>
      <title>April Release Highlights</title>
      <link>https://opensource.posit.co/blog/2026-04-07_april-newsletter/</link>
      <pubDate>Tue, 07 Apr 2026 00:00:00 +0000</pubDate>
      <guid>https://opensource.posit.co/blog/2026-04-07_april-newsletter/</guid>
      <dc:creator>Cindy Tong</dc:creator><description><![CDATA[<div class="callout callout-tip" role="note" aria-label="Tip">
<div class="callout-header">
<span class="callout-title">Tip</span>
</div>
<div class="callout-body">
<p>






<a href="https://posit.co/positron-updates-signup/" target="_blank" rel="noopener">Subscribe</a>
 to get this newsletter directly in your email inbox.</p>
</div>
</div>
<p>Welcome to the first edition of our Positron newsletter! Here, we will share highlights from our latest release, tips on how to be more productive with Positron, and useful resources.</p>
<p>We just returned from an in-person onsite in beautiful Monterey, California. During the trip, we got a chance to meet (some of us for the first time), touch grass and sand, and brainstorm ways we can improve to build better products for you.</p>
<figure>
<img src="https://opensource.posit.co/blog/2026-04-07_april-newsletter/images/monterey.png" alt="A view from Point Lobos State Natural Reserve" />
<figcaption aria-hidden="true">A view from Point Lobos State Natural Reserve</figcaption>
</figure>
<p>Let&rsquo;s get into the updates.</p>
<h2 id="key-product-updates">Key Product Updates
</h2>
<p>The April 2026 release of Positron brings significant improvements across:</p>
<ul>
<li>


  
  
  





<a href="#positron-server-for-academic-use-via-jupyterhub">Positron Server for Academic Use</a>
 via JupyterHub</li>
<li>


  
  
  





<a href="#ai-next-steps-in-the-native-jupyter-notebook-editor">AI enhancements</a>
: Next Steps in Jupyter Notebooks, Agent Skills, and Azure AI Foundry Support</li>
<li>


  
  
  





<a href="#telemetry-update-anonymous-session-identifiers">Telemetry updates</a>
</li>
<li>


  
  
  





<a href="#rstudio-addins-support">R improvements</a>
: Addins, Debugging, and more</li>
<li>


  
  
  





<a href="#data-explorer-faster-with-multiple-dataframes">Data Explorer Performance Improvement</a>
</li>
<li>


  
  
  





<a href="#windows-arm-is-generally-available">Windows ARM in GA</a>
</li>
<li>


  
  
  





<a href="#whats-coming-next">What&rsquo;s Coming Next</a>
: Inline Outputs, Packages Pane, and Posit Assistant</li>
</ul>
<p>Here&rsquo;s a look at the key features that shipped with the April 2026 release.</p>
<h3 id="positron-server-for-academic-use-via-jupyterhub">Positron Server for Academic Use via JupyterHub
</h3>
<p><strong>What we built:</strong> Academic institutions can now offer Positron Server to their students at no cost through JupyterHub (





  


  
  

<a href="https://opensource.posit.co/blog/2026-04-06_positron-server-jupyterhub">blog post</a>
). If your institution already runs JupyterHub, you can add Positron as a launcher option alongside JupyterLab, with no additional infrastructure required. Students simply log in and select Positron from the launcher, getting the full Positron experience including rich Python and R support, the extension marketplace, and (optionally) Positron Assistant.</p>
<p><strong>Why this matters:</strong> This removes the barrier for students and educators who want to use Positron in a classroom setting. No local installs, no configuration headaches &mdash; just a familiar JupyterHub login with Positron ready to go.</p>
<p><strong>Get started:</strong> 






<a href="https://github.com/posit-dev/positron/blob/main/LICENSE.txt" target="_blank" rel="noopener">Review the eligibility criteria</a>
 and send an email to 






<a href="mailto:academic-licenses@posit.co">academic-licenses@posit.co</a>
 to request a free teaching license.</p>
<h3 id="ai-next-steps-in-the-native-jupyter-notebook-editor">AI Next Steps in the Native Jupyter Notebook Editor
</h3>























  
  
    <div class="w-full aspect-video">
      <video
        src="https://opensource.posit.co/blog/2026-04-07_april-newsletter/images/notebook-next-step-suggestions.mov"
        class="w-full h-full object-contain"
        
        controls></video>
    </div>
  




<p><strong>What we built:</strong> AI Next Steps uses the Positron Assistant to analyze your current cell output and suggest a logical next step in a &ldquo;ghost cell&rdquo; at the bottom of your notebook. If you just loaded a CSV, it might suggest data cleaning steps or a visualization, without you needing to open a chat pane or write a prompt. Suggestions stay aligned with the notebook&rsquo;s live kernel state, updating as your code and outputs change.</p>
<p><strong>Why this matters</strong>: The design came out of interviews with data scientists who kept telling us the same thing: switching to a chat pane mid-analysis breaks their concentration. AI Next Steps sits at the bottom of your notebook and updates as your outputs change. You just run a cell, and if there&rsquo;s a logical next step, it surfaces, with no prompt required.</p>
<p><strong>Get started:</strong> Enable the feature by setting 






<a href="positron://settings/positron.assistant.notebook.ghostCellSuggestions.enabled"><code>positron.assistant.notebook.ghostCellSuggestions.enabled</code></a>
 to <code>true</code> in your settings. When you run a cell, look for the ghost cell suggestion at the bottom of the notebook, accept, reject, or hide it.</p>
<h3 id="agent-skills-in-positron-assistant">Agent Skills in Positron Assistant
</h3>
<p><strong>What we built:</strong> Agent skills &mdash; reusable, structured capabilities that extend what agents can do in agent.md files &mdash; are now integrated into Positron (






<a href="https://github.com/posit-dev/positron/issues/11753" target="_blank" rel="noopener">#11753</a>
). Skills let agents execute multi-step workflows like &ldquo;profile this dataset and suggest cleaning steps&rdquo; or &ldquo;run this test suite and summarize failures,&rdquo; so you define a task once and reuse it across sessions and projects.</p>
<p><strong>Why this matters:</strong> Skills make agents composable building blocks rather than one-off chat interactions. Instead of re-explaining a complex workflow every time, you codify it as a skill that any team member can use.</p>
<p><strong>Get started:</strong> Open the chat gear icon and select <strong>Skills</strong>, or run <em>Chat: Configure Skills</em> from the Command Palette.</p>
<h3 id="positron-assistant-now-supports-microsoft-foundry-as-a-provider">Positron Assistant Now Supports Microsoft Foundry as a Provider
</h3>
<p><strong>What we built:</strong> Positron Assistant now supports Microsoft Foundry as a model provider (






<a href="https://github.com/posit-dev/positron/issues/8583" target="_blank" rel="noopener">#8583</a>
) with API key-based access via a custom base URL.</p>
<p><strong>Why this matters:</strong> If your team runs on Azure and uses LLMs through Foundry, you can now use Positron Assistant with them.</p>
<p><strong>Get Started:</strong> In Positron Assistant&rsquo;s provider settings, set 






<a href="positron://settings/positron.assistant.provider.msFoundry.enable"><code>positron.assistant.provider.msFoundry.enable</code></a>
 to <code>true</code> to select Microsoft Foundry as a provider. You can authenticate with an API key and your Foundry endpoint URL.</p>
<img src="https://opensource.posit.co/blog/2026-04-07_april-newsletter/images/microsoft-foundry.png" data-fig-alt="Configuring Microsoft Foundry in Positron Assistant" />
<h3 id="telemetry-update-anonymous-session-identifiers">Telemetry Update: Anonymous Session Identifiers
</h3>
<p><strong>What we changed:</strong> Positron now generates an anonymous, random session identifier to help us understand usage patterns like session frequency and retention across releases. This identifier contains no personal information, account data, or workspace content; it&rsquo;s a cryptographically random UUID that cannot be linked to any other identifiers, including the identifier that VS Code uses for telemetry.</p>
<p><strong>Why we&rsquo;re doing this:</strong> As a free, source available project, we don&rsquo;t have traditional product analytics. Understanding whether people come back, how often they use Positron, and whether releases improve or regress the experience helps us prioritize the right work to build a better experience for you.</p>
<p>You can opt out by updating your settings outlined 






<a href="https://positron.posit.co/privacy.html" target="_blank" rel="noopener">here</a>
, or you can reset the anonymous identifier with the command <em>Preferences: Reset Anonymous Telemetry ID</em>. If you&rsquo;ve opted out of product updates, no session identifier is generated or sent.</p>
<h3 id="rstudio-addins-support">RStudio Addins Support
</h3>
<p><strong>What we built:</strong> Positron now supports running RStudio addins from R packages. If a package registers an addin (like styler, reprex, clipr, or shinyuieditor), you can run it directly from Positron (






<a href="https://github.com/posit-dev/positron/issues/1313" target="_blank" rel="noopener">#1313</a>
).</p>
<p><strong>Why this matters:</strong> This was one of our most upvoted issues this release (25 👍). Many R users rely on addins as part of their daily workflow for code formatting, generating reproducible examples, or launching Shiny tools.</p>
<p><strong>Get started:</strong> Open the Command Palette (<code>Ctrl-Shift-P</code> (windows), <code>Ctrl-Shift-P</code> (linux), <code>Command-Shift-P</code> (mac)) and search for <em>Run RStudio Addin</em>. You&rsquo;ll see a quick pick with all available addins from your installed packages.</p>
<img src="https://opensource.posit.co/blog/2026-04-07_april-newsletter/images/addins-support.png" data-fig-alt="RStudio Addins running in Positron" />
<h3 id="r-debugger--workflow-improvements">R Debugger &amp; Workflow Improvements
</h3>
<p><strong>What we built:</strong> The R debugger received a suite of improvements this release. In addition to conditional breakpoints, hit count breakpoints, and log breakpoints (






<a href="https://github.com/posit-dev/positron/issues/12360" target="_blank" rel="noopener">#12360</a>
), the debugger now supports error and warning breakpoints (






<a href="https://github.com/posit-dev/positron/issues/11797" target="_blank" rel="noopener">#11797</a>
), the ability to pause R at any time (






<a href="https://github.com/posit-dev/positron/issues/11799" target="_blank" rel="noopener">#11799</a>
), Watch Pane support (






<a href="https://github.com/posit-dev/positron/issues/1765" target="_blank" rel="noopener">#1765</a>
), and synchronization between the Console and Variables pane with the selected call stack frame (






<a href="https://github.com/posit-dev/positron/issues/3078" target="_blank" rel="noopener">#3078</a>
 and 






<a href="https://github.com/posit-dev/positron/issues/12131" target="_blank" rel="noopener">#12131</a>
).</p>
<p><strong>Why this matters:</strong> Advanced debugging in R has traditionally meant scattering <code>if (...) browser()</code> calls through your code or setting <code>options(error = recover)</code> by hand. These new features put Positron&rsquo;s R debugger on par with what you&rsquo;d expect from any modern language:</p>
<ul>
<li><strong>Conditional, hit count, and log breakpoints</strong> let you control exactly when breakpoints fire and print diagnostic info, all without touching your source code.</li>
<li><strong>Error and warning breakpoints</strong> drop you into the debugger the moment an error or warning is emitted, so you can inspect the state that caused it.</li>
<li><strong>Pause R at any time.</strong> If R is stuck in a long computation or an infinite loop, you can drop into the debugger mid-execution, look around, and resume by clicking <strong>Continue</strong>.</li>
<li><strong>Watch Pane</strong> lets you track expressions across debug steps. Prefix an expression with <code>/print</code> to see R&rsquo;s printed output (hover to get full output) instead of a structured variable.</li>
<li><strong>Synchronization with the call stack.</strong> Click any frame in the <strong>Call Stack</strong> view and the Console, completions, and Variables pane all switch to that frame&rsquo;s environment. The Console synchronization is like <code>recover()</code>, but built into the IDE.</li>
</ul>
<p><strong>Get started:</strong> Set a breakpoint in any R file, then right-click it and choose <strong>Edit Breakpoint</strong>. Select &ldquo;Expression&rdquo; to add a condition (e.g., <code>i &gt; 100</code>), &ldquo;Hit Count&rdquo; to break after N hits, or &ldquo;Log Message&rdquo; to print a message without pausing. For error and warning breakpoints, open the <strong>Breakpoints</strong> pane and enable them there. To pause R while code is running, use the command <em>Debug: Pause</em> or check the <strong>Interrupt</strong> breakpoint option in the <strong>Breakpoints</strong> pane. While debugging, add expressions in the <strong>Watch</strong> section of the debug sidebar and click on frames in the <strong>Call Stack</strong> to navigate environments.</p>























  
  
    <div class="w-full aspect-video">
      <video
        src="https://opensource.posit.co/blog/2026-04-07_april-newsletter/images/conditional-breakpoints.mov"
        class="w-full h-full object-contain"
        
        controls></video>
    </div>
  




<h3 id="data-explorer-faster-with-multiple-dataframes">Data Explorer: Faster with Multiple DataFrames
</h3>
<p><strong>What we built:</strong> We fixed two long-standing performance issues in the Data Explorer. Background Data Explorer tabs no longer trigger backend recomputation, and the summary panel no longer recalculates summary statistics for large DataFrames on every cell execution (






<a href="https://github.com/posit-dev/positron/issues/4279" target="_blank" rel="noopener">#4279</a>
 and 






<a href="https://github.com/posit-dev/positron/issues/2795" target="_blank" rel="noopener">#2795</a>
).</p>
<p><strong>Why this matters:</strong> If you work with multiple DataFrames open, you may have noticed lag as Positron recomputed statistics for tabs you weren&rsquo;t even looking at. That&rsquo;s gone now.</p>
<p><strong>Get started:</strong> Nothing to configure. When you open multiple DataFrames in the Data Explorer and switch between them, you should notice snappier performance, especially with large datasets.</p>
<h3 id="windows-arm-is-generally-available">Windows ARM Is Generally Available
</h3>
<p><strong>What we built:</strong> We started creating experimental builds for Windows ARM several months ago, and our early users have had good experiences with them. This release, we promoted the Windows ARM builds from experimental to stable and they are now available through all standard installation channels (






<a href="https://github.com/posit-dev/positron/issues/12207" target="_blank" rel="noopener">#12207</a>
).</p>
<p><strong>Why this matters:</strong> ARM-based devices are increasingly common for Windows users, whether you&rsquo;re a student or a professional. GA support means these users get the same Positron experience, including Quarto with R and Python support, without needing workarounds or experimental builds. Do be aware that the Windows ARM build bundles the non-ARM version of Quarto, which runs under emulation.</p>
<p><strong>Get started:</strong> Install Positron on your ARM-based Windows device through 






<a href="https://positron.posit.co/download.html" target="_blank" rel="noopener">standard installation channels</a>
.</p>
<p>View all issues in the 






<a href="https://github.com/posit-dev/positron/milestone/36" target="_blank" rel="noopener">2026.04.0 Release milestone</a>
.</p>
<h2 id="whats-coming-next">What&rsquo;s Coming Next
</h2>
<p>We are currently building the following features and we&rsquo;d love your feedback. Please share on 






<a href="https://github.com/posit-dev/positron/discussions" target="_blank" rel="noopener">GitHub</a>
. These early alpha features with some rough edges are available for testing by enabling their respective settings.</p>
<h3 id="inline-outputs-for-quarto-and-r-markdown-files">Inline Outputs for Quarto and R Markdown Files
</h3>
<p>This was the second most upvoted issue we have ever, ever had! We just completed an initial run to allow displaying inline outputs within Quarto and R Markdown files (






<a href="https://github.com/posit-dev/positron/issues/5640" target="_blank" rel="noopener">#5640</a>
), and it is available for early testing. Note that this experimental version, while it does get the basics into Positron, does not have support for many popular RStudio features. You can opt in to the experimental feature using the 






<a href="positron://settings/positron.quarto.inlineOutput.enabled"><code>positron.quarto.inlineOutput.enabled</code></a>
 setting.</p>
<img src="https://opensource.posit.co/blog/2026-04-07_april-newsletter/images/inline-output.png" data-fig-alt="Inline outputs rendered in a Quarto file" />
<h3 id="packages-pane-for-managing-environments">Packages Pane for Managing Environments
</h3>
<p>We are currently building out a new Packages pane that will allow you to install, update, and uninstall packages without leaving your workspace or needing to use the terminal (






<a href="https://github.com/posit-dev/positron/issues/11214" target="_blank" rel="noopener">#11214</a>
). We&rsquo;d love to hear your feedback on this 






<a href="https://github.com/posit-dev/positron/discussions/12863" target="_blank" rel="noopener">discussion thread</a>
.</p>
<h2 id="events-and-resources">Events and Resources
</h2>
<h3 id="explore-positrons-video-walkthroughs-on-youtube">Explore Positron&rsquo;s Video Walkthroughs on YouTube
</h3>
<p>We hosted a walkthrough of exploring GitHub data in a Jupyter Notebook and converting this into an interactive Shiny app with AI. 






<a href="https://www.youtube.com/watch?v=qrVkG89ndi8" target="_blank" rel="noopener">Catch up on the recording</a>
 or 






<a href="https://www.youtube.com/@PositPBC" target="_blank" rel="noopener">explore more Positron videos</a>
.</p>
<h3 id="registration-for-positconf2026-is-now-open">Registration for posit::conf(2026) Is Now Open!
</h3>
<p>Registration is officially open for posit::conf(2026)! Join the global data community in Houston or tune in online from September 14&ndash;16. 






<a href="https://posit.co/conference/" target="_blank" rel="noopener">Register today!</a>
</p>
<h3 id="how-we-chose-a-python-type-checker">How We Chose a Python Type Checker
</h3>
<p>Ever wondered about the decision making process behind how we chose which Python type checker to bundle in Positron? Check out Austin Dickey&rsquo;s 





  


  
  

<a href="https://opensource.posit.co/blog/2026-03-31_python-type-checkers">blog post</a>
 walking through his research and decision making process.</p>
<h2 id="community-affirmations">Community Affirmations
</h2>
<p>Thank you all for your support, ideas and engagement. We&rsquo;re building Positron in the open because the best ideas come from the people using it. If there&rsquo;s a feature you&rsquo;d love to see, 






<a href="https://github.com/posit-dev/positron/issues" target="_blank" rel="noopener">open an issue</a>
 or upvote an existing one, it genuinely shapes what we work on next.</p>
<p>Have a great April!</p>
<p>Positron Team</p>
]]></description>
      <enclosure url="https://opensource.posit.co/blog/2026-04-07_april-newsletter/images/addins-support.png" length="374293" type="image/png" />
    </item>
    <item>
      <title>Positron Server available for academic use via JupyterHub</title>
      <link>https://opensource.posit.co/blog/2026-04-06_positron-server-jupyterhub/</link>
      <pubDate>Mon, 06 Apr 2026 00:00:00 +0000</pubDate>
      <guid>https://opensource.posit.co/blog/2026-04-06_positron-server-jupyterhub/</guid>
      <dc:creator>Isabel Zimmerman</dc:creator><description><![CDATA[<p>Academic institutions can now offer Positron directly within their existing JupyterHub environments, giving students a robust data science IDE without needing a local install or new infrastructure.
With a free teaching license, institutions can provide Positron Server to currently enrolled students for use in coursework.
This makes it easy to deliver a consistent, fully featured data science environment to students without requiring local installation or setup.</p>
<p>Students can launch Positron the same way they would open JupyterLab or a notebook.
Just select it from the JupyterHub launcher and start working.</p>
<p><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://opensource.posit.co/blog/2026-04-06_positron-server-jupyterhub/images/jupyter-positron.gif"
      alt="" 
      loading="lazy"
    >
  </figure></div>
</p>
<p>Once launched, Positron provides the full IDE experience, including:</p>
<ul>
<li>Rich Python and R support</li>
<li>Access to the OpenVSX extension marketplace</li>
<li>Built in data viewer and variables explorer</li>
<li>Integrated help pane, debugger, version control and 






<a href="https://positron.posit.co/features.html" target="_blank" rel="noopener">other features</a>
 to help students level up when they&rsquo;re ready</li>
</ul>
<h2 id="how-it-works">How it works
</h2>
<p>Positron Server is designed to integrate directly with existing JupyterHub deployments.
It&rsquo;s compatible with JupyterHub environments running JupyterLab 4 and Python 3.9+.</p>
<p>It&rsquo;s installed via the 






<a href="https://github.com/posit-dev/jupyter-positron-server" target="_blank" rel="noopener"><code>jupyter-positron-server</code> Python package</a>
, built on Jupyter Server Proxy.
If you&rsquo;ve configured similar services before, setup will feel familiar.
This is not a standalone desktop install.
Rather, it lets you bring Positron into an existing JupyterHub setup.</p>
<h2 id="who-can-use-it">Who can use it?
</h2>
<p>This offering is available to academic institutions using Positron for teaching.
Under a free license, institutions can provide access to enrolled students, course participants, or staff involved in the delivery or receipt of educational programming.</p>
<p>Full eligibility details are available in the 






<a href="https://github.com/posit-dev/positron/blob/main/LICENSE.txt" target="_blank" rel="noopener">Positron Education License Rider</a>
.</p>
<h2 id="getting-started">Getting started
</h2>
<p>Hosting Positron for teaching purposes requires a free license key.
To get set up:</p>
<ol>
<li>Review the eligibility criteria in the 






<a href="https://github.com/posit-dev/positron/blob/main/LICENSE.txt" target="_blank" rel="noopener">Positron Education License Rider</a>
.</li>
<li>Email 






<a href="mailto:academic-licenses@posit.co">academic-licenses@posit.co</a>
 to request a teaching license.</li>
<li>Once your license is confirmed, follow the 






<a href="https://posit-dev.github.io/jupyter-positron-server/" target="_blank" rel="noopener"><code>jupyter-positron-server</code> documentation</a>
 to complete setup in your JupyterHub environment.</li>
</ol>
<div class="callout callout-tip" role="note" aria-label="Tip">
<div class="callout-header">
<span class="callout-title">Get in touch</span>
</div>
<div class="callout-body">
<p>Have questions or want to learn more?</p>
<p>Reach out to 






<a href="mailto:academic-licenses@posit.co">academic-licenses@posit.co</a>
 and let us know you&rsquo;re interested in Positron.
We&rsquo;ll help you navigate next steps!</p>
</div>
</div>
]]></description>
      <enclosure url="https://opensource.posit.co/blog/2026-04-06_positron-server-jupyterhub/images/social.png" length="129271" type="image/png" />
    </item>
    <item>
      <title>What&#39;s next: Quarto 2</title>
      <link>https://opensource.posit.co/blog/2026-04-06_whats-next-quarto-2/</link>
      <pubDate>Mon, 06 Apr 2026 00:00:00 +0000</pubDate>
      <guid>https://opensource.posit.co/blog/2026-04-06_whats-next-quarto-2/</guid>
      <dc:creator>Carlos Scheidegger</dc:creator><description><![CDATA[<p>We&rsquo;re excited to share an early look at Quarto 2. You might be aware that we 





  


  
  
    
  

<a href="https://opensource.posit.co/blog/2026-03-24_1.9-release/">recently released Quarto 1.9</a>
, with support for long-standing requests such as PDF accessibility. Quarto is an excellent choice for authors of scientific and technical documents, and the amount and quality of the work you create with it is genuinely humbling for us. Before anything else, we want to thank you for using Quarto; you&rsquo;re all quite literally the reason we build it.</p>
<p><strong>Quarto 2 is a full rewrite of the Quarto CLI, written from the ground up in Rust</strong> to better support your existing use cases, and enable a number of new, exciting use cases. Most importantly, Quarto 2 will include a built-in collaborative editor, and we plan on adding support for collaborative writing in Posit&rsquo;s commercial products such as Posit Cloud, Connect, and Workbench. With that said, the design of those integrations is still taking shape.</p>
<p>It is also very early in the project. If you interact with the Quarto project solely as a user of the tool, nothing in your workflow will change, and you should proceed as if you didn&rsquo;t know about our plans for Quarto 2. <strong>We don&rsquo;t expect to have a public release of Quarto 2 for at least 6 months. In addition, we will continue to develop and maintain parallel versions until Quarto 2 is a suitable replacement for users of Quarto 1</strong>.</p>
<p>Just like Quarto 1, Quarto 2 is open source and MIT licensed. The GitHub repository for Quarto 2 is currently 






<a href="https://github.com/quarto-dev/q2" target="_blank" rel="noopener"><code>quarto-dev/q2</code></a>
.</p>
<h2 id="why-quarto-2">Why Quarto 2?
</h2>
<p>There are some fundamental pain points in Quarto 1 that can&rsquo;t be solved incrementally. The goal of Quarto 2 is not to change how you currently work with Quarto; instead, we&rsquo;ve arrived at a point where incremental improvements do not provide the value you deserve given our team size and constraints. These are some of the things we want to do in Quarto 2:</p>
<ul>
<li>
<p><strong>A new Markdown parser enables tighter integration with editors for the entire rendering pipeline</strong>.
We know that good error messages, autocompletion, and YAML validation are some of your favorite features in Quarto 1. Quarto has about 1,000 different YAML configuration options, and we know how important it is to be able to provide good error messages. We want to extend this same idea to <em>everything</em> in your Quarto project: Markdown syntax errors, Lua filter errors, broken links, etc. Whenever possible, these should be flagged in your editor of choice.</p>
</li>
<li>
<p><strong>A fundamental solution for long-standing performance problems</strong>.
Quarto 1 is built by integrating a number of tools that work very well in isolation, but aren&rsquo;t designed to be performant when used together. A full rewrite of the Quarto core functionality in a single programming language will enable us to provide much better performance than before.</p>
</li>
<li>
<p><strong>A collaborative editor</strong>.
Quarto 2 will ship with a collaborative editor designed to work directly on the web as well as on the command-line. Keeping in the tradition and ethos of the Quarto project, this will include a robust open-source foundation based on 






<a href="https://automerge.org" target="_blank" rel="noopener">automerge</a>
, as well as a commercial solution for hosted project management. This follows the relationship between Quarto 1 and its integration with other Posit commercial offerings.</p>
</li>
<li>
<p><strong>A visual editor that works well alongside a source editor</strong>.
The visual editor we ship in RStudio, VS Code, and Positron works well if everyone working on the document is using the visual editor. On the other hand, if you choose the visual editor, but your colleague chooses the source editor, then you&rsquo;ll find that the experience is full of sharp edges. Quarto 2 is built from the ground up to support <em>bidirectional</em> editing workflows. A small change in your document using the visual editor shouldn&rsquo;t cause a large change in the <code>.qmd</code> file that is disruptive for your colleagues using a source editor.</p>
</li>
<li>
<p><strong>Support for Quarto 1 projects</strong>.
We aim for Quarto 2 to be backwards compatible with Quarto 1. Concretely, we&rsquo;re aiming to incorporate our Quarto 1 test suite directly into Quarto 2&rsquo;s project, including support for Pandoc and its output formats that our community depends on. Your existing extensions and projects should just work in Quarto 2. Early on, there will be gaps, and Quarto 2 will initially be a better fit for new projects.</p>
</li>
</ul>
<h2 id="what-happens-to-quarto-1-development">What happens to Quarto 1 development?
</h2>
<p>It&rsquo;s not going anywhere, and will be in active development for at least the next year. We&rsquo;ll still provide bugfixes, and accept pull requests.</p>
<h2 id="current-status">Current status
</h2>
<p>The development is happening in a 






<a href="https://github.com/quarto-dev/q2" target="_blank" rel="noopener">separate GitHub repository</a>
. Feel free to look around! However, this code base isn&rsquo;t ready for public consumption, and is very much in flux: that means we&rsquo;re not going to spend a lot of time answering architectural questions about it until things have settled, and all discussion of Quarto should remain in our current discussion forum and issue tracker.</p>
<p>There are big, interesting changes in the Quarto 2 architecture, and they deserve a longer exposition. We are working on those documents right now, and will share them with you in the next few weeks. Stay tuned!</p>
]]></description>
      <enclosure url="https://opensource.posit.co/blog/2026-04-06_whats-next-quarto-2/thumbnail.png" length="98981" type="image/png" />
    </item>
    <item>
      <title>Shiny for Python 1.6 brings toolbars and OpenTelemetry</title>
      <link>https://opensource.posit.co/blog/2026-04-02_shiny-python-1.6/</link>
      <pubDate>Thu, 02 Apr 2026 00:00:00 +0000</pubDate>
      <guid>https://opensource.posit.co/blog/2026-04-02_shiny-python-1.6/</guid>
      <dc:creator>Liz Nelson</dc:creator>
      <dc:creator>Barret Schloerke</dc:creator><description><![CDATA[<style>
  .panel-tabset .tab-content, .nav {
    border: none;
  }
  .panel-tabset.nav-centered .nav {
    justify-content: center;
  }
</style>
<p>We&rsquo;re pleased to announce that Shiny for Python <code>v1.6</code> is now 






<a href="https://pypi.org/project/shiny/" target="_blank" rel="noopener">available on PyPI</a>
!</p>
<p>Install it now with <code>pip install -U shiny</code>.</p>
<p>This release has two big additions: 


  
  
  





<a href="#toolbars">toolbar components</a>
 for building compact, modern UIs, and 


  
  
  





<a href="#opentelemetry">OpenTelemetry support</a>
 for understanding how your apps behave in production. A full list of changes is available in the 






<a href="https://github.com/posit-dev/py-shiny/blob/main/CHANGELOG.md" target="_blank" rel="noopener">CHANGELOG</a>
.</p>
<h2 id="toolbars">Toolbars
</h2>
<p>Toolbars are a new set of compact components designed to fit controls into tight spaces &mdash; card headers and footers, input labels, and text areas. They&rsquo;re perfect for dashboards that are running out of room, or for AI chat interfaces where you want to add controls without cluttering the layout.</p>
<p>The core components are:</p>
<table>
  <thead>
      <tr>
          <th>Component</th>
          <th>Description</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>ui.toolbar()</code></td>
          <td>Container for toolbar inputs</td>
      </tr>
      <tr>
          <td><code>ui.toolbar_input_button()</code></td>
          <td>A small action button</td>
      </tr>
      <tr>
          <td><code>ui.toolbar_input_select()</code></td>
          <td>A compact dropdown select</td>
      </tr>
      <tr>
          <td><code>ui.toolbar_divider()</code></td>
          <td>A visual separator</td>
      </tr>
      <tr>
          <td><code>ui.toolbar_spacer()</code></td>
          <td>Pushes items to opposite sides</td>
      </tr>
  </tbody>
</table>
<p>Each input also has a corresponding <code>ui.update_toolbar_input_*()</code> function for updating it dynamically.</p>
<h3 id="toolbars-in-card-headers-and-footers">Toolbars in card headers and footers
</h3>
<p>The most common use case is placing a toolbar in a card header to attach controls directly to a card&rsquo;s content:</p>
<figure>
<img src="https://opensource.posit.co/blog/2026-04-02_shiny-python-1.6/toolbar_ex1.png" alt="Example of a card with a toolbar input button and toolbar input select" />
<figcaption aria-hidden="true">Example of a card with a toolbar input button and toolbar input select</figcaption>
</figure>
<div class="code-block"><div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span><span class="lnt">20
</span><span class="lnt">21
</span><span class="lnt">22
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">faicons</span> <span class="kn">import</span> <span class="n">icon_svg</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">shiny.express</span> <span class="kn">import</span> <span class="nb">input</span><span class="p">,</span> <span class="n">render</span><span class="p">,</span> <span class="n">ui</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">with</span> <span class="n">ui</span><span class="o">.</span><span class="n">card</span><span class="p">(</span><span class="n">full_screen</span><span class="o">=</span><span class="kc">True</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="k">with</span> <span class="n">ui</span><span class="o">.</span><span class="n">card_header</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;Header&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="k">with</span> <span class="n">ui</span><span class="o">.</span><span class="n">toolbar</span><span class="p">(</span><span class="n">align</span><span class="o">=</span><span class="s2">&#34;right&#34;</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">            <span class="n">ui</span><span class="o">.</span><span class="n">toolbar_input_button</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">                <span class="nb">id</span><span class="o">=</span><span class="s2">&#34;action1&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                <span class="n">label</span><span class="o">=</span><span class="s2">&#34;Refresh&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                <span class="n">icon</span><span class="o">=</span><span class="n">icon_svg</span><span class="p">(</span><span class="s2">&#34;arrows-rotate&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">            <span class="p">)</span>
</span></span><span class="line"><span class="cl">            <span class="n">ui</span><span class="o">.</span><span class="n">toolbar_divider</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">            <span class="n">ui</span><span class="o">.</span><span class="n">toolbar_input_select</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">                <span class="nb">id</span><span class="o">=</span><span class="s2">&#34;options&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                <span class="n">label</span><span class="o">=</span><span class="s2">&#34;Filter&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                <span class="n">choices</span><span class="o">=</span><span class="p">[</span><span class="s2">&#34;ABC&#34;</span><span class="p">,</span> <span class="s2">&#34;CDE&#34;</span><span class="p">,</span> <span class="s2">&#34;EFG&#34;</span><span class="p">],</span>
</span></span><span class="line"><span class="cl">            <span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="nd">@render.text</span>
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="nf">toolbar_status</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="sa">f</span><span class="s2">&#34;Button clicks: </span><span class="si">{</span><span class="nb">input</span><span class="o">.</span><span class="n">action1</span><span class="p">()</span><span class="si">}</span><span class="s2">, Selected: </span><span class="si">{</span><span class="nb">input</span><span class="o">.</span><span class="n">options</span><span class="p">()</span><span class="si">}</span><span class="s2">&#34;</span></span></span></code></pre></td></tr></table>
</div>
</div></div>
<h3 id="toolbars-in-input-labels">Toolbars in input labels
</h3>
<p>You can also pass a toolbar as an input&rsquo;s <code>label</code> to add an info button for additional information or provide quick actions, like resetting an input value.</p>
<figure>
<img src="https://opensource.posit.co/blog/2026-04-02_shiny-python-1.6/toolbar_info_ex.png" alt="Example of an info toolbar button using a tooltip in an input label" />
<figcaption aria-hidden="true">Example of an info toolbar button using a tooltip in an input label</figcaption>
</figure>
<div class="code-block"><div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span><span class="lnt">20
</span><span class="lnt">21
</span><span class="lnt">22
</span><span class="lnt">23
</span><span class="lnt">24
</span><span class="lnt">25
</span><span class="lnt">26
</span><span class="lnt">27
</span><span class="lnt">28
</span><span class="lnt">29
</span><span class="lnt">30
</span><span class="lnt">31
</span><span class="lnt">32
</span><span class="lnt">33
</span><span class="lnt">34
</span><span class="lnt">35
</span><span class="lnt">36
</span><span class="lnt">37
</span><span class="lnt">38
</span><span class="lnt">39
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">faicons</span> <span class="kn">import</span> <span class="n">icon_svg</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">shiny.express</span> <span class="kn">import</span> <span class="n">ui</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">with</span> <span class="n">ui</span><span class="o">.</span><span class="n">card</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="n">ui</span><span class="o">.</span><span class="n">card_header</span><span class="p">(</span><span class="s2">&#34;Data Settings&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">ui</span><span class="o">.</span><span class="n">input_slider</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;threshold&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">label</span><span class="o">=</span><span class="n">ui</span><span class="o">.</span><span class="n">toolbar</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">            <span class="n">ui</span><span class="o">.</span><span class="n">toolbar_input_button</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">                <span class="s2">&#34;threshold_info&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                <span class="n">label</span><span class="o">=</span><span class="s2">&#34;About this setting&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                <span class="n">icon</span><span class="o">=</span><span class="n">icon_svg</span><span class="p">(</span><span class="s2">&#34;circle-info&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">                <span class="n">tooltip</span><span class="o">=</span><span class="s2">&#34;Standard deviations from the mean before a value is flagged as an outlier.&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="p">),</span>
</span></span><span class="line"><span class="cl">            <span class="s2">&#34;Outlier threshold&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="n">align</span><span class="o">=</span><span class="s2">&#34;left&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="p">),</span>
</span></span><span class="line"><span class="cl">        <span class="nb">min</span><span class="o">=</span><span class="mi">1</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="nb">max</span><span class="o">=</span><span class="mi">5</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">value</span><span class="o">=</span><span class="mi">2</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">step</span><span class="o">=</span><span class="mf">0.5</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">ui</span><span class="o">.</span><span class="n">input_numeric</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;sample_size&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">label</span><span class="o">=</span><span class="n">ui</span><span class="o">.</span><span class="n">toolbar</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">            <span class="n">ui</span><span class="o">.</span><span class="n">toolbar_input_button</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">                <span class="s2">&#34;sample_info&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                <span class="n">label</span><span class="o">=</span><span class="s2">&#34;About this setting&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                <span class="n">icon</span><span class="o">=</span><span class="n">icon_svg</span><span class="p">(</span><span class="s2">&#34;circle-info&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">                <span class="n">tooltip</span><span class="o">=</span><span class="s2">&#34;Number of observations to draw from the dataset for each analysis run.&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="p">),</span>
</span></span><span class="line"><span class="cl">            <span class="s2">&#34;Sample size&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="n">align</span><span class="o">=</span><span class="s2">&#34;left&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="p">),</span>
</span></span><span class="line"><span class="cl">        <span class="n">value</span><span class="o">=</span><span class="mi">100</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="nb">min</span><span class="o">=</span><span class="mi">10</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="nb">max</span><span class="o">=</span><span class="mi">1000</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">step</span><span class="o">=</span><span class="mi">10</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="p">)</span></span></span></code></pre></td></tr></table>
</div>
</div></div>
<h3 id="toolbars-in-text-areas">Toolbars in text areas
</h3>
<p>The <code>input_submit_textarea()</code> component accepts a <code>toolbar</code> parameter directly, making it easy to add contextual controls for AI chat interfaces and message composers:</p>
<figure>
<img src="https://opensource.posit.co/blog/2026-04-02_shiny-python-1.6/toolbar_textarea.png" alt="Example of a toolbar in an input submit textarea" />
<figcaption aria-hidden="true">Example of a toolbar in an input submit textarea</figcaption>
</figure>
<div class="code-block"><div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span><span class="lnt">20
</span><span class="lnt">21
</span><span class="lnt">22
</span><span class="lnt">23
</span><span class="lnt">24
</span><span class="lnt">25
</span><span class="lnt">26
</span><span class="lnt">27
</span><span class="lnt">28
</span><span class="lnt">29
</span><span class="lnt">30
</span><span class="lnt">31
</span><span class="lnt">32
</span><span class="lnt">33
</span><span class="lnt">34
</span><span class="lnt">35
</span><span class="lnt">36
</span><span class="lnt">37
</span><span class="lnt">38
</span><span class="lnt">39
</span><span class="lnt">40
</span><span class="lnt">41
</span><span class="lnt">42
</span><span class="lnt">43
</span><span class="lnt">44
</span><span class="lnt">45
</span><span class="lnt">46
</span><span class="lnt">47
</span><span class="lnt">48
</span><span class="lnt">49
</span><span class="lnt">50
</span><span class="lnt">51
</span><span class="lnt">52
</span><span class="lnt">53
</span><span class="lnt">54
</span><span class="lnt">55
</span><span class="lnt">56
</span><span class="lnt">57
</span><span class="lnt">58
</span><span class="lnt">59
</span><span class="lnt">60
</span><span class="lnt">61
</span><span class="lnt">62
</span><span class="lnt">63
</span><span class="lnt">64
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">faicons</span> <span class="kn">import</span> <span class="n">icon_svg</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">shiny</span> <span class="kn">import</span> <span class="n">reactive</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">shiny.express</span> <span class="kn">import</span> <span class="nb">input</span><span class="p">,</span> <span class="n">render</span><span class="p">,</span> <span class="n">ui</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">ui</span><span class="o">.</span><span class="n">page_opts</span><span class="p">(</span><span class="n">fillable</span><span class="o">=</span><span class="kc">False</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">messages</span> <span class="o">=</span> <span class="n">reactive</span><span class="o">.</span><span class="n">value</span><span class="p">([])</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">with</span> <span class="n">ui</span><span class="o">.</span><span class="n">card</span><span class="p">(</span><span class="n">full_screen</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span> <span class="n">height</span><span class="o">=</span><span class="s2">&#34;250px&#34;</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="n">ui</span><span class="o">.</span><span class="n">card_header</span><span class="p">(</span><span class="s2">&#34;Message Composer&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">with</span> <span class="n">ui</span><span class="o">.</span><span class="n">card_body</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">        <span class="n">ui</span><span class="o">.</span><span class="n">input_submit_textarea</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">            <span class="s2">&#34;message&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="n">label</span><span class="o">=</span><span class="s2">&#34;Message&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="n">placeholder</span><span class="o">=</span><span class="s2">&#34;Compose your message...&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="n">rows</span><span class="o">=</span><span class="mi">4</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="n">toolbar</span><span class="o">=</span><span class="n">ui</span><span class="o">.</span><span class="n">toolbar</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">                <span class="n">ui</span><span class="o">.</span><span class="n">toolbar_input_select</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">                    <span class="s2">&#34;priority&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                    <span class="n">label</span><span class="o">=</span><span class="s2">&#34;Priority&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                    <span class="n">choices</span><span class="o">=</span><span class="p">[</span><span class="s2">&#34;Low&#34;</span><span class="p">,</span> <span class="s2">&#34;Medium&#34;</span><span class="p">,</span> <span class="s2">&#34;High&#34;</span><span class="p">],</span>
</span></span><span class="line"><span class="cl">                    <span class="n">selected</span><span class="o">=</span><span class="s2">&#34;Medium&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                    <span class="n">icon</span><span class="o">=</span><span class="n">icon_svg</span><span class="p">(</span><span class="s2">&#34;flag&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">                <span class="p">),</span>
</span></span><span class="line"><span class="cl">                <span class="n">ui</span><span class="o">.</span><span class="n">toolbar_divider</span><span class="p">(),</span>
</span></span><span class="line"><span class="cl">                <span class="n">ui</span><span class="o">.</span><span class="n">toolbar_input_button</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">                    <span class="s2">&#34;attach&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                    <span class="n">label</span><span class="o">=</span><span class="s2">&#34;Attach&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                    <span class="n">icon</span><span class="o">=</span><span class="n">icon_svg</span><span class="p">(</span><span class="s2">&#34;paperclip&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">                <span class="p">),</span>
</span></span><span class="line"><span class="cl">                <span class="n">align</span><span class="o">=</span><span class="s2">&#34;right&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="p">),</span>
</span></span><span class="line"><span class="cl">        <span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">with</span> <span class="n">ui</span><span class="o">.</span><span class="n">card</span><span class="p">(</span><span class="n">full_screen</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span> <span class="n">height</span><span class="o">=</span><span class="s2">&#34;250px&#34;</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="n">ui</span><span class="o">.</span><span class="n">card_header</span><span class="p">(</span><span class="s2">&#34;Sent Messages&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">with</span> <span class="n">ui</span><span class="o">.</span><span class="n">card_body</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">        <span class="nd">@render.ui</span>
</span></span><span class="line"><span class="cl">        <span class="k">def</span> <span class="nf">messages_output</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">            <span class="n">msg_list</span> <span class="o">=</span> <span class="n">messages</span><span class="o">.</span><span class="n">get</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">            <span class="k">if</span> <span class="ow">not</span> <span class="n">msg_list</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">                <span class="k">return</span> <span class="n">ui</span><span class="o">.</span><span class="n">p</span><span class="p">(</span><span class="s2">&#34;No messages sent yet.&#34;</span><span class="p">,</span> <span class="n">style</span><span class="o">=</span><span class="s2">&#34;color: #888;&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">            <span class="k">return</span> <span class="n">ui</span><span class="o">.</span><span class="n">div</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">                <span class="o">*</span><span class="p">[</span>
</span></span><span class="line"><span class="cl">                    <span class="n">ui</span><span class="o">.</span><span class="n">p</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">                        <span class="sa">f</span><span class="s2">&#34;[</span><span class="si">{</span><span class="n">msg</span><span class="p">[</span><span class="s1">&#39;priority&#39;</span><span class="p">]</span><span class="si">}</span><span class="s2">] </span><span class="si">{</span><span class="n">msg</span><span class="p">[</span><span class="s1">&#39;text&#39;</span><span class="p">]</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                        <span class="n">style</span><span class="o">=</span><span class="s2">&#34;margin: 4px 0;&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                    <span class="p">)</span>
</span></span><span class="line"><span class="cl">                    <span class="k">for</span> <span class="n">msg</span> <span class="ow">in</span> <span class="nb">reversed</span><span class="p">(</span><span class="n">msg_list</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">                <span class="p">]</span>
</span></span><span class="line"><span class="cl">            <span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nd">@reactive.effect</span>
</span></span><span class="line"><span class="cl"><span class="nd">@reactive.event</span><span class="p">(</span><span class="nb">input</span><span class="o">.</span><span class="n">message</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">_</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="n">message_text</span> <span class="o">=</span> <span class="nb">input</span><span class="o">.</span><span class="n">message</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="n">message_text</span> <span class="ow">and</span> <span class="n">message_text</span><span class="o">.</span><span class="n">strip</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">        <span class="n">current_messages</span> <span class="o">=</span> <span class="nb">list</span><span class="p">(</span><span class="n">messages</span><span class="o">.</span><span class="n">get</span><span class="p">())</span>
</span></span><span class="line"><span class="cl">        <span class="n">current_messages</span><span class="o">.</span><span class="n">append</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">            <span class="p">{</span><span class="s2">&#34;text&#34;</span><span class="p">:</span> <span class="n">message_text</span><span class="p">,</span> <span class="s2">&#34;priority&#34;</span><span class="p">:</span> <span class="nb">input</span><span class="o">.</span><span class="n">priority</span><span class="p">()}</span>
</span></span><span class="line"><span class="cl">        <span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">messages</span><span class="o">.</span><span class="n">set</span><span class="p">(</span><span class="n">current_messages</span><span class="p">)</span></span></span></code></pre></td></tr></table>
</div>
</div></div>
<p>Toolbars are available in <code>py-shiny</code> and forthcoming in 






<a href="https://rstudio.github.io/bslib/" target="_blank" rel="noopener"><code>bslib</code></a>
 for R. For a complete walkthrough with full app examples, see the 






<a href="https://shiny.posit.co/py/components/layout/toolbar/" target="_blank" rel="noopener">Toolbar component page</a>
.</p>
<h2 id="opentelemetry">OpenTelemetry
</h2>
<p>Starting with Shiny <code>v1.6.0</code>, 






<a href="https://opentelemetry.io/" target="_blank" rel="noopener">OpenTelemetry</a>
 support is built directly into the framework.</p>
<p>OpenTelemetry (OTel) is a vendor-neutral observability standard that lets you collect telemetry data &mdash; traces, logs, and metrics &mdash; and send it to any compatible backend. For Shiny apps, this means you can finally answer questions like:</p>
<ul>
<li>Why is my app slow for certain users?</li>
<li>Which reactive expressions are taking the most time?</li>
<li>How long does it take for outputs to render?</li>
<li>What sequence of events occurs when a user interacts with my app?</li>
</ul>
<h3 id="getting-started">Getting started
</h3>
<p>The fastest way to get started is with 






<a href="https://logfire.pydantic.dev/" target="_blank" rel="noopener">Pydantic Logfire</a>
, which provides zero-configuration OTel setup:</p>
<div class="code-block"><div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">pip install logfire
</span></span><span class="line"><span class="cl">logfire auth</span></span></code></pre></td></tr></table>
</div>
</div></div>
<p>Then set an environment variable to tell Shiny what level of tracing to collect:</p>
<div class="code-block"><div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">export</span> <span class="nv">SHINY_OTEL_COLLECT</span><span class="o">=</span>reactivity</span></span></code></pre></td></tr></table>
</div>
</div></div>
<p>That&rsquo;s it &mdash; no changes to your app code required. Run your app and visit 






<a href="https://logfire.pydantic.dev/" target="_blank" rel="noopener">logfire.pydantic.dev</a>
 to see traces.</p>
<h3 id="otel-is-great-for-genai-apps">OTel is great for GenAI apps
</h3>
<p>Shiny&rsquo;s OTel integration pairs especially well with Generative AI applications. When a user reports that your chatbot feels slow, traces make it easy to pinpoint whether the delay is in the AI model request, streaming, tool execution, or a downstream reactive calculation.</p>
<p>The image below shows a trace from a weather forecast app powered by a Generative AI model. A single user session is captured in full detail:</p>
<figure>
<img src="https://opensource.posit.co/blog/2026-04-02_shiny-python-1.6/weather_app_ui.png" alt="The weather forecast Shiny app powered by a Generative AI model" />
<figcaption aria-hidden="true">The weather forecast Shiny app powered by a Generative AI model</figcaption>
</figure>
<figure>
<img src="https://opensource.posit.co/blog/2026-04-02_shiny-python-1.6/weather_app_logfire.png" alt="Trace in Logfire showing a full user session with reactive updates, model calls, and a tool invocation" />
<figcaption aria-hidden="true">Trace in Logfire showing a full user session with reactive updates, model calls, and a tool invocation</figcaption>
</figure>
<div class="callout callout-tip" role="note" aria-label="Tip">
<div class="callout-header">
<span class="callout-title">Collection levels</span>
</div>
<div class="callout-body">
<p><code>SHINY_OTEL_COLLECT</code> accepts three levels of detail:</p>
<ul>
<li><code>&quot;none&quot;</code> - No Shiny OpenTelemetry tracing</li>
<li><code>&quot;session&quot;</code> - Track session start and end</li>
<li><code>&quot;reactive_update&quot;</code> - Track reactive updates (includes <code>&quot;session&quot;</code> tracing)</li>
<li><code>&quot;reactivity&quot;</code> - Trace all reactive expressions (includes <code>&quot;reactive_update&quot;</code> tracing)</li>
<li><code>&quot;all&quot;</code> [Default] - Everything (currently equivalent to &ldquo;reactivity&rdquo;)</li>
</ul>
</div>
</div>
<h3 id="what-gets-traced-automatically">What gets traced automatically
</h3>
<p>Shiny automatically creates spans for all of the following &mdash; no manual instrumentation needed:</p>
<ul>
<li><strong>Session lifecycle</strong>: When sessions start and end, including HTTP request details</li>
<li><strong>Reactive updates</strong>: The entire cascade of reactive calculations triggered by an input change or a new output to be rendered</li>
<li><strong>Reactive expressions</strong>: Individual calculations such as <code>@reactive.calc</code>, <code>@reactive.effect</code>, <code>@render.*</code>, and other reactive constructs</li>
</ul>
<h3 id="works-with-any-otel-backend">Works with any OTel backend
</h3>
<p>Logfire is our recommended starting point, but Shiny&rsquo;s OTel integration is fully vendor-neutral. You can send traces to 






<a href="https://www.jaegertracing.io/" target="_blank" rel="noopener">Jaeger</a>
, 






<a href="https://zipkin.io/" target="_blank" rel="noopener">Zipkin</a>
, 






<a href="https://grafana.com/products/cloud/" target="_blank" rel="noopener">Grafana Cloud</a>
, 






<a href="https://langfuse.com/" target="_blank" rel="noopener">Langfuse</a>
, or any other OTLP-compatible backend.</p>
<p>For local debugging without a backend, install the OpenTelemetry SDK and use the console exporter:</p>
<div class="code-block"><div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">pip install <span class="s2">&#34;shiny[otel]&#34;</span></span></span></code></pre></td></tr></table>
</div>
</div></div>
<p>Full documentation &mdash; including custom spans, database instrumentation, and production considerations &mdash; is available in the 






<a href="https://shiny.posit.co/py/docs/opentelemetry.html" target="_blank" rel="noopener">OpenTelemetry guide</a>
.</p>
<h2 id="in-closing">In closing
</h2>
<p>We&rsquo;re excited to bring you these new features in Shiny <code>v1.6</code>. As always, if you have questions or feedback, 






<a href="https://discord.gg/yMGCamUMnS" target="_blank" rel="noopener">join us on Discord</a>
 or 






<a href="https://github.com/posit-dev/py-shiny/issues/new" target="_blank" rel="noopener">open an issue on GitHub</a>
. Happy Shiny-ing!</p>
]]></description>
      <enclosure url="https://opensource.posit.co/blog/2026-04-02_shiny-python-1.6/py-shiny-otel-toolbars.png" length="133106" type="image/png" />
    </item>
    <item>
      <title>How we chose Positron&#39;s Python type checker</title>
      <link>https://opensource.posit.co/blog/2026-03-31_python-type-checkers/</link>
      <pubDate>Tue, 31 Mar 2026 00:00:00 +0000</pubDate>
      <guid>https://opensource.posit.co/blog/2026-03-31_python-type-checkers/</guid>
      <dc:creator>Austin Dickey</dc:creator><description><![CDATA[<p><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://opensource.posit.co/blog/2026-03-31_python-type-checkers/images/social.png"
      alt="" 
      loading="lazy"
    >
  </figure></div>
</p>
<p>The open-source Python type checker and language server ecosystem has exploded. Over the past year or two, four language server extensions have appeared, each with a different take on what Python type checking should look like. We evaluated each of them to decide which one to bundle with Positron to enhance the Python data science experience.</p>
<h2 id="background">Background
</h2>
<p>The Language Server Protocol (LSP) is a cross-language, cross-IDE specification that allows different IDE extensions to contribute smart features like tab completions, hover info, and more. The four<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup> Python extensions in this post are powered by type checkers, which are Python-specific tools that catch bugs in your code before runtime by guessing and checking the types of your variables. They do this by <em>statically analyzing</em> your code before you run it.</p>
<div class="callout callout-tip" role="note" aria-label="Tip">
<div class="callout-header">
<span class="callout-title">Tip</span>
</div>
<div class="callout-body">
<p>Positron&rsquo;s built-in language server uses your running Python session to provide runtime-aware completions and hover previews too! Beyond what&rsquo;s in code, it knows your DataFrame column names, your dictionary keys, your environment variables, and more. But the tools evaluated in this post handle the <em>static analysis</em> side: type checking, go-to-definition, rename, and code actions. Both run concurrently, and Positron merges their results.</p>
</div>
</div>
<p>With AI tools writing more of your code, a good language server helps you read and navigate code you didn&rsquo;t write. LLM-generated code also introduces bugs that type checkers catch before you run anything. For data scientists, who rely on code to be the reproducibility layer, and who can&rsquo;t automate away human judgment, what matters is a tool that helps you understand and trust your code.</p>
<p>We did this evaluation in November 2025 but have refreshed the data in this post at the time of publish.</p>
<h2 id="the-contenders">The contenders
</h2>
<table>
  <thead>
      <tr>
          <th>Tool</th>
          <th>Backing</th>
          <th>Language</th>
          <th>License</th>
          <th style="text-align: center">Stars</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>






<a href="https://github.com/facebook/pyrefly" target="_blank" rel="noopener"><strong>Pyrefly</strong></a>
</td>
          <td>Meta</td>
          <td>Rust</td>
          <td>MIT</td>
          <td style="text-align: center">5.5K</td>
      </tr>
      <tr>
          <td>






<a href="https://github.com/astral-sh/ty" target="_blank" rel="noopener"><strong>ty</strong></a>
</td>
          <td>Astral (OpenAI)</td>
          <td>Rust</td>
          <td>MIT</td>
          <td style="text-align: center">17.8K</td>
      </tr>
      <tr>
          <td>






<a href="https://github.com/detachhead/basedpyright" target="_blank" rel="noopener"><strong>Basedpyright</strong></a>
</td>
          <td>Community</td>
          <td>TypeScript</td>
          <td>MIT</td>
          <td style="text-align: center">3.2K</td>
      </tr>
      <tr>
          <td>






<a href="https://github.com/zubanls/zuban" target="_blank" rel="noopener"><strong>Zuban</strong></a>
</td>
          <td>Indie</td>
          <td>Rust</td>
          <td>AGPL-3.0</td>
          <td style="text-align: center">1K</td>
      </tr>
  </tbody>
</table>
<p><strong>Pyrefly</strong> is Meta&rsquo;s successor to Pyre. It takes a fast, aggressive approach to type inference, being able to catch issues even in code with no type annotations. It reached 






<a href="https://github.com/facebook/pyrefly/releases/tag/0.42.0" target="_blank" rel="noopener">beta status</a>
 in November 2025.</p>
<p><strong>ty</strong> is from Astral, the team behind uv and ruff. 






<a href="https://openai.com/index/openai-to-acquire-astral/" target="_blank" rel="noopener">OpenAI announced its acquisition of Astral</a>
 recently; Astral has stated that ty, ruff, and uv will remain open source and MIT-licensed. It&rsquo;s the newest project, with a focus on speed and tight integration with the Astral toolchain. It reached 






<a href="https://astral.sh/blog/ty" target="_blank" rel="noopener">beta status</a>
 in December 2025 and follows a &ldquo;gradual guarantee&rdquo; philosophy (more on that below).</p>
<p><strong>Basedpyright</strong> is a community fork of Microsoft&rsquo;s Pyright type checker, with additional type-checking rules and LSP features baked in. It&rsquo;s the most mature of the four and has the largest contributor base.</p>
<p><strong>Zuban</strong> is from David Halter, the author of Jedi (the longtime Python autocompletion library). It aims for mypy compatibility and ships as a pip-installable tool.</p>
<h2 id="what-we-tested">What we tested
</h2>
<p>We tested each language server across several dimensions, roughly following the 






<a href="https://github.com/posit-dev/positron/issues/10300" target="_blank" rel="noopener">rubric we outlined publicly</a>
:</p>
<ul>
<li><strong>Feature completeness</strong>: Completions, hover, go-to-definition, rename, code actions, diagnostics, inlay hints, call hierarchy</li>
<li><strong>Correctness</strong>: How well does the type checker handle real-world Python code?</li>
<li><strong>Performance</strong>: Startup time and time to first completion</li>
<li><strong>Ecosystem</strong>: License, community health, development velocity, production readiness</li>
</ul>
<p>We tested inside Positron with a mix of data science and general Python code.</p>
<h2 id="feature-completeness">Feature completeness
</h2>
<p>Here are some screenshots of hovers, tab-completions, and diagnostics from each extension:</p>
<div class="panel-tabset">
<ul id="tabset-1" class="panel-tabset-tabby">
<li><a data-tabby-default href="#tabset-1-1">Pyrefly</a></li>
<li><a href="#tabset-1-2">ty</a></li>
<li><a href="#tabset-1-3">Basedpyright</a></li>
<li><a href="#tabset-1-4">Zuban</a></li>
</ul>
<div id="tabset-1-1">
<figure>
<img src="https://opensource.posit.co/blog/2026-03-31_python-type-checkers/images/pyrefly.png" alt="Clean documentation with some extra info; simple completions" />
<figcaption aria-hidden="true">Clean documentation with some extra info; simple completions</figcaption>
</figure>
</div>
<div id="tabset-1-2">
<figure>
<img src="https://opensource.posit.co/blog/2026-03-31_python-type-checkers/images/ty.png" alt="Red diagnostic due to invalid syntax; completions have extra info" />
<figcaption aria-hidden="true">Red diagnostic due to invalid syntax; completions have extra info</figcaption>
</figure>
</div>
<div id="tabset-1-3">
<figure>
<img src="https://opensource.posit.co/blog/2026-03-31_python-type-checkers/images/basedpyright.png" alt="One-line hover docs; completions for all dunder methods" />
<figcaption aria-hidden="true">One-line hover docs; completions for all dunder methods</figcaption>
</figure>
</div>
<div id="tabset-1-4">
<figure>
<img src="https://opensource.posit.co/blog/2026-03-31_python-type-checkers/images/zuban.png" alt="Extra hover docs from the class docstring; syntax diagnostic" />
<figcaption aria-hidden="true">Extra hover docs from the class docstring; syntax diagnostic</figcaption>
</figure>
</div>
</div>
<p>All four provide the core features you&rsquo;d expect: completions, hover documentation, go-to-definition, semantic highlighting, and diagnostics. The differences show up in the details.</p>
<h3 id="pyrefly">Pyrefly
</h3>
<p>Strong feature set. The hover documentation is the best of the four; <strong>Pyrefly</strong> renders it cleanly and sometimes includes hyperlinks to class definitions.</p>
<h3 id="ty">ty
</h3>
<p>Fast and clean, now in beta. The completion details can sometimes feel a little overwhelming, but can help when expanded.</p>
<h3 id="basedpyright">Basedpyright
</h3>
<p>Handles type checking comprehensively well. The main friction point: it surfaces a lot of warnings out of the box. If you&rsquo;re doing exploratory data science, a wall of type errors on your first <code>pandas</code> import can feel hostile. You can tune this down, but the defaults are oriented toward stricter use cases like package development.</p>
<h3 id="zuban">Zuban
</h3>
<p>The least mature of the four so far. Installation requires a two-step process (<code>pip install zuban</code>, then configure the interpreter), and the analysis is tied to that specific Python installation on saved files only. Third-party library completions only work when stubs are available, not from installed packages. Symbol renaming once broke standard library code in our testing.</p>
<h2 id="type-checking-philosophy">Type checking philosophy
</h2>
<p>The bigger difference between these tools isn&rsquo;t features but how they think about type checking.</p>
<h3 id="gradual-guarantee-vsaggressive-inference">Gradual guarantee vs. aggressive inference
</h3>
<p><strong>ty</strong> follows what&rsquo;s called the <em>gradual guarantee</em>: removing a type annotation from correct code should never introduce a type error. The idea is that type checking should be additive. You opt in by adding types, and the checker only flags things it&rsquo;s sure about.</p>
<p>The other extensions take the opposite approach. They always infer types from your code, even when you haven&rsquo;t written any annotations. This means they can catch bugs in completely untyped code, but it also means they may flag code that runs perfectly fine.</p>
<p>For example:</p>
<div class="code-block"><div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span><span class="lnt">7
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">my_list</span> <span class="o">=</span> <span class="p">[</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="n">my_list</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="s2">&#34;foo&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Pyrefly: bad-argument-type</span>
</span></span><span class="line"><span class="cl"><span class="c1"># ty: &lt;no error&gt;</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Basedpyright: reportArgumentType</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Zuban: arg-type</span></span></span></code></pre></td></tr></table>
</div>
</div></div>
<p><strong>Pyrefly</strong> infers <code>my_list</code> as <code>list[int]</code> and flags the <code>append(&quot;foo&quot;)</code> call as a type error. <strong>ty</strong> sees no annotations and stays silent. The code is dynamically typed and that&rsquo;s fine.</p>
<p>If you&rsquo;re doing exploratory data analysis and don&rsquo;t want to annotate everything, <strong>ty</strong>&rsquo;s restraint might be more comfortable. But if you&rsquo;re writing a library and want to catch bugs early, <strong>Pyrefly</strong>&rsquo;s aggressiveness is helpful. For example:</p>
<div class="code-block"><div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span><span class="lnt">7
</span><span class="lnt">8
</span><span class="lnt">9
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">process</span><span class="p">(</span><span class="n">data</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="nb">str</span><span class="p">(</span><span class="n">data</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">process</span><span class="p">(</span><span class="mi">42</span><span class="p">)</span> <span class="o">+</span> <span class="mi">1</span>  <span class="c1"># Raises a runtime AttributeError</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Pyrefly: unsupported-operation</span>
</span></span><span class="line"><span class="cl"><span class="c1"># ty: &lt;no error&gt;</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Basedpyright: reportOperatorIssue</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Zuban: operator</span></span></span></code></pre></td></tr></table>
</div>
</div></div>
<p><strong>Basedpyright</strong> and <strong>Zuban</strong> land somewhere in between, with <strong>Basedpyright</strong> leaning toward stricter checking and <strong>Zuban</strong> aiming for mypy compatibility. Each of these extensions has the ability to suppress certain diagnostics you actually see when typing if you wish.</p>
<p>For a deeper dive on this topic, Edward Li&rsquo;s 






<a href="https://blog.edward-li.com/tech/comparing-pyrefly-vs-ty/" target="_blank" rel="noopener">comparison of <strong>Pyrefly</strong> and <strong>ty</strong></a>
 and Rob Hand&rsquo;s 






<a href="https://sinon.github.io/future-python-type-checkers/" target="_blank" rel="noopener">overview of future Python type checkers</a>
 are both worth reading, though some bugs have been fixed since they were published.</p>
<h2 id="performance">Performance
</h2>
<p>We measured startup time (how long until the language server responds to an <code>initialize</code> request) and time to first completion (how long a <code>textDocument/completion</code> request takes after initialization) in a relatively small repository. We ran each measurement five times and averaged. As always, these results only represent our computer&rsquo;s experimental setup.</p>
<table>
  <thead>
      <tr>
          <th>LSP</th>
          <th style="text-align: center">Avg. startup (s)</th>
          <th style="text-align: center">Avg. first completion (ms)</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>Pyrefly</strong></td>
          <td style="text-align: center">5.8</td>
          <td style="text-align: center">190</td>
      </tr>
      <tr>
          <td><strong>ty</strong></td>
          <td style="text-align: center">2.2</td>
          <td style="text-align: center">88</td>
      </tr>
      <tr>
          <td><strong>Basedpyright</strong></td>
          <td style="text-align: center">3.1</td>
          <td style="text-align: center">112</td>
      </tr>
      <tr>
          <td><strong>Zuban</strong></td>
          <td style="text-align: center">N/A<sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup></td>
          <td style="text-align: center">97</td>
      </tr>
  </tbody>
</table>
<p><strong>ty</strong> was the fastest across the board. But the practical differences are small: a 3-second difference in startup happens once per session, and a 100ms difference in completions is imperceptible. All four are fast enough that differences are negligible for daily use.</p>
<h2 id="ecosystem-health">Ecosystem health
</h2>
<p>We also looked at each project&rsquo;s development velocity and community health metrics. A language server you rely on daily needs to keep up with Python&rsquo;s evolution.</p>
<table>
  <thead>
      <tr>
          <th></th>
          <th style="text-align: center"><strong>Pyrefly</strong></th>
          <th style="text-align: center"><strong>ty</strong></th>
          <th style="text-align: center"><strong>Basedpyright</strong></th>
          <th style="text-align: center"><strong>Zuban</strong></th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>GitHub stars</td>
          <td style="text-align: center">5.5K</td>
          <td style="text-align: center">17.8K</td>
          <td style="text-align: center">3.2K</td>
          <td style="text-align: center">1K</td>
      </tr>
      <tr>
          <td>Contributors</td>
          <td style="text-align: center">162</td>
          <td style="text-align: center">186<sup id="fnref:3"><a href="#fn:3" class="footnote-ref" role="doc-noteref">3</a></sup></td>
          <td style="text-align: center">82</td>
          <td style="text-align: center">17</td>
      </tr>
      <tr>
          <td>License</td>
          <td style="text-align: center">MIT</td>
          <td style="text-align: center">MIT</td>
          <td style="text-align: center">MIT</td>
          <td style="text-align: center">AGPL-3.0</td>
      </tr>
      <tr>
          <td>Releases (since Nov 2025)</td>
          <td style="text-align: center">17</td>
          <td style="text-align: center">29</td>
          <td style="text-align: center">10</td>
          <td style="text-align: center">9</td>
      </tr>
      <tr>
          <td>Release cadence</td>
          <td style="text-align: center">~weekly</td>
          <td style="text-align: center">~twice weekly</td>
          <td style="text-align: center">~biweekly</td>
          <td style="text-align: center">~biweekly</td>
      </tr>
      <tr>
          <td>Issues opened (90 days)</td>
          <td style="text-align: center">540</td>
          <td style="text-align: center">789</td>
          <td style="text-align: center">40</td>
          <td style="text-align: center">125</td>
      </tr>
      <tr>
          <td>Issues closed (90 days)</td>
          <td style="text-align: center">531</td>
          <td style="text-align: center">712</td>
          <td style="text-align: center">20</td>
          <td style="text-align: center">111</td>
      </tr>
  </tbody>
</table>
<p><strong>ty</strong> and <strong>Pyrefly</strong> are shipping fast. Both are on a weekly release cadence or higher with high issue throughput. <strong>ty</strong>&rsquo;s issue volume is notable: 789 issues opened in 90 days reflects both heavy adoption and active bug reporting. <strong>Pyrefly</strong> is closing more issues than it&rsquo;s opening, a good sign for a beta project.</p>
<p>Response times are quick. In a spot-check of recent issues, <strong>ty</strong> and <strong>Pyrefly</strong> both had first responses from core maintainers within minutes to hours. <strong>Basedpyright</strong>&rsquo;s maintainer responds quickly too, though at a lower volume. <strong>Zuban</strong>&rsquo;s maintainer often replies within an hour.</p>
<h2 id="what-we-chose">What we chose
</h2>
<p>We bundled <strong>Pyrefly</strong> as Positron&rsquo;s default Python language server.</p>
<p>The deciding factors:</p>
<ul>
<li><strong>Pyrefly</strong>&rsquo;s clean design decisions felt like the best fit for Positron. The hover docs are rendered and hyperlinked, with sources for type inference. The type inference catches real bugs without requiring you to annotate everything. While it has the strictest type checking, this is configured to a moderate level by default.</li>
<li>It has active development with strong backing. Meta has committed to making <strong>Pyrefly</strong> genuinely open-source and community-driven, with biweekly office hours and a public Discord. Development velocity is high.</li>
<li>It is MIT licensed, which allows us to bundle it into Positron.</li>
</ul>
<p>It wasn&rsquo;t a runaway winner. <strong>Basedpyright</strong> is more mature and feature-complete. <strong>ty</strong> has a lot of long-term potential, especially for ruff users and fans of the gradual guarantee, and is closing feature gaps fast. But for the specific use case of &ldquo;Python data science in an IDE,&rdquo; <strong>Pyrefly</strong> had the best balance of features, UX, and readiness.</p>
<h2 id="how-to-switch">How to switch
</h2>
<p>This space is competitive and moving fast, and you shouldn&rsquo;t feel locked in. Positron makes it straightforward to switch language servers:</p>
<ol>
<li>Open the <strong>Extensions</strong> view (<code>Ctrl-Shift-X</code> (linux), <code>Ctrl-Shift-X</code> (windows), <code>Command-Shift-X</code> (mac)).</li>
<li>Search for and install the language server you want to try (e.g., <code>basedpyright</code>, <code>ty</code>, or <code>zuban</code>).</li>
<li>Disable <strong>Pyrefly</strong>: search for <code>pyrefly</code> in Extensions, click <strong>Disable</strong>.</li>
<li>Reload the window with the command <em>Developer: Reload Window</em>.</li>
</ol>
<p>Or, if you want to keep <strong>Pyrefly</strong> installed but prevent it from auto-activating, you can use the 






<a href="positron://settings/extensions.allowed"><code>extensions.allowed</code></a>
 setting:</p>
<div class="code-block"><div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;extensions.allowed&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nt">&#34;meta.pyrefly&#34;</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="nt">&#34;*&#34;</span><span class="p">:</span> <span class="kc">true</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></td></tr></table>
</div>
</div></div>
<h2 id="whats-next">What&rsquo;s next
</h2>
<p>We started bundling <strong>Pyrefly</strong> in November and have been quite pleased with the results. It solved some longstanding user-requested issues (like better semantic highlighting) and feels snappier to users than our previous internal solution.</p>
<p><strong>ty</strong> is adding features at an aggressive pace and will likely close its remaining gaps. OpenAI&rsquo;s acquisition of Astral adds resources but also uncertainty; it&rsquo;s unclear how it will affect <strong>ty</strong>&rsquo;s priorities. <strong>Pyrefly</strong> continues to improve its type checking and performance (a recent release noted 






<a href="https://github.com/facebook/pyrefly/releases/tag/0.57.0" target="_blank" rel="noopener">20% faster PyTorch benchmarks</a>
). <strong>Basedpyright</strong> tracks upstream Pyright closely and keeps shipping.</p>
<p>Both <strong>ty</strong> and <strong>Pyrefly</strong> have been receptive to PRs that improve the experience for Positron users, which suggests they care about working well across editors, not just VS Code. For example, both contribute hover, completions, and semantic highlighting in the Positron Console.</p>
<p>We&rsquo;ll keep evaluating as these tools mature! Want to try Positron? 






<a href="https://positron.posit.co/download.html" target="_blank" rel="noopener">Download it here</a>
.</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>Another LSP extension is Pylance, which may be familiar to VS Code users, but due to licensing restrictions, Code-OSS forks like Positron cannot use it.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:2">
<p><strong>Zuban</strong> requires a multi-step manual startup, so we couldn&rsquo;t measure this automatically.&#160;<a href="#fnref:2" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:3">
<p>Edit (2026-04-01): A previous version of this post undercounted the number of contributors to <strong>ty</strong>. The updated script to fetch stats lives 






<a href="https://github.com/posit-dev/positron-website/blob/main/blog/posts/2026-03-31-python-type-checkers/fetch_stats.py" target="_blank" rel="noopener">here</a>
.&#160;<a href="#fnref:3" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]></description>
      <enclosure url="https://opensource.posit.co/blog/2026-03-31_python-type-checkers/images/social.png" length="30311" type="image/png" />
    </item>
    <item>
      <title>tabpfn 0.1.0</title>
      <link>https://opensource.posit.co/blog/2026-03-31_tabpfn-0-1-0/</link>
      <pubDate>Tue, 31 Mar 2026 00:00:00 +0000</pubDate>
      <guid>https://opensource.posit.co/blog/2026-03-31_tabpfn-0-1-0/</guid>
      <dc:creator>Max Kuhn</dc:creator><description><![CDATA[<!--
TODO:
* [ ] Look over / edit the post's title in the yaml
* [ ] Edit (or delete) the description; note this appears in the Twitter card
* [ ] Pick category and tags (see existing with `hugodown::tidy_show_meta()`)
* [ ] Find photo & update yaml metadata
* [ ] Create `thumbnail-sq.jpg`; height and width should be equal
* [ ] Create `thumbnail-wd.jpg`; width should be >5x height
* [ ] `hugodown::use_tidy_thumbnails()`
* [ ] Add intro sentence, e.g. the standard tagline for the package
* [ ] `usethis::use_tidy_thanks()`
-->
<p>We&rsquo;re stoked to announce the release of 






<a href="https://tabpfn.tidymodels.org/" target="_blank" rel="noopener">tabpfn</a>
 0.1.0. 






<a href="https://github.com/PriorLabs/TabPFN" target="_blank" rel="noopener">TabPFN</a>
 is a precompiled deep learning Python model for prediction. The <em>R package tabpfn</em> is an interface to this model via reticulate.</p>
<p>You can install it from CRAN with:</p>
<div class="code-block"><div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">install.packages</span><span class="p">(</span><span class="s">&#34;tabpfn&#34;</span><span class="p">)</span></span></span></code></pre></td></tr></table>
</div>
</div></div>
<h2 id="what-is-tabpfn">What is TabPFN?
</h2>
<p>The &ldquo;tab&rdquo; means <em>tabular</em>, which is code for everyday rectangular data structures that we find in csv files and databases.</p>
<p>The &ldquo;pfn&rdquo; is more complicated &ndash; it stands for &ldquo;prior fitted network&rdquo;. The model is trained on fully synthetic datasets. The developers created a complex graph model that can simulate a wide variety of data-generating methods, including correlation structures, distributional skewness, missing-data mechanisms, interactions, latent variables, and more. It can also simulate random supervised relationships linking potential predictors to the outcome data. The training process for the model simulated a very large number of these data sets that, in effect, constitute a &ldquo;training set data point&rdquo;. For example, during training, if a batch size of 64 was used, that means 64 randomly generated datasets were used in that iteration.</p>
<p>From these data sets, a complex deep learning model is created that captures a huge number of possible relationships. The model is sophisticated enough and trained in a manner that allows it to effectively emulate Bayesian estimation.</p>
<p>When we use the pre-trained model, our training set matters, even though there is no new estimation. The model includes an 






<a href="https://en.wikipedia.org/wiki/Attention_%28machine_learning%29" target="_blank" rel="noopener">attention mechanism</a>
 that &ldquo;primes the model&rdquo; by focusing on the types of relationships in your training data. In that way, the pre-fitted network is deliberately biased to effectively predict our new samples. This leads to 






<a href="https://scholar.google.com/scholar?as_sdt=0%2C7&amp;as_vis=1&amp;q=%22in&#43;context&#43;learning%22" target="_blank" rel="noopener">in-context learning</a>
.</p>
<p>And it works; in fact, it works really well.</p>
<h2 id="license-for-the-underyling-model">License for the Underyling Model
</h2>
<p>






<a href="https://priorlabs.ai/" target="_blank" rel="noopener">PriorLabs</a>
 created TabPFN. Version 2.5 of the model, which contained several improvements, requires an API key for accessing the model parameter. Without one, an error occurs:</p>
<blockquote>
<p>This model is gated and requires you to accept its terms.  Please follow these steps: 1. Visit 






<a href="https://huggingface.co/Prior-Labs/tabpfn_2_5" target="_blank" rel="noopener">https://huggingface.co/Prior-Labs/tabpfn_2_5</a>
 in your browser and accept the terms of use. 2. Log in to your Hugging Face account via the command line by running: hf auth login (Alternatively, you can set the <code>HF_TOKEN</code> environment variable with a read token).</p>
</blockquote>
<p>The license includes provisions for &ldquo;Non-Commercial Use Only&rdquo; if you are just trying it out.</p>
<p>Instructions for installing the package and obtaining the API key are in the 


  
  
  





<a href="https://tabpfn.tidymodels.org/reference/tab_pfn.html#license-requirements" target="_blank" rel="noopener">package&rsquo;s manual</a>
.</p>
<p>Also, the model is most efficient when a GPU is available (by an order of magnitude or two). This may seem obvious to anyone already working with deep learning models, but it is a fairly new requirement for those strictly working with traditional tabular data models.</p>
<h2 id="usage">Usage
</h2>
<p>The syntax is idiomatic R: it supports fitting interfaces via data frames/vectors, formulas, and recipes. The standard R <code>predict()</code> method is used for prediction. <code>augument()</code> is also available for prediction.</p>
<p>When evaluating pre-trained models, there is a possibility that they may have memorized well-known datasets (e.g., Ames housing, Palmer penguins). TabPFN isn&rsquo;t trained that way, but just in case we are worried about that, we&rsquo;ll use lesser-known data. 






<a href="https://scholar.google.com/scholar?as_sdt=0%2C7&amp;q=Worley%2C&#43;B.&#43;A.&#43;%281987%29.&#43;%22Deterministic&#43;uncertainty&#43;analysis%22" target="_blank" rel="noopener">Worley (1987)</a>
 derived a mechanistic model for the flow rate of liquids from two aquifers positioned vertically (i.e., the &ldquo;upper&rdquo; and &ldquo;lower&rdquo; aquifers). We&rsquo;ll generate some of that data and add completely noisy predictors to increase the difficulty. The outcome is very skewed, so we&rsquo;ll log that too.</p>
<p>Additionally, we&rsquo;ll load the tidymodels library for simulation, data splitting, and visualization.</p>
<div class="code-block"><div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span><span class="lnt">7
</span><span class="lnt">8
</span><span class="lnt">9
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">tabpfn</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">tidymodels</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">probably</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">set.seed</span><span class="p">(</span><span class="m">17</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">aquifier_data</span> <span class="o">&lt;-</span>
</span></span><span class="line"><span class="cl"> <span class="nf">sim_regression</span><span class="p">(</span><span class="m">2000</span><span class="p">,</span>  <span class="n">method</span> <span class="o">=</span> <span class="s">&#34;worley_1987&#34;</span><span class="p">)</span> <span class="o">|&gt;</span>
</span></span><span class="line"><span class="cl"> <span class="nf">bind_cols</span><span class="p">(</span><span class="nf">sim_noise</span><span class="p">(</span><span class="m">2000</span><span class="p">,</span> <span class="m">50</span><span class="p">))</span> <span class="o">|&gt;</span>
</span></span><span class="line"><span class="cl"> <span class="nf">mutate</span><span class="p">(</span><span class="n">outcome</span> <span class="o">=</span> <span class="nf">log10</span><span class="p">(</span><span class="n">outcome</span><span class="p">))</span></span></span></code></pre></td></tr></table>
</div>
</div></div>
<p>We&rsquo;ll use a stratified 3:1 training and testing split:</p>
<div class="code-block"><div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">set.seed</span><span class="p">(</span><span class="m">8223</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">aquifier_split</span> <span class="o">&lt;-</span> <span class="nf">initial_split</span><span class="p">(</span><span class="n">aquifier_data</span><span class="p">,</span> <span class="n">strata</span> <span class="o">=</span> <span class="n">outcome</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">aquifier_split</span></span></span></code></pre></td></tr></table>
</div>
</div></div>
<div class="code-block"><pre tabindex="0"><code>## &lt;Training/Testing/Total&gt;
## &lt;1500/500/2000&gt;</code></pre></div>
<div class="code-block"><div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="n">aquifier_train</span> <span class="o">&lt;-</span> <span class="nf">training</span><span class="p">(</span><span class="n">aquifier_split</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">aquifier_test</span>  <span class="o">&lt;-</span> <span class="nf">testing</span><span class="p">(</span><span class="n">aquifier_split</span><span class="p">)</span></span></span></code></pre></td></tr></table>
</div>
</div></div>
<p>and &ldquo;fit&rdquo; the model:</p>
<div class="code-block"><div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="n">tab_fit</span> <span class="o">&lt;-</span> <span class="nf">tab_pfn</span><span class="p">(</span><span class="n">outcome</span> <span class="o">~</span> <span class="n">.,</span> <span class="n">data</span> <span class="o">=</span> <span class="n">aquifier_train</span><span class="p">)</span></span></span></code></pre></td></tr></table>
</div>
</div></div>
<p>Again, the model does not actually fit anything new. This computes the embeddings for the training set data and stores them for the prediction stage.</p>
<p>To make predictions, <code>predict()</code> returns the model&rsquo;s results. As previously mentioned, a GPU is not strictly required for these computations. However, if more than a trivial amount of data are being predicted, execution time can be very long.</p>
<p>Since we&rsquo;ll want to evaluate and plot the data, we&rsquo;ll use <code>augment()</code>, which just runs <code>predict()</code> and binds the results to the data being predicted:</p>
<div class="code-block"><div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="n">tab_pred</span> <span class="o">&lt;-</span> <span class="nf">augment</span><span class="p">(</span><span class="n">tab_fit</span><span class="p">,</span> <span class="n">aquifier_test</span><span class="p">)</span></span></span></code></pre></td></tr></table>
</div>
</div></div>
<p>How does it work?</p>
<div class="code-block"><div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="n">tab_pred</span> <span class="o">|&gt;</span> <span class="nf">metrics</span><span class="p">(</span><span class="n">outcome</span><span class="p">,</span> <span class="n">.pred</span><span class="p">)</span></span></span></code></pre></td></tr></table>
</div>
</div></div>
<div class="code-block"><pre tabindex="0"><code>## # A tibble: 3 × 3
##   .metric .estimator .estimate
##   &lt;chr&gt;   &lt;chr&gt;          &lt;dbl&gt;
## 1 rmse    standard      0.104 
## 2 rsq     standard      0.937 
## 3 mae     standard      0.0829</code></pre></div>
<div class="code-block"><div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="n">tab_pred</span> <span class="o">|&gt;</span> <span class="nf">cal_plot_regression</span><span class="p">(</span><span class="n">outcome</span><span class="p">,</span> <span class="n">.pred</span><span class="p">)</span></span></span></code></pre></td></tr></table>
</div>
</div></div>
<p><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://opensource.posit.co/blog/2026-03-31_tabpfn-0-1-0/figure/unnamed-chunk-6-1.png"
      alt="plot of chunk unnamed-chunk-6" 
      loading="lazy"
    >
  </figure></div>
</p>
<p>That looks good, especially with no training.</p>
<h2 id="next-steps">Next Steps
</h2>
<p>There is a lot more functionality to add to the package, including additional prediction types and interpretability tools. Many of these are available in 






<a href="https://github.com/priorlabs/tabpfn-extensions" target="_blank" rel="noopener">extensions</a>
.</p>
<p>We&rsquo;ll also add a new parsnip model type for TabPFN and other integrations with tidymodels in the summer.</p>
<h2 id="acknowledgements">Acknowledgements
</h2>
<p>A huge thanks to Tomasz Kalinowski and Daniel Falbel for their support on this and all of their hard work on reticulate and torch.</p>
<p>Thanks also to the contributors to date: 






<a href="https://github.com/frankiethull" target="_blank" rel="noopener">@frankiethull</a>
, 






<a href="https://github.com/mthulin" target="_blank" rel="noopener">@mthulin</a>
, and 






<a href="https://github.com/t-kalinowski" target="_blank" rel="noopener">@t-kalinowski</a>
.</p>
]]></description>
      <enclosure url="https://opensource.posit.co/blog/2026-03-31_tabpfn-0-1-0/thumbnail-wd.jpg" length="57422" type="image/jpeg" />
    </item>
    <item>
      <title>Typst Books, Article Layout, and `typst-gather`</title>
      <link>https://opensource.posit.co/blog/2026-03-31_typst-books-and-more/</link>
      <pubDate>Tue, 31 Mar 2026 00:00:00 +0000</pubDate>
      <guid>https://opensource.posit.co/blog/2026-03-31_typst-books-and-more/</guid>
      <dc:creator>Gordon Woodhull</dc:creator><description><![CDATA[<p>Typst is a lightning-fast typesetting system that provides a modern alternative to LaTeX.</p>
<p>The Typst ecosystem is thriving, and Quarto 1.9 brings Typst much closer to feature parity with LaTeX:</p>
<ul>
<li>Typst books</li>
<li>Article layout in Typst</li>
<li>Bundling of Typst packages for offline rendering</li>
</ul>
<h2 id="typst-books">Typst books
</h2>
<p>In Quarto 1.9, a project with type <code>book</code> and format <code>typst</code> is now rendered as a single document with multiple chapters and other book content.</p>
<div class="code-block code-with-filename" role="group" aria-labelledby="code-filename-0">
  <div class="code-with-filename-label" id="code-filename-0"><span class="font-mono text-sm">_quarto.yml</span></div><div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">project</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">book</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">book</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">title</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;My Book&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">author</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;Jane Doe&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">chapters</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">index.qmd</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">intro.qmd</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">summary.qmd</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">format</span><span class="p">:</span><span class="w"> </span><span class="l">typst</span></span></span></code></pre></td></tr></table>
</div>
</div></div>
<div class="grid gap-12 items-start md:grid-cols-4">
<div class="prose max-w-none">
<figure>
<img src="https://opensource.posit.co/blog/2026-03-31_typst-books-and-more/typst-book-part-page.png" data-fig-alt="A Typst book rendered with the orange-book extension, showing the part one page with a colored background and table of contents" alt="Part page" />
<figcaption aria-hidden="true">Part page</figcaption>
</figure>
</div>
<div class="prose max-w-none">
<figure>
<img src="https://opensource.posit.co/blog/2026-03-31_typst-books-and-more/typst-book-1.png" data-fig-alt="A Typst book rendered with the orange-book extension, showing the chapter one page with colored headers and sidebar navigation" alt="Chapter page" />
<figcaption aria-hidden="true">Chapter page</figcaption>
</figure>
</div>
<div class="prose max-w-none">
<figure>
<img src="https://opensource.posit.co/blog/2026-03-31_typst-books-and-more/typst-book-2.png" data-fig-alt="A Typst book rendered with the orange-book extension, showing the second page from chapter one with colored headers and sidebar navigation" alt="Chapter content" />
<figcaption aria-hidden="true">Chapter content</figcaption>
</figure>
</div>
<div class="prose max-w-none">
<figure>
<img src="https://opensource.posit.co/blog/2026-03-31_typst-books-and-more/typst-book-3.png" data-fig-alt="A Typst book rendered with the orange-book extension, showing the chapter two page with colored headers and sidebar navigation" alt="Next chapter" />
<figcaption aria-hidden="true">Next chapter</figcaption>
</figure>
</div>
</div>
<p>All book features previously available in the LaTeX format are now available in Typst:</p>
<ul>
<li>Parts and Chapters</li>
<li>Appendices</li>
<li>Cross-references and chapter-based numbering</li>
<li>Table of Contents</li>
</ul>
<p>List-of-Figures and List-of-Tables support is 






<a href="https://github.com/quarto-dev/quarto-cli/issues/14081" target="_blank" rel="noopener">coming soon</a>
.</p>
<p>The default Typst book uses the bundled Quarto 






<a href="https://github.com/quarto-ext/orange-book" target="_blank" rel="noopener">quarto-orange-book</a>
 extension, which uses 


  
  
  





<a href="#typst-gather"><code>typst-gather</code></a>
 to bundle the Typst 






<a href="https://typst.app/universe/package/orange-book" target="_blank" rel="noopener">orange-book</a>
 package. Orange-book provides a textbook-style layout with colored chapter headers and sidebars.</p>
<p>The orange-book extension supports 






<a href="https://quarto.org/docs/authoring/brand.html" target="_blank" rel="noopener">brand.yml</a>
 customization &mdash; it uses the <code>primary</code> color for chapter headers and sidebars, and the <code>medium</code> logo on the title page. The screenshots above were generated with this <code>_brand.yml</code>:</p>
<div class="code-block code-with-filename" role="group" aria-labelledby="code-filename-1">
  <div class="code-with-filename-label" id="code-filename-1"><span class="font-mono text-sm">_brand.yml</span></div><div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">color</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">primary</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;#F36619&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">secondary</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;#2E86AB&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">logo</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">images</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">test-logo</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">path</span><span class="p">:</span><span class="w"> </span><span class="l">logo.svg</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">alt</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;Test Logo&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">medium</span><span class="p">:</span><span class="w"> </span><span class="l">test-logo</span></span></span></code></pre></td></tr></table>
</div>
</div></div>
<p>Since Typst books are implemented as Quarto 






<a href="https://quarto.org/docs/extensions/formats.html" target="_blank" rel="noopener">Format Extensions</a>
, you can customize the appearance by creating your own extension. Typst partials define the overall book structure, while Lua filters handle the necessary AST transformations.</p>
<h2 id="article-layout-in-typst">Article layout in Typst
</h2>
<p>Also in Quarto 1.9, all 






<a href="https://quarto.org/docs/authoring/article-layout.html" target="_blank" rel="noopener">Article Layout</a>
 features now work in Typst, via the Typst 






<a href="https://typst.app/universe/package/marginalia/" target="_blank" rel="noopener">Marginalia</a>
 package.</p>
<p>Specifically:</p>
<ul>
<li>Figures, tables, code listings, and equations can be placed in the margin using the <code>.column-margin</code> class or the <code>column: margin</code> code cell option.</li>
<li>You can also target specific output types with <code>fig-column: margin</code> or <code>tbl-column: margin</code>.</li>
<li>Figure, table, and code listing captions can be placed in the margin with <code>cap-location: margin</code> (or <code>fig-cap-location: margin</code> and <code>tbl-cap-location: margin</code> for specific types).</li>
<li>Footnotes and citations can be displayed in the margin with <code>reference-location: margin</code> and <code>citation-location: margin</code>. When margin citations are enabled, the bibliography is suppressed.</li>
<li>Asides (<code>.aside</code> class) place content in the margin without a footnote number.</li>
</ul>
<div class="grid gap-12 items-start md:grid-cols-3">
<div class="prose max-w-none">
<figure>
<img src="https://opensource.posit.co/blog/2026-03-31_typst-books-and-more/typst-article.png" data-group="article" data-fig-alt="A page of a Typst article with a margin note and a margin figure using the Marginalia package" alt="Margin note and figure" />
<figcaption aria-hidden="true">Margin note and figure</figcaption>
</figure>
</div>
<div class="prose max-w-none">
<figure>
<img src="https://opensource.posit.co/blog/2026-03-31_typst-books-and-more/typst-article-2.png" data-group="article" data-fig-alt="A page of a Typst article using margin captions" alt="Margin captions" />
<figcaption aria-hidden="true">Margin captions</figcaption>
</figure>
</div>
<div class="prose max-w-none">
<figure>
<img src="https://opensource.posit.co/blog/2026-03-31_typst-books-and-more/typst-article-3.png" data-group="article" data-fig-alt="A page of a Typst article using margin references" alt="Margin references" />
<figcaption aria-hidden="true">Margin references</figcaption>
</figure>
</div>
</div>
<div class="callout callout-warning" role="note" aria-label="Warning">
<div class="callout-header">
<span class="callout-title">Books with article layout are functional, but need work</span>
</div>
<div class="callout-body">
<p>You can combine book and article layout, but there are some layout quirks when combining the two. We&rsquo;ll work with the orange-book author to integrate Marginalia into the book template.</p>
</div>
</div>
<h2 id="typst-gather"><code>typst-gather</code>
</h2>
<p>Quarto 1.9 automatically stages Typst packages &mdash; from your extensions, from Quarto&rsquo;s bundled extensions, and from Quarto itself &mdash; into the <code>.quarto/</code> cache directory before calling <code>typst compile</code>. This means Typst documents render offline without needing network access.</p>
<p>To make this work, extension authors use the new 






<a href="https://quarto.org/docs/advanced/typst/typst-gather.html" target="_blank" rel="noopener"><code>typst-gather</code></a>
 tool, which scans their <code>.typ</code> files for <code>@preview</code> imports and downloads the packages into the extension directory. Authors run <code>quarto call typst-gather</code> and commit the results. Users of the extension will have the packages staged without any downloads.</p>
<p>This means 


  
  
  





<a href="https://quarto.org/docs/output-formats/typst-custom.html#custom-formats" target="_blank" rel="noopener">Custom Typst Formats</a>
 can depend on Typst packages without copying and pasting Typst code, making them simpler and easier to maintain.</p>
<p>Both Typst books and article layout are built on <code>typst-gather</code> &mdash; orange-book depends on the Typst 






<a href="https://typst.app/universe/package/orange-book" target="_blank" rel="noopener">orange-book</a>
 package, and article layout depends on 






<a href="https://typst.app/universe/package/marginalia/" target="_blank" rel="noopener">Marginalia</a>
. As the Typst package ecosystem grows, we&rsquo;re excited to see what the community builds with Typst packages.</p>
]]></description>
      <enclosure url="https://opensource.posit.co/blog/2026-03-31_typst-books-and-more/typst-article-landscape.png" length="409281" type="image/png" />
    </item>
    <item>
      <title>Quarto 1.9</title>
      <link>https://opensource.posit.co/blog/2026-03-24_1.9-release/</link>
      <pubDate>Tue, 24 Mar 2026 00:00:00 +0000</pubDate>
      <guid>https://opensource.posit.co/blog/2026-03-24_1.9-release/</guid>
      <dc:creator>Charlotte Wickham</dc:creator><description><![CDATA[<p>Quarto 1.9 is out! You can get the current release from the 






<a href="https://quarto.org/docs/download/index.html" target="_blank" rel="noopener">download page</a>
.</p>
<p>Sharing your work just got easier with integrated Posit Connect Cloud publishing. Typst users will appreciate book project support and article layouts, while experimental PDF accessibility standards bring PDF/A and PDF/UA compliance to both LaTeX and Typst. This release also introduces LLM-friendly output for websites, the <code>quarto use brand</code> command for keeping your brand assets in sync, and list tables for authoring complex tables with familiar bullet syntax.</p>
<p>You can read about these improvements and some other highlights below. You can find all the changes in this version in the 






<a href="https://quarto.org/docs/download/changelog/1.9/" target="_blank" rel="noopener">Release Notes</a>
.</p>
<h2 id="publish-to-posit-connect-cloud">Publish to Posit Connect Cloud
</h2>
<p>You can now publish documents and websites to 






<a href="https://connect.posit.cloud" target="_blank" rel="noopener">Posit Connect Cloud</a>
 directly from the command line.
For example, publish your Quarto website project with:</p>
<div class="code-block code-with-filename" role="group" aria-labelledby="code-filename-0">
  <div class="code-with-filename-label" id="code-filename-0"><span class="font-mono text-sm">Terminal</span></div><div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">quarto publish posit-connect-cloud</span></span></code></pre></td></tr></table>
</div>
</div></div>
<p>Posit Connect Cloud is a hosted platform for sharing data applications and documents without managing your own infrastructure. It includes a free tier for unlimited static document publishing.
Read more in 






<a href="https://quarto.org/docs/publishing/posit-connect-cloud.html" target="_blank" rel="noopener">Publishing &gt; Posit Connect Cloud</a>
.</p>
<h2 id="improvements-to-typst-support">Improvements to Typst Support
</h2>
<p>Quarto 1.9 brings substantial improvements to Typst output:</p>
<ul>
<li>


  
  
  





<a href="https://quarto.org/docs/books/book-output.html#typst-output" target="_blank" rel="noopener">Book projects</a>
 can now render to Typst via the bundled <code>orange-book</code> extension, with chapter numbering, cross-references, and professional textbook styling.</li>
<li>






<a href="https://quarto.org/docs/authoring/article-layout.html" target="_blank" rel="noopener">Article layout</a>
 support lets you place content in the margins, create full-width figures, or add side notes.</li>
<li>New options: <code>mathfont</code>, <code>codefont</code>, <code>linestretch</code>, <code>linkcolor</code>, <code>citecolor</code>, <code>filecolor</code>, <code>thanks</code>, and <code>abstract-title</code>.</li>
<li>


  
  
  





<a href="https://quarto.org/docs/output-formats/typst.html#theorems" target="_blank" rel="noopener">Theorem styling</a>
 with four appearance options: <code>simple</code>, <code>fancy</code>, <code>clouds</code>, or <code>rainbow</code>.</li>
</ul>
<p>See 





  


  
  
    
  

<a href="https://opensource.posit.co/blog/2026-03-31_typst-books-and-more/">this blog post</a>
 for details on all the Typst improvements.</p>
<h2 id="pdf-accessibility-experimental">PDF Accessibility (Experimental)
</h2>
<p>We&rsquo;re rolling out experimental support for PDF accessibility standards in 1.9. The new <code>pdf-standard</code> option enables PDF/A archival formats and PDF/UA accessibility compliance for both LaTeX and Typst outputs. Alt text from <code>fig-alt</code> attributes now passes through to PDF for screen reader support, and Typst gains support for alt text on cross-referenced equations.</p>
<p>Read more in our 





  


  
  
    
  

<a href="https://opensource.posit.co/blog/2026-03-05_pdf-accessibility-and-standards/">PDF Accessibility and Standards blog post</a>
 or the documentation for 


  
  
  





<a href="https://quarto.org/docs/output-formats/pdf-basics.html#pdf-accessibility-standards" target="_blank" rel="noopener">LaTeX</a>
 and 


  
  
  





<a href="https://quarto.org/docs/output-formats/typst.html#pdf-accessibility-standards" target="_blank" rel="noopener">Typst</a>
.</p>
<h2 id="output-for-llms">Output for LLMs
</h2>
<p>Quarto can now generate 






<a href="https://llmstxt.org/" target="_blank" rel="noopener">llms.txt</a>
 format output for your website, making your content more accessible to large language models and AI-powered tools.</p>
<p>Enable it in your website configuration:</p>
<div class="code-block code-with-filename" role="group" aria-labelledby="code-filename-1">
  <div class="code-with-filename-label" id="code-filename-1"><span class="font-mono text-sm">_quarto.yml</span></div><div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">website</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">title</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;My Documentation&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">llms-txt</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span></span></span></code></pre></td></tr></table>
</div>
</div></div>
<p>When you render your site, Quarto creates:</p>
<ul>
<li>An <code>llms.txt</code> index file at the root of your site listing all pages</li>
<li>A <code>.llms.md</code> markdown file alongside each HTML page (e.g., <code>guide.html</code> gets <code>guide.llms.md</code>)</li>
</ul>
<p>The markdown files contain clean versions of your content&mdash;navigation, sidebars, and scripts are stripped out; tables, code blocks, and callouts are converted to standard markdown.</p>
<p>Read more, including how to customize what appears in LLM output, in 






<a href="https://quarto.org/docs/websites/website-llms.html" target="_blank" rel="noopener">Websites &gt; Output for LLMs</a>
.</p>
<h2 id="quarto-use-brand-command"><code>quarto use brand</code> Command
</h2>
<p>Keep your project&rsquo;s brand assets in sync with an external source using the new <code>quarto use brand</code> command:</p>
<div class="code-block code-with-filename" role="group" aria-labelledby="code-filename-2">
  <div class="code-with-filename-label" id="code-filename-2"><span class="font-mono text-sm">Terminal</span></div><div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">quarto use brand myorg/shared-brand</span></span></code></pre></td></tr></table>
</div>
</div></div>
<p>The command copies brand files from a GitHub repository, local directory, or zip archive into your project&rsquo;s <code>_brand/</code> directory. Quarto walks you through each step&mdash;confirming trust for remote sources, creating the directory if needed, and asking whether to overwrite or remove files.</p>
<p>See 


  
  
  





<a href="https://quarto.org/docs/authoring/brand.html#quarto-use-brand" target="_blank" rel="noopener">Guide &gt; Brand</a>
 for <code>--dry-run</code>, <code>--force</code>, and other options.</p>
<h2 id="list-tables">List Tables
</h2>
<p>List tables provide a new syntax for creating tables with complex content&mdash;multiple paragraphs, code blocks, or nested lists&mdash;using familiar bullet syntax instead of grid table formatting:</p>
<div class="grid gap-12 items-start md:grid-cols-2">
<div class="prose max-w-none">
<div class="code-block"><div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-markdown" data-lang="markdown"><span class="line"><span class="cl">::: {.list-table}
</span></span><span class="line"><span class="cl"><span class="k">-</span> - Function
</span></span><span class="line"><span class="cl">  <span class="k">-</span> Description
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">-</span> - <span class="sb">`sum()`</span>
</span></span><span class="line"><span class="cl">  <span class="k">-</span> Add values:
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    ``<span class="sb">`python
</span></span></span><span class="line"><span class="cl"><span class="sb">    sum([1, 2, 3])
</span></span></span><span class="line"><span class="cl"><span class="sb">    `</span>`<span class="sb">`
</span></span></span><span class="line"><span class="cl"><span class="sb">
</span></span></span><span class="line"><span class="cl"><span class="sb">- - `</span>len()`
</span></span><span class="line"><span class="cl">  <span class="k">-</span> Count items:
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">-</span> Works on lists
</span></span><span class="line"><span class="cl">    <span class="k">-</span> Works on strings
</span></span><span class="line"><span class="cl">:::</span></span></code></pre></td></tr></table>
</div>
</div></div>
</div>
<div class="prose max-w-none">
<table>
<thead>
<tr>
<th>Function</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><p><code>sum()</code></p></td>
<td><p>Add values:</p>
<div class="sourceCode" id="cb1"><pre class="sourceCode python"><code class="sourceCode python"><span id="cb1-1"><a href="#cb1-1" aria-hidden="true" tabindex="-1"></a><span class="bu">sum</span>([<span class="dv">1</span>, <span class="dv">2</span>, <span class="dv">3</span>])</span></code></pre></div></td>
</tr>
<tr>
<td><p><code>len()</code></p></td>
<td><p>Count items:</p>
<ul>
<li>Works on lists</li>
<li>Works on strings</li>
</ul></td>
</tr>
</tbody>
</table>
</div>
</div>
<p>Each top-level bullet represents a row; nested bullets represent cells. This syntax is much easier to maintain than grid tables, especially when cells contain code or other block elements.</p>
<p>List tables support all the usual table features: captions, cross-references, column widths, and alignment. Thanks to Martin Fischer for the original development, with contributions from Albert Krewinkel and William Lupton.</p>
<p>Find all the details in 


  
  
  





<a href="https://quarto.org/docs/authoring/tables.html#list-tables" target="_blank" rel="noopener">Guide &gt; Tables</a>
.</p>
<h2 id="other-highlights">Other Highlights
</h2>
<ul>
<li>
<p>


  
  
  





<a href="https://quarto.org/docs/websites/website-search.html#search-result-highlighting" target="_blank" rel="noopener">Search Result Highlighting</a>
: Improved highlighting of search terms on destination pages, with persistent marks, automatic tab activation for matches inside tabsets, and cross-element highlighting for multi-word searches.</p>
</li>
<li>
<p>Privacy-focused features for websites:</p>
<ul>
<li>
<p>


  
  
  





<a href="https://quarto.org/docs/websites/website-tools.html#cookie-consent" target="_blank" rel="noopener">A privacy-first default for cookie consent</a>
: The default for cookie consent has changed to <code>type: express</code>, providing opt-in consent that blocks cookies until users explicitly agree. This privacy-conscious default is designed with modern privacy regulations in mind.</p>
</li>
<li>
<p>


  
  
  





<a href="https://quarto.org/docs/websites/website-search.html#cookie-consent-and-user-tracking" target="_blank" rel="noopener">Algolia Search Insights avoids cookies</a>
: Use Algolia Insights now uses persistent cookies only if <code>cookie-consent</code> is active, and the user has opted-in.</p>
</li>
<li>
<p>


  
  
  





<a href="https://quarto.org/docs/websites/website-tools.html#plausible-analytics" target="_blank" rel="noopener">Use Plausible Analytics</a>
: Add privacy-friendly Plausible Analytics to websites via the <code>plausible-analytics</code> configuration option.</p>
</li>
</ul>
</li>
<li>
<p>


  
  
  





<a href="https://quarto.org/docs/authoring/videos.html#accessibility-label" target="_blank" rel="noopener"><code>aria-label</code> for videos</a>
: Improve accessibility of embedded videos by providing custom descriptive labels for screen readers instead of the default &ldquo;Video Player&rdquo; label.</p>
</li>
<li>
<p>


  
  
  





<a href="https://quarto.org/docs/output-formats/pdf-basics.html#syntax-highlighting" target="_blank" rel="noopener">New <code>syntax-highlighting</code> Option</a>
: Replaces the deprecated <code>highlight-style</code> (Pandoc 3.8). Supports style names, custom <code>.theme</code> files, <code>none</code>, or <code>idiomatic</code> for format-native highlighting.</p>
</li>
<li>
<p>Metadata and brand extensions now work without a <code>_quarto.yml</code> project. A temporary default project is created in memory.</p>
</li>
<li>
<p>






<a href="https://quarto.org/docs/extensions/engine.html" target="_blank" rel="noopener">Engine extensions</a>
 allow replacement of the execution engine:</p>
<ul>
<li>Julia is now a bundled extension instead of being built-in.</li>
<li><strong>quarto-marimo</strong> will soon change from a filter extension to an engine extension.</li>
<li>New <code>quarto create extension engine</code> command.</li>
<li>New <code>quarto call build-ts-extension</code> command.</li>
<li>New <strong>Quarto API</strong> for engine extensions to use. (This is in flux and will not be documented for the next few releases, but 






<a href="https://quarto-dev.github.io/dev-notes/posts/2026-03-04/" target="_blank" rel="noopener">there is a dev blog post about it</a>
.)</li>
</ul>
</li>
</ul>
<p>Dependency updates:</p>
<ul>
<li><code>pandoc</code> updated to 3.8.3</li>
<li><code>typst</code> updated to 0.14.2</li>
<li><code>esbuild</code> updated to 0.25.10</li>
<li><code>deno</code> updated to 2.4.5</li>
<li><code>mermaid</code> updated to 11.12.0</li>
</ul>
<h2 id="acknowledgements">Acknowledgements
</h2>
<p>One of the early proposals for PDF accessibility and alt text in the LaTeX ecosystem was provided to us by 






<a href="https://github.com/Schiano-NOAA" target="_blank" rel="noopener">Sam Schiano</a>
 and 






<a href="https://github.com/sbreitbart-NOAA" target="_blank" rel="noopener">Sophie Breitbart</a>
. We want to thank them for bringing into our attention the approach they used in their 






<a href="https://nmfs-ost.github.io/asar/" target="_blank" rel="noopener"><code>{asar}</code> R package</a>
, which influenced some of our design.</p>
<p>In addition, we&rsquo;d like to say a huge thank you to everyone who contributed to this release by opening issues and pull requests:</p>
<p>






<a href="https://github.com/CoryMcCartan" target="_blank" rel="noopener">CoryMcCartan</a>
,







<a href="https://github.com/DanChaltiel" target="_blank" rel="noopener">DanChaltiel</a>
,







<a href="https://github.com/Data-Wise" target="_blank" rel="noopener">Data-Wise</a>
,







<a href="https://github.com/FrankwaP" target="_blank" rel="noopener">FrankwaP</a>
,







<a href="https://github.com/Joao-O-Santos" target="_blank" rel="noopener">Joao-O-Santos</a>
,







<a href="https://github.com/LukasDSauer" target="_blank" rel="noopener">LukasDSauer</a>
,







<a href="https://github.com/MBe-iUS" target="_blank" rel="noopener">MBe-iUS</a>
,







<a href="https://github.com/MarcoPortmann" target="_blank" rel="noopener">MarcoPortmann</a>
,







<a href="https://github.com/MariaBarrioSchez" target="_blank" rel="noopener">MariaBarrioSchez</a>
,







<a href="https://github.com/MateusMolina" target="_blank" rel="noopener">MateusMolina</a>
,







<a href="https://github.com/Selbosh" target="_blank" rel="noopener">Selbosh</a>
,







<a href="https://github.com/ThePurox" target="_blank" rel="noopener">ThePurox</a>
,







<a href="https://github.com/TucoFernandes" target="_blank" rel="noopener">TucoFernandes</a>
,







<a href="https://github.com/aecoleman" target="_blank" rel="noopener">aecoleman</a>
,







<a href="https://github.com/amirhome61" target="_blank" rel="noopener">amirhome61</a>
,







<a href="https://github.com/andrewheiss" target="_blank" rel="noopener">andrewheiss</a>
,







<a href="https://github.com/azankl" target="_blank" rel="noopener">azankl</a>
,







<a href="https://github.com/bensoltoff" target="_blank" rel="noopener">bensoltoff</a>
,







<a href="https://github.com/bruvellu" target="_blank" rel="noopener">bruvellu</a>
,







<a href="https://github.com/byzheng" target="_blank" rel="noopener">byzheng</a>
,







<a href="https://github.com/cbrnr" target="_blank" rel="noopener">cbrnr</a>
,







<a href="https://github.com/chendaniely" target="_blank" rel="noopener">chendaniely</a>
,







<a href="https://github.com/chi-raag" target="_blank" rel="noopener">chi-raag</a>
,







<a href="https://github.com/christopherkenny" target="_blank" rel="noopener">christopherkenny</a>
,







<a href="https://github.com/coatless" target="_blank" rel="noopener">coatless</a>
,







<a href="https://github.com/cynthiahqy" target="_blank" rel="noopener">cynthiahqy</a>
,







<a href="https://github.com/darwindarak" target="_blank" rel="noopener">darwindarak</a>
,







<a href="https://github.com/davidskalinder" target="_blank" rel="noopener">davidskalinder</a>
,







<a href="https://github.com/dmenne" target="_blank" rel="noopener">dmenne</a>
,







<a href="https://github.com/fconil" target="_blank" rel="noopener">fconil</a>
,







<a href="https://github.com/fkgruber" target="_blank" rel="noopener">fkgruber</a>
,







<a href="https://github.com/fkohrt" target="_blank" rel="noopener">fkohrt</a>
,







<a href="https://github.com/fredguth" target="_blank" rel="noopener">fredguth</a>
,







<a href="https://github.com/gadenbuie" target="_blank" rel="noopener">gadenbuie</a>
,







<a href="https://github.com/apps/github-actions" target="_blank" rel="noopener">github-actions[bot]</a>
,







<a href="https://github.com/gsathler-vi" target="_blank" rel="noopener">gsathler-vi</a>
,







<a href="https://github.com/hamgamb" target="_blank" rel="noopener">hamgamb</a>
,







<a href="https://github.com/herosi" target="_blank" rel="noopener">herosi</a>
,







<a href="https://github.com/icarusz" target="_blank" rel="noopener">icarusz</a>
,







<a href="https://github.com/idavydov" target="_blank" rel="noopener">idavydov</a>
,







<a href="https://github.com/jeremy886" target="_blank" rel="noopener">jeremy886</a>
,







<a href="https://github.com/jkrumbiegel" target="_blank" rel="noopener">jkrumbiegel</a>
,







<a href="https://github.com/jmcphers" target="_blank" rel="noopener">jmcphers</a>
,







<a href="https://github.com/jonas37" target="_blank" rel="noopener">jonas37</a>
,







<a href="https://github.com/jorherre" target="_blank" rel="noopener">jorherre</a>
,







<a href="https://github.com/jreades" target="_blank" rel="noopener">jreades</a>
,







<a href="https://github.com/jromanowska" target="_blank" rel="noopener">jromanowska</a>
,







<a href="https://github.com/jtbayly" target="_blank" rel="noopener">jtbayly</a>
,







<a href="https://github.com/juleswg23" target="_blank" rel="noopener">juleswg23</a>
,







<a href="https://github.com/juliasilge" target="_blank" rel="noopener">juliasilge</a>
,







<a href="https://github.com/kathsherratt" target="_blank" rel="noopener">kathsherratt</a>
,







<a href="https://github.com/kusnezoff-alexander" target="_blank" rel="noopener">kusnezoff-alexander</a>
,







<a href="https://github.com/lrrichter" target="_blank" rel="noopener">lrrichter</a>
,







<a href="https://github.com/lwjohnst86" target="_blank" rel="noopener">lwjohnst86</a>
,







<a href="https://github.com/maelle" target="_blank" rel="noopener">maelle</a>
,







<a href="https://github.com/matthiasbaitsch" target="_blank" rel="noopener">matthiasbaitsch</a>
,







<a href="https://github.com/mipmip" target="_blank" rel="noopener">mipmip</a>
,







<a href="https://github.com/mstrms2000" target="_blank" rel="noopener">mstrms2000</a>
,







<a href="https://github.com/multimeric" target="_blank" rel="noopener">multimeric</a>
,







<a href="https://github.com/mvuorre" target="_blank" rel="noopener">mvuorre</a>
,







<a href="https://github.com/mykolaskrynnyk" target="_blank" rel="noopener">mykolaskrynnyk</a>
,







<a href="https://github.com/nichtich" target="_blank" rel="noopener">nichtich</a>
,







<a href="https://github.com/nithinmkp" target="_blank" rel="noopener">nithinmkp</a>
,







<a href="https://github.com/nrichers" target="_blank" rel="noopener">nrichers</a>
,







<a href="https://github.com/orbsmiv" target="_blank" rel="noopener">orbsmiv</a>
,







<a href="https://github.com/paytonej" target="_blank" rel="noopener">paytonej</a>
,







<a href="https://github.com/petrelharp" target="_blank" rel="noopener">petrelharp</a>
,







<a href="https://github.com/phongphuhanam" target="_blank" rel="noopener">phongphuhanam</a>
,







<a href="https://github.com/pm-gusmano" target="_blank" rel="noopener">pm-gusmano</a>
,







<a href="https://github.com/posit-snyk-bot" target="_blank" rel="noopener">posit-snyk-bot</a>
,







<a href="https://github.com/prosoitos" target="_blank" rel="noopener">prosoitos</a>
,







<a href="https://github.com/rabyj" target="_blank" rel="noopener">rabyj</a>
,







<a href="https://github.com/sasja-san" target="_blank" rel="noopener">sasja-san</a>
,







<a href="https://github.com/sbwiecko" target="_blank" rel="noopener">sbwiecko</a>
,







<a href="https://github.com/serialc" target="_blank" rel="noopener">serialc</a>
,







<a href="https://github.com/spaette" target="_blank" rel="noopener">spaette</a>
,







<a href="https://github.com/spraetor" target="_blank" rel="noopener">spraetor</a>
,







<a href="https://github.com/stragu" target="_blank" rel="noopener">stragu</a>
,







<a href="https://github.com/szimmer" target="_blank" rel="noopener">szimmer</a>
,







<a href="https://github.com/the-solipsist" target="_blank" rel="noopener">the-solipsist</a>
,







<a href="https://github.com/thomasp85" target="_blank" rel="noopener">thomasp85</a>
,







<a href="https://github.com/yyzeng" target="_blank" rel="noopener">yyzeng</a>
,







<a href="https://github.com/zhe00a" target="_blank" rel="noopener">zhe00a</a>
.</p>
<p>The airplane departure emoji in the 






<a href="thumbnail.png">listing and social card image</a>
 for this post comes from <a href="https://openmoji.org/" class="external">OpenMoji</a>&ndash; the open-source emoji and icon project. License: <a href="https://creativecommons.org/licenses/by-sa/4.0/#" class="external">CC BY-SA 4.0</a></p>
]]></description>
      <enclosure url="https://opensource.posit.co/blog/2026-03-24_1.9-release/thumbnail.png" length="57935" type="image/png" />
    </item>
    <item>
      <title>2026 Posit Internships</title>
      <link>https://opensource.posit.co/blog/2026-03-20_2026-internships/</link>
      <pubDate>Fri, 20 Mar 2026 00:00:00 +0000</pubDate>
      <guid>https://opensource.posit.co/blog/2026-03-20_2026-internships/</guid>
      <dc:creator>Max Kuhn</dc:creator>
      <dc:creator>Carson Sievert</dc:creator>
      <dc:creator>Tomasz Kalinowski</dc:creator>
      <dc:creator>Andrew Holz</dc:creator><description><![CDATA[<!--
TODO:
* [ ] Look over / edit the post's title in the yaml
* [ ] Edit (or delete) the description; note this appears in the Twitter card
* [ ] Pick category and tags (see existing with `hugodown::tidy_show_meta()`)
* [ ] Find photo & update yaml metadata
* [ ] Create `thumbnail-sq.jpg`; height and width should be equal
* [ ] Create `thumbnail-wd.jpg`; width should be >5x height
* [ ] `hugodown::use_tidy_thumbnails()`
* [ ] Add intro sentence, e.g. the standard tagline for the package
* [ ] `usethis::use_tidy_thanks()`
-->
<p>We are once again chuffed to offer summer internships.</p>
<p>Our internship program has been a great success over the years. If you want to know what it is like, many of our alumni have written about it:</p>
<ul>
<li>2016: 






<a href="https://www.data-imaginist.com/posts/2016-10-31-becoming-the-intern/" target="_blank" rel="noopener">Thomas Lin Pederson</a>
</li>
<li>2017: 






  
  

<a href="https://opensource.posit.co/blog/2017-09-08_lucy-internship/">Lucy D&rsquo;Agostino McGowan</a>
 and 






  
  

<a href="https://opensource.posit.co/blog/2017-09-08_ggplot2-internship/">Kara Woo</a>
</li>
<li>2018: 






<a href="https://www.alexpghayes.com/post/2018-08-10_a-summer-with-rstudio/" target="_blank" rel="noopener">Alex Hayes</a>
, 






<a href="https://fbchow.rbind.io/2018/07/27/rstudio-summer-internship/" target="_blank" rel="noopener">Fanny Chow</a>
, 






<a href="https://irene.rbind.io/post/summer-rstudio/" target="_blank" rel="noopener">Irene Steves</a>
, and 






<a href="https://www.danaseidel.com/2018-09-01-ATidySummer/" target="_blank" rel="noopener">Dana Paige Seidel</a>
</li>
<li>2019: 






  
  

<a href="https://opensource.posit.co/blog/2019-12-02_this-is-not-like-the-others/">Marly Gotti</a>
 and 






<a href="https://dewey.dunnington.ca/post/2019/a-summer-of-rstudio-and-ggplot2/" target="_blank" rel="noopener">Dewey Dunnington</a>
</li>
<li>2020: 






  
  

<a href="https://opensource.posit.co/blog/2020-06-30_tidymodels-internship/">Simon Couch</a>
</li>
<li>2022: 






<a href="https://www.mm218.dev/posts/2022-08-15-last-summer/" target="_blank" rel="noopener">Mike Mahoney</a>
</li>
<li>2025: 






<a href="https://franceslinyc.github.io/posts/2025-08-20-posit-internship/" target="_blank" rel="noopener">Frances Lin</a>
.</li>
</ul>
<p>Three past interns are current Posit employees: Thomas Lin Pederson, Kara Woo, and Simon Couch.</p>
<h2 id="2026-positions">2026 Positions
</h2>
<p>This year, we have four positions in different groups. The positions are US-based and range from 10-12 weeks, starting on May 26, 2026. See the link at the bottom for the details.</p>
<h3 id="skills-and-evals-intern-pydata-team">Skills and Evals Intern (PyData Team)
</h3>
<p>The PyData team is looking for an intern to help make AI agents better at using our Python open-source projects by writing skills and evaluations for common user tasks.</p>
<p>The core of the role is to identify the tasks users perform with our tools — such as 






<a href="https://plotnine.org/" target="_blank" rel="noopener">Plotnine</a>
 and 






<a href="https://posit-dev.github.io/great-tables/articles/intro.html" target="_blank" rel="noopener">Great Tables</a>
 — translate them into clear skill definitions that agents can use, and build evaluations that measure whether agents can reliably complete those tasks. This includes writing prompts, creating example workflows, and developing automated tests that measure how well agents perform. A major focus will be on applying the emerging skills format, while the broader goal is to improve documentation, examples, and API design across the PyData ecosystem in ways that make our tools work better with AI-assisted workflows.</p>
<h3 id="r-modeling-intern-tidymodels-team">R Modeling Intern (Tidymodels Team)
</h3>
<p>The 






<a href="https://www.tidymodels.org/" target="_blank" rel="noopener">tidymodels</a>
 R internship is focused on different tasks, including: expand content on 






<a href="https://www.tidymodels.org/" target="_blank" rel="noopener">tidymodels.org</a>
, expanding tabular deep learning models (in 






<a href="https://brulee.tidymodels.org/" target="_blank" rel="noopener">brulee</a>
), additional performance metrics for 


  
  
  





<a href="https://www.tidymodels.org/learn/#category=survival%20analysis" target="_blank" rel="noopener">survival analysis models</a>
, modernizing the 






<a href="https://topepo.github.io/caret/" target="_blank" rel="noopener">caret package</a>
, and/or Rust bindings for predictive models. The intern is welcome to suggest R-based projects focused on modeling and/or data analysis.</p>
<h3 id="shiny-accessibility-and-testing-intern-shiny-team">Shiny Accessibility and Testing Intern (Shiny Team)
</h3>
<p>The 






<a href="https://shiny.posit.co/" target="_blank" rel="noopener">Shiny</a>
 team is looking for an intern to help advance accessibility and testing across the Shiny framework. You&rsquo;ll audit Shiny components against Web Content Accessibility Guidelines (WCAG), implement fixes, improve test coverage, and contribute to documentation that helps the broader community build accessible Shiny apps.</p>
<p>Some of the harder problems in this role aren&rsquo;t strictly code problems. Shiny&rsquo;s components are built for flexible, abstract usage, so you can&rsquo;t always anticipate how they&rsquo;ll end up on a page. Making them accessible means understanding HTML semantics and WCAG well enough to exercise good judgment and make sensible compromises when there isn&rsquo;t one clear right answer. Candidates should be comfortable with Git and GitHub, have solid working knowledge of HTML/CSS, and have experience in at least one of R, Python, or JavaScript. Familiarity with WCAG, assistive technology, automated testing frameworks, or open-source workflows is a plus.</p>
<h3 id="software-engineering-intern-posit-connect-team">Software Engineering Intern (Posit Connect Team)
</h3>
<p>The 






<a href="https://posit.co/products/enterprise/connect/" target="_blank" rel="noopener">Posit Connect</a>
 team is looking for an intern to contribute to the development and quality of Connect, Posit&rsquo;s professional platform for publishing and sharing data science and AI applications at scale. The primary focus of the internship will be to contribute reports and applications to the 






<a href="https://docs.posit.co/connect/user/publishing-connect-gallery/" target="_blank" rel="noopener">Connect Gallery</a>
, an open-source collection of useful extensions and example content. These Python, R, and 






<a href="https://quarto.org/" target="_blank" rel="noopener">Quarto</a>
 projects help data science teams realize the full potential of the product and allow us to experiment with new features. In the process of building these apps, you will have the opportunity to contribute to the Connect product as well.</p>
<h2 id="applying">Applying
</h2>
<p>To apply, make sure that you have a GitHub handle and follow this link:</p>
<p><strong>






<a href="https://posit.co/job-detail/?gh_jid=7674250003" target="_blank" rel="noopener"><code>https://posit.co/job-detail/?gh_jid=7674250003</code></a>
</strong></p>
<p>We can&rsquo;t wait to get started and look forward to reading your applications.</p>
]]></description>
      <enclosure url="https://opensource.posit.co/blog/2026-03-20_2026-internships/thumbnail-wd.jpg" length="36135" type="image/jpeg" />
    </item>
    <item>
      <title>Native Jupyter Notebook Support Has Arrived in Positron</title>
      <link>https://opensource.posit.co/blog/2026-03-16_notebooks-march-announcement/</link>
      <pubDate>Mon, 16 Mar 2026 00:00:00 +0000</pubDate>
      <guid>https://opensource.posit.co/blog/2026-03-16_notebooks-march-announcement/</guid>
      <dc:creator>Cindy Tong</dc:creator><description><![CDATA[<p>Positron now ships with a native 






<a href="https://positron.posit.co/positron-notebook-editor.html" target="_blank" rel="noopener">Jupyter Notebook Editor</a>
, a new unified experience we built from the ground up for working with Jupyter notebooks within Positron.</p>
<h2 id="why-we-built-our-own-notebook-editor">Why we built our own notebook editor
</h2>
<p>We built the Positron Notebook Editor to treat your .ipynb files as first-class citizens in an IDE tailored specifically for data science workflows.</p>
<p>Up to this point, Positron used the 






<a href="https://positron.posit.co/legacy-notebook-editor.html" target="_blank" rel="noopener">legacy Code OSS notebook editor</a>
 that powers VS Code. While functional, this editor was designed for general-purpose development and not specifically for data science workflows. The tradeoffs show up in small ways that compound over time: limited context for AI assistance, no deep integration with your variables or data, and a user experience that treats <code>.ipynb</code> files as just another file type.</p>
<p>We wanted notebooks to feel like a first-class part of a data science IDE, so we built our own native notebook editor.</p>
<p>If you missed the 






<a href="https://posit.co/blog/announcing-the-positron-notebook-editor-for-jupyter-notebooks/" target="_blank" rel="noopener">original February announcement</a>
, that post covers our initial reasoning in more detail.</p>
<figure>
<img src="https://opensource.posit.co/blog/2026-03-16_notebooks-march-announcement/positron-notebook.png" alt="Positron Notebook Editor" />
<figcaption aria-hidden="true">Positron Notebook Editor</figcaption>
</figure>
<h2 id="whats-included-out-of-the-box">What&rsquo;s included out of the box
</h2>
<p>The Positron Notebook Editor brings the core capabilities of Positron directly into your notebook workflow:</p>
<p><strong>






<a href="https://positron.posit.co/variables-pane.html" target="_blank" rel="noopener">Variables Pane</a>
</strong>: Variables update in real time as you run cells. No need to print or inspect manually.</p>
<p><strong>






<a href="https://positron.posit.co/data-explorer.html" target="_blank" rel="noopener">Data Explorer</a>
</strong>: When a cell returns a Pandas or Polars DataFrame, you get an inline data viewer. Open the full Data Explorer to sort, filter, and profile your data. Any filtering or cleaning you do can be converted into code, so your analysis stays reproducible without writing repetitive <code>df.head()</code> or <code>df.describe()</code> calls.</p>























  
  
    <div class="w-full aspect-video">
      <video
        src="https://positron.posit.co/videos/notebook-data-explorer-variables.mp4"
        class="w-full h-full object-contain"
        
        controls></video>
    </div>
  




<p><strong>






<a href="https://positron.posit.co/assistant.html" target="_blank" rel="noopener">AI Assistant</a>
</strong>: The Assistant has access to your notebook&rsquo;s full context, including cell states, execution history, and outputs like images and tables. It can suggest edits, reorder cells, and run code with your permission. You can inspect exactly what context it&rsquo;s using and follow along as it works.</p>























  
  
    <div class="w-full aspect-video">
      <video
        src="https://positron.posit.co/videos/notebook-ai-quick-actions.mp4"
        class="w-full h-full object-contain"
        
        controls></video>
    </div>
  




<p><strong>






<a href="https://positron.posit.co/help-pane.html" target="_blank" rel="noopener">Help Pane</a>
</strong>: Python and R documentation is available inline, with hyperlinks, without switching to a browser.</p>























  
  
    <div class="w-full aspect-video">
      <video
        src="https://positron.posit.co/videos/notebook-help.mp4"
        class="w-full h-full object-contain"
        
        controls></video>
    </div>
  




<p><strong>






<a href="https://positron.posit.co/publish-to-connect.html" target="_blank" rel="noopener">Publisher</a>
</strong>: Deploy your <code>.ipynb</code> notebooks directly to Connect or Connect Cloud, where you can manage access, schedule runs, and view telemetry.</p>























  
  
    <div class="w-full aspect-video">
      <video
        src="https://positron.posit.co/videos/notebook-publish-connect.mp4"
        class="w-full h-full object-contain"
        
        controls></video>
    </div>
  




<h2 id="a-sample-notebook-workflow">A sample notebook workflow
</h2>
<p>Now that you have all these capabilities in one place, your workflow might look something like this:</p>
<ol>
<li>Import your data using Pandas or Polars.</li>
<li>Run your notebook cells and watch variables update in the pane as cells run.</li>
<li>Explore your DataFrame in the inline Data Explorer. Sort and filter without writing any code.</li>
<li>Use Assistant to generate a visualization based on your filtered data or AI quick actions to recommend next steps.</li>
<li>When the analysis is ready to share, use an AI action to add markdown headers and notes.</li>
<li>Publish the notebook to Connect or Connect Cloud to share with your colleagues.</li>
</ol>
<h2 id="whats-coming-next">What&rsquo;s coming next
</h2>
<p>The roadmap includes SQL support, improved version control, R improvements, and more. You can view and vote on items in the 






<a href="https://github.com/posit-dev/positron/issues?q=is%3Aissue%20state%3Aopen%20label%3A%22area%3A%20notebooks-jupyter%22" target="_blank" rel="noopener">GitHub roadmap</a>
.</p>
<h2 id="get-started-with-the-alpha">Get started with the alpha
</h2>
<ol>
<li>






<a href="https://positron.posit.co/download.html" target="_blank" rel="noopener">Download Positron</a>
 and install a release from February 2026 or later.</li>
<li>Enable the alpha by setting 






<a href="positron://settings/positron.notebook.enabled"><code>positron.notebook.enabled</code></a>
 to <code>true</code> in your settings.</li>
<li>Try the 






<a href="https://github.com/posit-dev/positron-demos-notebooks" target="_blank" rel="noopener">tutorial repository</a>
 for examples that use the new features.</li>
<li>Share feedback in 






<a href="https://github.com/posit-dev/positron/discussions" target="_blank" rel="noopener">GitHub Discussions</a>
 or 






<a href="https://scheduler.zoom.us/cindy-tong/improving-the-positron-notebook-experience" target="_blank" rel="noopener">book time to talk with us directly</a>
.</li>
</ol>
<p>We&rsquo;re excited to hear how you use the Positron Notebook Editor as we continuously improve the experience.</p>
]]></description>
      <enclosure url="https://opensource.posit.co/blog/2026-03-16_notebooks-march-announcement/positron-notebook.png" length="444187" type="image/png" />
    </item>
    <item>
      <title>orbital 0.5.0</title>
      <link>https://opensource.posit.co/blog/2026-03-13_orbital-0-5-0/</link>
      <pubDate>Fri, 13 Mar 2026 00:00:00 +0000</pubDate>
      <guid>https://opensource.posit.co/blog/2026-03-13_orbital-0-5-0/</guid>
      <dc:creator>Emil Hvitfeldt</dc:creator><description><![CDATA[<!--
TODO:
* [x] Look over / edit the post's title in the yaml
* [x] Edit (or delete) the description; note this appears in the Twitter card
* [x] Pick category and tags (see existing with [`hugodown::tidy_show_meta()`](https://rdrr.io/pkg/hugodown/man/use_tidy_post.html))
* [x] Find photo & update yaml metadata
* [x] Create `thumbnail-sq.jpg`; height and width should be equal
* [x] Create `thumbnail-wd.jpg`; width should be >5x height
* [x] [`hugodown::use_tidy_thumbnails()`](https://rdrr.io/pkg/hugodown/man/use_tidy_post.html)
* [x] Add intro sentence, e.g. the standard tagline for the package
* [x] [`usethis::use_tidy_thanks()`](https://usethis.r-lib.org/reference/use_tidy_thanks.html)
-->
<p>We&rsquo;re over the moon to announce the release of 






<a href="https://orbital.tidymodels.org/" target="_blank" rel="noopener">orbital</a>
 0.5.0. orbital lets you predict in databases using tidymodels workflows. orbital uses 






<a href="https://tidypredict.tidymodels.org/" target="_blank" rel="noopener">tidypredict</a>
 under the hood to translate fitted models into expressions. This post will also cover things from tidypredict&rsquo;s 1.1.0 release.</p>
<p>This blogpost is about the R orbital package, but there&rsquo;s also a 






<a href="https://posit-dev.github.io/orbital/" target="_blank" rel="noopener">python version</a>
 that works on scikit-learn models.</p>
<p>You can install both from CRAN with:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nf'><a href='https://rdrr.io/r/utils/install.packages.html'>install.packages</a></span><span class='o'>(</span><span class='nf'><a href='https://rdrr.io/r/base/c.html'>c</a></span><span class='o'>(</span><span class='s'>"orbital"</span>, <span class='s'>"tidypredict"</span><span class='o'>)</span><span class='o'>)</span></span></code></pre>
</div>
<p>This blog post will cover the highlights, which are support for more models, faster performance, and more vignettes.</p>
<p>You can see a full list of changes in the 


  
  
  





<a href="https://orbital.tidymodels.org/news/index.html#orbital-050" target="_blank" rel="noopener">orbital release notes</a>
 and 


  
  
  





<a href="https://tidypredict.tidymodels.org/news/index.html#tidypredict-110" target="_blank" rel="noopener">tidypredict release notes</a>
.</p>
<h2 id="newly-supported-models">Newly supported models
</h2>
<p>We have added support for new models as well as more prediction types for existing supported models.</p>
<p>The newly supported models are.</p>
<ul>
<li><code>decision_tree(engine = &quot;rpart&quot;)</code></li>
<li><code>boost_tree(engine = &quot;lightgbm&quot;)</code></li>
<li><code>boost_tree(engine = &quot;catboost&quot;)</code> (More on this soon)</li>
</ul>
<p>All of which support regression, classification and probability estimates.</p>
<p>The following models now also support classification and probability estimates in addition to regression.</p>
<ul>
<li><code>mars(engine = &quot;earth&quot;)</code></li>
<li><code>multinom_reg(engine = &quot;glmnet&quot;)</code></li>
<li><code>rand_forest(engine = &quot;randomForest&quot;)</code></li>
<li><code>rand_forest(engine = &quot;ranger&quot;)</code></li>
</ul>
<p>If there is a model type you specifically need 






<a href="https://github.com/tidymodels/tidypredict/issues/232" target="_blank" rel="noopener">please let us know</a>
 so we can prioritize new additions.</p>
<h2 id="nested-case_when-support">Nested <code>case_when()</code> support
</h2>
<p>All tree based models were previously implemented as a flat <code>case_when()</code> statement. This means that a small tree with 3 leaves would look like this.</p>
<div class="code-block"><div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">case_when</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="n">x</span> <span class="o">&lt;=</span> <span class="m">5</span> <span class="o">&amp;</span> <span class="n">y</span> <span class="o">&lt;=</span> <span class="m">3</span> <span class="o">~</span> <span class="s">&#34;low&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">x</span> <span class="o">&lt;=</span> <span class="m">5</span> <span class="o">&amp;</span> <span class="n">y</span> <span class="o">&gt;</span> <span class="m">3</span>  <span class="o">~</span> <span class="s">&#34;med&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">x</span> <span class="o">&gt;</span> <span class="m">5</span>           <span class="o">~</span> <span class="s">&#34;high&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span></span></span></code></pre></td></tr></table>
</div>
</div></div>
<p>And while this works, it comes with a number of downsides. In this example we have to calculate <code>x &lt;= 5</code> more than once. This might not be that big of a deal in this sized tree but it compounds very fast as the tree grows deeper.</p>
<p>We are also not using the information effectively. Since trees are exhaustive we shouldn&rsquo;t have to calculate the last condition as all other choices have been ruled out. With these considerations in mind we have switched all trees to be expressed as nested <code>case_when()</code> statements.</p>
<div class="code-block"><div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span><span class="lnt">7
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">case_when</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="n">x</span> <span class="o">&lt;=</span> <span class="m">5</span> <span class="o">~</span> <span class="nf">case_when</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">y</span> <span class="o">&lt;=</span> <span class="m">3</span> <span class="o">~</span> <span class="s">&#34;low&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">.default</span> <span class="o">=</span> <span class="s">&#34;med&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="p">),</span>
</span></span><span class="line"><span class="cl">  <span class="n">.default</span> <span class="o">=</span> <span class="s">&#34;high&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span></span></span></code></pre></td></tr></table>
</div>
</div></div>
<p>This <code>case_when()</code> evaluates exactly the same as the previous flat <code>case_when()</code> statement. While this might be a little harder to read it provides a lot of benefit in terms of performance. Each condition is evaluated at most 1 time. This has a really big influence on the computational speed.</p>
<p>This also means that the R version of orbital now matches what the 






<a href="https://posit-dev.github.io/orbital/" target="_blank" rel="noopener">python version of orbital</a>
 does when creating a tree.</p>
<h2 id="new-separate_trees-argument">New <code>separate_trees</code> argument
</h2>
<p>Some models like the ensemble tree models can be represented as a combination of multiple smaller models.This typically manifests as a single massive expression in the following format:</p>
<div class="code-block"><div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="n">.pred</span> <span class="o">=</span> <span class="s">&#34;(tree1) + (tree2) + (tree3) + ... + (tree100)&#34;</span></span></span></code></pre></td></tr></table>
</div>
</div></div>
<p>This can create trouble for two main reasons. The first one is that this can cause us to hit expression nesting depth when trying to execute these in a database if we have too many trees or have too deep trees. The second related issue is that databases will not be able to recognize that these trees could be calculated in parallel and combined afterwards.</p>
<p>This is where the new <code>separate_trees</code> argument comes in. When setting <code>separate_trees = TRUE</code> in 






<a href="https://orbital.tidymodels.org/reference/orbital.html" target="_blank" rel="noopener"><code>orbital()</code></a>
 you change the internal representation of the orbital object to not have a single massive expression for <code>.pred</code> and instead split them out into multiple expressions like so.</p>
<div class="code-block"><div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="n">.pred_tree_001</span> <span class="o">=</span> <span class="s">&#34;case_when(...)&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">.pred_tree_002</span> <span class="o">=</span> <span class="s">&#34;case_when(...)&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">.pred_tree_003</span> <span class="o">=</span> <span class="s">&#34;case_when(...)&#34;</span>
</span></span><span class="line"><span class="cl"><span class="kc">...</span>
</span></span><span class="line"><span class="cl"><span class="n">.pred</span> <span class="o">=</span> <span class="s">&#34;.pred_tree_001 + .pred_tree_002 + .pred_tree_003 + ...&#34;</span></span></span></code></pre></td></tr></table>
</div>
</div></div>
<p>This representation allows the database query optimizer to potentially evaluate trees in parallel, since each intermediate column is independent.</p>
<p>The <code>separate_trees</code> argument works for the following engines.</p>
<ul>
<li>xgboost</li>
<li>lightgbm</li>
<li>catboost</li>
<li>ranger</li>
<li>randomForest</li>
</ul>
<p>This change alone allows us to work with model types previously not possible with orbital. Together with the nested tree support you can now productionize some of the most popular machine learning models.</p>
<h2 id="splines-support">splines support
</h2>
<p>Spline transformations are commonly used in preprocessing to capture non-linear relationships between predictors and the outcome.</p>
<p>With this release, orbital now supports <code>step_spline_b()</code>, <code>step_spline_convex()</code>, <code>step_spline_monotone()</code>, <code>step_spline_natural()</code>, and <code>step_spline_nonnegative()</code> from the recipes package. Under the hood, splines are translated into piecewise polynomial expressions that can be evaluated directly in SQL.</p>
<h2 id="more-vignettes">More vignettes
</h2>
<p>We have added a handful of new vignettes as well in this release.</p>
<ul>
<li>
<p>






<a href="https://orbital.tidymodels.org/articles/sql-size.html" target="_blank" rel="noopener">SQL expression sizes</a>
: Goes over how different hyperparameters in models affect SQL sizes. This is useful information especially when working with boosted trees as there are many different combinations of hyperparameters that produce similar performance at different SQL expression sizes. With a little effort you could pick a model that runs 10-100 times faster with minimal loss in predictive performance.</p>
</li>
<li>
<p>






<a href="https://orbital.tidymodels.org/articles/separate-trees.html" target="_blank" rel="noopener">Parallel tree evaluation in databases</a>
: A more in-depth look at how the <code>separate_trees</code> argument works. Also includes a section on why and when you should consider using it.</p>
</li>
<li>
<p>






<a href="https://orbital.tidymodels.org/articles/database-deployment.html" target="_blank" rel="noopener">Database deployment</a>
: Shows examples of how we can deploy an orbital model using tables and views.</p>
</li>
<li>
<p>






<a href="https://tidypredict.tidymodels.org/articles/float-precision.html" target="_blank" rel="noopener">Float precision at split boundaries</a>
: Some models like xgboost and Cubist models operate on 32-bit doubles instead of on 64-bit doubles like we have in R. This can cause some problems where predictions don&rsquo;t match exactly. If you use any of these models you should read this vignette to see if this issue is a dealbreaker for you or not.</p>
</li>
</ul>
<h2 id="acknowledgements">Acknowledgements
</h2>
<p>A special thanks to 






<a href="https://github.com/emilyriederer" target="_blank" rel="noopener">Emily Riederer</a>
 who helped workshop and benchmark these new features.</p>
]]></description>
      <enclosure url="https://opensource.posit.co/blog/2026-03-13_orbital-0-5-0/thumbnail-wd.jpg" length="96543" type="image/jpeg" />
    </item>
    <item>
      <title>Outgrowing your laptop with R and Positron</title>
      <link>https://opensource.posit.co/blog/2026-03-05_outgrow-your-laptop/</link>
      <pubDate>Thu, 05 Mar 2026 00:00:00 +0000</pubDate>
      <guid>https://opensource.posit.co/blog/2026-03-05_outgrow-your-laptop/</guid>
      <dc:creator>Julia Silge</dc:creator><description><![CDATA[<h2 id="my-data-is-too-big-for-my-laptop">My data is too big for my laptop!
</h2>
<p>Last week, I had the pleasure of giving a talk to 






<a href="https://www.meetup.com/rladies-abuja/" target="_blank" rel="noopener">R-Ladies Abuja</a>
 about how Positron can grow with you as you work on data that is too large for your laptop. The talk was recorded, and you can find it on YouTube here:</p>















  

  
  
  
    
    
  

  
  










  
  
    <div class="w-full aspect-video">
      <iframe
        src="https://www.youtube.com/embed/sPZsH0eaUpQ"
        class="w-full h-full"
        
        frameborder="0"
        allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
        allowfullscreen></iframe>
    </div>
  




<p>I opened this talk discussing how I first learned about these &ldquo;beyond your laptop&rdquo; technologies, typically in a organization where these technologies were already in use and seemed specific to infrastructure there. I later came to understand that these technologies are actually related to each other and understanding one can really help when you need to pick up another one. I pointed out some of the Positron features that are designed to make it easier to work with these technologies:</p>
<ul>
<li>






<a href="https://positron.posit.co/data-explorer.html" target="_blank" rel="noopener">Data Explorer</a>
</li>
<li>






<a href="https://positron.posit.co/connections-pane.html" target="_blank" rel="noopener">Connections Pane</a>
</li>
<li>






<a href="https://positron.posit.co/remote-ssh.html" target="_blank" rel="noopener">Remote SSH sessions</a>
</li>
</ul>
<h2 id="check-out-my-slides">Check out my slides
</h2>
<p>If you&rsquo;d like to check out my slides, they are 






<a href="https://juliasilge.github.io/rladies-abuja/" target="_blank" rel="noopener">available as well</a>
:</p>
<iframe src="https://juliasilge.github.io/rladies-abuja/" width="100%" style="aspect-ratio: 16/9; border: none;">
</iframe>
]]></description>
    </item>
    <item>
      <title>PDF Accessibility and Standards</title>
      <link>https://opensource.posit.co/blog/2026-03-05_pdf-accessibility-and-standards/</link>
      <pubDate>Thu, 05 Mar 2026 00:00:00 +0000</pubDate>
      <guid>https://opensource.posit.co/blog/2026-03-05_pdf-accessibility-and-standards/</guid>
      <dc:creator>Gordon Woodhull</dc:creator><description><![CDATA[<div class="callout callout-note" role="note" aria-label="Note">
<div class="callout-header">
<span class="callout-title">Pre-release Feature</span>
</div>
<div class="callout-body">
<p>This feature is new in the upcoming Quarto 1.9 release. To use the feature now, you&rsquo;ll need to 






<a href="https://quarto.org/docs/download/prerelease.html" target="_blank" rel="noopener">download and install</a>
 the Quarto pre-release.</p>
</div>
</div>
<p>2025 was a big year for PDF accessibility. LaTeX and Typst both released support for PDF tagging and accessibility standards, just in time for new regulations in the 






<a href="https://en.wikipedia.org/wiki/European_Accessibility_Act" target="_blank" rel="noopener">EU</a>
 (June 2025) and 






<a href="https://accessible.org/ada-title-ii-web-accessibility/" target="_blank" rel="noopener">US</a>
 (April 2026).</p>
<p>Quarto 1.9 brings this support to you as a Quarto user.</p>
<h2 id="what-pdf-standards-do">What PDF Standards Do
</h2>
<p>Currently LaTeX supports the newer UA-2 standard, and Typst supports the older UA-1 standard. Typst is likely to have UA-2 support later in 2026.</p>
<p>Both standards instruct the PDF renderer to provide screen readers:</p>
<ul>
<li>The semantic structure of the text (title, heading, paragraph, figure, etc)</li>
<li>The natural reading order</li>
<li>Spatial coordinates for highlighting and assistive navigation</li>
<li>Required metadata such as title and language</li>
</ul>
<h2 id="how-to-enable-a-pdf-standard-in-quarto">How to enable a PDF Standard in Quarto
</h2>
<p>In Quarto 1.9, specify a PDF standard for your document or project with <code>pdf-standard</code></p>
<div class="grid gap-12 items-start md:grid-cols-2">
<div class="prose max-w-none">
<p><strong>PDF (LaTeX)</strong></p>
<div class="code-block"><div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">format</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">pdf</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">pdf-standard</span><span class="p">:</span><span class="w"> </span><span class="l">ua-2</span></span></span></code></pre></td></tr></table>
</div>
</div></div>
</div>
<div class="prose max-w-none">
<p><strong>Typst</strong></p>
<div class="code-block"><div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">format</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">typst</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">pdf-standard</span><span class="p">:</span><span class="w"> </span><span class="l">ua-1</span></span></span></code></pre></td></tr></table>
</div>
</div></div>
</div>
</div>
<p><code>pdf-standard</code> takes a single standard name or list of standard names. PDF version is used if provided in the list, but otherwise inferred from the standard.</p>
<p>If you specify a PDF standard, Quarto first instructs LaTeX or Typst to use the standard when producing the PDF, and then validates the output PDF against the standard using veraPDF, an open-source PDF validation tool. If veraPDF is not installed, you&rsquo;ll get a warning but still receive a PDF &ndash; it just won&rsquo;t be validated.</p>
<div class="callout callout-note" role="note" aria-label="Note">
<div class="callout-header">
<span class="callout-title">Installing veraPDF</span>
</div>
<div class="callout-body">
<p>To install veraPDF, you&rsquo;ll first need Java, then run:</p>
<div class="code-block code-with-filename" role="group" aria-labelledby="code-filename-2">
  <div class="code-with-filename-label" id="code-filename-2"><span class="font-mono text-sm">Terminal</span></div><div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">quarto install verapdf</span></span></code></pre></td></tr></table>
</div>
</div></div>
</div>
</div>
<p>When a document passes validation, you&rsquo;ll see output like:</p>
<pre><code>[verapdf]: Validating my-document.pdf against PDF/UA-2... PASSED
</code></pre>
<h2 id="creating-accessible-pdfs">Creating accessible PDFs
</h2>
<p>Quarto&rsquo;s Markdown-based workflow handles many accessibility requirements automatically:</p>
<ul>
<li>Document metadata (title, author, date, language) flows into the PDF&rsquo;s built-in metadata fields.</li>
<li>The semantic structure of Markdown satisfies PDF tagging requirements. For Typst this is always enabled; for LaTeX it is enabled when you specify a standard that requires it.</li>
<li>Alt text for images is carried through to the PDF for screen readers.</li>
</ul>
<p>But you do need to make sure your document has:</p>
<ul>
<li>A <strong>title</strong> in the YAML front matter.</li>
<li><strong>Alt text for every image</strong>, specified with <code>fig-alt</code>. See 


  
  
  





<a href="https://quarto.org/docs/authoring/figures.html#alt-text" target="_blank" rel="noopener">Figures</a>
 for details.</li>
</ul>
<p>See the 


  
  
  





<a href="https://quarto.org/docs/output-formats/pdf-basics.html#accessibility-requirements" target="_blank" rel="noopener">LaTeX</a>
 and 


  
  
  





<a href="https://quarto.org/docs/output-formats/typst.html#accessibility-requirements" target="_blank" rel="noopener">Typst</a>
 documentation for more details.</p>
<h2 id="if-your-document-fails-validation">If your document fails validation
</h2>
<p>LaTeX does not perform validation during PDF generation, so if veraPDF validation fails, that&rsquo;s a warning, and you still get a partially-accessible PDF as long as you use <code>pdf-standard: ua-2</code>.</p>
<p>Typst fails and does not produce a PDF if its built-in validation fails during PDF generation. However, in Typst all accessibility features are on by default, so you can generate a partially-accessible PDF by rendering without <code>pdf-standard</code>.</p>
<h2 id="current-limitations">Current limitations
</h2>
<p>We ran our test suite &ndash; 188 LaTeX examples and 317 Typst examples &ndash; to find where Quarto PDFs do not yet pass UA-1 or UA-2, and where users will need to change their documents.</p>
<h3 id="latex">LaTeX
</h3>
<p>Margin content is the biggest structural blocker. If you use <code>.column-margin</code> divs, <code>cap-location: margin</code>, <code>reference-location: margin</code>, or <code>citation-location: margin</code>, the resulting PDF will not pass UA-2. The underlying <code>sidenotes</code> and <code>marginnote</code> LaTeX packages 






<a href="https://github.com/quarto-dev/quarto-cli/issues/14103" target="_blank" rel="noopener">do not cooperate with PDF tagging</a>
.</p>
<p>(Margin content does work with Typst and passes UA-1 &ndash; see 


  
  
  





<a href="https://quarto.org/docs/output-formats/typst.html#article-layout" target="_blank" rel="noopener">Typst Article Layout</a>
.)</p>
<p>There are smaller upstream issues in Pandoc, LaTeX, and LaTeX packages, 


  
  
  





<a href="https://github.com/quarto-dev/quarto-cli/pull/14097#issuecomment-3947653207" target="_blank" rel="noopener">documented here</a>
.</p>
<h3 id="typst">Typst
</h3>
<p>In our tests, Typst catches every UA-1 violation, and fails to generate the PDF. veraPDF did not detect any violation that Typst did not.</p>
<p>Typst also seems to do a very good job of generating UA-1 compliant output by default &ndash; almost all errors were due to missing titles or missing alt text.</p>
<p>However, we did discover that 


  
  
  





<a href="https://quarto.org/docs/books/book-output.html#typst-output" target="_blank" rel="noopener">Typst books</a>
 are not yet compliant. There is a 






<a href="https://github.com/flavio20002/typst-orange-template/issues/38" target="_blank" rel="noopener">structural problem with the Typst orange-book package</a>
 and we&rsquo;ll work with the maintainers to correct it.</p>
<h2 id="conclusion">Conclusion
</h2>
<p>Although Typst currently targets an the earlier UA-1 standard, today it seems to offer better PDF accessibility than LaTeX.</p>
<p>We expect PDF accessibility support to improve through the LaTeX ecosystem throughout 2026 as awareness of UA-2 and the new regulations spreads.</p>
<p>If you run into accessibility issues with PDF output, please search the 






<a href="https://github.com/orgs/quarto-dev/discussions" target="_blank" rel="noopener">Quarto discussions</a>
 and open a new one with the <code>accessibility</code> label for any issues you discover.</p>
]]></description>
      <enclosure url="https://opensource.posit.co/blog/2026-03-05_pdf-accessibility-and-standards/thumbnail.png" length="41719" type="image/png" />
    </item>
    <item>
      <title>Rapp 0.3.0</title>
      <link>https://opensource.posit.co/blog/2026-02-18_rapp-0-3-0/</link>
      <pubDate>Wed, 18 Feb 2026 00:00:00 +0000</pubDate>
      <guid>https://opensource.posit.co/blog/2026-02-18_rapp-0-3-0/</guid>
      <dc:creator>Tomasz Kalinowski</dc:creator><description><![CDATA[<p>We&rsquo;re excited to share our first tidyverse blog post for Rapp, alongside the <code>0.3.0</code> release. Rapp helps you turn R scripts into polished command-line tools, with argument parsing and help generation built in.</p>
<h2 id="why-a-command-line-interface-for-r">Why a command-line interface for R?
</h2>
<p>A command-line interface (CLI) lets you run programs from a terminal, without opening an IDE or starting an interactive R session. This is useful when you want to:</p>
<ul>
<li>automate tasks via cron jobs, scheduled tasks, or CI/CD pipelines</li>
<li>chain R scripts together with other tools in data pipelines</li>
<li>let others run your R code without needing to know R</li>
<li>package reusable tools that feel native to the terminal</li>
<li>expose specific actions through a clean interface that LLM agents can invoke</li>
</ul>
<p>There are several established packages for building CLIs in R, including argparse, optparse, and docopt, where you explicitly parse and handle command-line arguments in code. Rapp takes a different approach: it derives the CLI surface from the structure of your R script and injects values at runtime, so you never need to handle CLI arguments manually.</p>
<h2 id="how-rapp-works">How Rapp works
</h2>
<p>At its core, Rapp is an alternative front-end to R: a drop-in replacement for <code>Rscript</code> that automatically turns common R expression patterns into command-line options, switches, positional arguments, and subcommands. You write normal R code and Rapp handles the CLI surface.</p>
<p>Rapp also uses special <code>#|</code> comments (similar to Quarto&rsquo;s YAML-in-comments syntax) to add metadata such as help descriptions and short aliases.</p>
<h2 id="a-tiny-example">A tiny example
</h2>
<p>Here&rsquo;s a complete Rapp script (from the package examples), a coin flipper:</p>
<div class="code-block"><div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="c1">#!/usr/bin/env Rapp</span>
</span></span><span class="line"><span class="cl"><span class="c1">#| name: flip-coin</span>
</span></span><span class="line"><span class="cl"><span class="c1">#| description: |</span>
</span></span><span class="line"><span class="cl"><span class="c1">#|   Flip a coin.</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">#| description: Number of coin flips</span>
</span></span><span class="line"><span class="cl"><span class="c1">#| short: &#39;n&#39;</span>
</span></span><span class="line"><span class="cl"><span class="n">flips</span> <span class="o">&lt;-</span> <span class="m">1L</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">sep</span> <span class="o">&lt;-</span> <span class="s">&#34; &#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">wrap</span> <span class="o">&lt;-</span> <span class="kc">TRUE</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">seed</span> <span class="o">&lt;-</span> <span class="kc">NA_integer_</span>
</span></span><span class="line"><span class="cl"><span class="kr">if</span> <span class="p">(</span><span class="o">!</span><span class="nf">is.na</span><span class="p">(</span><span class="n">seed</span><span class="p">))</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nf">set.seed</span><span class="p">(</span><span class="n">seed</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">cat</span><span class="p">(</span><span class="nf">sample</span><span class="p">(</span><span class="nf">c</span><span class="p">(</span><span class="s">&#34;heads&#34;</span><span class="p">,</span> <span class="s">&#34;tails&#34;</span><span class="p">),</span> <span class="n">flips</span><span class="p">,</span> <span class="kc">TRUE</span><span class="p">),</span> <span class="n">sep</span> <span class="o">=</span> <span class="n">sep</span><span class="p">,</span> <span class="n">fill</span> <span class="o">=</span> <span class="n">wrap</span><span class="p">)</span></span></span></code></pre></td></tr></table>
</div>
</div></div>
<p>Let&rsquo;s break down how Rapp interprets this script:</p>
<table>
  <thead>
      <tr>
          <th>R code</th>
          <th>Generated CLI option</th>
          <th>What it does</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>flips &lt;- 1L</code></td>
          <td><code>--flips</code> or <code>-n</code></td>
          <td>Integer option with default of 1</td>
      </tr>
      <tr>
          <td><code>sep &lt;- &quot; &quot;</code></td>
          <td><code>--sep</code></td>
          <td>String option with default of <code>&quot; &quot;</code></td>
      </tr>
      <tr>
          <td><code>wrap &lt;- TRUE</code></td>
          <td><code>--wrap</code> / <code>--no-wrap</code></td>
          <td>Boolean toggle (TRUE/FALSE becomes on/off)</td>
      </tr>
      <tr>
          <td><code>seed &lt;- NA_integer_</code></td>
          <td><code>--seed</code></td>
          <td>Optional integer (NA means &ldquo;not set&rdquo;)</td>
      </tr>
  </tbody>
</table>
<p>The <code>#| short: 'n'</code> comment adds <code>-n</code> as a short alias for <code>--flips</code>. The <code>#!/usr/bin/env Rapp</code> line (called a &ldquo;shebang&rdquo;) lets you run the script directly on macOS and Linux without typing <code>Rapp</code> first.</p>
<h3 id="running-the-script">Running the script
</h3>
<p>With Rapp installed and <code>flip-coin</code> available on your <code>PATH</code> (see 


  
  
  





<a href="#get-started">Get started</a>
 below), you can run the app from the terminal:</p>
<div class="code-block"><div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">flip-coin -n <span class="m">3</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; heads tails heads</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">flip-coin --seed <span class="m">42</span> -n <span class="m">5</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; tails heads tails tails heads</span></span></span></code></pre></td></tr></table>
</div>
</div></div>
<h3 id="auto-generated-help">Auto-generated help
</h3>
<p>Rapp generates <code>--help</code> from your script (and <code>--help-yaml</code> if you want a machine-readable spec):</p>
<div class="code-block"><div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">flip-coin --help</span></span></code></pre></td></tr></table>
</div>
</div></div>
<div class="code-block"><div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span><span class="lnt">7
</span><span class="lnt">8
</span><span class="lnt">9
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Usage: flip-coin [OPTIONS]
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Flip a coin.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Options:
</span></span><span class="line"><span class="cl">  -n, --flips &lt;FLIPS&gt;  Number of coin flips [default: 1] [type: integer]
</span></span><span class="line"><span class="cl">  --sep &lt;SEP&gt;          [default: &#34; &#34;] [type: string]
</span></span><span class="line"><span class="cl">  --wrap / --no-wrap   [default: true] Disable with `--no-wrap`.
</span></span><span class="line"><span class="cl">  --seed &lt;SEED&gt;        [default: NA] [type: integer]</span></span></code></pre></td></tr></table>
</div>
</div></div>
<div class="callout-warning">
<h2 id="breaking-change-in-030-positional-arguments-are-now-required-by-default">Breaking change in 0.3.0: positional arguments are now required by default
</h2>
<p>If you&rsquo;re upgrading from an earlier version of Rapp, note that positional arguments are now <strong>required</strong> unless explicitly marked optional.</p>
<div class="code-block"><div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="c1"># Before 0.3.0: this positional was optional</span>
</span></span><span class="line"><span class="cl"><span class="n">name</span> <span class="o">&lt;-</span> <span class="kc">NULL</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># In 0.3.0+: add this comment to keep it optional</span>
</span></span><span class="line"><span class="cl"><span class="c1">#| required: false</span>
</span></span><span class="line"><span class="cl"><span class="n">name</span> <span class="o">&lt;-</span> <span class="kc">NULL</span></span></span></code></pre></td></tr></table>
</div>
</div></div>
<p>If your scripts use positional arguments with <code>NULL</code> defaults that should remain optional, add <code>#| required: false</code> above them.</p>
</div>
<h2 id="highlights-in-030">Highlights in 0.3.0
</h2>
<p>Rapp will be new to most readers, so rather than listing every change, here are the main ideas (and what&rsquo;s improved in 0.3.0).</p>
<h3 id="options-switches-and-repeatable-flags-from-plain-r">Options, switches, and repeatable flags from plain R
</h3>
<p>Rapp recognizes a small set of &ldquo;declarative&rdquo; patterns at the top level of your script:</p>
<ul>
<li>Scalar literals like <code>flips &lt;- 1L</code> become options like <code>--flips 10</code>.</li>
<li>Logical defaults like <code>wrap &lt;- TRUE</code> become toggles like <code>--wrap</code> / <code>--no-wrap</code>.</li>
<li><code>#| short: n</code> adds a short alias like <code>-n</code> (new in 0.3.0).</li>
<li>






<a href="https://rdrr.io/r/base/c.html" target="_blank" rel="noopener"><code>c()</code></a>
 and 






<a href="https://rdrr.io/r/base/list.html" target="_blank" rel="noopener"><code>list()</code></a>
 defaults declare repeatable options (new in 0.3.0): callers can supply the same flag multiple times and values are appended.</li>
</ul>
<h3 id="subcommands-with-switch">Subcommands with <code>switch()</code>
</h3>
<p>Rapp can now turn a 






<a href="https://rdrr.io/r/base/switch.html" target="_blank" rel="noopener"><code>switch()</code></a>
 block into subcommands (and you can nest 






<a href="https://rdrr.io/r/base/switch.html" target="_blank" rel="noopener"><code>switch()</code></a>
 blocks for nested commands). Here&rsquo;s a small sketch of a <code>todo</code>-style app:</p>
<div class="code-block"><div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span><span class="lnt">20
</span><span class="lnt">21
</span><span class="lnt">22
</span><span class="lnt">23
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="c1">#!/usr/bin/env Rapp</span>
</span></span><span class="line"><span class="cl"><span class="c1">#| name: todo</span>
</span></span><span class="line"><span class="cl"><span class="c1">#| description: Manage a simple todo list.</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">#| description: Path to the todo list file.</span>
</span></span><span class="line"><span class="cl"><span class="c1">#| short: s</span>
</span></span><span class="line"><span class="cl"><span class="n">store</span> <span class="o">&lt;-</span> <span class="s">&#34;.todo.yml&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">switch</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="n">command</span> <span class="o">&lt;-</span> <span class="s">&#34;&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="c1">#| description: Display the todos</span>
</span></span><span class="line"><span class="cl">  <span class="n">list</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">limit</span> <span class="o">&lt;-</span> <span class="m">30L</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># ...</span>
</span></span><span class="line"><span class="cl">  <span class="p">},</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="c1">#| description: Add a new todo</span>
</span></span><span class="line"><span class="cl">  <span class="n">add</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">task</span> <span class="o">&lt;-</span> <span class="kc">NULL</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># ...</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span></span></span></code></pre></td></tr></table>
</div>
</div></div>
<p>Help is scoped to the command you&rsquo;re asking about, so <code>todo --help</code> lists the commands, and <code>todo list --help</code> shows just the options/arguments for <code>list</code> (plus any parent/global options).</p>
<h3 id="installable-launchers-for-package-clis">Installable launchers for package CLIs
</h3>
<p>A big part of sharing CLI tools is making them easy to run after installation. In <code>0.3.0</code>, <code>install_pkg_cli_apps()</code> installs lightweight launchers for scripts in a package&rsquo;s <code>exec/</code> directory that use either <code>#!/usr/bin/env Rapp</code> or <code>#!/usr/bin/env Rscript</code>:</p>
<div class="code-block"><div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="n">Rapp</span><span class="o">::</span><span class="nf">install_pkg_cli_apps</span><span class="p">(</span><span class="s">&#34;mypackage&#34;</span><span class="p">)</span></span></span></code></pre></td></tr></table>
</div>
</div></div>
<p>(There&rsquo;s also <code>uninstall_pkg_cli_apps()</code> to remove a package&rsquo;s launchers.)</p>
<h2 id="get-started">Get started
</h2>
<p>Here&rsquo;s the quickest path to your first Rapp script:</p>
<div class="code-block"><div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="c1"># 1. Install the package</span>
</span></span><span class="line"><span class="cl"><span class="nf">install.packages</span><span class="p">(</span><span class="s">&#34;Rapp&#34;</span><span class="p">)</span></span></span></code></pre></td></tr></table>
</div>
</div></div>
<div class="code-block"><div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="c1"># 2. Install the command-line launcher</span>
</span></span><span class="line"><span class="cl"><span class="n">Rapp</span><span class="o">::</span><span class="nf">install_pkg_cli_apps</span><span class="p">(</span><span class="s">&#34;Rapp&#34;</span><span class="p">)</span></span></span></code></pre></td></tr></table>
</div>
</div></div>
<p>Then create a script (e.g., <code>hello.R</code>):</p>
<div class="code-block"><div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="c1">#!/usr/bin/env Rapp</span>
</span></span><span class="line"><span class="cl"><span class="c1">#| name: hello</span>
</span></span><span class="line"><span class="cl"><span class="c1">#| description: Say hello</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">name</span> <span class="o">&lt;-</span> <span class="s">&#34;world&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nf">cat</span><span class="p">(</span><span class="s">&#34;Hello,&#34;</span><span class="p">,</span> <span class="n">name</span><span class="p">,</span> <span class="s">&#34;\n&#34;</span><span class="p">)</span></span></span></code></pre></td></tr></table>
</div>
</div></div>
<p>And run it:</p>
<div class="code-block"><div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">Rapp hello.R --name <span class="s2">&#34;R users&#34;</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; Hello, R users</span></span></span></code></pre></td></tr></table>
</div>
</div></div>
<h3 id="learn-more">Learn more
</h3>
<p>To dig deeper into Rapp:</p>
<ul>
<li>browse examples in the package: <code>system.file(&quot;examples&quot;, package = &quot;Rapp&quot;)</code></li>
<li>read the full documentation: 






<a href="https://github.com/r-lib/Rapp" target="_blank" rel="noopener">https://github.com/r-lib/Rapp</a>
</li>
<li>note that Rapp requires R ≥ 4.1.0</li>
</ul>
<p>If you try Rapp, we&rsquo;d love feedback! We especially want to hear about your experiences with edge cases in argument parsing, help output, and how commands should feel. Issues and ideas are welcome at 






<a href="https://github.com/r-lib/Rapp/issues" target="_blank" rel="noopener">https://github.com/r-lib/Rapp/issues</a>
.</p>
]]></description>
      <enclosure url="https://opensource.posit.co/blog/2026-02-18_rapp-0-3-0/thumbnail-wd.jpg" length="89445" type="image/jpeg" />
    </item>
    <item>
      <title>mirai 2.6.0</title>
      <link>https://opensource.posit.co/blog/2026-02-12_mirai-2-6-0/</link>
      <pubDate>Thu, 12 Feb 2026 00:00:00 +0000</pubDate>
      <guid>https://opensource.posit.co/blog/2026-02-12_mirai-2-6-0/</guid>
      <dc:creator>Charlie Gao</dc:creator><description><![CDATA[<p>






<a href="https://mirai.r-lib.org" target="_blank" rel="noopener">mirai</a>
 2.6.0 is now on CRAN. mirai is R&rsquo;s framework for parallel and asynchronous computing. If you&rsquo;re fitting models, running simulations, or building Shiny apps, mirai lets you spread that work across multiple processes &ndash; locally or on remote infrastructure.</p>
<p>With this release, it bridges the gap between your laptop and enterprise infrastructure &ndash; the same code you prototype locally now deploys to Posit Workbench or any cloud HTTP API, with a single function call.</p>
<p>You can install it from CRAN with:</p>
<div class="code-block"><div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">install.packages</span><span class="p">(</span><span class="s">&#34;mirai&#34;</span><span class="p">)</span></span></span></code></pre></td></tr></table>
</div>
</div></div>
<p>The flagship feature for this release is the HTTP launcher for deploying daemons to cloud and enterprise platforms. This release also brings a C-level dispatcher for minimal task dispatch overhead, 






<a href="https://mirai.r-lib.org/reference/race_mirai.html" target="_blank" rel="noopener"><code>race_mirai()</code></a>
 for process-as-completed patterns, synchronous mode for debugging, and daemon synchronization for remote deployments. You can see a full list of changes in the 


  
  
  





<a href="https://mirai.r-lib.org/news/#mirai-260" target="_blank" rel="noopener">release notes</a>
.</p>
<h2 id="how-mirai-works">How mirai works
</h2>
<p>If you&rsquo;ve ever waited for a loop to finish fitting models, processing files, or calling APIs, mirai can help. Any task that&rsquo;s repeated independently across items is a candidate for parallel execution.</p>
<p>The 






  
  
    
  

<a href="https://opensource.posit.co/blog/2025-09-05_mirai-2-5-0/">previous release post</a>
 covered mirai&rsquo;s design philosophy in detail. Here&rsquo;s a brief overview for readers encountering mirai for the first time.</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='kr'><a href='https://rdrr.io/r/base/library.html'>library</a></span><span class='o'>(</span><span class='nv'><a href='https://mirai.r-lib.org'>mirai</a></span><span class='o'>)</span></span>
<span><span class='c'># Set up 4 background processes</span></span>
<span><span class='nf'><a href='https://mirai.r-lib.org/reference/daemons.html'>daemons</a></span><span class='o'>(</span><span class='m'>4</span><span class='o'>)</span></span>
<span></span>
<span><span class='c'># Send work -- non-blocking, returns immediately</span></span>
<span><span class='nv'>m</span> <span class='o'>&lt;-</span> <span class='nf'><a href='https://mirai.r-lib.org/reference/mirai.html'>mirai</a></span><span class='o'>(</span><span class='o'>&#123;</span></span>
<span>  <span class='nf'><a href='https://rdrr.io/r/base/Sys.sleep.html'>Sys.sleep</a></span><span class='o'>(</span><span class='m'>1</span><span class='o'>)</span></span>
<span>  <span class='m'>100</span> <span class='o'>+</span> <span class='m'>42</span></span>
<span><span class='o'>&#125;</span><span class='o'>)</span></span>
<span><span class='nv'>m</span></span>
<span><span class='c'>#&gt; &lt; mirai [] &gt;</span></span>
<span></span><span></span>
<span><span class='c'># Collect the result when ready</span></span>
<span><span class='nv'>m</span><span class='o'>[</span><span class='o'>]</span></span>
<span><span class='c'>#&gt; [1] 142</span></span>
<span></span><span></span>
<span><span class='c'># Shut down</span></span>
<span><span class='nf'><a href='https://mirai.r-lib.org/reference/daemons.html'>daemons</a></span><span class='o'>(</span><span class='m'>0</span><span class='o'>)</span></span></code></pre>
</div>
<p>That&rsquo;s mirai in a nutshell: 






<a href="https://mirai.r-lib.org/reference/daemons.html" target="_blank" rel="noopener"><code>daemons()</code></a>
 to set up workers, 






<a href="https://mirai.r-lib.org/reference/mirai.html" target="_blank" rel="noopener"><code>mirai()</code></a>
 to send work, <code>[]</code> to collect results. Everything else builds on this.</p>
<p>In mirai&rsquo;s hub architecture, the host session listens at a URL and <em>daemons</em> &ndash; background R processes that do the actual work &ndash; connect to it. You send tasks with 






<a href="https://mirai.r-lib.org/reference/mirai.html" target="_blank" rel="noopener"><code>mirai()</code></a>
, and the dispatcher routes them to available daemons in first-in, first-out (FIFO) order.</p>
<p>This design enables dynamic scaling: daemons can connect and disconnect at any time without disrupting the host. Add capacity when you need it, release it when you don&rsquo;t.</p>
<img src="https://opensource.posit.co/blog/2026-02-12_mirai-2-6-0/architecture.svg" alt="Hub architecture diagram showing compute profiles with daemons connecting to host" width="100%" />
<p>A single compute profile can mix daemons launched by different methods, and you can run multiple profiles simultaneously to direct different tasks to different resources. The basic syntax for each deployment method:</p>
<table>
  <thead>
      <tr>
          <th>Deploy to</th>
          <th>Setup</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Local</td>
          <td><code>daemons(4)</code></td>
      </tr>
      <tr>
          <td>Remote (SSH)</td>
          <td><code>daemons(url = host_url(), remote = ssh_config(...))</code></td>
      </tr>
      <tr>
          <td>HPC cluster (Slurm, SGE, PBS, LSF)</td>
          <td><code>daemons(url = host_url(), remote = cluster_config())</code></td>
      </tr>
      <tr>
          <td>HTTP API / Posit Workbench</td>
          <td><code>daemons(url = host_url(), remote = http_config())</code></td>
      </tr>
  </tbody>
</table>
<p>Change one line and your local prototype runs on a Slurm cluster. Change it again and it runs on Posit Workbench. Your analysis code stays identical.</p>
<h2 id="the-async-foundation-for-the-modern-r-stack">The async foundation for the modern R stack
</h2>
<p>mirai has become the convergence point for asynchronous and parallel computing across the R ecosystem.</p>
<p>It is the 






<a href="https://rstudio.github.io/promises/articles/promises_04_mirai.html" target="_blank" rel="noopener">recommended async backend</a>
 for 






<a href="https://shiny.posit.co/" target="_blank" rel="noopener">Shiny</a>
 &ndash; if you&rsquo;re building production Shiny apps, you should be using mirai. It is the <em>only</em> async backend for the next-generation 






<a href="https://plumber2.posit.co/" target="_blank" rel="noopener">plumber2</a>
 &ndash; if you&rsquo;re building APIs with plumber2, you&rsquo;re already using mirai.</p>
<p>It is the parallel backend for 






<a href="https://purrr.tidyverse.org/" target="_blank" rel="noopener">purrr</a>
 &ndash; if you use <code>map()</code>, mirai is how you make it parallel. Wrap your function in 






<a href="https://purrr.tidyverse.org/reference/in_parallel.html" target="_blank" rel="noopener"><code>in_parallel()</code></a>
, set up daemons, and your map calls run across all of them:</p>
<div class="code-block"><div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">purrr</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nf">daemons</span><span class="p">(</span><span class="m">4</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">models</span> <span class="o">&lt;-</span> <span class="nf">split</span><span class="p">(</span><span class="n">mtcars</span><span class="p">,</span> <span class="n">mtcars</span><span class="o">$</span><span class="n">cyl</span><span class="p">)</span> <span class="o">|&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nf">map</span><span class="p">(</span><span class="nf">in_parallel</span><span class="p">(</span><span class="nf">\</span><span class="p">(</span><span class="n">x</span><span class="p">)</span> <span class="nf">lm</span><span class="p">(</span><span class="n">mpg</span> <span class="o">~</span> <span class="n">wt</span> <span class="o">+</span> <span class="n">hp</span><span class="p">,</span> <span class="n">data</span> <span class="o">=</span> <span class="n">x</span><span class="p">)))</span>
</span></span><span class="line"><span class="cl"><span class="nf">daemons</span><span class="p">(</span><span class="m">0</span><span class="p">)</span></span></span></code></pre></td></tr></table>
</div>
</div></div>
<p>It powers 






<a href="https://docs.ropensci.org/targets/" target="_blank" rel="noopener">targets</a>
 &ndash; the pipeline orchestration tool for reproducible analysis. And most recently, 






<a href="https://ragnar.tidyverse.org/" target="_blank" rel="noopener">ragnar</a>
 &ndash; the Tidyverse package for retrieval-augmented generation (RAG) &ndash; adopted mirai for its parallel processing.</p>
<p>As an 






<a href="https://stat.ethz.ch/R-manual/R-devel/library/parallel/html/makeCluster.html" target="_blank" rel="noopener">official alternative communications backend</a>
 for R&rsquo;s <code>parallel</code> package, mirai underpins workflows from interactive web applications to pipeline orchestration to AI-powered document processing.</p>
<p>Learn mirai, and you&rsquo;ve learned the async primitive that powers the modern R stack. The same two concepts &ndash; 






<a href="https://mirai.r-lib.org/reference/daemons.html" target="_blank" rel="noopener"><code>daemons()</code></a>
 to set up workers, 






<a href="https://mirai.r-lib.org/reference/mirai.html" target="_blank" rel="noopener"><code>mirai()</code></a>
 to send work &ndash; are all you need to keep a Shiny app responsive or run async tasks in production.</p>
<h2 id="http-launcher">HTTP launcher
</h2>
<p>This release extends the &ldquo;deploy everywhere&rdquo; principle with 






<a href="https://mirai.r-lib.org/reference/http_config.html" target="_blank" rel="noopener"><code>http_config()</code></a>
, a new remote launch configuration that deploys daemons via HTTP API calls &ndash; any platform with an HTTP API for launching jobs.</p>
<h3 id="posit-workbench">Posit Workbench
</h3>
<p>Many organizations use 






<a href="https://posit.co/products/enterprise/workbench/" target="_blank" rel="noopener">Posit Workbench</a>
 to run research and data science at scale. mirai now integrates directly with it.<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup> Call 






<a href="https://mirai.r-lib.org/reference/http_config.html" target="_blank" rel="noopener"><code>http_config()</code></a>
 with no arguments and it auto-configures using the Workbench environment:</p>
<div class="code-block"><div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">daemons</span><span class="p">(</span><span class="n">n</span> <span class="o">=</span> <span class="m">4</span><span class="p">,</span> <span class="n">url</span> <span class="o">=</span> <span class="nf">host_url</span><span class="p">(),</span> <span class="n">remote</span> <span class="o">=</span> <span class="nf">http_config</span><span class="p">())</span></span></span></code></pre></td></tr></table>
</div>
</div></div>
<p>That&rsquo;s it. Four daemons launch as Workbench jobs, connect back to your session, and you can start sending work to them.</p>
<figure>
<img src="https://opensource.posit.co/blog/2026-02-12_mirai-2-6-0/workbench.png" alt="Posit Workbench session showing launched mirai daemons" />
<figcaption aria-hidden="true">Posit Workbench session showing launched mirai daemons</figcaption>
</figure>
<p>Here&rsquo;s what that looks like in practice: you&rsquo;re developing a model in your Workbench session. Fitting it locally is slow. Add that line, and those fits fan out across four Workbench-managed compute jobs. When you&rsquo;re done, <code>daemons(0)</code> releases them. No YAML, no job scripts, no leaving your R session &ndash; resource allocation, access control, and job lifecycle are all handled by the platform.</p>
<p>If you&rsquo;ve been bitten by expired tokens in long-running sessions, 






<a href="https://mirai.r-lib.org/reference/http_config.html" target="_blank" rel="noopener"><code>http_config()</code></a>
 is designed to prevent that. Under the hood, it stores <em>functions</em> rather than static values for credentials and endpoint URLs. These functions are called at the moment daemons actually launch, so session cookies and API tokens are always fresh &ndash; even if you created the configuration hours earlier.</p>
<p>See the mirai vignette for 


  
  
  





<a href="https://mirai.r-lib.org/articles/v01-reference.html#troubleshooting" target="_blank" rel="noopener">troubleshooting</a>
 remote launches.</p>
<h3 id="custom-apis">Custom APIs
</h3>
<p>The HTTP launcher works with any HTTP API, not just Workbench. Supply your own endpoint, authentication, and request body:</p>
<div class="code-block"><div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">daemons</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="n">n</span> <span class="o">=</span> <span class="m">2</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">url</span> <span class="o">=</span> <span class="nf">host_url</span><span class="p">(),</span>
</span></span><span class="line"><span class="cl">  <span class="n">remote</span> <span class="o">=</span> <span class="nf">http_config</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">url</span> <span class="o">=</span> <span class="s">&#34;https://api.example.com/launch&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">method</span> <span class="o">=</span> <span class="s">&#34;POST&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">token</span> <span class="o">=</span> <span class="kr">function</span><span class="p">()</span> <span class="nf">Sys.getenv</span><span class="p">(</span><span class="s">&#34;MY_API_KEY&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">    <span class="n">data</span> <span class="o">=</span> <span class="s">&#39;{&#34;command&#34;: &#34;%s&#34;}&#39;</span>
</span></span><span class="line"><span class="cl">  <span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span></span></span></code></pre></td></tr></table>
</div>
</div></div>
<p>The <code>&quot;%s&quot;</code> placeholder in <code>data</code> is where mirai inserts the daemon launch command at launch time. Each argument can be a plain value or a function &ndash; use functions for anything that changes between launches (tokens, cookies, dynamic URLs).</p>
<p>This opens up a wide range of deployment targets: Kubernetes job APIs, other cloud container services, or any internal job scheduler with an HTTP interface. If you can launch a process with an HTTP call, mirai can use it.</p>
<h2 id="c-level-dispatcher">C-level dispatcher
</h2>
<p>The overhead of distributing your tasks is now negligible. In a 






<a href="https://mirai.r-lib.org/reference/mirai_map.html" target="_blank" rel="noopener"><code>mirai_map()</code></a>
 over thousands of items, what you measure is the time of your actual computation, not the framework &ndash; per-task dispatch overhead is now in the tens of microseconds, where existing R parallelism solutions typically operate in the millisecond range.</p>
<p>Under the hood, the dispatcher &ndash; the process that sits between your session and the daemons, routing tasks to available workers &ndash; has been re-implemented entirely in C code within 






<a href="https://nanonext.r-lib.org" target="_blank" rel="noopener">nanonext</a>
. This eliminates the R interpreter overhead that remained, while the dispatcher continues to be event-driven and consume zero CPU when idle.</p>
<p>This also removes the bottleneck when coordinating large numbers of daemons, which matters directly for the kind of scaled-out deployments that the HTTP launcher enables &ndash; dozens of Workbench jobs or cloud instances all connecting to a single dispatcher. The two features are designed to work together: deploy broadly, dispatch efficiently. mirai is built to scale from 2 cores on your laptop to 200 across a cluster, without the framework slowing you down.</p>
<h2 id="race_mirai"><code>race_mirai()</code>
</h2>
<p>






<a href="https://mirai.r-lib.org/reference/race_mirai.html" target="_blank" rel="noopener"><code>race_mirai()</code></a>
 lets you process results as they arrive, rather than waiting for the slowest task. Suppose you&rsquo;re fitting 10 models with different hyperparameters in parallel &ndash; some converge quickly, others take much longer. Without 






<a href="https://mirai.r-lib.org/reference/race_mirai.html" target="_blank" rel="noopener"><code>race_mirai()</code></a>
, you wait for the slowest fit to complete before seeing any results. With it, you can inspect or save each model the instant it finishes &ndash; updating a progress display, freeing memory, or deciding whether to continue the remaining fits at all.</p>
<p>






<a href="https://mirai.r-lib.org/reference/race_mirai.html" target="_blank" rel="noopener"><code>race_mirai()</code></a>
 returns the integer <em>index</em> of the first resolved mirai. This makes the &ldquo;process as completed&rdquo; pattern clean and efficient:</p>
<div class="code-block"><div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">daemons</span><span class="p">(</span><span class="m">4</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Launch 10 model fits in parallel</span>
</span></span><span class="line"><span class="cl"><span class="n">fits</span> <span class="o">&lt;-</span> <span class="nf">lapply</span><span class="p">(</span><span class="n">param_grid</span><span class="p">,</span> <span class="kr">function</span><span class="p">(</span><span class="n">p</span><span class="p">)</span> <span class="nf">mirai</span><span class="p">(</span><span class="nf">fit_model</span><span class="p">(</span><span class="n">data</span><span class="p">,</span> <span class="n">p</span><span class="p">),</span> <span class="n">data</span> <span class="o">=</span> <span class="n">data</span><span class="p">,</span> <span class="n">p</span> <span class="o">=</span> <span class="n">p</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Process each result as soon as it&#39;s ready</span>
</span></span><span class="line"><span class="cl"><span class="n">remaining</span> <span class="o">&lt;-</span> <span class="n">fits</span>
</span></span><span class="line"><span class="cl"><span class="kr">while</span> <span class="p">(</span><span class="nf">length</span><span class="p">(</span><span class="n">remaining</span><span class="p">)</span> <span class="o">&gt;</span> <span class="m">0</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="n">idx</span> <span class="o">&lt;-</span> <span class="nf">race_mirai</span><span class="p">(</span><span class="n">remaining</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="nf">cat</span><span class="p">(</span><span class="s">&#34;Finished model with params:&#34;</span><span class="p">,</span> <span class="n">remaining[[idx]]</span><span class="o">$</span><span class="n">data</span><span class="o">$</span><span class="n">p</span><span class="p">,</span> <span class="s">&#34;\n&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="n">remaining</span> <span class="o">&lt;-</span> <span class="n">remaining[</span><span class="o">-</span><span class="n">idx]</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">daemons</span><span class="p">(</span><span class="m">0</span><span class="p">)</span></span></span></code></pre></td></tr></table>
</div>
</div></div>
<p>Send off a batch of tasks, then process results in the order they finish &ndash; no polling, no wasted time waiting on the slowest one. If any mirai is already resolved when you call 






<a href="https://mirai.r-lib.org/reference/race_mirai.html" target="_blank" rel="noopener"><code>race_mirai()</code></a>
, it returns immediately. This pattern applies whenever tasks have variable completion times &ndash; parallel model fits, API calls, simulations, or any batch where you want to stream results as they land.</p>
<h2 id="synchronous-mode">Synchronous mode
</h2>
<p>When tasks don&rsquo;t behave as expected, you need a way to inspect them interactively.</p>
<p>Without synchronous mode, errors in a mirai return as <code>miraiError</code> objects &ndash; you can see that something went wrong, but you can&rsquo;t step through the code to find out why. The task ran in a separate process, and by the time you see the error, that process has moved on.</p>
<p><code>daemons(sync = TRUE)</code>, introduced in 2.5.1, solves this. It runs everything in the current process &ndash; no background processes, no networking &ndash; just sequential execution. You can use 






<a href="https://rdrr.io/r/base/browser.html" target="_blank" rel="noopener"><code>browser()</code></a>
 and other interactive debugging tools directly:</p>
<div class="code-block"><div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span><span class="lnt">7
</span><span class="lnt">8
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">daemons</span><span class="p">(</span><span class="n">sync</span> <span class="o">=</span> <span class="kc">TRUE</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nf">mirai</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nf">browser</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="n">mypkg</span><span class="o">::</span><span class="nf">some_complex_function</span><span class="p">(</span><span class="n">x</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="p">},</span>
</span></span><span class="line"><span class="cl">  <span class="n">x</span> <span class="o">=</span> <span class="n">my_data</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span></span></span></code></pre></td></tr></table>
</div>
</div></div>
<p>You can scope synchronous mode to a specific compute profile, isolating the problematic task for inspection while the rest of your pipeline keeps running in parallel.</p>
<h2 id="daemon-synchronization-with-everywhere">Daemon synchronization with <code>everywhere()</code>
</h2>
<p>






<a href="https://mirai.r-lib.org/reference/everywhere.html" target="_blank" rel="noopener"><code>everywhere()</code></a>
 runs setup operations on all daemons &ndash; loading packages, sourcing scripts, or preparing datasets &ndash; so they&rsquo;re ready before you send work.</p>
<p>When launching remote daemons &ndash; via SSH, HPC schedulers, or the new HTTP launcher &ndash; there&rsquo;s an inherent delay between requesting a daemon and that daemon being ready to accept work. The new <code>.min</code> argument ensures that setup has completed on at least that many daemons before returning:</p>
<div class="code-block"><div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span><span class="lnt">7
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">daemons</span><span class="p">(</span><span class="n">n</span> <span class="o">=</span> <span class="m">8</span><span class="p">,</span> <span class="n">url</span> <span class="o">=</span> <span class="nf">host_url</span><span class="p">(),</span> <span class="n">remote</span> <span class="o">=</span> <span class="nf">http_config</span><span class="p">())</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Wait until all 8 daemons are connected before continuing</span>
</span></span><span class="line"><span class="cl"><span class="nf">everywhere</span><span class="p">(</span><span class="nf">library</span><span class="p">(</span><span class="n">mypackage</span><span class="p">),</span> <span class="n">.min</span> <span class="o">=</span> <span class="m">8</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Now send work once all daemons are ready</span>
</span></span><span class="line"><span class="cl"><span class="n">mp</span> <span class="o">&lt;-</span> <span class="nf">mirai_map</span><span class="p">(</span><span class="n">tasks</span><span class="p">,</span> <span class="n">process</span><span class="p">)</span></span></span></code></pre></td></tr></table>
</div>
</div></div>
<p>This creates a synchronization point, ensuring your pipeline doesn&rsquo;t start sending work before all daemons are ready. It&rsquo;s especially useful for remote deployments where connection times are unpredictable.</p>
<h2 id="minor-improvements-and-fixes">Minor improvements and fixes
</h2>
<ul>
<li><code>miraiError</code> objects now have 






<a href="https://rdrr.io/r/base/conditions.html" target="_blank" rel="noopener"><code>conditionCall()</code></a>
 and 






<a href="https://rdrr.io/r/base/conditions.html" target="_blank" rel="noopener"><code>conditionMessage()</code></a>
 methods, making them easier to use with R&rsquo;s standard condition handling.</li>
<li>The default exit behavior for daemons has been updated with a 200ms grace period before forceful termination, which allows OpenTelemetry disconnection events to be traced.</li>
<li>OpenTelemetry span names and attributes have been revised to better follow semantic conventions.</li>
<li>






<a href="https://mirai.r-lib.org/reference/daemons.html" target="_blank" rel="noopener"><code>daemons()</code></a>
 now properly validates that <code>url</code> is a character value where supplied.</li>
<li>Fixed a bug where repeated mirai cancellation could sometimes cause a daemon to exit prematurely.</li>
</ul>
<h2 id="try-it-now">Try it now
</h2>
<div class="code-block"><div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span><span class="lnt">7
</span><span class="lnt">8
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">install.packages</span><span class="p">(</span><span class="s">&#34;mirai&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">mirai</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">daemons</span><span class="p">(</span><span class="m">4</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nf">system.time</span><span class="p">(</span><span class="nf">mirai_map</span><span class="p">(</span><span class="m">1</span><span class="o">:</span><span class="m">4</span><span class="p">,</span> <span class="nf">\</span><span class="p">(</span><span class="n">x</span><span class="p">)</span> <span class="nf">Sys.sleep</span><span class="p">(</span><span class="m">1</span><span class="p">))</span><span class="n">[]</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;    user  system elapsed</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;   0.000   0.001   1.003</span>
</span></span><span class="line"><span class="cl"><span class="nf">daemons</span><span class="p">(</span><span class="m">0</span><span class="p">)</span></span></span></code></pre></td></tr></table>
</div>
</div></div>
<p>Four one-second tasks, one second of wall time. If those were four model fits that each took a minute, you&rsquo;d go from four minutes down to one &ndash; and if you needed more power, switching to Workbench or a Slurm cluster is a one-line change. Visit 






<a href="https://mirai.r-lib.org" target="_blank" rel="noopener">mirai.r-lib.org</a>
 for the full documentation.</p>
<h2 id="acknowledgements">Acknowledgements
</h2>
<p>A big thank you to all the folks who helped make this release happen:</p>
<p>






<a href="https://github.com/agilly" target="_blank" rel="noopener">@agilly</a>
, 






<a href="https://github.com/aimundo" target="_blank" rel="noopener">@aimundo</a>
, 






<a href="https://github.com/barnabasharris" target="_blank" rel="noopener">@barnabasharris</a>
, 






<a href="https://github.com/beevabeeva" target="_blank" rel="noopener">@beevabeeva</a>
, 






<a href="https://github.com/boshek" target="_blank" rel="noopener">@boshek</a>
, 






<a href="https://github.com/eliocamp" target="_blank" rel="noopener">@eliocamp</a>
, 






<a href="https://github.com/jan-swissre" target="_blank" rel="noopener">@jan-swissre</a>
, 






<a href="https://github.com/jeroenjanssens" target="_blank" rel="noopener">@jeroenjanssens</a>
, 






<a href="https://github.com/kentqin-cve" target="_blank" rel="noopener">@kentqin-cve</a>
, 






<a href="https://github.com/mcol" target="_blank" rel="noopener">@mcol</a>
, 






<a href="https://github.com/michaelmayer2" target="_blank" rel="noopener">@michaelmayer2</a>
, 






<a href="https://github.com/pmac0451" target="_blank" rel="noopener">@pmac0451</a>
, 






<a href="https://github.com/r2evans" target="_blank" rel="noopener">@r2evans</a>
, 






<a href="https://github.com/shikokuchuo" target="_blank" rel="noopener">@shikokuchuo</a>
, 






<a href="https://github.com/t-kalinowski" target="_blank" rel="noopener">@t-kalinowski</a>
, 






<a href="https://github.com/VincentGuyader" target="_blank" rel="noopener">@VincentGuyader</a>
, 






<a href="https://github.com/wlandau" target="_blank" rel="noopener">@wlandau</a>
, and 






<a href="https://github.com/xwanner" target="_blank" rel="noopener">@xwanner</a>
.</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>Requires Posit Workbench version 2026.01 or later, which enables launcher authentication using the session cookie.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]></description>
      <enclosure url="https://opensource.posit.co/blog/2026-02-12_mirai-2-6-0/thumbnail-wd.jpg" length="208853" type="image/jpeg" />
    </item>
    <item>
      <title>`dplyr::if_else()` and `dplyr::case_when()` are up to 30x faster</title>
      <link>https://opensource.posit.co/blog/2026-02-10_dplyr-performance/</link>
      <pubDate>Tue, 10 Feb 2026 00:00:00 +0000</pubDate>
      <guid>https://opensource.posit.co/blog/2026-02-10_dplyr-performance/</guid>
      <dc:creator>Davis Vaughan</dc:creator><description><![CDATA[<p>In this technical post, we&rsquo;ll dive into some performance improvements we&rsquo;ve made to dplyr 1.2.0 to make 






<a href="https://dplyr.tidyverse.org/reference/if_else.html" target="_blank" rel="noopener"><code>if_else()</code></a>
 and 






<a href="https://dplyr.tidyverse.org/reference/case-and-replace-when.html" target="_blank" rel="noopener"><code>case_when()</code></a>
 up to 30x faster and use up to 10x less memory.</p>
<p>If you haven&rsquo;t seen our 






  
  

<a href="https://opensource.posit.co/blog/2026-02-04_dplyr-1-2-0/">previous post</a>
 about the exciting new features in dplyr 1.2.0, you&rsquo;ll want to go check that out first!</p>
<p>Here&rsquo;s a before-and-after benchmark with 






<a href="https://dplyr.tidyverse.org/reference/if_else.html" target="_blank" rel="noopener"><code>if_else()</code></a>
:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='c'># Using https://github.com/DavisVaughan/cross</span></span>
<span><span class='nf'>cross</span><span class='nf'>::</span><span class='nf'><a href='https://rdrr.io/pkg/cross/man/bench_versions.html'>bench_versions</a></span><span class='o'>(</span>pkgs <span class='o'>=</span> <span class='nf'><a href='https://rdrr.io/r/base/c.html'>c</a></span><span class='o'>(</span><span class='s'>"tidyverse/dplyr@v1.1.4"</span>, <span class='s'>"tidyverse/dplyr"</span><span class='o'>)</span>, <span class='o'>&#123;</span></span>
<span>  <span class='kr'><a href='https://rdrr.io/r/base/library.html'>library</a></span><span class='o'>(</span><span class='nv'><a href='https://dplyr.tidyverse.org'>dplyr</a></span><span class='o'>)</span></span>
<span>  <span class='nf'><a href='https://rdrr.io/r/base/Random.html'>set.seed</a></span><span class='o'>(</span><span class='m'>123</span><span class='o'>)</span></span>
<span></span>
<span>  <span class='nv'>condition</span> <span class='o'>&lt;-</span> <span class='nf'><a href='https://rdrr.io/r/base/sample.html'>sample</a></span><span class='o'>(</span><span class='nf'><a href='https://rdrr.io/r/base/c.html'>c</a></span><span class='o'>(</span><span class='kc'>TRUE</span>, <span class='kc'>FALSE</span>, <span class='kc'>NA</span><span class='o'>)</span>, size <span class='o'>=</span> <span class='m'>1e7</span>, replace <span class='o'>=</span> <span class='kc'>TRUE</span><span class='o'>)</span></span>
<span>  <span class='nv'>x</span> <span class='o'>&lt;-</span> <span class='nf'><a href='https://rdrr.io/r/base/sample.html'>sample</a></span><span class='o'>(</span><span class='m'>10</span>, size <span class='o'>=</span> <span class='m'>1e7</span>, replace <span class='o'>=</span> <span class='kc'>TRUE</span><span class='o'>)</span></span>
<span>  <span class='nv'>y</span> <span class='o'>&lt;-</span> <span class='nf'><a href='https://rdrr.io/r/base/sample.html'>sample</a></span><span class='o'>(</span><span class='m'>10</span>, size <span class='o'>=</span> <span class='m'>1e7</span>, replace <span class='o'>=</span> <span class='kc'>TRUE</span><span class='o'>)</span></span>
<span>  <span class='nv'>z</span> <span class='o'>&lt;-</span> <span class='nf'><a href='https://rdrr.io/r/base/sample.html'>sample</a></span><span class='o'>(</span><span class='m'>10</span>, size <span class='o'>=</span> <span class='m'>1e7</span>, replace <span class='o'>=</span> <span class='kc'>TRUE</span><span class='o'>)</span></span>
<span></span>
<span>  <span class='nf'>bench</span><span class='nf'>::</span><span class='nf'><a href='https://bench.r-lib.org/reference/mark.html'>mark</a></span><span class='o'>(</span>if_else <span class='o'>=</span> <span class='nf'><a href='https://dplyr.tidyverse.org/reference/if_else.html'>if_else</a></span><span class='o'>(</span><span class='nv'>condition</span>, <span class='nv'>x</span>, <span class='nv'>y</span>, missing <span class='o'>=</span> <span class='nv'>z</span><span class='o'>)</span><span class='o'>)</span></span>
<span><span class='o'>&#125;</span><span class='o'>)</span></span></code></pre>
</div>
<div class="code-block"><div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="c1">#&gt; # A tibble: 2 × 6</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;   pkg                    expression      min   median `itr/sec` mem_alloc</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;   &lt;chr&gt;                  &lt;bch:expr&gt; &lt;bch:tm&gt; &lt;bch:tm&gt;     &lt;dbl&gt; &lt;bch:byt&gt;</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 1 tidyverse/dplyr@v1.1.4 if_else    248.25ms 249.25ms      4.02   381.6MB</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 2 tidyverse/dplyr        if_else      7.27ms   7.51ms    132.      38.2MB</span></span></span></code></pre></td></tr></table>
</div>
</div></div>
<p>And with 






<a href="https://dplyr.tidyverse.org/reference/case-and-replace-when.html" target="_blank" rel="noopener"><code>case_when()</code></a>
:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nf'>cross</span><span class='nf'>::</span><span class='nf'><a href='https://rdrr.io/pkg/cross/man/bench_versions.html'>bench_versions</a></span><span class='o'>(</span>pkgs <span class='o'>=</span> <span class='nf'><a href='https://rdrr.io/r/base/c.html'>c</a></span><span class='o'>(</span><span class='s'>"tidyverse/dplyr@v1.1.4"</span>, <span class='s'>"tidyverse/dplyr"</span><span class='o'>)</span>, <span class='o'>&#123;</span></span>
<span>  <span class='kr'><a href='https://rdrr.io/r/base/library.html'>library</a></span><span class='o'>(</span><span class='nv'><a href='https://dplyr.tidyverse.org'>dplyr</a></span><span class='o'>)</span></span>
<span>  <span class='nf'><a href='https://rdrr.io/r/base/Random.html'>set.seed</a></span><span class='o'>(</span><span class='m'>123</span><span class='o'>)</span></span>
<span></span>
<span>  <span class='nv'>column</span> <span class='o'>&lt;-</span> <span class='nf'><a href='https://rdrr.io/r/base/sample.html'>sample</a></span><span class='o'>(</span><span class='m'>100</span>, size <span class='o'>=</span> <span class='m'>1e7</span>, replace <span class='o'>=</span> <span class='kc'>TRUE</span><span class='o'>)</span></span>
<span></span>
<span>  <span class='nv'>x_condition</span> <span class='o'>&lt;-</span> <span class='nv'>column</span> <span class='o'>&lt;</span> <span class='m'>20</span></span>
<span>  <span class='nv'>y_condition</span> <span class='o'>&lt;-</span> <span class='nv'>column</span> <span class='o'>&lt;</span> <span class='m'>50</span></span>
<span>  <span class='nv'>z_condition</span> <span class='o'>&lt;-</span> <span class='nv'>column</span> <span class='o'>&lt;</span> <span class='m'>80</span></span>
<span></span>
<span>  <span class='nv'>x</span> <span class='o'>&lt;-</span> <span class='nf'><a href='https://rdrr.io/r/base/sample.html'>sample</a></span><span class='o'>(</span><span class='m'>10</span>, size <span class='o'>=</span> <span class='m'>1e7</span>, replace <span class='o'>=</span> <span class='kc'>TRUE</span><span class='o'>)</span></span>
<span>  <span class='nv'>y</span> <span class='o'>&lt;-</span> <span class='nf'><a href='https://rdrr.io/r/base/sample.html'>sample</a></span><span class='o'>(</span><span class='m'>10</span>, size <span class='o'>=</span> <span class='m'>1e7</span>, replace <span class='o'>=</span> <span class='kc'>TRUE</span><span class='o'>)</span></span>
<span>  <span class='nv'>z</span> <span class='o'>&lt;-</span> <span class='nf'><a href='https://rdrr.io/r/base/sample.html'>sample</a></span><span class='o'>(</span><span class='m'>10</span>, size <span class='o'>=</span> <span class='m'>1e7</span>, replace <span class='o'>=</span> <span class='kc'>TRUE</span><span class='o'>)</span></span>
<span></span>
<span>  <span class='nf'>bench</span><span class='nf'>::</span><span class='nf'><a href='https://bench.r-lib.org/reference/mark.html'>mark</a></span><span class='o'>(</span></span>
<span>    case_when <span class='o'>=</span> <span class='nf'><a href='https://dplyr.tidyverse.org/reference/case-and-replace-when.html'>case_when</a></span><span class='o'>(</span></span>
<span>      <span class='nv'>x_condition</span> <span class='o'>~</span> <span class='nv'>x</span>,</span>
<span>      <span class='nv'>y_condition</span> <span class='o'>~</span> <span class='nv'>y</span>,</span>
<span>      <span class='nv'>z_condition</span> <span class='o'>~</span> <span class='nv'>z</span></span>
<span>    <span class='o'>)</span></span>
<span>  <span class='o'>)</span></span>
<span><span class='o'>&#125;</span><span class='o'>)</span></span></code></pre>
</div>
<div class="code-block"><div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="c1">#&gt; # A tibble: 2 × 6</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;   pkg                    expression      min   median `itr/sec` mem_alloc</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;   &lt;chr&gt;                  &lt;bch:expr&gt; &lt;bch:tm&gt; &lt;bch:tm&gt;     &lt;dbl&gt; &lt;bch:byt&gt;</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 1 tidyverse/dplyr@v1.1.4 case_when   228.3ms  231.2ms      4.33   419.9MB</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 2 tidyverse/dplyr        case_when    15.5ms   15.8ms     62.8     38.3MB</span></span></span></code></pre></td></tr></table>
</div>
</div></div>
<p>So a 33x speed improvement for 






<a href="https://dplyr.tidyverse.org/reference/if_else.html" target="_blank" rel="noopener"><code>if_else()</code></a>
, a 15x speed improvement for 






<a href="https://dplyr.tidyverse.org/reference/case-and-replace-when.html" target="_blank" rel="noopener"><code>case_when()</code></a>
, and a 10x improvement in memory usage for both! In the rest of this post, we&rsquo;ll explain how we&rsquo;ve achieved these numbers.</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='kr'><a href='https://rdrr.io/r/base/library.html'>library</a></span><span class='o'>(</span><span class='nv'><a href='https://dplyr.tidyverse.org'>dplyr</a></span><span class='o'>)</span></span></code></pre>
</div>
<h2 id="lets-talk-memory">Let&rsquo;s talk memory
</h2>
<p>We&rsquo;ll start with 






<a href="https://dplyr.tidyverse.org/reference/case-and-replace-when.html" target="_blank" rel="noopener"><code>case_when()</code></a>
, because 






<a href="https://dplyr.tidyverse.org/reference/if_else.html" target="_blank" rel="noopener"><code>if_else()</code></a>
 is actually just a small variant of that.</p>
<p>The most important place to start is with the memory usage. Memory usage and raw speed are often related, as allocating memory takes time. Let&rsquo;s look at the memory usage of 






<a href="https://dplyr.tidyverse.org/reference/case-and-replace-when.html" target="_blank" rel="noopener"><code>case_when()</code></a>
 in dplyr 1.1.4:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nf'><a href='https://rdrr.io/r/base/Random.html'>set.seed</a></span><span class='o'>(</span><span class='m'>123</span><span class='o'>)</span></span>
<span></span>
<span><span class='nv'>column</span> <span class='o'>&lt;-</span> <span class='nf'><a href='https://rdrr.io/r/base/sample.html'>sample</a></span><span class='o'>(</span><span class='m'>100</span>, size <span class='o'>=</span> <span class='m'>1e7</span>, replace <span class='o'>=</span> <span class='kc'>TRUE</span><span class='o'>)</span></span>
<span></span>
<span><span class='nv'>x_condition</span> <span class='o'>&lt;-</span> <span class='nv'>column</span> <span class='o'>&lt;</span> <span class='m'>20</span></span>
<span><span class='nv'>y_condition</span> <span class='o'>&lt;-</span> <span class='nv'>column</span> <span class='o'>&lt;</span> <span class='m'>50</span></span>
<span><span class='nv'>z_condition</span> <span class='o'>&lt;-</span> <span class='nv'>column</span> <span class='o'>&lt;</span> <span class='m'>80</span></span>
<span></span>
<span><span class='nv'>x</span> <span class='o'>&lt;-</span> <span class='nf'><a href='https://rdrr.io/r/base/sample.html'>sample</a></span><span class='o'>(</span><span class='m'>10</span>, size <span class='o'>=</span> <span class='m'>1e7</span>, replace <span class='o'>=</span> <span class='kc'>TRUE</span><span class='o'>)</span></span>
<span><span class='nv'>y</span> <span class='o'>&lt;-</span> <span class='nf'><a href='https://rdrr.io/r/base/sample.html'>sample</a></span><span class='o'>(</span><span class='m'>10</span>, size <span class='o'>=</span> <span class='m'>1e7</span>, replace <span class='o'>=</span> <span class='kc'>TRUE</span><span class='o'>)</span></span>
<span><span class='nv'>z</span> <span class='o'>&lt;-</span> <span class='nf'><a href='https://rdrr.io/r/base/sample.html'>sample</a></span><span class='o'>(</span><span class='m'>10</span>, size <span class='o'>=</span> <span class='m'>1e7</span>, replace <span class='o'>=</span> <span class='kc'>TRUE</span><span class='o'>)</span></span></code></pre>
</div>
<div class="code-block"><div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span><span class="lnt">20
</span><span class="lnt">21
</span><span class="lnt">22
</span><span class="lnt">23
</span><span class="lnt">24
</span><span class="lnt">25
</span><span class="lnt">26
</span><span class="lnt">27
</span><span class="lnt">28
</span><span class="lnt">29
</span><span class="lnt">30
</span><span class="lnt">31
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="n">profmem</span><span class="o">::</span><span class="nf">profmem</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="n">threshold</span> <span class="o">=</span> <span class="m">1000</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nf">case_when</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">x_condition</span> <span class="o">~</span> <span class="n">x</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">y_condition</span> <span class="o">~</span> <span class="n">y</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">z_condition</span> <span class="o">~</span> <span class="n">z</span>
</span></span><span class="line"><span class="cl">  <span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; Rprofmem memory profiling of:</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; case_when(x_condition ~ x, y_condition ~ y, z_condition ~ z)</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; Memory allocations (&gt;= 1000 bytes):</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;        what     bytes                                           calls</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 1     alloc  40000048     case_when() -&gt; vec_case_when() -&gt; vec_rep()</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 2     alloc  40000048                  case_when() -&gt; vec_case_when()</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 3     alloc  40000056       case_when() -&gt; vec_case_when() -&gt; which()</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 4     alloc   7600664       case_when() -&gt; vec_case_when() -&gt; which()</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 5     alloc  40000048                  case_when() -&gt; vec_case_when()</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 6     alloc  40000056       case_when() -&gt; vec_case_when() -&gt; which()</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 7     alloc  12003312       case_when() -&gt; vec_case_when() -&gt; which()</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 8     alloc  40000048                  case_when() -&gt; vec_case_when()</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 9     alloc  40000056       case_when() -&gt; vec_case_when() -&gt; which()</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 10    alloc  11996112       case_when() -&gt; vec_case_when() -&gt; which()</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 11    alloc  40000056       case_when() -&gt; vec_case_when() -&gt; which()</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 12    alloc   8400112       case_when() -&gt; vec_case_when() -&gt; which()</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 13    alloc   7600664   case_when() -&gt; vec_case_when() -&gt; vec_slice()</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 14    alloc  12003312   case_when() -&gt; vec_case_when() -&gt; vec_slice()</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 15    alloc  11996112   case_when() -&gt; vec_case_when() -&gt; vec_slice()</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 16    alloc   8400112 case_when() -&gt; vec_case_when() -&gt; vec_recycle()</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 17    alloc  40000048 case_when() -&gt; vec_case_when() -&gt; list_unchop()</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; total       440000864</span></span></span></code></pre></td></tr></table>
</div>
</div></div>
<p>That&rsquo;s a lot of allocations! And it&rsquo;s pretty hard to understand where they are coming from without a bit more explanation. For that, we&rsquo;re actually going to &ldquo;manually&rdquo; implement an underpowered version of 






<a href="https://dplyr.tidyverse.org/reference/case-and-replace-when.html" target="_blank" rel="noopener"><code>case_when()</code></a>
 for this example.</p>
<p>Here&rsquo;s a diagram of what we need to accomplish:</p>
<p><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://opensource.posit.co/blog/2026-02-10_dplyr-performance/images/x-y-z-default.png"
      alt="" 
      loading="lazy"
    >
  </figure></div>
</p>
<p>In bullets:</p>
<ul>
<li><code>x_condition</code> selects the blue elements of <code>x</code></li>
<li><code>y_condition</code> selects the red elements of <code>y</code></li>
<li><code>z_condition</code> selects the green elements of <code>z</code></li>
<li>A <code>default</code> is built around the unused locations</li>
<li>We combine all of the pieces into <code>out</code></li>
</ul>
<p>The trickiest part about 






<a href="https://dplyr.tidyverse.org/reference/case-and-replace-when.html" target="_blank" rel="noopener"><code>case_when()</code></a>
 is handling places where <code>x_condition</code> and <code>y_condition</code> overlap. In the image, even though both <code>x</code> and <code>y</code> are selected at location 5, only the value of <code>x</code> is retained since it is hit &ldquo;first&rdquo;. This forces us to have to modify <code>y_condition</code> to avoid already &ldquo;used&rdquo; locations.</p>
<p>An R implementation that computes these modified locations might look like:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nv'>n</span> <span class='o'>&lt;-</span> <span class='nf'><a href='https://rdrr.io/r/base/length.html'>length</a></span><span class='o'>(</span><span class='nv'>x_condition</span><span class='o'>)</span></span>
<span></span>
<span><span class='nv'>unused</span> <span class='o'>&lt;-</span> <span class='nf'><a href='https://rdrr.io/r/base/rep.html'>rep</a></span><span class='o'>(</span><span class='kc'>TRUE</span>, times <span class='o'>=</span> <span class='nv'>n</span><span class='o'>)</span> <span class='c'># 1</span></span>
<span></span>
<span><span class='nv'>x_loc</span> <span class='o'>&lt;-</span> <span class='nv'>unused</span> <span class='o'>&amp;</span> <span class='nv'>x_condition</span> <span class='c'># 2</span></span>
<span><span class='nv'>x_loc</span> <span class='o'>&lt;-</span> <span class='nf'><a href='https://rdrr.io/r/base/which.html'>which</a></span><span class='o'>(</span><span class='nv'>x_loc</span><span class='o'>)</span> <span class='c'># 3,4</span></span>
<span><span class='nv'>unused</span><span class='o'>[</span><span class='nv'>x_loc</span><span class='o'>]</span> <span class='o'>&lt;-</span> <span class='kc'>FALSE</span></span>
<span></span>
<span><span class='nv'>y_loc</span> <span class='o'>&lt;-</span> <span class='nv'>unused</span> <span class='o'>&amp;</span> <span class='nv'>y_condition</span> <span class='c'># 5</span></span>
<span><span class='nv'>y_loc</span> <span class='o'>&lt;-</span> <span class='nf'><a href='https://rdrr.io/r/base/which.html'>which</a></span><span class='o'>(</span><span class='nv'>y_loc</span><span class='o'>)</span> <span class='c'># 6,7</span></span>
<span><span class='nv'>unused</span><span class='o'>[</span><span class='nv'>y_loc</span><span class='o'>]</span> <span class='o'>&lt;-</span> <span class='kc'>FALSE</span></span>
<span></span>
<span><span class='nv'>z_loc</span> <span class='o'>&lt;-</span> <span class='nv'>unused</span> <span class='o'>&amp;</span> <span class='nv'>z_condition</span> <span class='c'># 8</span></span>
<span><span class='nv'>z_loc</span> <span class='o'>&lt;-</span> <span class='nf'><a href='https://rdrr.io/r/base/which.html'>which</a></span><span class='o'>(</span><span class='nv'>z_loc</span><span class='o'>)</span> <span class='c'># 9,10</span></span>
<span><span class='nv'>unused</span><span class='o'>[</span><span class='nv'>z_loc</span><span class='o'>]</span> <span class='o'>&lt;-</span> <span class='kc'>FALSE</span></span></code></pre>
</div>
<p>Anything that is still <code>unused</code> falls through to the <code>default</code>:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nv'>default</span> <span class='o'>&lt;-</span> <span class='kc'>NA_integer_</span></span>
<span><span class='nv'>default_loc</span> <span class='o'>&lt;-</span> <span class='nf'><a href='https://rdrr.io/r/base/which.html'>which</a></span><span class='o'>(</span><span class='nv'>unused</span><span class='o'>)</span> <span class='c'># 11,12</span></span></code></pre>
</div>
<p>With <code>x_loc</code>, <code>y_loc</code>, <code>z_loc</code>, and <code>default_loc</code> in hand, we can build the output from the pieces:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nv'>out</span> <span class='o'>&lt;-</span> <span class='nf'><a href='https://rdrr.io/r/base/vector.html'>vector</a></span><span class='o'>(</span><span class='s'>"integer"</span>, length <span class='o'>=</span> <span class='nv'>n</span><span class='o'>)</span> <span class='c'># 17</span></span>
<span></span>
<span><span class='nv'>out</span><span class='o'>[</span><span class='nv'>x_loc</span><span class='o'>]</span> <span class='o'>&lt;-</span> <span class='nv'>x</span><span class='o'>[</span><span class='nv'>x_loc</span><span class='o'>]</span> <span class='c'># 13</span></span>
<span><span class='nv'>out</span><span class='o'>[</span><span class='nv'>y_loc</span><span class='o'>]</span> <span class='o'>&lt;-</span> <span class='nv'>y</span><span class='o'>[</span><span class='nv'>y_loc</span><span class='o'>]</span> <span class='c'># 14</span></span>
<span><span class='nv'>out</span><span class='o'>[</span><span class='nv'>z_loc</span><span class='o'>]</span> <span class='o'>&lt;-</span> <span class='nv'>z</span><span class='o'>[</span><span class='nv'>z_loc</span><span class='o'>]</span> <span class='c'># 15</span></span>
<span></span>
<span><span class='nv'>out</span><span class='o'>[</span><span class='nv'>default_loc</span><span class='o'>]</span> <span class='o'>&lt;-</span> <span class='nf'><a href='https://rdrr.io/r/base/rep.html'>rep</a></span><span class='o'>(</span><span class='nv'>default</span>, times <span class='o'>=</span> <span class='nf'><a href='https://rdrr.io/r/base/length.html'>length</a></span><span class='o'>(</span><span class='nv'>default_loc</span><span class='o'>)</span><span class='o'>)</span> <span class='c'># 16</span></span></code></pre>
</div>
<p>And sure enough, this is identical to 






<a href="https://dplyr.tidyverse.org/reference/case-and-replace-when.html" target="_blank" rel="noopener"><code>case_when()</code></a>
:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nf'><a href='https://rdrr.io/r/base/identical.html'>identical</a></span><span class='o'>(</span></span>
<span>  <span class='nv'>out</span>,</span>
<span>  <span class='nf'><a href='https://dplyr.tidyverse.org/reference/case-and-replace-when.html'>case_when</a></span><span class='o'>(</span></span>
<span>    <span class='nv'>x_condition</span> <span class='o'>~</span> <span class='nv'>x</span>,</span>
<span>    <span class='nv'>y_condition</span> <span class='o'>~</span> <span class='nv'>y</span>,</span>
<span>    <span class='nv'>z_condition</span> <span class='o'>~</span> <span class='nv'>z</span></span>
<span>  <span class='o'>)</span></span>
<span><span class='o'>)</span></span>
<span><span class='c'>#&gt; [1] TRUE</span></span>
<span></span></code></pre>
</div>
<p>You might be wondering what all of the comments with numbers beside them mean. Those actually map 1:1 with the allocations that 






<a href="https://dplyr.tidyverse.org/reference/case-and-replace-when.html" target="_blank" rel="noopener"><code>case_when()</code></a>
 was emitting. In fact, we can now split up those allocations into their respective role:</p>
<div class="code-block"><div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span><span class="lnt">20
</span><span class="lnt">21
</span><span class="lnt">22
</span><span class="lnt">23
</span><span class="lnt">24
</span><span class="lnt">25
</span><span class="lnt">26
</span><span class="lnt">27
</span><span class="lnt">28
</span><span class="lnt">29
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="c1">#&gt; # Tracking `unused` locations</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 1     alloc  40000048     case_when() -&gt; vec_case_when() -&gt; vec_rep()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; # Computing `x_loc`, `y_loc`, and `z_loc`</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 2     alloc  40000048                  case_when() -&gt; vec_case_when()</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 3     alloc  40000056       case_when() -&gt; vec_case_when() -&gt; which()</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 4     alloc   7600664       case_when() -&gt; vec_case_when() -&gt; which()</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 5     alloc  40000048                  case_when() -&gt; vec_case_when()</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 6     alloc  40000056       case_when() -&gt; vec_case_when() -&gt; which()</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 7     alloc  12003312       case_when() -&gt; vec_case_when() -&gt; which()</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 8     alloc  40000048                  case_when() -&gt; vec_case_when()</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 9     alloc  40000056       case_when() -&gt; vec_case_when() -&gt; which()</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 10    alloc  11996112       case_when() -&gt; vec_case_when() -&gt; which()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; # Computing `default_loc`</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 11    alloc  40000056       case_when() -&gt; vec_case_when() -&gt; which()</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 12    alloc   8400112       case_when() -&gt; vec_case_when() -&gt; which()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; # Slicing `x`, `y`, and `z` to align with `x_loc`, `y_loc`, and `z_loc`</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 13    alloc   7600664   case_when() -&gt; vec_case_when() -&gt; vec_slice()</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 14    alloc  12003312   case_when() -&gt; vec_case_when() -&gt; vec_slice()</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 15    alloc  11996112   case_when() -&gt; vec_case_when() -&gt; vec_slice()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; # Recycling `default` of `NA` to align with `default_loc`</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 16    alloc   8400112 case_when() -&gt; vec_case_when() -&gt; vec_recycle()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; # Final output container, which we assign `x`, `y`, `z`, and `default` into</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; # at locations `x_loc`, `y_loc`, and `z_loc`</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 17    alloc  40000048 case_when() -&gt; vec_case_when() -&gt; list_unchop()</span></span></span></code></pre></td></tr></table>
</div>
</div></div>
<p>We sought to remove every one of these allocations except for the last one, which is the final output container that is returned to the user. In other words, we were after this, which is the actual profmem result of this 






<a href="https://dplyr.tidyverse.org/reference/case-and-replace-when.html" target="_blank" rel="noopener"><code>case_when()</code></a>
 call in dplyr 1.2.0:</p>
<div class="code-block"><div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span><span class="lnt">7
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="c1">#&gt; Rprofmem memory profiling of:</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; case_when(x_condition ~ x, y_condition ~ y, z_condition ~ z)</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; Memory allocations (&gt;= 1000 bytes):</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;        what    bytes                          calls</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 1     alloc 40000048 case_when() -&gt; vec_case_when()</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; total       40000048</span></span></span></code></pre></td></tr></table>
</div>
</div></div>
<h2 id="sliced-assignment">Sliced assignment
</h2>
<p>To work towards this, let&rsquo;s focus on what happens to <code>x</code> throughout this process:</p>
<p><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://opensource.posit.co/blog/2026-02-10_dplyr-performance/images/just-x.png"
      alt="" 
      loading="lazy"
    >
  </figure></div>
</p>
<p>We had a hypothesis that we could cut out the intermediate work here. Ideally, we&rsquo;d take the logical LHS <code>x_condition</code> and the RHS <code>x</code> and map that straight into the output, with no extra allocations:</p>
<p><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://opensource.posit.co/blog/2026-02-10_dplyr-performance/images/just-x-ideal.png"
      alt="" 
      loading="lazy"
    >
  </figure></div>
</p>
<p>But this just wasn&rsquo;t possible with the way that assignment typically works in R!</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nv'>x</span> <span class='o'>&lt;-</span> <span class='nf'><a href='https://rdrr.io/r/base/c.html'>c</a></span><span class='o'>(</span><span class='s'>"a"</span>, <span class='s'>"b"</span>, <span class='s'>"c"</span>, <span class='s'>"d"</span>, <span class='s'>"e"</span>, <span class='s'>"f"</span>, <span class='s'>"g"</span><span class='o'>)</span></span>
<span><span class='nv'>x_condition</span> <span class='o'>&lt;-</span> <span class='nf'><a href='https://rdrr.io/r/base/c.html'>c</a></span><span class='o'>(</span><span class='kc'>FALSE</span>, <span class='kc'>TRUE</span>, <span class='kc'>FALSE</span>, <span class='kc'>FALSE</span>, <span class='kc'>TRUE</span>, <span class='kc'>FALSE</span>, <span class='kc'>FALSE</span><span class='o'>)</span></span>
<span><span class='nv'>out</span> <span class='o'>&lt;-</span> <span class='nf'><a href='https://rdrr.io/r/base/vector.html'>vector</a></span><span class='o'>(</span><span class='s'>"character"</span>, length <span class='o'>=</span> <span class='nf'><a href='https://rdrr.io/r/base/length.html'>length</a></span><span class='o'>(</span><span class='nv'>x_condition</span><span class='o'>)</span><span class='o'>)</span></span>
<span></span>
<span><span class='nv'>out</span><span class='o'>[</span><span class='nv'>x_condition</span><span class='o'>]</span> <span class='o'>&lt;-</span> <span class='nv'>x</span></span>
<span><span class='c'>#&gt; Warning in out[x_condition] &lt;- x: number of items to replace is not a multiple of replacement length</span></span>
<span></span></code></pre>
</div>
<p>Instead, you must pre-slice <code>x</code> to a length that matches the locations that <code>x_condition</code> points to in <code>out</code>, i.e.:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nv'>out</span><span class='o'>[</span><span class='nv'>x_condition</span><span class='o'>]</span> <span class='o'>&lt;-</span> <span class='nv'>x</span><span class='o'>[</span><span class='nv'>x_condition</span><span class='o'>]</span></span></code></pre>
</div>
<p>Now, in 






<a href="https://dplyr.tidyverse.org/reference/case-and-replace-when.html" target="_blank" rel="noopener"><code>case_when()</code></a>
 we don&rsquo;t actually use <code>[&lt;-</code> for assignment or <code>[</code> for slicing. Instead, we use tools from 






<a href="https://vctrs.r-lib.org/" target="_blank" rel="noopener">vctrs</a>
, a low level package for building consistent tidyverse functions. In this case, we&rsquo;d use <code>vctrs::vec_assign()</code> and <code>vctrs::vec_slice()</code>:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nv'>out</span> <span class='o'>&lt;-</span> <span class='nf'>vctrs</span><span class='nf'>::</span><span class='nf'><a href='https://vctrs.r-lib.org/reference/vec_slice.html'>vec_assign</a></span><span class='o'>(</span><span class='nv'>out</span>, <span class='nv'>x_condition</span>, <span class='nf'>vctrs</span><span class='nf'>::</span><span class='nf'><a href='https://vctrs.r-lib.org/reference/vec_slice.html'>vec_slice</a></span><span class='o'>(</span><span class='nv'>x</span>, <span class='nv'>x_condition</span><span class='o'>)</span><span class='o'>)</span></span></code></pre>
</div>
<p>But <code>vec_assign()</code> had the same problem!</p>
<p>To solve this, we&rsquo;ve added a new boolean argument to <code>vec_assign()</code> called <code>slice_value</code>. You use it like this:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nv'>out</span> <span class='o'>&lt;-</span> <span class='nf'>vctrs</span><span class='nf'>::</span><span class='nf'><a href='https://vctrs.r-lib.org/reference/vec_slice.html'>vec_assign</a></span><span class='o'>(</span><span class='nv'>out</span>, <span class='nv'>x_condition</span>, <span class='nv'>x</span>, slice_value <span class='o'>=</span> <span class='kc'>TRUE</span><span class='o'>)</span></span></code></pre>
</div>
<p>With <code>slice_value = TRUE</code>, <code>vec_assign()</code> assumes that both <code>out</code> and <code>x</code> are the same length and that <code>x_condition</code> applies to <em>both</em> of these. Internally, rather than materializing <code>x[x_condition]</code>, we instead just loop over both <code>out</code> and <code>x</code> at the same time (at C level) and copy over values from <code>x</code> whenever <code>x_condition</code> is <code>TRUE</code>.</p>
<p>This is huge! It means that allocations 13-15 from above related to slicing <code>x</code>, <code>y</code>, and <code>z</code> all disappear.</p>
<h2 id="logical-indices">Logical <code>i</code>ndices
</h2>
<p>You might have noticed that we&rsquo;ve been using <code>which()</code> quite a bit in the above algorithm. This turns a logical vector of <code>TRUE</code> and <code>FALSE</code> into an integer vector of locations pointing to where the logical vector was <code>TRUE</code>:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nv'>x_condition</span> <span class='o'>&lt;-</span> <span class='nf'><a href='https://rdrr.io/r/base/c.html'>c</a></span><span class='o'>(</span><span class='kc'>TRUE</span>, <span class='kc'>FALSE</span>, <span class='kc'>TRUE</span>, <span class='kc'>FALSE</span>, <span class='kc'>FALSE</span>, <span class='kc'>TRUE</span><span class='o'>)</span></span>
<span><span class='nv'>x_loc</span> <span class='o'>&lt;-</span> <span class='nf'><a href='https://rdrr.io/r/base/which.html'>which</a></span><span class='o'>(</span><span class='nv'>x_condition</span><span class='o'>)</span></span>
<span><span class='nv'>x_loc</span></span>
<span><span class='c'>#&gt; [1] 1 3 6</span></span>
<span></span></code></pre>
</div>
<p>We perform this conversion up front due to how the following works at C level:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nv'>out</span><span class='o'>[</span><span class='nv'>x_condition</span><span class='o'>]</span> <span class='o'>&lt;-</span> <span class='nv'>x</span><span class='o'>[</span><span class='nv'>x_condition</span><span class='o'>]</span></span></code></pre>
</div>
<p>Both <code>[</code> and <code>[&lt;-</code> will convert a logical <code>x_condition</code> into the integer <code>x_loc</code> form before proceeding with the assignment, meaning that <code>which()</code> gets called twice if we don&rsquo;t do it once up front. And vctrs is the same way! Both <code>vec_assign()</code> and <code>vec_slice()</code> here would convert <code>x_condition</code> to an integer vector.</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nf'>vctrs</span><span class='nf'>::</span><span class='nf'><a href='https://vctrs.r-lib.org/reference/vec_slice.html'>vec_assign</a></span><span class='o'>(</span><span class='nv'>out</span>, <span class='nv'>x_condition</span>, <span class='nf'>vctrs</span><span class='nf'>::</span><span class='nf'><a href='https://vctrs.r-lib.org/reference/vec_slice.html'>vec_slice</a></span><span class='o'>(</span><span class='nv'>x</span>, <span class='nv'>x_condition</span><span class='o'>)</span><span class='o'>)</span></span></code></pre>
</div>
<p>Now, with the previous optimization we&rsquo;ve already seen that we can reduce this to:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nf'>vctrs</span><span class='nf'>::</span><span class='nf'><a href='https://vctrs.r-lib.org/reference/vec_slice.html'>vec_assign</a></span><span class='o'>(</span><span class='nv'>out</span>, <span class='nv'>x_condition</span>, <span class='nv'>x</span>, slice_value <span class='o'>=</span> <span class='kc'>TRUE</span><span class='o'>)</span></span></code></pre>
</div>
<p>But <code>vec_assign()</code> still converts a logical <code>x_condition</code> to integer locations internally before doing the assignment. So now it doesn&rsquo;t matter whether we do this conversion up front via <code>which()</code> or if we let <code>vec_assign()</code> do it, it still happens once per input. But we&rsquo;d like to avoid it entirely!</p>
<p>The solution here wasn&rsquo;t too magical, it just involved a good bit of grunt work. We&rsquo;ve added a path in <code>vec_assign()</code>&rsquo;s 


  
  
  





<a href="https://github.com/r-lib/vctrs/blob/94cea16b1ed3939aaa59c58dda75eedc75d6d075/src/slice-assign.c#L390-L417" target="_blank" rel="noopener">C code</a>
 that can handle logical indices like <code>x_condition</code> directly, rather than forcing them to be converted to integer locations first.</p>
<p>But this is a huge win, because it means that allocations 1-10, which were all related to 






<a href="https://rdrr.io/r/base/which.html" target="_blank" rel="noopener"><code>which()</code></a>
, can now be removed. <code>vec_assign()</code> will just handle that optimally for us without any extra allocations.</p>
<p>The nice part about an optimization like this is that any other existing code that is using <code>vec_assign()</code> with a logical index will also benefit from this without having to change a thing!</p>
<h2 id="default-handling"><code>default</code> handling
</h2>
<p>The remaining allocations are 11-12 and 16, which all have to do with the implied <code>default</code>. Allocations 11-12 were about figuring out where to put <code>default</code>, and allocation 16 was about recycling a typed size 1 <code>default</code> to the right size before assigning it into <code>out</code>.</p>
<p>As it turns out, we don&rsquo;t need any of this!</p>
<p>In vctrs, when we initialize any output container, we use <code>vec_init()</code>:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nf'>vctrs</span><span class='nf'>::</span><span class='nf'><a href='https://vctrs.r-lib.org/reference/vec_init.html'>vec_init</a></span><span class='o'>(</span><span class='nf'><a href='https://rdrr.io/r/base/integer.html'>integer</a></span><span class='o'>(</span><span class='o'>)</span>, n <span class='o'>=</span> <span class='m'>5</span><span class='o'>)</span></span>
<span><span class='c'>#&gt; [1] NA NA NA NA NA</span></span>
<span></span><span></span>
<span><span class='nf'>vctrs</span><span class='nf'>::</span><span class='nf'><a href='https://vctrs.r-lib.org/reference/vec_init.html'>vec_init</a></span><span class='o'>(</span><span class='nf'><a href='https://tibble.tidyverse.org/reference/tibble.html'>tibble</a></span><span class='o'>(</span>x <span class='o'>=</span> <span class='nf'><a href='https://rdrr.io/r/base/integer.html'>integer</a></span><span class='o'>(</span><span class='o'>)</span>, y <span class='o'>=</span> <span class='nf'><a href='https://rdrr.io/r/base/character.html'>character</a></span><span class='o'>(</span><span class='o'>)</span><span class='o'>)</span>, n <span class='o'>=</span> <span class='m'>5</span><span class='o'>)</span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'># A tibble: 5 × 2</span></span></span>
<span><span class='c'>#&gt;       x y    </span></span>
<span><span class='c'>#&gt;   <span style='color: #555555; font-style: italic;'>&lt;int&gt;</span> <span style='color: #555555; font-style: italic;'>&lt;chr&gt;</span></span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>1</span>    <span style='color: #BB0000;'>NA</span> <span style='color: #BB0000;'>NA</span>   </span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>2</span>    <span style='color: #BB0000;'>NA</span> <span style='color: #BB0000;'>NA</span>   </span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>3</span>    <span style='color: #BB0000;'>NA</span> <span style='color: #BB0000;'>NA</span>   </span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>4</span>    <span style='color: #BB0000;'>NA</span> <span style='color: #BB0000;'>NA</span>   </span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>5</span>    <span style='color: #BB0000;'>NA</span> <span style='color: #BB0000;'>NA</span></span></span>
<span></span></code></pre>
</div>
<p>This <em>already</em> has the implied <code>default</code> assigned to every location. We then overwrite this with <code>x</code>, <code>y</code>, and <code>z</code> at the appropriate locations, but anything left untouched by those is still set to the <code>default</code>, so we&rsquo;re done!</p>
<p>For cases where the user supplies their own <code>default</code>, things are slightly more complicated. We actually do have to compute a <code>default_loc</code> implied from <code>x_condition</code>, <code>y_condition</code>, and <code>z_condition</code>, but internally we do so using a C vector of <code>bool</code> (even more efficient than R&rsquo;s logical vector type), so the memory footprint is as small as it can be.</p>
<h2 id="the-first-wins-conundrum">The &ldquo;first wins&rdquo; conundrum
</h2>
<p>One thing we&rsquo;ve skipped over is the &ldquo;first wins&rdquo; behavior of 






<a href="https://dplyr.tidyverse.org/reference/case-and-replace-when.html" target="_blank" rel="noopener"><code>case_when()</code></a>
 mentioned earlier. Now that we&rsquo;ve removed <code>x_loc</code>, <code>y_loc</code>, and <code>z_loc</code>, which is where that was being handled, how do we keep this behavior without slowing things down?</p>
<p>To be explicit, we are talking about this feature of 






<a href="https://dplyr.tidyverse.org/reference/case-and-replace-when.html" target="_blank" rel="noopener"><code>case_when()</code></a>
 where only the first hit is kept when you have overlapping logical indices:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nv'>x</span> <span class='o'>&lt;-</span> <span class='nf'><a href='https://rdrr.io/r/base/c.html'>c</a></span><span class='o'>(</span><span class='s'>"x1"</span>, <span class='s'>"x2"</span>, <span class='s'>"x3"</span><span class='o'>)</span></span>
<span><span class='nv'>y</span> <span class='o'>&lt;-</span> <span class='nf'><a href='https://rdrr.io/r/base/c.html'>c</a></span><span class='o'>(</span><span class='s'>"y1"</span>, <span class='s'>"y2"</span>, <span class='s'>"y3"</span><span class='o'>)</span></span>
<span><span class='nv'>z</span> <span class='o'>&lt;-</span> <span class='nf'><a href='https://rdrr.io/r/base/c.html'>c</a></span><span class='o'>(</span><span class='s'>"z1"</span>, <span class='s'>"z2"</span>, <span class='s'>"z3"</span><span class='o'>)</span></span>
<span></span>
<span><span class='nv'>x_condition</span> <span class='o'>&lt;-</span> <span class='nf'><a href='https://rdrr.io/r/base/c.html'>c</a></span><span class='o'>(</span><span class='kc'>TRUE</span>, <span class='kc'>FALSE</span>, <span class='kc'>TRUE</span><span class='o'>)</span></span>
<span><span class='nv'>y_condition</span> <span class='o'>&lt;-</span> <span class='nf'><a href='https://rdrr.io/r/base/c.html'>c</a></span><span class='o'>(</span><span class='kc'>TRUE</span>, <span class='kc'>TRUE</span>, <span class='kc'>FALSE</span><span class='o'>)</span></span>
<span><span class='nv'>z_condition</span> <span class='o'>&lt;-</span> <span class='nf'><a href='https://rdrr.io/r/base/c.html'>c</a></span><span class='o'>(</span><span class='kc'>FALSE</span>, <span class='kc'>TRUE</span>, <span class='kc'>TRUE</span><span class='o'>)</span></span>
<span></span>
<span><span class='nf'><a href='https://dplyr.tidyverse.org/reference/case-and-replace-when.html'>case_when</a></span><span class='o'>(</span></span>
<span>  <span class='nv'>x_condition</span> <span class='o'>~</span> <span class='nv'>x</span>,</span>
<span>  <span class='nv'>y_condition</span> <span class='o'>~</span> <span class='nv'>y</span>,</span>
<span>  <span class='nv'>z_condition</span> <span class='o'>~</span> <span class='nv'>z</span></span>
<span><span class='o'>)</span></span>
<span><span class='c'>#&gt; [1] "x1" "y2" "x3"</span></span>
<span></span></code></pre>
</div>
<p>A naive approach doesn&rsquo;t work, as you end up with &ldquo;last wins&rdquo; behavior:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nv'>out</span> <span class='o'>&lt;-</span> <span class='nf'>vctrs</span><span class='nf'>::</span><span class='nf'><a href='https://vctrs.r-lib.org/reference/vec_init.html'>vec_init</a></span><span class='o'>(</span><span class='nf'><a href='https://rdrr.io/r/base/character.html'>character</a></span><span class='o'>(</span><span class='o'>)</span>, n <span class='o'>=</span> <span class='m'>3</span><span class='o'>)</span></span>
<span></span>
<span><span class='nv'>out</span> <span class='o'>&lt;-</span> <span class='nf'>vctrs</span><span class='nf'>::</span><span class='nf'><a href='https://vctrs.r-lib.org/reference/vec_slice.html'>vec_assign</a></span><span class='o'>(</span><span class='nv'>out</span>, <span class='nv'>x_condition</span>, <span class='nv'>x</span>, slice_value <span class='o'>=</span> <span class='kc'>TRUE</span><span class='o'>)</span></span>
<span><span class='nv'>out</span> <span class='o'>&lt;-</span> <span class='nf'>vctrs</span><span class='nf'>::</span><span class='nf'><a href='https://vctrs.r-lib.org/reference/vec_slice.html'>vec_assign</a></span><span class='o'>(</span><span class='nv'>out</span>, <span class='nv'>y_condition</span>, <span class='nv'>y</span>, slice_value <span class='o'>=</span> <span class='kc'>TRUE</span><span class='o'>)</span></span>
<span><span class='nv'>out</span> <span class='o'>&lt;-</span> <span class='nf'>vctrs</span><span class='nf'>::</span><span class='nf'><a href='https://vctrs.r-lib.org/reference/vec_slice.html'>vec_assign</a></span><span class='o'>(</span><span class='nv'>out</span>, <span class='nv'>z_condition</span>, <span class='nv'>z</span>, slice_value <span class='o'>=</span> <span class='kc'>TRUE</span><span class='o'>)</span></span>
<span></span>
<span><span class='c'># This is wrong!</span></span>
<span><span class='nv'>out</span></span>
<span><span class='c'>#&gt; [1] "y1" "z2" "z3"</span></span>
<span></span><span></span>
<span><span class='nf'><a href='https://rdrr.io/r/base/identical.html'>identical</a></span><span class='o'>(</span></span>
<span>  <span class='nv'>out</span>,</span>
<span>  <span class='nf'><a href='https://dplyr.tidyverse.org/reference/case-and-replace-when.html'>case_when</a></span><span class='o'>(</span></span>
<span>    <span class='nv'>x_condition</span> <span class='o'>~</span> <span class='nv'>x</span>,</span>
<span>    <span class='nv'>y_condition</span> <span class='o'>~</span> <span class='nv'>y</span>,</span>
<span>    <span class='nv'>z_condition</span> <span class='o'>~</span> <span class='nv'>z</span></span>
<span>  <span class='o'>)</span></span>
<span><span class='o'>)</span></span>
<span><span class='c'>#&gt; [1] FALSE</span></span>
<span></span></code></pre>
</div>
<p>Instead, 






<a href="https://dplyr.tidyverse.org/reference/case-and-replace-when.html" target="_blank" rel="noopener"><code>case_when()</code></a>
 just <em>iterates in reverse</em>, assigning <code>z</code>, then <code>y</code>, then <code>x</code>:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nv'>out</span> <span class='o'>&lt;-</span> <span class='nf'>vctrs</span><span class='nf'>::</span><span class='nf'><a href='https://vctrs.r-lib.org/reference/vec_init.html'>vec_init</a></span><span class='o'>(</span><span class='nf'><a href='https://rdrr.io/r/base/character.html'>character</a></span><span class='o'>(</span><span class='o'>)</span>, n <span class='o'>=</span> <span class='m'>3</span><span class='o'>)</span></span>
<span></span>
<span><span class='nv'>out</span> <span class='o'>&lt;-</span> <span class='nf'>vctrs</span><span class='nf'>::</span><span class='nf'><a href='https://vctrs.r-lib.org/reference/vec_slice.html'>vec_assign</a></span><span class='o'>(</span><span class='nv'>out</span>, <span class='nv'>z_condition</span>, <span class='nv'>z</span>, slice_value <span class='o'>=</span> <span class='kc'>TRUE</span><span class='o'>)</span></span>
<span><span class='nv'>out</span> <span class='o'>&lt;-</span> <span class='nf'>vctrs</span><span class='nf'>::</span><span class='nf'><a href='https://vctrs.r-lib.org/reference/vec_slice.html'>vec_assign</a></span><span class='o'>(</span><span class='nv'>out</span>, <span class='nv'>y_condition</span>, <span class='nv'>y</span>, slice_value <span class='o'>=</span> <span class='kc'>TRUE</span><span class='o'>)</span></span>
<span><span class='nv'>out</span> <span class='o'>&lt;-</span> <span class='nf'>vctrs</span><span class='nf'>::</span><span class='nf'><a href='https://vctrs.r-lib.org/reference/vec_slice.html'>vec_assign</a></span><span class='o'>(</span><span class='nv'>out</span>, <span class='nv'>x_condition</span>, <span class='nv'>x</span>, slice_value <span class='o'>=</span> <span class='kc'>TRUE</span><span class='o'>)</span></span>
<span></span>
<span><span class='nf'><a href='https://rdrr.io/r/base/identical.html'>identical</a></span><span class='o'>(</span></span>
<span>  <span class='nv'>out</span>,</span>
<span>  <span class='nf'><a href='https://dplyr.tidyverse.org/reference/case-and-replace-when.html'>case_when</a></span><span class='o'>(</span></span>
<span>    <span class='nv'>x_condition</span> <span class='o'>~</span> <span class='nv'>x</span>,</span>
<span>    <span class='nv'>y_condition</span> <span class='o'>~</span> <span class='nv'>y</span>,</span>
<span>    <span class='nv'>z_condition</span> <span class='o'>~</span> <span class='nv'>z</span></span>
<span>  <span class='o'>)</span></span>
<span><span class='o'>)</span></span>
<span><span class='c'>#&gt; [1] TRUE</span></span>
<span></span></code></pre>
</div>
<p>This diagram demonstrates how that works:</p>
<p><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://opensource.posit.co/blog/2026-02-10_dplyr-performance/images/case-when-reverse.png"
      alt="" 
      loading="lazy"
    >
  </figure></div>
</p>
<h2 id="optimizing-speed">Optimizing speed?
</h2>
<p>Now that we&rsquo;ve optimized the memory usage of 






<a href="https://dplyr.tidyverse.org/reference/case-and-replace-when.html" target="_blank" rel="noopener"><code>case_when()</code></a>
, you might be wondering if we did anything else to specifically optimize its speed. Not really! We have moved everything from R to C, but focusing our efforts on reducing memory also resulted in some pretty performant code, and there wasn&rsquo;t much left to optimize after that.</p>
<h2 id="if_else"><code>if_else()</code>
</h2>
<p>






<a href="https://dplyr.tidyverse.org/reference/if_else.html" target="_blank" rel="noopener"><code>if_else()</code></a>
 can actually be written as a form of 






<a href="https://dplyr.tidyverse.org/reference/case-and-replace-when.html" target="_blank" rel="noopener"><code>case_when()</code></a>
:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nf'><a href='https://dplyr.tidyverse.org/reference/if_else.html'>if_else</a></span><span class='o'>(</span><span class='nv'>condition</span>, <span class='nv'>true</span>, <span class='nv'>false</span>, <span class='nv'>missing</span><span class='o'>)</span></span>
<span></span>
<span><span class='nf'><a href='https://dplyr.tidyverse.org/reference/case-and-replace-when.html'>case_when</a></span><span class='o'>(</span></span>
<span>  <span class='nv'>condition</span> <span class='o'>~</span> <span class='nv'>true</span>,</span>
<span>  <span class='o'>!</span><span class='nv'>condition</span> <span class='o'>~</span> <span class='nv'>false</span>,</span>
<span>  <span class='nf'><a href='https://rdrr.io/r/base/NA.html'>is.na</a></span><span class='o'>(</span><span class='nv'>condition</span><span class='o'>)</span> <span class='o'>~</span> <span class='nv'>missing</span></span>
<span><span class='o'>)</span></span></code></pre>
</div>
<p>In our actual C implementation of 






<a href="https://dplyr.tidyverse.org/reference/if_else.html" target="_blank" rel="noopener"><code>if_else()</code></a>
, for simple types like integer, character, or numeric vectors we have an 


  
  
  





<a href="https://github.com/r-lib/vctrs/blob/94cea16b1ed3939aaa59c58dda75eedc75d6d075/src/if-else.c#L276-L505" target="_blank" rel="noopener">extremely fast path</a>
 that&rsquo;s even more optimized than this, but for anything with a class we pretty much use this exact 






<a href="https://dplyr.tidyverse.org/reference/case-and-replace-when.html" target="_blank" rel="noopener"><code>case_when()</code></a>
 approach.</p>
<h2 id="for-package-developers">For package developers
</h2>
<p>If you&rsquo;re a package developer, you&rsquo;ll be happy to know that vctrs itself now exposes low dependency versions of 






<a href="https://dplyr.tidyverse.org/reference/if_else.html" target="_blank" rel="noopener"><code>if_else()</code></a>
 and 






<a href="https://dplyr.tidyverse.org/reference/case-and-replace-when.html" target="_blank" rel="noopener"><code>case_when()</code></a>
, here&rsquo;s the full family:</p>
<ul>
<li><code>vec_if_else()</code></li>
<li><code>vec_case_when()</code></li>
<li><code>vec_replace_when()</code></li>
<li><code>vec_recode_values()</code></li>
<li><code>vec_replace_values()</code></li>
</ul>
<p>






<a href="https://dplyr.tidyverse.org/reference/if_else.html" target="_blank" rel="noopener"><code>dplyr::if_else()</code></a>
 and friends are now just very thin wrappers over these. Feel free to use the vctrs versions in your package if you need the consistency of the tidyverse without the heavy-ish dependency of dplyr.</p>
<h2 id="at-the-deepest-level-list_combine">At the deepest level, <code>list_combine()</code>
</h2>
<p>At the deepest level of all of this is one final new vctrs function, <code>list_combine()</code>. This is a flexible way to combine multiple vectors together at locations specified by <code>indices</code>.</p>
<p><code>list_combine()</code> powers all of <code>vec_case_when()</code>, <code>vec_replace_when()</code>, <code>vec_recode_values()</code>, <code>vec_replace_values()</code>, <code>vec_if_else()</code>, and even <code>vec_c()</code>, the tidyverse version of 






<a href="https://rdrr.io/r/base/c.html" target="_blank" rel="noopener"><code>c()</code></a>
.</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nf'><a href='https://rdrr.io/r/base/Random.html'>set.seed</a></span><span class='o'>(</span><span class='m'>123</span><span class='o'>)</span></span>
<span></span>
<span><span class='nv'>column</span> <span class='o'>&lt;-</span> <span class='nf'><a href='https://rdrr.io/r/base/sample.html'>sample</a></span><span class='o'>(</span><span class='m'>100</span>, size <span class='o'>=</span> <span class='m'>1e7</span>, replace <span class='o'>=</span> <span class='kc'>TRUE</span><span class='o'>)</span></span>
<span></span>
<span><span class='nv'>x_condition</span> <span class='o'>&lt;-</span> <span class='nv'>column</span> <span class='o'>&lt;</span> <span class='m'>20</span></span>
<span><span class='nv'>y_condition</span> <span class='o'>&lt;-</span> <span class='nv'>column</span> <span class='o'>&lt;</span> <span class='m'>50</span></span>
<span><span class='nv'>z_condition</span> <span class='o'>&lt;-</span> <span class='nv'>column</span> <span class='o'>&lt;</span> <span class='m'>80</span></span>
<span></span>
<span><span class='nv'>x</span> <span class='o'>&lt;-</span> <span class='nf'><a href='https://rdrr.io/r/base/sample.html'>sample</a></span><span class='o'>(</span><span class='m'>10</span>, size <span class='o'>=</span> <span class='m'>1e7</span>, replace <span class='o'>=</span> <span class='kc'>TRUE</span><span class='o'>)</span></span>
<span><span class='nv'>y</span> <span class='o'>&lt;-</span> <span class='nf'><a href='https://rdrr.io/r/base/sample.html'>sample</a></span><span class='o'>(</span><span class='m'>10</span>, size <span class='o'>=</span> <span class='m'>1e7</span>, replace <span class='o'>=</span> <span class='kc'>TRUE</span><span class='o'>)</span></span>
<span><span class='nv'>z</span> <span class='o'>&lt;-</span> <span class='nf'><a href='https://rdrr.io/r/base/sample.html'>sample</a></span><span class='o'>(</span><span class='m'>10</span>, size <span class='o'>=</span> <span class='m'>1e7</span>, replace <span class='o'>=</span> <span class='kc'>TRUE</span><span class='o'>)</span></span>
<span></span>
<span><span class='nv'>out</span> <span class='o'>&lt;-</span> <span class='nf'>vctrs</span><span class='nf'>::</span><span class='nf'><a href='https://vctrs.r-lib.org/reference/list_combine.html'>list_combine</a></span><span class='o'>(</span></span>
<span>  x <span class='o'>=</span> <span class='nf'><a href='https://rdrr.io/r/base/list.html'>list</a></span><span class='o'>(</span><span class='nv'>x</span>, <span class='nv'>y</span>, <span class='nv'>z</span><span class='o'>)</span>,</span>
<span></span>
<span>  <span class='c'># `indices` are allowed to be logical and aren't forced to integer</span></span>
<span>  indices <span class='o'>=</span> <span class='nf'><a href='https://rdrr.io/r/base/list.html'>list</a></span><span class='o'>(</span><span class='nv'>x_condition</span>, <span class='nv'>y_condition</span>, <span class='nv'>z_condition</span><span class='o'>)</span>,</span>
<span>  size <span class='o'>=</span> <span class='nf'><a href='https://rdrr.io/r/base/length.html'>length</a></span><span class='o'>(</span><span class='nv'>x_condition</span><span class='o'>)</span>,</span>
<span></span>
<span>  <span class='c'># When there are overlaps, take the "first"</span></span>
<span>  multiple <span class='o'>=</span> <span class='s'>"first"</span>,</span>
<span></span>
<span>  <span class='c'># Same as `slice_value` from `vec_assign()`</span></span>
<span>  slice_x <span class='o'>=</span> <span class='kc'>TRUE</span></span>
<span><span class='o'>)</span></span></code></pre>
</div>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nf'><a href='https://rdrr.io/r/base/identical.html'>identical</a></span><span class='o'>(</span></span>
<span>  <span class='nv'>out</span>,</span>
<span>  <span class='nf'><a href='https://dplyr.tidyverse.org/reference/case-and-replace-when.html'>case_when</a></span><span class='o'>(</span></span>
<span>    <span class='nv'>x_condition</span> <span class='o'>~</span> <span class='nv'>x</span>,</span>
<span>    <span class='nv'>y_condition</span> <span class='o'>~</span> <span class='nv'>y</span>,</span>
<span>    <span class='nv'>z_condition</span> <span class='o'>~</span> <span class='nv'>z</span></span>
<span>  <span class='o'>)</span></span>
<span><span class='o'>)</span></span>
<span><span class='c'>#&gt; [1] TRUE</span></span>
<span></span></code></pre>
</div>
]]></description>
      <enclosure url="https://opensource.posit.co/blog/2026-02-10_dplyr-performance/thumbnail-wd.jpg" length="179655" type="image/jpeg" />
    </item>
  </channel>
</rss>
