<?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>Best Practices on Posit Open Source</title>
    <link>https://opensource.posit.co/topics/best-practices/</link>
    <description>Recent content in Best Practices on Posit Open Source</description>
    <generator>Hugo -- gohugo.io</generator>
    <language>en-us</language>
    <lastBuildDate>Tue, 14 Apr 2026 00:00:00 +0000</lastBuildDate>
    <atom:link href="https://opensource.posit.co/topics/best-practices/index.xml" rel="self" type="application/rss+xml" />
    <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>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>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>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>nanonext 1.8.0</title>
      <link>https://opensource.posit.co/blog/2026-02-09_nanonext-1-8-0/</link>
      <pubDate>Mon, 09 Feb 2026 00:00:00 +0000</pubDate>
      <guid>https://opensource.posit.co/blog/2026-02-09_nanonext-1-8-0/</guid>
      <dc:creator>Charlie Gao</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>When we 






  
  
    
  

<a href="https://opensource.posit.co/blog/2025-09-02_nanonext-1-7-0/">introduced nanonext</a>
 last year, we showed how it connects R directly to Python, Go, Rust, and other languages through NNG&rsquo;s messaging protocols. We hinted at its web capabilities &ndash; but that was just the beginning.</p>
<p>R already has excellent web infrastructure. 






<a href="https://shiny.posit.co/" target="_blank" rel="noopener">Shiny</a>
 and 






<a href="https://plumber2.posit.co/" target="_blank" rel="noopener">plumber2</a>
 are the go-to tools for building interactive applications and REST APIs in R. They are both powered by 






<a href="https://rstudio.github.io/httpuv/" target="_blank" rel="noopener">httpuv</a>
. 






<a href="https://nanonext.r-lib.org" target="_blank" rel="noopener">nanonext</a>
 adds a complementary option at the httpuv level of the stack &ndash; a low-level streaming HTTP and WebSocket server built on NNG, giving developers fine-grained control over connections, streaming, and static file serving over TLS. nanonext is for when you need lower-level control &ndash; custom protocols, infrastructure endpoints, or embedding a server alongside an existing Shiny or plumber2 application.</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;nanonext&#34;</span><span class="p">)</span></span></span></code></pre></td></tr></table>
</div>
</div></div>
<p>You can see a full list of changes in the 


  
  
  





<a href="https://nanonext.r-lib.org/news/#nanonext-180" target="_blank" rel="noopener">release notes</a>
.</p>
<h2 id="streaming-httpwebsocket-server">Streaming HTTP/WebSocket server
</h2>
<p>The flagship feature of this release is 






<a href="https://nanonext.r-lib.org/reference/http_server.html" target="_blank" rel="noopener"><code>http_server()</code></a>
, a streaming HTTP and WebSocket server with full TLS support. Built on NNG&rsquo;s HTTP server architecture, it brings the same performance that powers nanonext&rsquo;s messaging layer to web serving.</p>
<p>One server, one port &ndash; HTTP endpoints, WebSocket connections, and streaming all coexist. Static files bypass R entirely, served natively by NNG. WebSocket and streaming connections run callbacks on R&rsquo;s main thread via the later package. Mbed TLS is built in for HTTPS/WSS, and there&rsquo;s no need to run separate processes or bind additional ports.</p>
<p>Because it shares the same event loop that Shiny uses, 






<a href="https://nanonext.r-lib.org/reference/http_server.html" target="_blank" rel="noopener"><code>http_server()</code></a>
 can run alongside a Shiny app in the same R process. You could spin up a nanonext server to handle health checks, serve static assets, or stream real-time events &ndash; while Shiny or plumber2 handles the application logic. They&rsquo;re designed to work together.</p>
<p>As part of our investment in expanding what&rsquo;s possible with R, we&rsquo;re already using nanonext at Posit to explore new real-time capabilities, and we&rsquo;re excited to see what the community builds with it.</p>
<h3 id="basic-http-server">Basic HTTP server
</h3>
<p>Where frameworks like plumber2 give you a full-featured API layer with routing, serialization, and documentation out of the box, 






<a href="https://nanonext.r-lib.org/reference/http_server.html" target="_blank" rel="noopener"><code>http_server()</code></a>
 gives you direct control over requests and responses &ndash; the kind of direct access you&rsquo;d reach for when building custom infrastructure or embedding a server inside a larger system.</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="nf">library</span><span class="p">(</span><span class="n">nanonext</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">server</span> <span class="o">&lt;-</span> <span class="nf">http_server</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;http://127.0.0.1:8080&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">handlers</span> <span class="o">=</span> <span class="nf">list</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="nf">handler</span><span class="p">(</span><span class="s">&#34;/&#34;</span><span class="p">,</span> <span class="kr">function</span><span class="p">(</span><span class="n">req</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nf">list</span><span class="p">(</span><span class="n">status</span> <span class="o">=</span> <span class="m">200L</span><span class="p">,</span> <span class="n">body</span> <span class="o">=</span> <span class="s">&#34;Hello from nanonext!&#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="nf">handler</span><span class="p">(</span><span class="s">&#34;/api/data&#34;</span><span class="p">,</span> <span class="kr">function</span><span class="p">(</span><span class="n">req</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nf">list</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="n">status</span> <span class="o">=</span> <span class="m">200L</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">headers</span> <span class="o">=</span> <span class="nf">c</span><span class="p">(</span><span class="s">&#34;Content-Type&#34;</span> <span class="o">=</span> <span class="s">&#34;application/json&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">        <span class="n">body</span> <span class="o">=</span> <span class="s">&#39;{&#34;value&#34;: 42}&#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 class="n">method</span> <span class="o">=</span> <span class="s">&#34;GET&#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 class="n">server</span><span class="o">$</span><span class="nf">start</span><span class="p">()</span></span></span></code></pre></td></tr></table>
</div>
</div></div>
<p>Handlers receive a request and return a response list. You can freely mix handler types in a single server:</p>
<table>
  <thead>
      <tr>
          <th style="text-align: left">Handler</th>
          <th style="text-align: left">Purpose</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: left">






<a href="https://nanonext.r-lib.org/reference/handler.html" target="_blank" rel="noopener"><code>handler()</code></a>
</td>
          <td style="text-align: left">HTTP request/response with R callback</td>
      </tr>
      <tr>
          <td style="text-align: left">






<a href="https://nanonext.r-lib.org/reference/handler_ws.html" target="_blank" rel="noopener"><code>handler_ws()</code></a>
</td>
          <td style="text-align: left">WebSocket with <code>on_message</code>, <code>on_open</code>, <code>on_close</code> callbacks</td>
      </tr>
      <tr>
          <td style="text-align: left">






<a href="https://nanonext.r-lib.org/reference/handler_stream.html" target="_blank" rel="noopener"><code>handler_stream()</code></a>
</td>
          <td style="text-align: left">Chunked HTTP streaming (SSE, NDJSON, custom)</td>
      </tr>
      <tr>
          <td style="text-align: left">






<a href="https://nanonext.r-lib.org/reference/handler_file.html" target="_blank" rel="noopener"><code>handler_file()</code></a>
</td>
          <td style="text-align: left">Serve a single static file</td>
      </tr>
      <tr>
          <td style="text-align: left">






<a href="https://nanonext.r-lib.org/reference/handler_directory.html" target="_blank" rel="noopener"><code>handler_directory()</code></a>
</td>
          <td style="text-align: left">Serve a directory tree with automatic MIME types</td>
      </tr>
      <tr>
          <td style="text-align: left">






<a href="https://nanonext.r-lib.org/reference/handler_inline.html" target="_blank" rel="noopener"><code>handler_inline()</code></a>
</td>
          <td style="text-align: left">Serve in-memory content</td>
      </tr>
      <tr>
          <td style="text-align: left">






<a href="https://nanonext.r-lib.org/reference/handler_redirect.html" target="_blank" rel="noopener"><code>handler_redirect()</code></a>
</td>
          <td style="text-align: left">HTTP redirect</td>
      </tr>
  </tbody>
</table>
<p>Specifying port <code>0</code> in the URL lets the operating system assign an available port. The actual port is reflected in <code>server$url</code> after <code>$start()</code>, so you can set up test servers without worrying about port conflicts.</p>
<h3 id="static-file-serving">Static file serving
</h3>
<p>Static handlers bypass R entirely &ndash; NNG serves content directly and efficiently:</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">handler_directory</span><span class="p">(</span><span class="s">&#34;/static&#34;</span><span class="p">,</span> <span class="s">&#34;www/assets&#34;</span><span class="p">)</span>  <span class="c1"># serve a folder</span>
</span></span><span class="line"><span class="cl"><span class="nf">handler_file</span><span class="p">(</span><span class="s">&#34;/favicon.ico&#34;</span><span class="p">,</span> <span class="s">&#34;favicon.ico&#34;</span><span class="p">)</span> <span class="c1"># serve a single file</span>
</span></span><span class="line"><span class="cl"><span class="nf">handler_inline</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="s">&#34;/robots.txt&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="s">&#34;User-agent: *\nDisallow:&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">content_type</span> <span class="o">=</span> <span class="s">&#34;text/plain&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span> <span class="c1"># serve in-memory content</span></span></span></code></pre></td></tr></table>
</div>
</div></div>
<p>For example, you can serve a rendered Quarto website with a single handler:</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="n">server</span> <span class="o">&lt;-</span> <span class="nf">http_server</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;http://127.0.0.1:0&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">handlers</span> <span class="o">=</span> <span class="nf">handler_directory</span><span class="p">(</span><span class="s">&#34;/&#34;</span><span class="p">,</span> <span class="s">&#34;_site&#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">server</span><span class="o">$</span><span class="nf">start</span><span class="p">()</span>
</span></span><span class="line"><span class="cl"><span class="n">server</span><span class="o">$</span><span class="n">url</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Browse to the URL to see your Quarto site</span></span></span></code></pre></td></tr></table>
</div>
</div></div>
<h3 id="websocket-server">WebSocket server
</h3>
<p>WebSockets provide full bidirectional communication &ndash; the server can push messages to the client, and the client can send messages back. WebSocket and HTTP handlers share the same server and port, so a browser can load a page over HTTP and open a WebSocket to the same origin &ndash; no cross-origin configuration needed:</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></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">server</span> <span class="o">&lt;-</span> <span class="nf">http_server</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;http://127.0.0.1:8080&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">handlers</span> <span class="o">=</span> <span class="nf">list</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="nf">handler</span><span class="p">(</span><span class="s">&#34;/&#34;</span><span class="p">,</span> <span class="kr">function</span><span class="p">(</span><span class="n">req</span><span class="p">)</span> <span class="nf">list</span><span class="p">(</span><span class="n">status</span> <span class="o">=</span> <span class="m">200L</span><span class="p">,</span> <span class="n">body</span> <span class="o">=</span> <span class="s">&#34;&lt;html&gt;...&lt;/html&gt;&#34;</span><span class="p">)),</span>
</span></span><span class="line"><span class="cl">    <span class="nf">handler_ws</span><span class="p">(</span><span class="s">&#34;/ws&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="n">on_message</span> <span class="o">=</span> <span class="kr">function</span><span class="p">(</span><span class="n">ws</span><span class="p">,</span> <span class="n">data</span><span class="p">)</span> <span class="n">ws</span><span class="o">$</span><span class="nf">send</span><span class="p">(</span><span class="n">data</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">      <span class="n">on_open</span> <span class="o">=</span> <span class="kr">function</span><span class="p">(</span><span class="n">ws</span><span class="p">)</span> <span class="nf">cat</span><span class="p">(</span><span class="s">&#34;connected:&#34;</span><span class="p">,</span> <span class="n">ws</span><span class="o">$</span><span class="n">id</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">on_close</span> <span class="o">=</span> <span class="kr">function</span><span class="p">(</span><span class="n">ws</span><span class="p">)</span> <span class="nf">cat</span><span class="p">(</span><span class="s">&#34;disconnected:&#34;</span><span class="p">,</span> <span class="n">ws</span><span class="o">$</span><span class="n">id</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="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></code></pre></td></tr></table>
</div>
</div></div>
<p>This makes it easy to build lightweight real-time services &ndash; monitoring endpoints or live-updating feeds that push results to the browser as they arrive.</p>
<h3 id="http-streaming-and-server-sent-events">HTTP streaming and Server-Sent Events
</h3>
<p>When you only need to push data in one direction &ndash; server to client &ndash; streaming is a lighter-weight alternative to WebSockets. It works over plain HTTP, so any client that speaks HTTP can consume the stream without needing a WebSocket library. 






<a href="https://nanonext.r-lib.org/reference/handler_stream.html" target="_blank" rel="noopener"><code>handler_stream()</code></a>
 enables chunked transfer encoding for streaming responses:</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-r" data-lang="r"><span class="line"><span class="cl"><span class="n">conns</span> <span class="o">&lt;-</span> <span class="nf">list</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">handler_stream</span><span class="p">(</span><span class="s">&#34;/events&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">on_request</span> <span class="o">=</span> <span class="kr">function</span><span class="p">(</span><span class="n">conn</span><span class="p">,</span> <span class="n">req</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">conn</span><span class="o">$</span><span class="nf">set_header</span><span class="p">(</span><span class="s">&#34;Content-Type&#34;</span><span class="p">,</span> <span class="s">&#34;text/event-stream&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">conn</span><span class="o">$</span><span class="nf">set_header</span><span class="p">(</span><span class="s">&#34;Cache-Control&#34;</span><span class="p">,</span> <span class="s">&#34;no-cache&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">conns[</span><span class="nf">[as.character</span><span class="p">(</span><span class="n">conn</span><span class="o">$</span><span class="n">id</span><span class="p">)</span><span class="n">]]</span> <span class="o">&lt;&lt;-</span> <span class="n">conn</span>
</span></span><span class="line"><span class="cl">    <span class="n">conn</span><span class="o">$</span><span class="nf">send</span><span class="p">(</span><span class="nf">format_sse</span><span class="p">(</span><span class="n">data</span> <span class="o">=</span> <span class="s">&#34;connected&#34;</span><span class="p">,</span> <span class="n">id</span> <span class="o">=</span> <span class="s">&#34;1&#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">on_close</span> <span class="o">=</span> <span class="kr">function</span><span class="p">(</span><span class="n">conn</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">conns[</span><span class="nf">[as.character</span><span class="p">(</span><span class="n">conn</span><span class="o">$</span><span class="n">id</span><span class="p">)</span><span class="n">]]</span> <span class="o">&lt;&lt;-</span> <span class="kc">NULL</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 






<a href="https://nanonext.r-lib.org/reference/format_sse.html" target="_blank" rel="noopener"><code>format_sse()</code></a>
 helper formats messages per the SSE specification. On the browser side, updates arrive automatically as they happen &ndash; no page refreshes or repeated requests needed. Streaming also supports NDJSON and custom formats &ndash; useful for streaming model training progress, sensor readings, monitoring endpoints, or pipeline notifications.</p>
<h3 id="tlsssl-support">TLS/SSL support
</h3>
<p>For HTTPS, pass a TLS configuration. nanonext bundles Mbed TLS, so there&rsquo;s nothing extra to install:</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="n">cert</span> <span class="o">&lt;-</span> <span class="nf">write_cert</span><span class="p">(</span><span class="n">cn</span> <span class="o">=</span> <span class="s">&#34;127.0.0.1&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">server</span> <span class="o">&lt;-</span> <span class="nf">http_server</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://127.0.0.1:0&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">handlers</span> <span class="o">=</span> <span class="nf">handler</span><span class="p">(</span><span class="s">&#34;/&#34;</span><span class="p">,</span> <span class="kr">function</span><span class="p">(</span><span class="n">req</span><span class="p">)</span> <span class="nf">list</span><span class="p">(</span><span class="n">status</span> <span class="o">=</span> <span class="m">200L</span><span class="p">,</span> <span class="n">body</span> <span class="o">=</span> <span class="s">&#34;Secure!&#34;</span><span class="p">)),</span>
</span></span><span class="line"><span class="cl">  <span class="n">tls</span> <span class="o">=</span> <span class="nf">tls_config</span><span class="p">(</span><span class="n">server</span> <span class="o">=</span> <span class="n">cert</span><span class="o">$</span><span class="n">server</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>
<h2 id="full-response-headers-for-http-client">Full response headers for HTTP client
</h2>
<p>






<a href="https://nanonext.r-lib.org/reference/ncurl.html" target="_blank" rel="noopener"><code>ncurl()</code></a>
 now accepts <code>response = TRUE</code> to return all response headers:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nv'>resp</span> <span class='o'>&lt;-</span> <span class='nf'><a href='https://nanonext.r-lib.org/reference/ncurl.html'>ncurl</a></span><span class='o'>(</span><span class='s'>"https://postman-echo.com/get"</span>, response <span class='o'>=</span> <span class='kc'>TRUE</span><span class='o'>)</span></span>
<span><span class='nv'>resp</span><span class='o'>$</span><span class='nv'>headers</span> <span class='o'>|&gt;</span> <span class='nf'><a href='https://rdrr.io/r/base/names.html'>names</a></span><span class='o'>(</span><span class='o'>)</span></span>
<span><span class='c'>#&gt;  [1] "Date"                          "Content-Type"                 </span></span>
<span><span class='c'>#&gt;  [3] "Content-Length"                "Connection"                   </span></span>
<span><span class='c'>#&gt;  [5] "CF-RAY"                        "etag"                         </span></span>
<span><span class='c'>#&gt;  [7] "vary"                          "Set-Cookie"                   </span></span>
<span><span class='c'>#&gt;  [9] "x-envoy-upstream-service-time" "cf-cache-status"              </span></span>
<span><span class='c'>#&gt; [11] "Server"</span></span>
<span></span></code></pre>
</div>
<p>Previously you could only request specific headers by name. Now you can retrieve the complete set &ndash; useful for inspecting rate limits, caching directives, and other metadata from REST APIs.</p>
<h2 id="async-http-with-shiny">Async HTTP with Shiny
</h2>
<p>If your Shiny app calls a REST API, a slow or unresponsive endpoint will block the R process and freeze the app for <em>all</em> users, not just the one who triggered the request. 






<a href="https://nanonext.r-lib.org/reference/ncurl_aio.html" target="_blank" rel="noopener"><code>ncurl_aio()</code></a>
 avoids this &ndash; it performs the HTTP call on a background thread and returns a promise, so the R process stays free to serve other sessions. It works anywhere that accepts a promise, including Shiny&rsquo;s ExtendedTask:</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></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">shiny</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">bslib</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">nanonext</span><span class="p">)</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">&lt;-</span> <span class="nf">page_fluid</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="nf">p</span><span class="p">(</span><span class="s">&#34;The time is &#34;</span><span class="p">,</span> <span class="nf">textOutput</span><span class="p">(</span><span class="s">&#34;current_time&#34;</span><span class="p">,</span> <span class="n">inline</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">hr</span><span class="p">(),</span>
</span></span><span class="line"><span class="cl">  <span class="nf">input_task_button</span><span class="p">(</span><span class="s">&#34;btn&#34;</span><span class="p">,</span> <span class="s">&#34;Fetch data&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">  <span class="nf">verbatimTextOutput</span><span class="p">(</span><span class="s">&#34;result&#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="n">server</span> <span class="o">&lt;-</span> <span class="kr">function</span><span class="p">(</span><span class="n">input</span><span class="p">,</span> <span class="n">output</span><span class="p">,</span> <span class="n">session</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="n">output</span><span class="o">$</span><span class="n">current_time</span> <span class="o">&lt;-</span> <span class="nf">renderText</span><span class="p">({</span>
</span></span><span class="line"><span class="cl">    <span class="nf">invalidateLater</span><span class="p">(</span><span class="m">1000</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="nf">format</span><span class="p">(</span><span class="nf">Sys.time</span><span class="p">(),</span> <span class="s">&#34;%H:%M:%S %p&#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="n">task</span> <span class="o">&lt;-</span> <span class="n">ExtendedTask</span><span class="o">$</span><span class="nf">new</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="kr">function</span><span class="p">()</span> <span class="nf">ncurl_aio</span><span class="p">(</span><span class="s">&#34;https://postman-echo.com/get&#34;</span><span class="p">,</span> <span class="n">response</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 class="o">|&gt;</span> <span class="nf">bind_task_button</span><span class="p">(</span><span class="s">&#34;btn&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="nf">observeEvent</span><span class="p">(</span><span class="n">input</span><span class="o">$</span><span class="n">btn</span><span class="p">,</span> <span class="n">task</span><span class="o">$</span><span class="nf">invoke</span><span class="p">())</span>
</span></span><span class="line"><span class="cl">  <span class="n">output</span><span class="o">$</span><span class="n">result</span> <span class="o">&lt;-</span> <span class="nf">renderPrint</span><span class="p">(</span><span class="n">task</span><span class="o">$</span><span class="nf">result</span><span class="p">()</span><span class="o">$</span><span class="n">headers</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">shinyApp</span><span class="p">(</span><span class="n">ui</span><span class="p">,</span> <span class="n">server</span><span class="p">)</span></span></span></code></pre></td></tr></table>
</div>
</div></div>
<h2 id="new-documentation">New documentation
</h2>
<p>The package documentation has been reorganized into focused, self-contained guides:</p>
<table>
  <thead>
      <tr>
          <th style="text-align: left">Guide</th>
          <th style="text-align: left">Topics</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: left">






<a href="https://nanonext.r-lib.org/articles/nanonext.html" target="_blank" rel="noopener">Quick Reference</a>
</td>
          <td style="text-align: left">At-a-glance API overview</td>
      </tr>
      <tr>
          <td style="text-align: left">






<a href="https://nanonext.r-lib.org/articles/v01-messaging.html" target="_blank" rel="noopener">Messaging</a>
</td>
          <td style="text-align: left">Cross-language exchange, async I/O, synchronization</td>
      </tr>
      <tr>
          <td style="text-align: left">






<a href="https://nanonext.r-lib.org/articles/v02-protocols.html" target="_blank" rel="noopener">Protocols</a>
</td>
          <td style="text-align: left">req/rep, pub/sub, surveyor/respondent</td>
      </tr>
      <tr>
          <td style="text-align: left">






<a href="https://nanonext.r-lib.org/articles/v03-configuration.html" target="_blank" rel="noopener">Configuration</a>
</td>
          <td style="text-align: left">TLS, options, serialization</td>
      </tr>
      <tr>
          <td style="text-align: left">






<a href="https://nanonext.r-lib.org/articles/v04-web.html" target="_blank" rel="noopener">Web Toolkit</a>
</td>
          <td style="text-align: left">HTTP client/server, WebSocket, streaming</td>
      </tr>
  </tbody>
</table>
<p>Whether you need a quick API cheatsheet or a deep dive into WebSocket chat servers, the new vignettes are designed to get you up and running fast.</p>
<h2 id="bug-fixes-and-improvements">Bug fixes and improvements
</h2>
<p>A new 






<a href="https://nanonext.r-lib.org/reference/race_aio.html" target="_blank" rel="noopener"><code>race_aio()</code></a>
 function returns the index of the first resolved async operation in a list &ndash; useful when waiting on multiple concurrent operations and you want to act on whichever completes first.</p>
<p>This release also fixes two critical issues &ndash; one affecting TLS operations in fresh sessions with newer system versions of Mbed TLS, another when custom serialization hooks threw errors. Error handling is now more graceful throughout, with closed streams returning error values instead of throwing. Under the hood, serialization, streaming, and async sends are all faster, and the bundled Mbed TLS is updated to 3.6.5 LTS. Building from source no longer requires <code>xz</code>.</p>
<h2 id="looking-ahead">Looking ahead
</h2>
<p>nanonext gives R a new building block for web infrastructure &ndash; one that complements httpuv. We see it as part of a broader investment in making R a first-class platform for real-time, connected applications. If you want to dig deeper, visit the 






<a href="https://nanonext.r-lib.org" target="_blank" rel="noopener">package website</a>
 or explore the source on 






<a href="https://github.com/r-lib/nanonext" target="_blank" rel="noopener">GitHub</a>
.</p>
<h2 id="acknowledgements">Acknowledgements
</h2>
<p>A big thank you to everyone who contributed to this release:</p>
<p>






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






<a href="https://github.com/shikokuchuo" target="_blank" rel="noopener">@shikokuchuo</a>
.</p>
]]></description>
      <enclosure url="https://opensource.posit.co/blog/2026-02-09_nanonext-1-8-0/thumbnail-wd.jpg" length="278046" type="image/jpeg" />
    </item>
    <item>
      <title>yaml12: YAML 1.2 for R and Python</title>
      <link>https://opensource.posit.co/blog/2026-01-07_yaml12-0-1-0/</link>
      <pubDate>Wed, 07 Jan 2026 00:00:00 +0000</pubDate>
      <guid>https://opensource.posit.co/blog/2026-01-07_yaml12-0-1-0/</guid>
      <dc:creator>Tomasz Kalinowski</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
* [ ] [`usethis::use_tidy_thanks()`](https://usethis.r-lib.org/reference/use_tidy_thanks.html) (optional)
-->
<p>Today we&rsquo;re announcing two new packages for parsing and emitting YAML 1.2: 






<a href="https://posit-dev.github.io/r-yaml12/" target="_blank" rel="noopener"><code>yaml12</code></a>
 for R and 






<a href="https://posit-dev.github.io/py-yaml12/" target="_blank" rel="noopener"><code>py-yaml12</code></a>
 for Python.</p>
<p>Both packages are implemented in Rust and built on the excellent 






<a href="https://github.com/saphyr-rs/saphyr" target="_blank" rel="noopener"><code>saphyr</code></a>
 crate. They share the same design goals: predictable YAML 1.2 typing, explicit control over tag interpretation via handlers, and clean round-tripping of unhandled tags.</p>
<p>Before we get into the details, a quick note on how this relates to the existing R 






<a href="https://github.com/r-lib/yaml" target="_blank" rel="noopener"><code>yaml</code></a>
 package. The R <code>yaml</code> package is now in 






<a href="https://github.com/r-lib" target="_blank" rel="noopener">r-lib</a>
, and we&rsquo;ve taken over maintenance after years of stewardship by its original author, Jeremy Stephens, and later by Shawn Garbett.</p>
<p>If <code>yaml</code> already works for you, there&rsquo;s no need to switch. <code>yaml12</code> is an experiment providing consistent R and Python bindings to a new Rust library specifically for YAML 1.2, which, as we&rsquo;ll see below, has some particular advantages.</p>
<h2 id="install">Install
</h2>
<p>Install the R package from CRAN:</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='s'>"yaml12"</span><span class='o'>)</span></span></code></pre>
</div>
<p>Install the Python package from PyPI:</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 py-yaml12</span></span></code></pre></td></tr></table>
</div>
</div></div>
<h2 id="quick-start-r">Quick start (R)
</h2>
<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://posit-dev.github.io/r-yaml12/'>yaml12</a></span><span class='o'>)</span></span>
<span></span>
<span><span class='nv'>yaml</span> <span class='o'>&lt;-</span> <span class='s'>"</span></span>
<span><span class='s'>title: A modern YAML parser and emitter written in Rust</span></span>
<span><span class='s'>properties: [fast, correct, safe, simple]</span></span>
<span><span class='s'>"</span></span>
<span></span>
<span><span class='nv'>doc</span> <span class='o'>&lt;-</span> <span class='nf'><a href='https://posit-dev.github.io/r-yaml12/reference/parse_yaml.html'>parse_yaml</a></span><span class='o'>(</span><span class='nv'>yaml</span><span class='o'>)</span></span>
<span><span class='nf'><a href='https://rdrr.io/r/utils/str.html'>str</a></span><span class='o'>(</span><span class='nv'>doc</span><span class='o'>)</span></span>
<span><span class='c'>#&gt; List of 2</span></span>
<span><span class='c'>#&gt;  $ title     : chr "A modern YAML parser and emitter written in Rust"</span></span>
<span><span class='c'>#&gt;  $ properties: chr [1:4] "fast" "correct" "safe" "simple"</span></span>
<span></span></code></pre>
</div>
<p>Round-trip back to YAML:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nv'>obj</span> <span class='o'>&lt;-</span> <span class='nf'><a href='https://rdrr.io/r/base/list.html'>list</a></span><span class='o'>(</span></span>
<span>  seq <span class='o'>=</span> <span class='m'>1</span><span class='o'>:</span><span class='m'>2</span>,</span>
<span>  map <span class='o'>=</span> <span class='nf'><a href='https://rdrr.io/r/base/list.html'>list</a></span><span class='o'>(</span>key <span class='o'>=</span> <span class='s'>"value"</span><span class='o'>)</span>,</span>
<span>  tagged <span class='o'>=</span> <span class='nf'><a href='https://rdrr.io/r/base/structure.html'>structure</a></span><span class='o'>(</span><span class='s'>"1 + 1"</span>, yaml_tag <span class='o'>=</span> <span class='s'>"!expr"</span><span class='o'>)</span></span>
<span><span class='o'>)</span></span>
<span><span class='nf'><a href='https://posit-dev.github.io/r-yaml12/reference/format_yaml.html'>write_yaml</a></span><span class='o'>(</span><span class='nv'>obj</span><span class='o'>)</span></span>
<span><span class='c'>#&gt; ---</span></span>
<span><span class='c'>#&gt; seq:</span></span>
<span><span class='c'>#&gt;   - 1</span></span>
<span><span class='c'>#&gt;   - 2</span></span>
<span><span class='c'>#&gt; map:</span></span>
<span><span class='c'>#&gt;   key: value</span></span>
<span><span class='c'>#&gt; tagged: !expr 1 + 1</span></span>
<span><span class='c'>#&gt; ...</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 class='nv'>obj</span>, <span class='nf'><a href='https://posit-dev.github.io/r-yaml12/reference/parse_yaml.html'>parse_yaml</a></span><span class='o'>(</span><span class='nf'><a href='https://posit-dev.github.io/r-yaml12/reference/format_yaml.html'>format_yaml</a></span><span class='o'>(</span><span class='nv'>obj</span><span class='o'>)</span><span class='o'>)</span><span class='o'>)</span></span>
<span><span class='c'>#&gt; [1] TRUE</span></span>
<span></span></code></pre>
</div>
<h2 id="quick-start-python">Quick start (Python)
</h2>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'># Install from PyPI:
#   python -m pip install py-yaml12
from yaml12 import parse_yaml, format_yaml, Yaml

yaml_text = """
title: A modern YAML parser and emitter written in Rust
properties: [fast, correct, safe, simple]
"""

doc = parse_yaml(yaml_text)

assert doc == {
  "title": "A modern YAML parser and emitter written in Rust",
  "properties": ["fast", "correct", "safe", "simple"]
}

assert doc == parse_yaml(format_yaml(doc))

# Tagged values
tagged = parse_yaml("!expr 1 + 1")
assert tagged == Yaml(value="1 + 1", tag="!expr")
</code></pre>
</div>
<h2 id="why-yaml-12">Why YAML 1.2?
</h2>
<p>YAML 1.2 tightened up a number of ambiguous implicit conversions. In particular, plain scalars like <code>on</code>/<code>off</code>/<code>yes</code>/<code>no</code>/<code>y</code>/<code>n</code> are strings in the 1.2 core schema, and YAML 1.2 removed sexagesimal (base-60) parsing, so values like <code>1:2</code> are not treated as numbers.</p>
<p>YAML 1.2 also removed <code>!!timestamp</code>, <code>!!binary</code>, and <code>!!omap</code> from the set of core types, which further reduces implicit coercions (for example, getting a date/time object when you expected a string). If you want to interpret those values, you can do so explicitly via tags and handlers.</p>
<p>That makes YAML a better default for configuration files, front matter, and data interchange. You get fewer surprises and fewer &ldquo;why did this become a boolean?&rdquo; moments (or &ldquo;why did this become a date?&rdquo;).</p>
<h2 id="highlights">Highlights
</h2>
<h3 id="a-consistent-api-in-r-and-python">A consistent API in R and Python
</h3>
<p>The two packages intentionally share the same high-level functions:</p>
<ul>
<li>






<a href="https://posit-dev.github.io/r-yaml12/reference/parse_yaml.html" target="_blank" rel="noopener"><code>parse_yaml()</code></a>
: Parse YAML from a string</li>
<li>






<a href="https://posit-dev.github.io/r-yaml12/reference/parse_yaml.html" target="_blank" rel="noopener"><code>read_yaml()</code></a>
: Read YAML from a file</li>
<li>






<a href="https://posit-dev.github.io/r-yaml12/reference/format_yaml.html" target="_blank" rel="noopener"><code>format_yaml()</code></a>
: Format values as YAML (to a string)</li>
<li>






<a href="https://posit-dev.github.io/r-yaml12/reference/format_yaml.html" target="_blank" rel="noopener"><code>write_yaml()</code></a>
: Write YAML to a file (or stdout)</li>
</ul>
<h3 id="tags-and-handlers-opt-in-meaning-safe-defaults">Tags and handlers (opt-in, meaning, safe defaults)
</h3>
<p>In YAML, tags are explicit annotations like <code>!expr</code> or <code>!!timestamp</code> that attach type and meaning to a value.</p>
<p>Tags are preserved by default:</p>
<ul>
<li>In R, tags are kept in a <code>yaml_tag</code> attribute.</li>
<li>In Python, tags are kept by wrapping values in a <code>Yaml()</code> object.</li>
</ul>
<p>Handlers let you opt into custom behavior for tags (including tags on mapping keys) while keeping parsing as a data-only operation by default.</p>
<p>If you used R <code>yaml</code>&rsquo;s <code>!expr</code> tag to evaluate expressions, you can recreate that behavior by registering a handler, but it&rsquo;s only recommended when parsing trusted YAML, since evaluating arbitrary code is a security risk. For untrusted input, the default behavior is safer because it keeps <code>!expr</code> as data and does not execute code.</p>
<p>R example:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='c'># by default, tags are kept as data</span></span>
<span><span class='nf'><a href='https://rdrr.io/r/base/dput.html'>dput</a></span><span class='o'>(</span><span class='nf'><a href='https://posit-dev.github.io/r-yaml12/reference/parse_yaml.html'>parse_yaml</a></span><span class='o'>(</span><span class='s'>"!expr 1 + 1"</span><span class='o'>)</span><span class='o'>)</span></span>
<span><span class='c'>#&gt; structure("1 + 1", yaml_tag = "!expr")</span></span>
<span></span><span></span>
<span><span class='c'># Add a handler to process tagged nodes (like the &#123;yaml&#125; package does)</span></span>
<span><span class='nv'>handlers</span> <span class='o'>&lt;-</span> <span class='nf'><a href='https://rdrr.io/r/base/list.html'>list</a></span><span class='o'>(</span><span class='s'>"!expr"</span> <span class='o'>=</span> \<span class='o'>(</span><span class='nv'>x</span><span class='o'>)</span> <span class='nf'><a href='https://rdrr.io/r/base/eval.html'>eval</a></span><span class='o'>(</span><span class='nf'><a href='https://rdrr.io/r/base/parse.html'>str2expression</a></span><span class='o'>(</span><span class='nv'>x</span><span class='o'>)</span>, <span class='nf'><a href='https://rdrr.io/r/base/environment.html'>globalenv</a></span><span class='o'>(</span><span class='o'>)</span><span class='o'>)</span><span class='o'>)</span></span>
<span><span class='nf'><a href='https://posit-dev.github.io/r-yaml12/reference/parse_yaml.html'>parse_yaml</a></span><span class='o'>(</span><span class='s'>"!expr 1 + 1"</span>, handlers <span class='o'>=</span> <span class='nv'>handlers</span><span class='o'>)</span></span>
<span><span class='c'>#&gt; [1] 2</span></span>
<span></span></code></pre>
</div>
<p>Python example:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'>from yaml12 import parse_yaml

handlers = {"!expr": eval}  # use with trusted input only
parse_yaml("!expr 1 + 1", handlers=handlers)

#> 2
</code></pre>
</div>
<h3 id="simplification-and-missing-values-r">Simplification and missing values (R)
</h3>
<p>In R, 






<a href="https://posit-dev.github.io/r-yaml12/reference/parse_yaml.html" target="_blank" rel="noopener"><code>parse_yaml()</code></a>
 can simplify homogeneous sequences to vectors. When it does, YAML <code>null</code> becomes the appropriate <code>NA</code> type:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nf'><a href='https://posit-dev.github.io/r-yaml12/reference/parse_yaml.html'>parse_yaml</a></span><span class='o'>(</span><span class='s'>"[1, 2, 3, null]"</span><span class='o'>)</span></span>
<span><span class='c'>#&gt; [1]  1  2  3 NA</span></span>
<span></span><span></span>
<span><span class='nf'><a href='https://rdrr.io/r/utils/str.html'>str</a></span><span class='o'>(</span><span class='nf'><a href='https://posit-dev.github.io/r-yaml12/reference/parse_yaml.html'>parse_yaml</a></span><span class='o'>(</span><span class='s'>"[1, 2, 3, null]"</span>, simplify <span class='o'>=</span> <span class='kc'>FALSE</span><span class='o'>)</span><span class='o'>)</span></span>
<span><span class='c'>#&gt; List of 4</span></span>
<span><span class='c'>#&gt;  $ : int 1</span></span>
<span><span class='c'>#&gt;  $ : int 2</span></span>
<span><span class='c'>#&gt;  $ : int 3</span></span>
<span><span class='c'>#&gt;  $ : NULL</span></span>
<span></span></code></pre>
</div>
<h3 id="non-string-mapping-keys">Non-string mapping keys
</h3>
<p>YAML allows mapping keys that aren&rsquo;t plain strings (numbers, booleans, tagged scalars, even sequences and mappings). Both packages preserve these safely:</p>
<ul>
<li>In R, you&rsquo;ll get a regular named list plus a <code>yaml_keys</code> attribute when needed.</li>
<li>In Python, unhashable keys (like lists/dicts) are wrapped in <code>Yaml</code> so they can still be used as <code>dict</code> keys and round-trip correctly.</li>
</ul>
<p>R example:</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/dput.html'>dput</a></span><span class='o'>(</span><span class='nf'><a href='https://posit-dev.github.io/r-yaml12/reference/parse_yaml.html'>parse_yaml</a></span><span class='o'>(</span><span class='s'>"&#123;a: b&#125;: c"</span><span class='o'>)</span><span class='o'>)</span></span>
<span><span class='c'>#&gt; structure(list("c"), names = "", yaml_keys = list(list(a = "b")))</span></span>
<span></span></code></pre>
</div>
<p>Python example:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'>from yaml12 import parse_yaml, Yaml

doc = parse_yaml("{a: b}: c")
assert doc == {Yaml({'a': 'b'}): 'c'}
</code></pre>
</div>
<h3 id="mapping-order-is-preserved">Mapping order is preserved
</h3>
<p>YAML mappings are ordered. <code>yaml12</code> preserves mapping/dictionary order when parsing and formatting, so the order you see in a YAML file (or emit) round-trips in both R and Python.</p>
<h3 id="document-streams-and-front-matter">Document streams and front matter
</h3>
<p>Both packages support multi-document YAML streams with <code>multi = TRUE</code>. When <code>multi = FALSE</code> (the default), parsing stops after the first document, which is handy for extracting YAML front matter from text that continues with non-YAML content.</p>
<p>Example:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nv'>yaml</span> <span class='o'>&lt;-</span> <span class='s'>"</span></span>
<span><span class='s'>---</span></span>
<span><span class='s'>title: Extracting YAML front matter</span></span>
<span><span class='s'>---</span></span>
<span><span class='s'>This is technically now the second document in a YAML stream</span></span>
<span><span class='s'>"</span></span>
<span><span class='nf'><a href='https://rdrr.io/r/utils/str.html'>str</a></span><span class='o'>(</span><span class='nf'><a href='https://posit-dev.github.io/r-yaml12/reference/parse_yaml.html'>parse_yaml</a></span><span class='o'>(</span><span class='nv'>yaml</span><span class='o'>)</span><span class='o'>)</span></span>
<span><span class='c'>#&gt; List of 1</span></span>
<span><span class='c'>#&gt;  $ title: chr "Extracting YAML front matter"</span></span>
<span></span><span><span class='nf'><a href='https://rdrr.io/r/utils/str.html'>str</a></span><span class='o'>(</span><span class='nf'><a href='https://posit-dev.github.io/r-yaml12/reference/parse_yaml.html'>parse_yaml</a></span><span class='o'>(</span><span class='nv'>yaml</span>, multi <span class='o'>=</span> <span class='kc'>TRUE</span><span class='o'>)</span><span class='o'>)</span></span>
<span><span class='c'>#&gt; List of 2</span></span>
<span><span class='c'>#&gt;  $ :List of 1</span></span>
<span><span class='c'>#&gt;   ..$ title: chr "Extracting YAML front matter"</span></span>
<span><span class='c'>#&gt;  $ : chr "This is technically now the second document in a YAML stream"</span></span>
<span></span></code></pre>
</div>
<h3 id="performance-and-safety-notes">Performance and safety notes
</h3>
<p><code>yaml12</code> is implemented in Rust and written with performance and safety in mind. It avoids unnecessary allocations, copies, and extra traversals where possible. In Python, <code>py-yaml12</code> (imported as <code>yaml12</code>) also releases the GIL for large parses and serializations.</p>
<p>In typical usage, the R package <code>yaml12</code> is ~2× faster than the <code>yaml</code> package, and the Python package <code>py-yaml12</code> is ≥50× faster than default <code>PyYAML</code> in the benchmarks (






<a href="https://posit-dev.github.io/r-yaml12/articles/benchmarks.html" target="_blank" rel="noopener">R benchmarks</a>
; 


  
  
  





<a href="https://posit-dev.github.io/py-yaml12/benchmarks/#read-performance" target="_blank" rel="noopener">Python benchmarks</a>
).</p>
<p>Tags are preserved by default, and interpreting them (including any kind of evaluation) is always an explicit opt-in via handlers. Plain scalars follow the YAML 1.2 core schema rules for predictable typing.</p>
<p>In Python, <code>py-yaml12</code> ships prebuilt wheels for common platforms. If you do need to build from source, you&rsquo;ll need a Rust toolchain. In R, <code>yaml12</code> is available from CRAN (including binaries on common platforms).</p>
<h2 id="wrapping-up">Wrapping up
</h2>
<p>If you work with YAML as a data format for configuration, front matter, or data interchange, we hope <code>yaml12</code> (R) and <code>py-yaml12</code> (Python) help you parse and emit YAML 1.2 predictably. If you run into YAML that doesn&rsquo;t behave as expected, we&rsquo;d love to hear about it in the issue trackers: 






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






<a href="https://github.com/posit-dev/py-yaml12/issues" target="_blank" rel="noopener">py-yaml12</a>
.</p>
<h2 id="learn-more">Learn more
</h2>
<ul>
<li>R package docs: 






<a href="https://posit-dev.github.io/r-yaml12/" target="_blank" rel="noopener">https://posit-dev.github.io/r-yaml12/</a>
</li>
<li>R package on CRAN: 






<a href="https://cran.r-project.org/package=yaml12" target="_blank" rel="noopener">https://cran.r-project.org/package=yaml12</a>
</li>
<li>Python package docs: 






<a href="https://posit-dev.github.io/py-yaml12/" target="_blank" rel="noopener">https://posit-dev.github.io/py-yaml12/</a>
</li>
<li>Python package on PyPI: 






<a href="https://pypi.org/project/py-yaml12/" target="_blank" rel="noopener">https://pypi.org/project/py-yaml12/</a>
</li>
</ul>
<h2 id="acknowledgements">Acknowledgements
</h2>
<p>Both packages build on the fantastic work in the YAML ecosystem, especially the <code>saphyr</code> Rust crate and the 






<a href="https://github.com/yaml/yaml-test-suite" target="_blank" rel="noopener">yaml-test-suite</a>
.</p>
]]></description>
      <enclosure url="https://opensource.posit.co/blog/2026-01-07_yaml12-0-1-0/thumbnail-wd.jpg" length="477241" type="image/jpeg" />
    </item>
    <item>
      <title>testthat 3.3.0</title>
      <link>https://opensource.posit.co/blog/2025-11-13_testthat-3-3-0/</link>
      <pubDate>Thu, 13 Nov 2025 00:00:00 +0000</pubDate>
      <guid>https://opensource.posit.co/blog/2025-11-13_testthat-3-3-0/</guid>
      <dc:creator>Hadley Wickham</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 chuffed to announce the release of 






<a href="https://testthat.r-lib.org" target="_blank" rel="noopener">testthat</a>
 3.3.0. testthat is a testing framework for R that makes it easy to turn your existing informal tests into formal, automated tests that you can rerun quickly and easily.</p>
<p>You can install it 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='s'>"testthat"</span><span class='o'>)</span></span></code></pre>
</div>
<p>This blog post highlights the most important changes in this release, including lifecycle changes that removed long-deprecated mocking functions, improvements to expectations and their error messages, and a variety of new features that make testing easier and more robust. You can see a full list of changes in the 






<a href="https://github.com/r-lib/testthat/releases/tag/v3.3.0" target="_blank" rel="noopener">release notes</a>
.</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://testthat.r-lib.org'>testthat</a></span><span class='o'>)</span></span></code></pre>
</div>
<h2 id="claude-code-experiences">Claude Code experiences
</h2>
<p>Before we dive into the changes, I wanted to talk a little bit about some changes to my development process, as I used this release as an opportunity to learn 






<a href="https://www.claude.com/product/claude-code" target="_blank" rel="noopener">Claude Code</a>
. This is the first package where I&rsquo;ve really used AI to support the development of many features and I thought it might be useful to share my experience.</p>
<p>Overall it was a successful experiment. It helped me close over 100 issues in what felt like less time than usual. I don&rsquo;t have any hard numbers, but my gut feeling is that it was maybe a 10-20% improvement to my development velocity. This is still significant, especially since I&rsquo;m an experienced R programmer and my workflow has been pretty stable for the last few years. I mostly used Claude for smaller, well-defined tasks where I had a good sense of what was needed. I found it particularly useful for refactoring, where it was easy to say precisely what I wanted, but executing the changes required a bunch of fiddly edits across many files.</p>
<p>I also found it generally useful for getting over the &ldquo;activation energy hump&rdquo;: there were a few issues that had been stagnating for years because they felt like they were going to be hard to do and with relatively limited payoff. I let Claude Code loose on a few of these and found it super useful. It only produced code I was really happy with a couple of times, but every time it gave me something to react to (often with strong negative feelings!) and that got me started actually engaging with the problem.</p>
<p>If you&rsquo;re interested in using Claude Code yourself, there are a couple of files you might find useful. My 






<a href="https://github.com/r-lib/testthat/blob/main/.claude/CLAUDE.md" target="_blank" rel="noopener"><code>CLAUDE.md</code></a>
 tells Claude how to execute a devtools-based workflow, along with a few pointers to resolve common issues. My 






<a href="https://github.com/r-lib/testthat/blob/main/.claude/settings.json" target="_blank" rel="noopener"><code>settings.json</code></a>
 allows Claude to run longer without human intervention, doing things that should mostly be safe. One note of caution: these settings do allow Claude to run R code, which does allow it to do practically anything. In my experience, Claude only used R to run tests or documentation.</p>
<p>I also experimented with using Claude Code to review PRs. It was just barely useful enough that I kept it turned on for my own PRs, but I didn&rsquo;t bother trying to get it to work for contributed PRs. Most of the time it either gave a thumbs up or bad advice, but every now and then it would pick up a small error.</p>
<p>(I&rsquo;ve also used Claude Code to proofread this blog post!)</p>
<h2 id="lifecycle-changes">Lifecycle changes
</h2>
<p>The biggest change in this release is that 






<a href="https://testthat.r-lib.org/reference/with_mock.html" target="_blank" rel="noopener"><code>local_mock()</code></a>
 and 






<a href="https://testthat.r-lib.org/reference/with_mock.html" target="_blank" rel="noopener"><code>with_mock()</code></a>
 are defunct. They were deprecated in 3.0.0 (2020-10-31) because it was becoming clear that the technique that made them work would be disallowed in a future version of R. This has now happened in R 4.5.0, so the functions have been removed. Removing 






<a href="https://testthat.r-lib.org/reference/with_mock.html" target="_blank" rel="noopener"><code>local_mock()</code></a>
 and 






<a href="https://testthat.r-lib.org/reference/with_mock.html" target="_blank" rel="noopener"><code>with_mock()</code></a>
 was a fairly disruptive change, affecting ~100 CRAN packages, but it had to be done, and I&rsquo;ve been working on notifying package developers since January so everyone had plenty of time to update. Fortunately, the needed changes are generally small, since the newer 






<a href="https://testthat.r-lib.org/reference/local_mocked_bindings.html" target="_blank" rel="noopener"><code>local_mocked_bindings()</code></a>
 and 






<a href="https://testthat.r-lib.org/reference/local_mocked_bindings.html" target="_blank" rel="noopener"><code>with_mocked_bindings()</code></a>
 can solve most additional needs. (If you haven&rsquo;t heard of mocking before, you can read the new <code>vignette(&quot;mocking&quot;)</code> to learn what it is and why you might want to use it.)</p>
<p>Other lifecycle changes:</p>
<ul>
<li>
<p>testthat now requires R 4.1. This follows 






  
  

<a href="https://opensource.posit.co/blog/2019-04-01_r-version-support/">our supported version policy</a>
, which documents our commitment to support five versions of R (the current version and four previous versions). We&rsquo;re excited to be able to finally take advantage of the base pipe and compact anonymous functions (i.e. <code>\(x) x + 1</code>)!</p>
</li>
<li>
<p><code>is_null()</code>/<code>matches()</code>, deprecated in 2.0.0 (2017-12-19), and <code>is_true()</code>/<code>is_false()</code>, deprecated in 2.1.0 (2019-04-23), have been removed. These conflicted with other tidyverse functions so we pushed their deprecation through, even though we have generally left the old 






<a href="https://testthat.r-lib.org/reference/test_that.html" target="_blank" rel="noopener"><code>test_that()</code></a>
 API untouched.</p>
</li>
<li>
<p><code>expect_snapshot(binary)</code>, soft deprecated in 3.0.3 (2021-06-16), is now fully deprecated. <code>test_files(wrap)</code>, deprecated in 3.0.0 (2020-10-31), has now been removed.</p>
</li>
<li>
<p>There were a few other changes that broke existing packages. The most impactful change was to start checking the inputs to 






<a href="https://testthat.r-lib.org/reference/expect.html" target="_blank" rel="noopener"><code>expect()</code></a>
 which, despite the name, is actually an internal helper. That revealed a surprising number of packages were accidentally using 






<a href="https://testthat.r-lib.org/reference/expect.html" target="_blank" rel="noopener"><code>expect()</code></a>
 instead of 






<a href="https://testthat.r-lib.org/reference/logical-expectations.html" target="_blank" rel="noopener"><code>expect_true()</code></a>
 or 






<a href="https://testthat.r-lib.org/reference/equality-expectations.html" target="_blank" rel="noopener"><code>expect_equal()</code></a>
. We don&rsquo;t technically consider this a breaking change because it revealed off-label function usage: the function API hasn&rsquo;t changed; you just now learn when you&rsquo;re using it incorrectly.</p>
</li>
</ul>
<p>If you&rsquo;re interested in the process we use to manage the release of a package that breaks its reverse dependencies, you might like to read 






<a href="https://github.com/r-lib/testthat/issues/2021" target="_blank" rel="noopener">the issue</a>
 where I track all the problems and prepare PRs to fix them.</p>
<h2 id="expectations-and-the-interactive-testing-experience">Expectations and the interactive testing experience
</h2>
<p>A lot of work in this release was prompted by an overhaul of <code>vignette(&quot;custom-expectations&quot;)</code>, which describes how to create your own expectations that work just like testthat&rsquo;s. This is a long time coming, and as I was working on it, I realized that I didn&rsquo;t really know how to write new expectations, which had led to a lot of variation in the existing implementations. This kicked off a bunch of experimentation and iterating, leading to a swath of improvements:</p>
<ul>
<li>
<p>All expectations have new failure messages: they now state what was expected, what was actually received, and, if possible, they clearly illustrate the difference.</p>
</li>
<li>
<p>Expectations now consistently return the value of the first argument, regardless of whether the expectation succeeds or fails (the only exception is 






<a href="https://testthat.r-lib.org/reference/expect_error.html" target="_blank" rel="noopener"><code>expect_error()</code></a>
 and friends which return the captured condition so that you can perform additional checks on the condition object). This is a relatively subtle change that won&rsquo;t affect tests that already pass, but it does improve failures when you pipe together multiple expectations.</p>
</li>
<li>
<p>A new 






<a href="https://testthat.r-lib.org/reference/fail.html" target="_blank" rel="noopener"><code>pass()</code></a>
 function makes it clear how to signal when an expectation succeeds. All existing expectations were rewritten to use 






<a href="https://testthat.r-lib.org/reference/fail.html" target="_blank" rel="noopener"><code>pass()</code></a>
 and (the existing) 






<a href="https://testthat.r-lib.org/reference/fail.html" target="_blank" rel="noopener"><code>fail()</code></a>
 instead of 






<a href="https://testthat.r-lib.org/reference/expect.html" target="_blank" rel="noopener"><code>expect()</code></a>
, which I think makes the flow of logic easier to understand.</p>
</li>
<li>
<p>Improved 






<a href="https://testthat.r-lib.org/reference/expect_success.html" target="_blank" rel="noopener"><code>expect_success()</code></a>
 and 






<a href="https://testthat.r-lib.org/reference/expect_success.html" target="_blank" rel="noopener"><code>expect_failure()</code></a>
 expectations now test that an expectation always returns exactly one success or failure (this ensures that the counts that you see in the reporters are correct).</p>
</li>
</ul>
<p>This new framework helped us write six new expectations:</p>
<ul>
<li>
<p>






<a href="https://testthat.r-lib.org/reference/expect_all_equal.html" target="_blank" rel="noopener"><code>expect_all_equal()</code></a>
, 






<a href="https://testthat.r-lib.org/reference/expect_all_equal.html" target="_blank" rel="noopener"><code>expect_all_true()</code></a>
, and 






<a href="https://testthat.r-lib.org/reference/expect_all_equal.html" target="_blank" rel="noopener"><code>expect_all_false()</code></a>
 check that every element of a vector has the same value, giving better error messages than <code>expect_true(all(...))</code>:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nf'><a href='https://testthat.r-lib.org/reference/test_that.html'>test_that</a></span><span class='o'>(</span><span class='s'>"some test"</span>, <span class='o'>&#123;</span></span>
<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='m'>0.408</span>, <span class='m'>0.961</span>, <span class='m'>0.883</span>, <span class='m'>0.46</span>, <span class='m'>0.537</span>, <span class='m'>0.961</span>, <span class='m'>0.851</span>, <span class='m'>0.887</span>, <span class='m'>0.023</span><span class='o'>)</span></span>
<span>  <span class='nf'><a href='https://testthat.r-lib.org/reference/expect_all_equal.html'>expect_all_true</a></span><span class='o'>(</span><span class='nv'>x</span> <span class='o'>&lt;</span> <span class='m'>0.95</span><span class='o'>)</span></span>
<span><span class='o'>&#125;</span><span class='o'>)</span></span>
<span><span class='c'>#&gt; ── <span style='color: #BBBB00; font-weight: bold;'>Failure</span><span style='font-weight: bold;'>: some test</span> ────────────────────────────────────────────────</span></span>
<span><span class='c'>#&gt; Expected every element of `x &lt; 0.95` to equal TRUE.</span></span>
<span><span class='c'>#&gt; Differences:</span></span>
<span><span class='c'>#&gt; `actual`:   <span style='color: #555555;'>TRUE</span> <span style='color: #00BB00;'>FALSE</span> <span style='color: #555555;'>TRUE</span> <span style='color: #555555;'>TRUE</span> <span style='color: #555555;'>TRUE</span> <span style='color: #00BB00;'>FALSE</span> <span style='color: #555555;'>TRUE</span> <span style='color: #555555;'>TRUE</span> <span style='color: #555555;'>TRUE</span></span></span>
<span><span class='c'>#&gt; `expected`: <span style='color: #555555;'>TRUE</span> <span style='color: #00BB00;'>TRUE</span>  <span style='color: #555555;'>TRUE</span> <span style='color: #555555;'>TRUE</span> <span style='color: #555555;'>TRUE</span> <span style='color: #00BB00;'>TRUE</span>  <span style='color: #555555;'>TRUE</span> <span style='color: #555555;'>TRUE</span> <span style='color: #555555;'>TRUE</span></span></span>
<span></span><span><span class='c'>#&gt; <span style='color: #BBBB00; font-weight: bold;'>Error</span><span style='font-weight: bold;'>:</span></span></span>
<span><span class='c'>#&gt; <span style='color: #BBBB00;'>!</span> Test failed with 1 failure and 0 successes.</span></span>
<span></span></code></pre>
</div>
</li>
<li>
<p>






<a href="https://testthat.r-lib.org/reference/expect_setequal.html" target="_blank" rel="noopener"><code>expect_disjoint()</code></a>
, by 






<a href="https://github.com/stibu81" target="_blank" rel="noopener">@stibu81</a>
, expects values to be absent:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nf'><a href='https://testthat.r-lib.org/reference/test_that.html'>test_that</a></span><span class='o'>(</span><span class='s'>""</span>, <span class='o'>&#123;</span></span>
<span>  <span class='nf'><a href='https://testthat.r-lib.org/reference/expect_setequal.html'>expect_disjoint</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'>"a"</span>, <span class='s'>"b"</span>, <span class='s'>"c"</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'>"c"</span>, <span class='s'>"d"</span>, <span class='s'>"e"</span><span class='o'>)</span><span class='o'>)</span></span>
<span><span class='o'>&#125;</span><span class='o'>)</span></span>
<span><span class='c'>#&gt; ── <span style='color: #BBBB00; font-weight: bold;'>Failure</span><span style='font-weight: bold;'>: </span> ─────────────────────────────────────────────────────────</span></span>
<span><span class='c'>#&gt; Expected `c("a", "b", "c")` to be disjoint from `c("c", "d", "e")`.</span></span>
<span><span class='c'>#&gt; Actual: "a", "b", "c"</span></span>
<span><span class='c'>#&gt; Expected: None of "c", "d", "e"</span></span>
<span><span class='c'>#&gt; Invalid: "c"</span></span>
<span></span><span><span class='c'>#&gt; <span style='color: #BBBB00; font-weight: bold;'>Error</span><span style='font-weight: bold;'>:</span></span></span>
<span><span class='c'>#&gt; <span style='color: #BBBB00;'>!</span> Test failed with 1 failure and 0 successes.</span></span>
<span></span></code></pre>
</div>
</li>
<li>
<p>






<a href="https://testthat.r-lib.org/reference/inheritance-expectations.html" target="_blank" rel="noopener"><code>expect_r6_class()</code></a>
 expects an R6 object:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nf'><a href='https://testthat.r-lib.org/reference/test_that.html'>test_that</a></span><span class='o'>(</span><span class='s'>""</span>, <span class='o'>&#123;</span></span>
<span>  <span class='nv'>x</span> <span class='o'>&lt;-</span> <span class='m'>10</span></span>
<span>  <span class='nf'><a href='https://testthat.r-lib.org/reference/inheritance-expectations.html'>expect_r6_class</a></span><span class='o'>(</span><span class='nv'>x</span>, <span class='s'>"foo"</span><span class='o'>)</span></span>
<span></span>
<span>  <span class='nv'>x</span> <span class='o'>&lt;-</span> <span class='nf'>R6</span><span class='nf'>::</span><span class='kr'><a href='https://r6.r-lib.org/reference/R6Class.html'>R6Class</a></span><span class='o'>(</span><span class='s'>"bar"</span><span class='o'>)</span><span class='o'>$</span><span class='nf'>new</span><span class='o'>(</span><span class='o'>)</span></span>
<span>  <span class='nf'><a href='https://testthat.r-lib.org/reference/inheritance-expectations.html'>expect_r6_class</a></span><span class='o'>(</span><span class='nv'>x</span>, <span class='s'>"foo"</span><span class='o'>)</span></span>
<span><span class='o'>&#125;</span><span class='o'>)</span></span>
<span><span class='c'>#&gt; ── <span style='color: #BBBB00; font-weight: bold;'>Failure</span><span style='font-weight: bold;'>: </span> ─────────────────────────────────────────────────────────</span></span>
<span><span class='c'>#&gt; Expected `x` to be an R6 object.</span></span>
<span><span class='c'>#&gt; Actual OO type: none.</span></span>
<span><span class='c'>#&gt; ── <span style='color: #BBBB00; font-weight: bold;'>Failure</span><span style='font-weight: bold;'>: </span> ─────────────────────────────────────────────────────────</span></span>
<span><span class='c'>#&gt; Expected `x` to inherit from "foo".</span></span>
<span><span class='c'>#&gt; Actual class: "bar"/"R6".</span></span>
<span></span><span><span class='c'>#&gt; <span style='color: #BBBB00; font-weight: bold;'>Error</span><span style='font-weight: bold;'>:</span></span></span>
<span><span class='c'>#&gt; <span style='color: #BBBB00;'>!</span> Test failed with 2 failures and 0 successes.</span></span>
<span></span></code></pre>
</div>
</li>
<li>
<p>






<a href="https://testthat.r-lib.org/reference/expect_length.html" target="_blank" rel="noopener"><code>expect_shape()</code></a>
, by 






<a href="https://github.com/michaelchirico" target="_blank" rel="noopener">@michaelchirico</a>
, expects a specific shape (i.e., 






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






<a href="https://rdrr.io/r/base/nrow.html" target="_blank" rel="noopener"><code>ncol()</code></a>
, or 






<a href="https://rdrr.io/r/base/dim.html" target="_blank" rel="noopener"><code>dim()</code></a>
):</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nf'><a href='https://testthat.r-lib.org/reference/test_that.html'>test_that</a></span><span class='o'>(</span><span class='s'>"show off expect_shape() failure messages"</span>, <span class='o'>&#123;</span></span>
<span>  <span class='nv'>x</span> <span class='o'>&lt;-</span> <span class='nf'><a href='https://rdrr.io/r/base/matrix.html'>matrix</a></span><span class='o'>(</span><span class='m'>1</span><span class='o'>:</span><span class='m'>9</span>, nrow <span class='o'>=</span> <span class='m'>3</span><span class='o'>)</span></span>
<span>  <span class='nf'><a href='https://testthat.r-lib.org/reference/expect_length.html'>expect_shape</a></span><span class='o'>(</span><span class='nv'>x</span>, nrow <span class='o'>=</span> <span class='m'>4</span><span class='o'>)</span></span>
<span>  <span class='nf'><a href='https://testthat.r-lib.org/reference/expect_length.html'>expect_shape</a></span><span class='o'>(</span><span class='nv'>x</span>, dim <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='m'>3</span>, <span class='m'>3</span>, <span class='m'>3</span><span class='o'>)</span><span class='o'>)</span></span>
<span>  <span class='nf'><a href='https://testthat.r-lib.org/reference/expect_length.html'>expect_shape</a></span><span class='o'>(</span><span class='nv'>x</span>, dim <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='m'>3</span>, <span class='m'>4</span><span class='o'>)</span><span class='o'>)</span></span>
<span><span class='o'>&#125;</span><span class='o'>)</span></span>
<span><span class='c'>#&gt; ── <span style='color: #BBBB00; font-weight: bold;'>Failure</span><span style='font-weight: bold;'>: show off expect_shape() failure messages</span> ─────────────────</span></span>
<span><span class='c'>#&gt; Expected `x` to have 4 rows.</span></span>
<span><span class='c'>#&gt; Actual rows: 3.</span></span>
<span><span class='c'>#&gt; ── <span style='color: #BBBB00; font-weight: bold;'>Failure</span><span style='font-weight: bold;'>: show off expect_shape() failure messages</span> ─────────────────</span></span>
<span><span class='c'>#&gt; Expected `x` to have 3 dimensions.</span></span>
<span><span class='c'>#&gt; Actual dimensions: 2.</span></span>
<span><span class='c'>#&gt; ── <span style='color: #BBBB00; font-weight: bold;'>Failure</span><span style='font-weight: bold;'>: show off expect_shape() failure messages</span> ─────────────────</span></span>
<span><span class='c'>#&gt; Expected `x` to have dim (3, 4).</span></span>
<span><span class='c'>#&gt; Actual dim: (3, 3).</span></span>
<span></span><span><span class='c'>#&gt; <span style='color: #BBBB00; font-weight: bold;'>Error</span><span style='font-weight: bold;'>:</span></span></span>
<span><span class='c'>#&gt; <span style='color: #BBBB00;'>!</span> Test failed with 3 failures and 0 successes.</span></span>
<span></span></code></pre>
</div>
</li>
</ul>
<p>As you can see from the examples above, when you run a single test interactively (i.e. not as a part of a test suite) you now see exactly how many expectations succeeded and failed.</p>
<h2 id="other-new-features">Other new features
</h2>
<ul>
<li>
<p>testthat generally does a better job of handling nested tests, aka subtests, where you put a 






<a href="https://testthat.r-lib.org/reference/test_that.html" target="_blank" rel="noopener"><code>test_that()</code></a>
 inside another 






<a href="https://testthat.r-lib.org/reference/test_that.html" target="_blank" rel="noopener"><code>test_that()</code></a>
, or more typically 






<a href="https://testthat.r-lib.org/reference/describe.html" target="_blank" rel="noopener"><code>it()</code></a>
 inside of 






<a href="https://testthat.r-lib.org/reference/describe.html" target="_blank" rel="noopener"><code>describe()</code></a>
. Subtests will now generate more informative failure messages, free from duplication, with more informative skips if any subtests don&rsquo;t contain any expectations.</p>
</li>
<li>
<p>The snapshot experience has been significantly improved, with all known bugs fixed and some new helpers added: 






<a href="https://testthat.r-lib.org/reference/snapshot_accept.html" target="_blank" rel="noopener"><code>snapshot_reject()</code></a>
 rejects all modified snapshots by deleting the <code>.new</code> variants, and 






<a href="https://testthat.r-lib.org/reference/snapshot_download_gh.html" target="_blank" rel="noopener"><code>snapshot_download_gh()</code></a>
 makes it easy to get snapshots off GitHub and into your local package. Additionally, 






<a href="https://testthat.r-lib.org/reference/expect_snapshot.html" target="_blank" rel="noopener"><code>expect_snapshot()</code></a>
 and friends will now fail when creating a new snapshot on CI, as that&rsquo;s usually a signal that you&rsquo;ve forgotten to run the snapshot code locally before committing.</p>
</li>
<li>
<p>On CRAN, 






<a href="https://testthat.r-lib.org/reference/test_that.html" target="_blank" rel="noopener"><code>test_that()</code></a>
 will automatically skip if a package is not installed, which means that you no longer need to check if suggested packages are installed in your tests.</p>
</li>
<li>
<p><code>vignette(&quot;mocking&quot;)</code> explains mocking in detail, and new 






<a href="https://testthat.r-lib.org/reference/local_mocked_s3_method.html" target="_blank" rel="noopener"><code>local_mocked_s3_method()</code></a>
, 






<a href="https://testthat.r-lib.org/reference/local_mocked_s3_method.html" target="_blank" rel="noopener"><code>local_mocked_s4_method()</code></a>
, and 






<a href="https://testthat.r-lib.org/reference/local_mocked_r6_class.html" target="_blank" rel="noopener"><code>local_mocked_r6_class()</code></a>
 make it easier to mock S3 and S4 methods and R6 classes.</p>
</li>
<li>
<p>






<a href="https://testthat.r-lib.org/reference/test_dir.html" target="_blank" rel="noopener"><code>test_dir()</code></a>
, 






<a href="https://testthat.r-lib.org/reference/test_package.html" target="_blank" rel="noopener"><code>test_check()</code></a>
, and friends gain a <code>shuffle</code> argument that uses 






<a href="https://rdrr.io/r/base/sample.html" target="_blank" rel="noopener"><code>sample()</code></a>
 to randomly reorder the top-level expressions in each test file. This random reordering surfaces dependencies between tests and code outside of any test, as well as dependencies between tests, helping you find and eliminate unintentional dependencies.</p>
</li>
<li>
<p>






<a href="https://testthat.r-lib.org/reference/try_again.html" target="_blank" rel="noopener"><code>try_again()</code></a>
 is now publicized, as it&rsquo;s a useful tool for testing flaky code:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nv'>flaky_function</span> <span class='o'>&lt;-</span> <span class='kr'>function</span><span class='o'>(</span><span class='o'>)</span> <span class='o'>&#123;</span></span>
  <span>  <span class='kr'>if</span> <span class='o'>(</span><span class='nf'><a href='https://rdrr.io/r/stats/Uniform.html'>runif</a></span><span class='o'>(</span><span class='m'>1</span><span class='o'>)</span> <span class='o'>&lt;</span> <span class='m'>0.1</span><span class='o'>)</span> <span class='m'>0</span> <span class='kr'>else</span> <span class='m'>1</span></span>
  <span><span class='o'>&#125;</span></span>
  <span></span>
  <span><span class='c'># 10% chance of failure:</span></span>
  <span><span class='nf'><a href='https://testthat.r-lib.org/reference/test_that.html'>test_that</a></span><span class='o'>(</span><span class='s'>"my flaky test is ok"</span>, <span class='o'>&#123;</span></span>
  <span>  <span class='nf'><a href='https://testthat.r-lib.org/reference/skip.html'>skip_on_cran</a></span><span class='o'>(</span><span class='o'>)</span></span>
  <span>  <span class='nf'><a href='https://testthat.r-lib.org/reference/equality-expectations.html'>expect_equal</a></span><span class='o'>(</span><span class='nf'>flaky_function</span><span class='o'>(</span><span class='o'>)</span>, <span class='m'>1</span><span class='o'>)</span></span>
  <span><span class='o'>&#125;</span><span class='o'>)</span></span>
  <span></span>
  <span><span class='c'># 1% chance of failure:</span></span>
  <span><span class='nf'><a href='https://testthat.r-lib.org/reference/test_that.html'>test_that</a></span><span class='o'>(</span><span class='s'>"my flaky test is ok"</span>, <span class='o'>&#123;</span></span>
  <span>  <span class='nf'><a href='https://testthat.r-lib.org/reference/skip.html'>skip_on_cran</a></span><span class='o'>(</span><span class='o'>)</span></span>
  <span>  <span class='nf'><a href='https://testthat.r-lib.org/reference/try_again.html'>try_again</a></span><span class='o'>(</span><span class='m'>1</span>, <span class='nf'><a href='https://testthat.r-lib.org/reference/equality-expectations.html'>expect_equal</a></span><span class='o'>(</span><span class='nf'>flaky_function</span><span class='o'>(</span><span class='o'>)</span>, <span class='m'>1</span><span class='o'>)</span><span class='o'>)</span></span>
  <span><span class='o'>&#125;</span><span class='o'>)</span></span>
  <span></span>
  <span><span class='c'># 0.1% chance of failure:</span></span>
  <span><span class='nf'><a href='https://testthat.r-lib.org/reference/test_that.html'>test_that</a></span><span class='o'>(</span><span class='s'>"my flaky test is ok"</span>, <span class='o'>&#123;</span></span>
  <span>  <span class='nf'><a href='https://testthat.r-lib.org/reference/skip.html'>skip_on_cran</a></span><span class='o'>(</span><span class='o'>)</span></span>
  <span>  <span class='nf'><a href='https://testthat.r-lib.org/reference/try_again.html'>try_again</a></span><span class='o'>(</span><span class='m'>2</span>, <span class='nf'><a href='https://testthat.r-lib.org/reference/equality-expectations.html'>expect_equal</a></span><span class='o'>(</span><span class='nf'>flaky_function</span><span class='o'>(</span><span class='o'>)</span>, <span class='m'>1</span><span class='o'>)</span><span class='o'>)</span></span>
  <span><span class='o'>&#125;</span><span class='o'>)</span></span></code></pre>
</div>
<p>Note that it&rsquo;s still good practice to skip such tests on CRAN.</p>
</li>
<li>
<p>New 






<a href="https://testthat.r-lib.org/reference/skip.html" target="_blank" rel="noopener"><code>skip_unless_r()</code></a>
 skips tests on unsuitable versions of R. It has a convenient syntax so you can use, e.g., <code>skip_unless_r(&quot;&gt;= 4.1.0&quot;)</code> to skip tests that require 






<a href="https://rdrr.io/r/base/dots.html" target="_blank" rel="noopener"><code>...names()</code></a>
.</p>
</li>
<li>
<p>New <code>SlowReporter</code> makes it easier to find the slowest tests in your package. You can run it with <code>devtools::test(reporter = &quot;slow&quot;)</code>.</p>
</li>
<li>
<p>New <code>vignette(&quot;challenging-functions&quot;)</code> provides an index to other documentation organized by various challenges.</p>
</li>
</ul>
<h2 id="acknowledgements">Acknowledgements
</h2>
<p>A big thank you to all the folks who helped make this release happen: 






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






<a href="https://github.com/tylermorganwall" target="_blank" rel="noopener">@tylermorganwall</a>
.</p>
]]></description>
      <enclosure url="https://opensource.posit.co/blog/2025-11-13_testthat-3-3-0/thumbnail-wd.jpg" length="116709" type="image/jpeg" />
    </item>
    <item>
      <title>pkgdown 2.2.0</title>
      <link>https://opensource.posit.co/blog/2025-11-06_pkgdown-2-2-0/</link>
      <pubDate>Thu, 06 Nov 2025 00:00:00 +0000</pubDate>
      <guid>https://opensource.posit.co/blog/2025-11-06_pkgdown-2-2-0/</guid>
      <dc:creator>Hadley Wickham</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 delighted to announce the release of 






<a href="https://pkgdown.r-lib.org" target="_blank" rel="noopener">pkgdown</a>
 2.2.0. pkgdown is designed to make it quick and easy to build a beautiful and accessible website for your package.</p>
<p>You can install it 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='s'>"pkgdown"</span><span class='o'>)</span></span></code></pre>
</div>
<p>This version of pkgdown has one major change: a new 






<a href="https://pkgdown.r-lib.org/reference/build_llm_docs.html" target="_blank" rel="noopener"><code>pkgdown::build_llm_docs()</code></a>
 function that automatically creates files that make it easier for LLMs to read your documentation. Concretely, this means two things:</p>
<ul>
<li>
<p>You&rsquo;ll get an <code>llms.txt</code> at the root directory of your site. 






<a href="https://llmstxt.org" target="_blank" rel="noopener"><code>llms.txt</code></a>
 is an emerging standard that provides an easy way for an LLM to get an overview of your site. pkgdown creates an overview by combining your README, your function index, and your article index: this should give the LLM a broad overview of what your package does, along with links to find out more.</p>
</li>
<li>
<p>Every existing <code>.html</code> on your site gets a corresponding <code>.md</code> file. These are generally easier for LLMs to understand because they contain just the content of the site, without any extraneous styling.</p>
</li>
</ul>
<p>If you don&rsquo;t want to generate these files, just add the following to your <code>_pkgdown.yaml</code>:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'>llm-docs: false
</code></pre>
</div>
<p>This release also includes new translations for Dutch and Japanese, removal of the long-deprecated <code>autolink_html()</code> and <code>preview_page()</code>, and a handful of other bug fixes and minor improvements. You can read about them all in the 






<a href="https://github.com/r-lib/pkgdown/releases/tag/v2.2.0" target="_blank" rel="noopener">release notes</a>
.</p>
<h2 id="acknowledgements">Acknowledgements
</h2>
<p>As always, a big thanks to everyone who helped make this release possible: 






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






<a href="https://github.com/usrbinr" target="_blank" rel="noopener">@usrbinr</a>
.</p>
]]></description>
      <enclosure url="https://opensource.posit.co/blog/2025-11-06_pkgdown-2-2-0/thumbnail-wd.jpg" length="353997" type="image/jpeg" />
    </item>
    <item>
      <title>mirai 2.5.0</title>
      <link>https://opensource.posit.co/blog/2025-09-05_mirai-2-5-0/</link>
      <pubDate>Fri, 05 Sep 2025 00:00:00 +0000</pubDate>
      <guid>https://opensource.posit.co/blog/2025-09-05_mirai-2-5-0/</guid>
      <dc:creator>Charlie Gao</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 excited to announce 






<a href="https://mirai.r-lib.org" target="_blank" rel="noopener">mirai</a>
 2.5.0, bringing production-grade async computing to R!</p>
<p>This milestone release delivers enhanced observability through OpenTelemetry, reproducible parallel RNG, and key user interface improvements. We&rsquo;ve also packed in twice as many 






<a href="https://mirai.r-lib.org/news/index.html" target="_blank" rel="noopener">changes</a>
 as usual - going all out in delivering a round of quality-of-life fixes to make your use of mirai even smoother!</p>
<p>You can install it 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='s'>"mirai"</span><span class='o'>)</span></span></code></pre>
</div>
<h2 id="introduction-to-mirai">Introduction to mirai
</h2>
<p>mirai (Japanese for &lsquo;future&rsquo;) provides a clean, modern approach to parallel computing in R. Built on current communication technologies, it delivers extreme performance through professional-grade scheduling and an event-driven architecture.</p>
<p>It continues to evolve as the foundation for asynchronous and parallel computing across the R ecosystem, powering everything from 






<a href="https://rstudio.github.io/promises/articles/promises_04_mirai.html" target="_blank" rel="noopener">async Shiny</a>
 applications to 






  
  

<a href="https://opensource.posit.co/blog/2025-07-10_purrr-1-1-0-parallel/">parallel map</a>
 in purrr to 


  
  
  





<a href="https://tune.tidymodels.org/news/index.html#parallel-processing-2-0-0" target="_blank" rel="noopener">hyperparameter tuning</a>
 in tidymodels.</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>
<span><span class='c'># Set up persistent 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'># Async evaluation - non-blocking</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'># Results are available 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 persistent 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'>0</span><span class='o'>)</span></span></code></pre>
</div>
<h2 id="a-unique-design-philosophy">A unique design philosophy
</h2>
<p><strong>Modern foundation</strong>: mirai builds on 






<a href="https://nanonext.r-lib.org" target="_blank" rel="noopener">nanonext</a>
, the R binding to Nanomsg Next Generation, a high-performance messaging library designed for distributed systems. This means that it&rsquo;s using the very latest technologies, and supports the most optimal connections out of the box: IPC (inter-process communications), TCP or secure TLS. It also extends base R&rsquo;s serialization mechanism to support custom serialization of newer cross-language data formats such as safetensors, Arrow and Polars.</p>
<p><strong>Extreme performance</strong>: as a consequence of its solid technological foundation, mirai has the proven capacity to scale to millions of concurrent tasks over thousands of connections. Moreover, it delivers up to 1,000x the efficiency and responsiveness of other alternatives. A key innovation is the implementation of event-driven promises that react with zero latency - this provides an extra edge for real-time applications such as live inference or Shiny apps.</p>
<p><strong>Production first</strong>: mirai provides a clear mental model for parallel computation, with a clean separation of a user&rsquo;s current environment with that in which a mirai is evaluated. This explicitness and simplicity helps avoid common pitfalls that can afflict parallel processing, such as capturing incorrect or extraneous variables. Transparency and robustness are key to mirai&rsquo;s design, and are achieved by minimizing complexity, and eliminating all hidden state with no reliance on options or environment variables. Finally, its integration with OpenTelemetry provides for production-grade observability.</p>
<p><strong>Deploy everywhere</strong>: deployment of daemon processes is made through a consistent interface across local, remote (SSH), and 






<a href="https://shikokuchuo.net/posts/27-mirai-240/" target="_blank" rel="noopener">HPC environments</a>
 (Slurm, SGE, PBS, LSF). Compute profiles are daemons settings that are managed independently, such that you can be connected to all three resource types simultaneously. You then have the freedom to distribute workload to the most appropriate resource for any given task - especially important if tasks have differing requirements such as GPU compute.</p>
<h2 id="opentelemetry-integration">OpenTelemetry integration
</h2>
<p>New in mirai 2.5.0: complete observability of mirai requests through OpenTelemetry traces. This is a core feature that completes the final pillar in mirai&rsquo;s &lsquo;production first&rsquo; design philosophy.</p>
<p>When tracing is enabled via the otel and otelsdk packages, you can monitor the entire lifecycle of your async computations, from creation through to evaluation, making it easier to debug and optimize performance in production environments. This is especially powerful when used in conjunction with other otel-enabled packages (such as an upcoming Shiny release), providing end-to-end observability across your entire application stack.</p>
<figure>
<img src="https://opensource.posit.co/blog/2025-09-05_mirai-2-5-0/otel-screenshot.png" alt="Illustrative OpenTelemetry span structure shown in a Jaeger collector UI" />
<figcaption aria-hidden="true"><em>Illustrative OpenTelemetry span structure shown in a Jaeger collector UI</em></figcaption>
</figure>
<h2 id="reproducible-parallel-rng">Reproducible parallel RNG
</h2>
<p>Introduced in mirai 2.4.1: reproducible parallel random number generation. Developed in consultation with our tidymodels colleagues and core members of the mlr team, this is a great example of the R community pulling together to solve a common problem. It addresses a long-standing challenge in parallel computing in R, important for reproducible science.</p>
<p>mirai has, since its early days, used L&rsquo;Ecuyer-CMRG streams for statistically-sound parallel RNG. Streams essentially cut into the RNG&rsquo;s period (a very long sequence of pseudo-random numbers) at intervals that are far apart from each other that they do not in practice overlap. This ensures that statistical results obtained from parallel computations remain correct and valid.</p>
<p>Previously, we only offered the following option, matching the behaviour of base R&rsquo;s parallel package:</p>
<p><strong>Default behaviour</strong> <code>daemons(seed = NULL)</code>: creates independent streams for each daemon. This ensures statistical validity but not numerical reproducibility between runs.</p>
<p>Now, we also offer the following option:</p>
<p><strong>Reproducible mode</strong> <code>daemons(seed = integer)</code>: creates a stream for each 






<a href="https://mirai.r-lib.org/reference/mirai.html" target="_blank" rel="noopener"><code>mirai()</code></a>
 call rather than each daemon. This guarantees identical results across runs, regardless of the number of daemons used.</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='c'># Always provides identical results:</span></span>
<span></span>
<span><span class='nf'><a href='https://rdrr.io/r/base/with.html'>with</a></span><span class='o'>(</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'>3</span>, seed <span class='o'>=</span> <span class='m'>1234L</span><span class='o'>)</span>,</span>
<span>  <span class='nf'><a href='https://mirai.r-lib.org/reference/mirai_map.html'>mirai_map</a></span><span class='o'>(</span><span class='m'>1</span><span class='o'>:</span><span class='m'>3</span>, <span class='nv'>rnorm</span>, .args <span class='o'>=</span> <span class='nf'><a href='https://rdrr.io/r/base/list.html'>list</a></span><span class='o'>(</span>mean <span class='o'>=</span> <span class='m'>20</span>, sd <span class='o'>=</span> <span class='m'>2</span><span class='o'>)</span><span class='o'>)</span><span class='o'>[</span><span class='o'>]</span></span>
<span><span class='o'>)</span></span>
<span><span class='c'>#&gt; [[1]]</span></span>
<span><span class='c'>#&gt; [1] 19.86409</span></span>
<span><span class='c'>#&gt; </span></span>
<span><span class='c'>#&gt; [[2]]</span></span>
<span><span class='c'>#&gt; [1] 19.55834 22.30159</span></span>
<span><span class='c'>#&gt; </span></span>
<span><span class='c'>#&gt; [[3]]</span></span>
<span><span class='c'>#&gt; [1] 20.62193 23.06144 19.61896</span></span>
<span></span></code></pre>
</div>
<h2 id="user-interface-improvements">User interface improvements
</h2>
<h3 id="compute-profile-helper-functions">Compute profile helper functions
</h3>
<p>






<a href="https://mirai.r-lib.org/reference/with_daemons.html" target="_blank" rel="noopener"><code>with_daemons()</code></a>
 and 






<a href="https://mirai.r-lib.org/reference/with_daemons.html" target="_blank" rel="noopener"><code>local_daemons()</code></a>
 make working with compute profiles much more convenient by allowing the temporary switching of contexts. This means that developers can continue to write mirai code without worrying about the resources on which it is eventually run. End-users now have the ability to change the destination of any mirai computation dynamically using one of these scoped helpers.</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="c1"># Work with specific compute profiles</span>
</span></span><span class="line"><span class="cl"><span class="nf">with_daemons</span><span class="p">(</span><span class="s">&#34;gpu&#34;</span><span class="p">,</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="n">result</span> <span class="o">&lt;-</span> <span class="nf">mirai</span><span class="p">(</span><span class="nf">gpu_intensive_task</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="c1"># Local version for use inside functions</span>
</span></span><span class="line"><span class="cl"><span class="n">async_gpu_intensive_task</span> <span class="o">&lt;-</span> <span class="kr">function</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nf">local_daemons</span><span class="p">(</span><span class="s">&#34;gpu&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="nf">mirai</span><span class="p">(</span><span class="nf">gpu_intensive_task</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="re-designed-daemons">Re-designed <code>daemons()</code>
</h3>
<p>Creating new daemons is now more ergonomic, as it automatically resets existing ones. This provides for more convenient use in contexts such as notebooks, where cells may be run out of order. Manual <code>daemons(0)</code> calls are no longer required to reset daemons.</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"># Old approach</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 class="c1"># Had to reset first</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></span><span class="line"><span class="cl"><span class="c1"># New approach - automatic reset</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 class="c1"># Just works, resets if needed</span></span></span></code></pre></td></tr></table>
</div>
</div></div>
<h3 id="new-info-function">New <code>info()</code> function
</h3>
<p>Provides a more succinct alternative to 






<a href="https://mirai.r-lib.org/reference/status.html" target="_blank" rel="noopener"><code>status()</code></a>
 for reporting key statistics. This is optimized and is now a supported developer interface for programmatic use.</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nf'><a href='https://mirai.r-lib.org/reference/info.html'>info</a></span><span class='o'>(</span><span class='o'>)</span></span>
<span><span class='c'>#&gt; connections  cumulative    awaiting   executing   completed </span></span>
<span><span class='c'>#&gt;           4           4           8           4           2</span></span></code></pre>
</div>
<h2 id="acknowledgements">Acknowledgements
</h2>
<p>We extend our gratitude to the R community for their continued feedback and contributions. Special thanks to all contributors who helped shape this release through feature requests, bug reports, and code contributions: 






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






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






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






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






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






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






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






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






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






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






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






<a href="https://github.com/wlandau" target="_blank" rel="noopener">@wlandau</a>
.</p>
]]></description>
      <enclosure url="https://opensource.posit.co/blog/2025-09-05_mirai-2-5-0/thumbnail-wd.jpg" length="794691" type="image/jpeg" />
    </item>
    <item>
      <title>nanonext 1.7.0</title>
      <link>https://opensource.posit.co/blog/2025-09-02_nanonext-1-7-0/</link>
      <pubDate>Tue, 02 Sep 2025 00:00:00 +0000</pubDate>
      <guid>https://opensource.posit.co/blog/2025-09-02_nanonext-1-7-0/</guid>
      <dc:creator>Charlie Gao</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)
-->
<h1 id="introducing-nanonext-breaking-down-language-barriers-in-data-science">Introducing nanonext: breaking down language barriers in data science
</h1>
<p>We&rsquo;re excited to welcome 






<a href="https://nanonext.r-lib.org" target="_blank" rel="noopener">nanonext</a>
 to the r-lib family! nanonext is R&rsquo;s binding to NNG (Nanomsg Next Generation), a high-performance C messaging library that implements scalability protocols for distributed systems. Because NNG has bindings and ports across multiple languages&mdash;including Python, Go, Rust, C++, and many others&mdash;nanonext enables seamless interoperability between R and other modern programming languages.</p>
<p>The latest version 1.7.0 brings enhanced reliability with improved HTTP client functionality, better handling of custom serialization methods, and more robust event-driven promises.</p>
<p>Get started by installing the package from CRAN now:</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;nanonext&#34;</span><span class="p">)</span></span></span></code></pre></td></tr></table>
</div>
</div></div>
<h2 id="the-challenge-multi-language-data-science">The challenge: multi-language data science
</h2>
<p>If you&rsquo;ve worked in data science, you&rsquo;ve likely encountered this scenario: your workflow spans multiple programming languages. Perhaps you have Python models, R analysis scripts, Go services, or C++ performance libraries that all need to work together.</p>
<p>Traditionally, making these components communicate means:</p>
<ul>
<li>Writing data to files and reading them back</li>
<li>Building REST APIs and making HTTP calls</li>
<li>Handling different serialization formats like JSON or protocol buffers</li>
<li>Dealing with the latency and complexity that comes with each approach</li>
</ul>
<h2 id="the-solution-nngs-scalability-protocols">The solution: NNG&rsquo;s scalability protocols
</h2>
<p>nanonext changes this by bringing NNG&rsquo;s scalability protocols to R. NNG is a high-performance C library that implements standardized communication patterns like request/reply, publish/subscribe, and pipeline architectures. Any process using NNG can communicate directly with any other NNG process using the same protocol&mdash;regardless of the programming language.</p>
<p>This means that for simple atomic vector types, your R data doesn&rsquo;t even need to be serialized and converted to a common format, enabling direct, real-time communication between processes. For more complex data types, as long as serialization and unserialization methods exist in both languages, then nanonext can be used to send and receive arbitrary binary data.</p>
<h2 id="what-can-you-do-with-nanonext">What can you do with nanonext?
</h2>
<p>nanonext opens up several powerful possibilities for R users:</p>
<p><strong>🔗 Cross-language integration</strong>: Connect R directly with Python machine learning models, Go microservices, Rust compute engines, or C libraries without intermediate files or complex APIs.</p>
<p><strong>⚡ Real-time data pipelines</strong>: Build data systems where different components process data as it flows, perfect for live dashboards or high-frequency analytics.</p>
<p><strong>📡 Modern web integration</strong>: Create WebSocket clients, make asynchronous HTTP requests, or build real-time APIs that integrate with web services.</p>
<p><strong>🚀 Asynchronous programming</strong>: Write non-blocking R code that can handle multiple operations simultaneously, improving performance for concurrent tasks.</p>
<h2 id="python--r-interoperability-example">Python ↔ R interoperability example
</h2>
<p>Here&rsquo;s a concrete example of R and Python working together through NNG&rsquo;s protocols. Both processes use their respective NNG bindings to establish a direct communication channel using NNG&rsquo;s &lsquo;pair&rsquo; protocol&mdash;a simple one-to-one bidirectional communication pattern.</p>
<p><strong>Python side</strong> (using pynng):</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">import</span> <span class="nn">numpy</span> <span class="k">as</span> <span class="nn">np</span>
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">pynng</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Create socket to communicate with R</span>
</span></span><span class="line"><span class="cl"><span class="n">socket</span> <span class="o">=</span> <span class="n">pynng</span><span class="o">.</span><span class="n">Pair0</span><span class="p">(</span><span class="n">listen</span><span class="o">=</span><span class="s2">&#34;ipc:///tmp/nanonext.socket&#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"># Wait for data from R</span>
</span></span><span class="line"><span class="cl"><span class="n">raw</span> <span class="o">=</span> <span class="n">socket</span><span class="o">.</span><span class="n">recv</span><span class="p">()</span>
</span></span><span class="line"><span class="cl"><span class="n">array</span> <span class="o">=</span> <span class="n">np</span><span class="o">.</span><span class="n">frombuffer</span><span class="p">(</span><span class="n">raw</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">array</span><span class="p">)</span>  <span class="c1"># [1.1 2.2 3.3 4.4 5.5]</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Process data (could be ML model prediction, transformation, etc.)</span>
</span></span><span class="line"><span class="cl"><span class="n">processed</span> <span class="o">=</span> <span class="n">array</span> <span class="o">*</span> <span class="mi">2</span>  <span class="c1"># Simple example: multiply by 2</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Send back to R as bytes</span>
</span></span><span class="line"><span class="cl"><span class="n">socket</span><span class="o">.</span><span class="n">send</span><span class="p">(</span><span class="n">processed</span><span class="o">.</span><span class="n">tobytes</span><span class="p">())</span>
</span></span><span class="line"><span class="cl"><span class="n">socket</span><span class="o">.</span><span class="n">close</span><span class="p">()</span></span></span></code></pre></td></tr></table>
</div>
</div></div>
<p><strong>R side</strong> (using nanonext):</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-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">nanonext</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Connect to Python process</span>
</span></span><span class="line"><span class="cl"><span class="n">sock</span> <span class="o">&lt;-</span> <span class="nf">socket</span><span class="p">(</span><span class="s">&#34;pair&#34;</span><span class="p">,</span> <span class="n">dial</span> <span class="o">=</span> <span class="s">&#34;ipc:///tmp/nanonext.socket&#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"># Send numeric data directly to Python (no serialization needed!)</span>
</span></span><span class="line"><span class="cl"><span class="n">sock</span> <span class="o">|&gt;</span> <span class="nf">send</span><span class="p">(</span><span class="nf">c</span><span class="p">(</span><span class="m">1.1</span><span class="p">,</span> <span class="m">2.2</span><span class="p">,</span> <span class="m">3.3</span><span class="p">,</span> <span class="m">4.4</span><span class="p">,</span> <span class="m">5.5</span><span class="p">),</span> <span class="n">mode</span> <span class="o">=</span> <span class="s">&#34;raw&#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"># Receive processed results back from Python</span>
</span></span><span class="line"><span class="cl"><span class="n">processed_data</span> <span class="o">&lt;-</span> <span class="n">sock</span> <span class="o">|&gt;</span> <span class="nf">recv</span><span class="p">(</span><span class="n">mode</span> <span class="o">=</span> <span class="s">&#34;double&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nf">print</span><span class="p">(</span><span class="n">processed_data</span><span class="p">)</span>  <span class="c1"># [1] 2.2 4.4 6.6 8.8 11.0</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Continue with R-specific analysis</span>
</span></span><span class="line"><span class="cl"><span class="nf">summary</span><span class="p">(</span><span class="n">processed_data</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nf">plot</span><span class="p">(</span><span class="n">processed_data</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">close</span><span class="p">(</span><span class="n">sock</span><span class="p">)</span></span></span></code></pre></td></tr></table>
</div>
</div></div>
<p><strong>What just happened?</strong> Your R session sent numerical data directly to Python&rsquo;s memory space, Python processed it (possibly performing inference on a complex ML model), and sent results back&mdash;all happening in memory without touching the disk. This is orders of magnitude faster than traditional file-based approaches.</p>
<h2 id="why-this-matters">Why this matters
</h2>
<p><strong>Speed</strong>: Direct memory communication is dramatically faster than file I/O or HTTP requests. Your R session doesn&rsquo;t wait for disk writes or network round-trips.</p>
<p><strong>Simplicity</strong>: No need to design REST APIs, manage file formats, or handle serialization. Send R vectors directly and receive results back.</p>
<p><strong>Flexibility</strong>: nanonext supports different communication patterns (one-to-one, one-to-many, publish/subscribe) and transport methods (IPC, TCP, WebSocket).</p>
<p><strong>Reliability</strong>: Built on NNG, a mature library that powers mission-critical systems in technology, finance and many other industries.</p>
<p><strong>Asynchronous</strong>: Your R session stays responsive. Send a request for a long-running computation and continue working while it processes in the background.</p>
<h2 id="the-future-is-multi-language">The future is multi-language
</h2>
<p>nanonext facilitates a shift toward more flexible, performance-oriented data science workflows. Instead of being locked into a single language or accepting the overhead of traditional integration methods, you can now build systems where each component uses the best tool for the job.</p>
<p>We&rsquo;re excited to see how the R community uses these capabilities.</p>
<p>Ready to try it? Visit the 






<a href="https://nanonext.r-lib.org" target="_blank" rel="noopener">package website</a>
 for comprehensive documentation, or explore the code at the 






<a href="https://github.com/r-lib/nanonext" target="_blank" rel="noopener">GitHub repository</a>
.</p>
]]></description>
      <enclosure url="https://opensource.posit.co/blog/2025-09-02_nanonext-1-7-0/thumbnail-wd.jpg" length="99873" type="image/jpeg" />
    </item>
    <item>
      <title>Air 0.7.0</title>
      <link>https://opensource.posit.co/blog/2025-06-11_air-0-7-0/</link>
      <pubDate>Wed, 11 Jun 2025 00:00:00 +0000</pubDate>
      <guid>https://opensource.posit.co/blog/2025-06-11_air-0-7-0/</guid>
      <dc:creator>Davis Vaughan</dc:creator>
      <dc:creator>Lionel Henry</dc:creator><description><![CDATA[<p>We&rsquo;re very excited to announce 






<a href="https://posit-dev.github.io/air/" target="_blank" rel="noopener">Air 0.7.0</a>
, a new release of our extremely fast R formatter. This post will act as a roundup of releases 0.5.0 through 0.7.0, including: even better Positron support, a new feature called autobracing, and an official GitHub Action! If you haven&rsquo;t heard of Air, read our 






  
  

<a href="https://opensource.posit.co/blog/2025-02-21_air/">announcement blog post</a>
 first to get up to speed. To install Air, read our 






<a href="https://posit-dev.github.io/air/editors.html" target="_blank" rel="noopener">editors guide</a>
.</p>
<h2 id="positron">Positron
</h2>
<p>The 






<a href="https://open-vsx.org/extension/posit/air-vscode" target="_blank" rel="noopener">Air extension</a>
 is now included in 






<a href="https://positron.posit.co/" target="_blank" rel="noopener">Positron</a>
 by default, and will automatically keep itself up to date. We&rsquo;ve been working hard to ensure that Air leaves a positive first impression, and we think that having Positron come batteries included with Air really helps with that! Positron now also ships with 






<a href="https://docs.astral.sh/ruff/" target="_blank" rel="noopener">Ruff</a>
, the extremely fast Python formatter and linter, ensuring that you have a great editing experience out of the box, no matter which language you prefer.</p>
<p>We&rsquo;ve also streamlined the process of adding Air to a new or existing project. With dev usethis, you can now run 






<a href="https://usethis.r-lib.org/dev/reference/use_air.html" target="_blank" rel="noopener"><code>usethis::use_air()</code></a>
 to automatically configure recommended Air settings. In particular, this will:</p>
<ul>
<li>
<p>Create an 


  
  
  





<a href="https://posit-dev.github.io/air/configuration.html#configuration-recommendations" target="_blank" rel="noopener">empty <code>air.toml</code></a>
.</p>
</li>
<li>
<p>Create <code>.vscode/settings.json</code> filled with the following settings. This enables <code>Format on Save</code> within your workspace.</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;[r]&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nt">&#34;editor.formatOnSave&#34;</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="nt">&#34;editor.defaultFormatter&#34;</span><span class="p">:</span> <span class="s2">&#34;Posit.air-vscode&#34;</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>
</li>
<li>
<p>Create <code>.vscode/extensions.json</code> filled with the following settings. This automatically prompts contributors that don&rsquo;t have the Air extension to install it when they open your workspace, ensuring that everyone is using the same formatter!</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-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;recommendations&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;Posit.air-vscode&#34;</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>
</li>
<li>
<p>Update your <code>.Rbuildignore</code> to exclude Air related configuration, if you&rsquo;re working on an R package.</p>
</li>
</ul>
<p>Once you&rsquo;ve used usethis to configure Air, you can now immediately reformat your entire workspace by running <code>Air: Format Workspace Folder</code> from the Command Palette (accessible via <code>Cmd + Shift + P</code> on Mac/Linux, or <code>Ctrl + Shift + P</code> on Windows). I&rsquo;ve found that this is invaluable for adopting Air in an existing project!</p>
<p>To summarize, we&rsquo;ve reduced our advice on adding Air to an existing project down to:</p>
<ul>
<li>
<p>Open Positron</p>
</li>
<li>
<p>Run 






<a href="https://usethis.r-lib.org/reference/use_air.html" target="_blank" rel="noopener"><code>usethis::use_air()</code></a>
</p>
</li>
<li>
<p>Run <code>Air: Format Workspace Folder</code></p>
</li>
<li>
<p>Commit, push, and then enjoy using <code>Format on Save</code> forevermore 😄</p>
</li>
</ul>
<h2 id="more-editors">More editors!
</h2>
<p>Positron isn&rsquo;t the only editor that&rsquo;s received some love! We now have official documentation for using Air in the following editors:</p>
<ul>
<li>
<p>






<a href="https://posit-dev.github.io/air/editor-zed.html" target="_blank" rel="noopener">Zed</a>
</p>
</li>
<li>
<p>






<a href="https://posit-dev.github.io/air/editor-neovim.html" target="_blank" rel="noopener">Neovim</a>
</p>
</li>
<li>
<p>






<a href="https://posit-dev.github.io/air/editor-helix.html" target="_blank" rel="noopener">Helix</a>
</p>
</li>
</ul>
<p>We&rsquo;re very proud of the fact that Air can be used within any editor, not just RStudio and Positron! This documentation was a community effort - thanks in particular to 






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






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






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






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






<a href="https://github.com/wurli" target="_blank" rel="noopener">@wurli</a>
.</p>
<h2 id="autobracing">Autobracing
</h2>
<p>Autobracing is the process of adding braces (i.e. 






<a href="https://rdrr.io/r/base/Paren.html" target="_blank" rel="noopener"><code>{ }</code></a>
) to if statements, loops, and function definitions to create more consistent, readable, and portable code. It looks 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><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-r" data-lang="r"><span class="line"><span class="cl"><span class="kr">for</span> <span class="p">(</span><span class="n">i</span> <span class="kr">in</span> <span class="nf">seq_along</span><span class="p">(</span><span class="n">x</span><span class="p">))</span> <span class="n">x[[i]]</span> <span class="o">&lt;-</span> <span class="n">x[[i]]</span> <span class="o">+</span> <span class="m">1L</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Becomes:</span>
</span></span><span class="line"><span class="cl"><span class="kr">for</span> <span class="p">(</span><span class="n">i</span> <span class="kr">in</span> <span class="nf">seq_along</span><span class="p">(</span><span class="n">x</span><span class="p">))</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="n">x[[i]]</span> <span class="o">&lt;-</span> <span class="n">x[[i]]</span> <span class="o">+</span> <span class="m">1L</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="kr">function</span><span class="p">(</span><span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="nf">call_that_spans_lines</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">x</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">y</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">fixed_option</span> <span class="o">=</span> <span class="kc">FALSE</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"># Becomes:</span>
</span></span><span class="line"><span class="cl"><span class="kr">function</span><span class="p">(</span><span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nf">call_that_spans_lines</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">x</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">y</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">fixed_option</span> <span class="o">=</span> <span class="kc">FALSE</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>It&rsquo;s particularly important to autobrace multiline if statements for <em>portability</em>, which we roughly define as the ability to copy and paste that if statement into any context and have it still parse correctly. Consider the following if statement:</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="n">do_something</span> <span class="o">&lt;-</span> <span class="kr">function</span><span class="p">(</span><span class="n">this</span> <span class="o">=</span> <span class="kc">TRUE</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="kr">if</span> <span class="p">(</span><span class="n">this</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="nf">do_this</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">  <span class="kr">else</span> 
</span></span><span class="line"><span class="cl">    <span class="nf">do_that</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>As written, this is correct R code, but if you were to pull out the if statement and place it in a file at &ldquo;top level&rdquo; and try to run it, you&rsquo;d see a parse error:</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="kr">if</span> <span class="p">(</span><span class="n">this</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="nf">do_this</span><span class="p">()</span>
</span></span><span class="line"><span class="cl"><span class="kr">else</span> 
</span></span><span class="line"><span class="cl">  <span class="nf">do_that</span><span class="p">()</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; Error: unexpected &#39;else&#39;</span></span></span></code></pre></td></tr></table>
</div>
</div></div>
<p>In practice, this typically bites you when you&rsquo;re debugging and you send a chunk of lines to the console:</p>
<video controls autoplay loop muted width="100%" src="https://opensource.posit.co/blog/2025-06-11_air-0-7-0/video/portable-if-statement.mov" style="border: 2px solid #CCC;">
</video>
<p>Air autobraces this if statement to the following, which has no issues with portability:</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="n">do_something</span> <span class="o">&lt;-</span> <span class="kr">function</span><span class="p">(</span><span class="n">this</span> <span class="o">=</span> <span class="kc">TRUE</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="kr">if</span> <span class="p">(</span><span class="n">this</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nf">do_this</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span> <span class="kr">else</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nf">do_that</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></code></pre></td></tr></table>
</div>
</div></div>
<h3 id="give-side-effects-some-air">Give side effects some Air
</h3>
<p>We believe code that create <em>side effects</em> which modify state or affect control flow are important enough to live on their own line. For example, the following 






<a href="https://rdrr.io/r/base/stop.html" target="_blank" rel="noopener"><code>stop()</code></a>
 call is an example of a side effect, so it moves to its own line and is autobraced:</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="kr">if</span> <span class="p">(</span><span class="nf">anyNA</span><span class="p">(</span><span class="n">x</span><span class="p">))</span> <span class="nf">stop</span><span class="p">(</span><span class="s">&#34;`x` can&#39;t contain missing values.&#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"># Becomes:</span>
</span></span><span class="line"><span class="cl"><span class="kr">if</span> <span class="p">(</span><span class="nf">anyNA</span><span class="p">(</span><span class="n">x</span><span class="p">))</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nf">stop</span><span class="p">(</span><span class="s">&#34;`x` can&#39;t contain missing values.&#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>
<p>You might be thinking, &ldquo;But I like my single line if statements!&rdquo; We do too! Air still allows single line if statements if they look to be used for their <em>value</em> rather than for their <em>side effect</em>. These single line if statements are still allowed:</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">x</span> <span class="o">&lt;-</span> <span class="kr">if</span> <span class="p">(</span><span class="n">condition</span><span class="p">)</span> <span class="n">this</span> <span class="kr">else</span> <span class="n">that</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">x</span> <span class="o">&lt;-</span> <span class="n">x</span> <span class="o">%||%</span> <span class="kr">if</span> <span class="p">(</span><span class="n">condition</span><span class="p">)</span> <span class="n">this</span> <span class="kr">else</span> <span class="n">that</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">list</span><span class="p">(</span><span class="n">a</span> <span class="o">=</span> <span class="kr">if</span> <span class="p">(</span><span class="n">condition</span><span class="p">)</span> <span class="n">this</span> <span class="kr">else</span> <span class="n">that</span><span class="p">)</span></span></span></code></pre></td></tr></table>
</div>
</div></div>
<p>Similarly, single line function definitions are also still allowed if they don&rsquo;t already have braces and don&rsquo;t exceed the line length:</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="n">add_one</span> <span class="o">&lt;-</span> <span class="kr">function</span><span class="p">(</span><span class="n">x</span><span class="p">)</span> <span class="n">x</span> <span class="o">+</span> <span class="m">1</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">bools</span> <span class="o">&lt;-</span> <span class="nf">map_lgl</span><span class="p">(</span><span class="n">xs</span><span class="p">,</span> <span class="kr">function</span><span class="p">(</span><span class="n">x</span><span class="p">)</span> <span class="nf">is.logical</span><span class="p">(</span><span class="n">x</span><span class="p">)</span> <span class="o">&amp;&amp;</span> <span class="nf">length</span><span class="p">(</span><span class="n">x</span><span class="p">)</span> <span class="o">==</span> <span class="m">1L</span> <span class="o">&amp;&amp;</span> <span class="o">!</span><span class="nf">is.na</span><span class="p">(</span><span class="n">x</span><span class="p">))</span></span></span></code></pre></td></tr></table>
</div>
</div></div>
<p>For the full set of rules, check out our 


  
  
  





<a href="https://posit-dev.github.io/air/formatter.html#autobracing" target="_blank" rel="noopener">documentation on autobracing</a>
.</p>
<h2 id="empty-braces">Empty braces
</h2>
<p>You may have noticed the following forced expansion of empty 






<a href="https://rdrr.io/r/base/Paren.html" target="_blank" rel="noopener"><code>{}</code></a>
 in previous versions of Air:</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></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">dummy</span> <span class="o">&lt;-</span> <span class="kr">function</span><span class="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"># Previously became:</span>
</span></span><span class="line"><span class="cl"><span class="n">dummy</span> <span class="o">&lt;-</span> <span class="kr">function</span><span class="p">()</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">tryCatch</span><span class="p">(</span><span class="n">fn</span><span class="p">,</span> <span class="n">error</span> <span class="o">=</span> <span class="kr">function</span><span class="p">(</span><span class="n">e</span><span class="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"># Previously became:</span>
</span></span><span class="line"><span class="cl"><span class="nf">tryCatch</span><span class="p">(</span><span class="n">fn</span><span class="p">,</span> <span class="n">error</span> <span class="o">=</span> <span class="kr">function</span><span class="p">(</span><span class="n">e</span><span class="p">)</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">my_fn</span><span class="p">(</span><span class="n">expr</span> <span class="o">=</span> <span class="p">{},</span> <span class="n">option</span> <span class="o">=</span> <span class="kc">TRUE</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Previously became:</span>
</span></span><span class="line"><span class="cl"><span class="nf">my_fn</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="n">expr</span> <span class="o">=</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">option</span> <span class="o">=</span> <span class="kc">TRUE</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span></span></span></code></pre></td></tr></table>
</div>
</div></div>
<p>As of 0.7.0, empty braces 






<a href="https://rdrr.io/r/base/Paren.html" target="_blank" rel="noopener"><code>{}</code></a>
 are now never expanded, which retains the original form of each of these examples.</p>
<h2 id="skip-configuration"><code>skip</code> configuration
</h2>
<p>In 


  
  
  





  
  

<a href="https://opensource.posit.co/blog/2025-02-21_air/#how-can-i-disable-formatting">our release post</a>
, we detailed how to disable formatting using a <code># fmt: skip</code> comment for a single expression, or a <code># fmt: skip file</code> comment for an entire file. Skip comments are useful for disabling formatting for one-off function calls, but sometimes you may find yourself repeatedly using functions from a domain specific language (DSL) that doesn&rsquo;t follow conventional formatting rules. For example, the igraph package contains a DSL for constructing a graph from a literal representation:</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">igraph</span><span class="o">::</span><span class="nf">graph_from_literal</span><span class="p">(</span><span class="n">A</span> <span class="o">+-+</span> <span class="n">B</span> <span class="o">+---+</span> <span class="n">C</span> <span class="o">++</span> <span class="n">D</span> <span class="o">+</span> <span class="n">E</span><span class="p">)</span></span></span></code></pre></td></tr></table>
</div>
</div></div>
<p>By default, Air would format this as:</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">igraph</span><span class="o">::</span><span class="nf">graph_from_literal</span><span class="p">(</span><span class="n">A</span> <span class="o">+</span> <span class="o">-+</span><span class="n">B</span> <span class="o">+</span> <span class="o">---+</span><span class="n">C</span> <span class="o">+</span> <span class="o">+</span><span class="n">D</span> <span class="o">+</span> <span class="n">E</span><span class="p">)</span></span></span></code></pre></td></tr></table>
</div>
</div></div>
<p>If you use <code>graph_from_literal()</code> often, it would be annoying to add <code># fmt: skip</code> comments at every call site. Instead, <code>air.toml</code> now supports a <code>skip</code> field that allows you to specify function names that you never want formatting for. Specifying this would retain the original formatting of the <code>graph_from_literal()</code> call, even without a <code># fmt: skip</code> comment:</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-toml" data-lang="toml"><span class="line"><span class="cl"><span class="nx">skip</span> <span class="p">=</span> <span class="p">[</span><span class="s2">&#34;graph_from_literal&#34;</span><span class="p">]</span></span></span></code></pre></td></tr></table>
</div>
</div></div>
<p>In the short term, you may also want to use this for 






<a href="https://tibble.tidyverse.org/reference/tribble.html" target="_blank" rel="noopener"><code>tibble::tribble()</code></a>
 calls, i.e. <code>skip = [&quot;tribble&quot;]</code>. In the long term, we&rsquo;re hoping to provide more sophisticated tooling for formatting using a 






<a href="https://github.com/posit-dev/air/issues/113" target="_blank" rel="noopener">specified alignment</a>
.</p>
<h2 id="github-action">GitHub Action
</h2>
<p>Air now has an official GitHub Action, 






<a href="https://github.com/posit-dev/setup-air" target="_blank" rel="noopener"><code>setup-air</code></a>
. This action really only has one job - to get Air installed on your GitHub runner and put on the <code>PATH</code>. The basic usage is:</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">name</span><span class="p">:</span><span class="w"> </span><span class="l">Install Air</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">posit-dev/setup-air@v1</span></span></span></code></pre></td></tr></table>
</div>
</div></div>
<p>If you need to pin a version:</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">name</span><span class="p">:</span><span class="w"> </span><span class="l">Install Air 0.4.4</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">posit-dev/setup-air@v1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">with</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">version</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;0.4.4&#34;</span></span></span></code></pre></td></tr></table>
</div>
</div></div>
<p>From there, you can call Air&rsquo;s CLI in downstream steps. A minimal workflow that errors if any files require formatting might look like:</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-yaml" data-lang="yaml"><span class="line"><span class="cl">- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Install Air</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">posit-dev/setup-air@v1</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">name</span><span class="p">:</span><span class="w"> </span><span class="l">Check formatting</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">run</span><span class="p">:</span><span class="w"> </span><span class="l">air format . --check</span></span></span></code></pre></td></tr></table>
</div>
</div></div>
<p>Rather than creating the workflow file yourself, we instead recommend using usethis to pull in our 






<a href="https://github.com/posit-dev/setup-air/blob/main/examples/format-suggest.yaml" target="_blank" rel="noopener">example workflow</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></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">usethis</span><span class="o">::</span><span class="nf">use_github_action</span><span class="p">(</span><span class="n">url</span> <span class="o">=</span> <span class="s">&#34;https://github.com/posit-dev/setup-air/blob/main/examples/format-suggest.yaml&#34;</span><span class="p">)</span></span></span></code></pre></td></tr></table>
</div>
</div></div>
<p>This is a special workflow that runs on pull requests. It calls <code>air format</code> and then uses 






<a href="https://github.com/reviewdog/action-suggester" target="_blank" rel="noopener"><code>reviewdog/action-suggester</code></a>
 to push any formatting diffs as GitHub Suggestion comments on your pull request. It looks like this:</p>
<p><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://opensource.posit.co/blog/2025-06-11_air-0-7-0/figs/format-suggest-example.png"
      alt="" 
      loading="lazy"
    >
  </figure></div>
</p>
<p>You can accept all suggestions in a single batch, which will then rerun the format check, along with any other GitHub workflows (like an R package check), so you can feel confident that accepting the changes hasn&rsquo;t broken anything.</p>
<p>We like this workflow because it provides an easy way for external contributors who aren&rsquo;t using Air to still abide by your formatting rules. The external contributor can even accept the suggestions themselves, so by the time you look at their pull request it&rsquo;s already good to go from a formatting perspective ✅!</p>
<h2 id="acknowledgements">Acknowledgements
</h2>
<p>A big thanks to the 49 users who helped make this release possible by finding bugs, discussing issues, contributing documentation, and writing code: 






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






<a href="https://github.com/yutannihilation" target="_blank" rel="noopener">@yutannihilation</a>
.</p>
]]></description>
      <enclosure url="https://opensource.posit.co/blog/2025-06-11_air-0-7-0/thumbnail-wd.jpg" length="182665" type="image/jpeg" />
    </item>
    <item>
      <title>Fonts in R</title>
      <link>https://opensource.posit.co/blog/2025-05-12_fonts-in-r/</link>
      <pubDate>Mon, 12 May 2025 00:00:00 +0000</pubDate>
      <guid>https://opensource.posit.co/blog/2025-05-12_fonts-in-r/</guid>
      <dc:creator>Thomas Lin Pedersen</dc:creator><description><![CDATA[<style type='text/css'>
pre {
  text-wrap: nowrap;
  overflow-x: scroll;
}
figure {
  margin-top: 2em;
}
figcaption {
  text-align: center;
  margin-top: 1em;
}
table {
  max-width: 99%
}
</style>
<!--
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>(An updated version of this blog post will be available at 






<a href="https://systemfonts.r-lib.org" target="_blank" rel="noopener">the systemfonts webpage</a>
)</p>
<p>The purpose of this document is to give you a thorough overview of fonts in R. However, for this to be possible, you&rsquo;ll first need a basic understanding of fonts in general. If you already have a thorough understanding of digital typography you can skip to 


  
  
  





<a href="#font-handling-in-r">the next section</a>
.</p>
<h2 id="digital-typography">Digital typography
</h2>
<p>Many books could be, and have been, written about the subject of typography. This blog post is not meant to be an exhaustive deep dive into all areas of this vast subject. Rather, it is meant to give you just enough understanding of core concepts and terminology to appreciate how it all plays into using fonts in R.</p>
<h3 id="typeface-or-font">Typeface or font?
</h3>
<p>There is a good chance that you, like 99% of world, use &ldquo;font&rdquo; as the term describing &ldquo;the look&rdquo; of the letters you type. You may, perhaps, have heard the term &ldquo;typeface&rdquo; as well and thought it synonymous. This is in fact slightly wrong, and a great deal of typography snobbery has been dealt out on that account (much like the distinction between packages and libraries in R). It is a rather inconsequential mix-up for the most part, especially because 99% of the population wouldn&rsquo;t bat an eye if you use them interchangeably. However, the distinction between the two serves as a good starting point to talk about other terms in digital typography as well as the nature of font files, so let&rsquo;s dive in.</p>
<p>When most people use the word &ldquo;font&rdquo; or &ldquo;font family&rdquo;, what they are actually describing is a typeface. A <strong>typeface</strong> is a style of lettering that forms a cohesive whole. As an example, consider the well-known &ldquo;Helvetica&rdquo; typeface. This name embraces many different weights (bold, normal, light) as well as slanted (italic) and upright. However, all of these variations are all as much Helvetica as the others - they are all part of the same typeface.</p>
<p>A <strong>font</strong> is a subset of a typeface, describing a particular variation of the typeface, i.e. the combination of weight, width, and slant that comes together to describe the specific subset of a typeface that is used. We typically give a specific combination of these features a name, like &ldquo;bold&rdquo; or &ldquo;medium&rdquo; or &ldquo;italic&rdquo;, which we call the <strong>font style</strong><sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup>. In other words, a font is a particularly style within a typeface.</p>
<div class="highlight">
<div class="figure" style="text-align: center">
<img src="https://opensource.posit.co/blog/2025-05-12_fonts-in-r/figs/unnamed-chunk-2-1.png" alt="Different fonts from the Avenir Next typeface" width="700px" />
<p class="caption">
Different fonts from the Avenir Next typeface
</p>
</div>
</div>
<p>In the rest of this document we will use the terms typeface and font with the meaning described above.</p>
<h3 id="font-files">Font files
</h3>
<p>Next, we need to talk about how typefaces are represented for use by computers. Font files record information on how to draw the individual glyphs (characters), but also instructions about how to draw sequences of glyphs like distance adjustments (kerning) and substitution rules (ligatures). Font files typically encode a single font but can encode a full typeface:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nv'>typefaces</span> <span class='o'>&lt;-</span> <span class='nf'>systemfonts</span><span class='nf'>::</span><span class='nf'><a href='https://systemfonts.r-lib.org/reference/system_fonts.html'>system_fonts</a></span><span class='o'>(</span><span class='o'>)</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'>"path"</span>, <span class='s'>"index"</span>, <span class='s'>"family"</span>, <span class='s'>"style"</span><span class='o'>)</span><span class='o'>]</span></span>
<span></span>
<span><span class='c'># Full typeface in one file</span></span>
<span><span class='nv'>typefaces</span><span class='o'>[</span><span class='nv'>typefaces</span><span class='o'>$</span><span class='nv'>family</span> <span class='o'>==</span> <span class='s'>"Helvetica"</span>, <span class='o'>]</span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'># A tibble: 6 × 4</span></span></span>
<span><span class='c'>#&gt;   path                                index family    style        </span></span>
<span><span class='c'>#&gt;   <span style='color: #555555; font-style: italic;'>&lt;chr&gt;</span>                               <span style='color: #555555; font-style: italic;'>&lt;int&gt;</span> <span style='color: #555555; font-style: italic;'>&lt;chr&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> /System/Library/Fonts/Helvetica.ttc     2 Helvetica Oblique      </span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>2</span> /System/Library/Fonts/Helvetica.ttc     4 Helvetica Light        </span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>3</span> /System/Library/Fonts/Helvetica.ttc     5 Helvetica Light Oblique</span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>4</span> /System/Library/Fonts/Helvetica.ttc     1 Helvetica Bold         </span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>5</span> /System/Library/Fonts/Helvetica.ttc     3 Helvetica Bold Oblique </span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>6</span> /System/Library/Fonts/Helvetica.ttc     0 Helvetica Regular</span></span>
<span></span><span></span>
<span><span class='c'># One font per font file</span></span>
<span><span class='nv'>typefaces</span><span class='o'>[</span><span class='nv'>typefaces</span><span class='o'>$</span><span class='nv'>family</span> <span class='o'>==</span> <span class='s'>"Arial"</span>, <span class='o'>]</span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'># A tibble: 4 × 4</span></span></span>
<span><span class='c'>#&gt;   path                                                     index family style      </span></span>
<span><span class='c'>#&gt;   <span style='color: #555555; font-style: italic;'>&lt;chr&gt;</span>                                                    <span style='color: #555555; font-style: italic;'>&lt;int&gt;</span> <span style='color: #555555; font-style: italic;'>&lt;chr&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> /System/Library/Fonts/Supplemental/Arial.ttf                 0 Arial  Regular    </span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>2</span> /System/Library/Fonts/Supplemental/Arial Bold.ttf            0 Arial  Bold       </span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>3</span> /System/Library/Fonts/Supplemental/Arial Bold Italic.ttf     0 Arial  Bold Italic</span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>4</span> /System/Library/Fonts/Supplemental/Arial Italic.ttf          0 Arial  Italic</span></span>
<span></span></code></pre>
</div>
<p>Here, each row is a font, with <strong>family</strong> giving the name of the typeface, and <strong>style</strong> the font style.</p>
<p>It took a considerable number of tries before the world managed to nail the digitial representation of fonts, leading to a proliferation of file types. As an R user, there are three formats that are particularly improtant:</p>
<ul>
<li>
<p><strong>TrueType</strong> (ttf/ttc). Truetype is the baseline format that all modern formats stand on top of. It was developed by Apple in the &rsquo;80s and became popular due to its great balance between size and quality. Fonts can be encoded, either as scalable paths, or as bitmaps of various sizes, the former generally being preferred as it allows for seamless scaling and small file size at the same time.</p>
</li>
<li>
<p><strong>OpenType</strong> (otf/otc). OpenType was created by Microsoft and Adobe to improve upon TrueType. While TrueType was a great success, the number of glyphs it could contain was limited and so was its support for selecting different features during 


  
  
  





<a href="#text-shaping">shaping</a>
. OpenType resolved these issues, so if you want access to advanced typography features you&rsquo;ll need a font in OpenType format.</p>
</li>
<li>
<p><strong>Web Open Font Format</strong> (woff/woff2). TrueType and OpenType tend to create large files. Since a large percentage of the text consumed today is delivered over the internet this creates a problem. WOFF resolves this problem by acting as a compression wrapper around TrueType/OpenType to reduce file sizes while also limiting the number of advanced features provided to those relevant to web fonts. The woff2 format is basically identical to woff except it uses the more efficient 






<a href="https://en.wikipedia.org/wiki/Brotli" target="_blank" rel="noopener">brotli</a>
 compression algorithm. WOFF was designed specifically to be delivered over the internet and support is still a bit limited outside of browsers.</p>
</li>
</ul>
<p>While we have mainly talked about font files as containers for the shape of glyphs, they also carries a lot of other information needed for rendering text in a way pleasant for reading. Font level information records a lot of stylistic information about typeface/font, statistics on the number of glyphs and how many different mappings between character encodings and glyphs it contains, and overall sizing information such as the maximum descend of the font, the position of an underline relative to the baseline etc. systemfonts provdies a convenient way to access this data from R:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nf'>dplyr</span><span class='nf'>::</span><span class='nf'><a href='https://pillar.r-lib.org/reference/glimpse.html'>glimpse</a></span><span class='o'>(</span><span class='nf'>systemfonts</span><span class='nf'>::</span><span class='nf'><a href='https://systemfonts.r-lib.org/reference/font_info.html'>font_info</a></span><span class='o'>(</span>family <span class='o'>=</span> <span class='s'>"Helvetica"</span><span class='o'>)</span><span class='o'>)</span></span>
<span><span class='c'>#&gt; Rows: 1</span></span>
<span><span class='c'>#&gt; Columns: 24</span></span>
<span><span class='c'>#&gt; $ path               <span style='color: #555555; font-style: italic;'>&lt;chr&gt;</span> "/System/Library/Fonts/Helvetica.ttc"</span></span>
<span><span class='c'>#&gt; $ index              <span style='color: #555555; font-style: italic;'>&lt;int&gt;</span> 0</span></span>
<span><span class='c'>#&gt; $ family             <span style='color: #555555; font-style: italic;'>&lt;chr&gt;</span> "Helvetica"</span></span>
<span><span class='c'>#&gt; $ style              <span style='color: #555555; font-style: italic;'>&lt;chr&gt;</span> "Regular"</span></span>
<span><span class='c'>#&gt; $ italic             <span style='color: #555555; font-style: italic;'>&lt;lgl&gt;</span> FALSE</span></span>
<span><span class='c'>#&gt; $ bold               <span style='color: #555555; font-style: italic;'>&lt;lgl&gt;</span> FALSE</span></span>
<span><span class='c'>#&gt; $ monospace          <span style='color: #555555; font-style: italic;'>&lt;lgl&gt;</span> FALSE</span></span>
<span><span class='c'>#&gt; $ weight             <span style='color: #555555; font-style: italic;'>&lt;ord&gt;</span> normal</span></span>
<span><span class='c'>#&gt; $ width              <span style='color: #555555; font-style: italic;'>&lt;ord&gt;</span> normal</span></span>
<span><span class='c'>#&gt; $ kerning            <span style='color: #555555; font-style: italic;'>&lt;lgl&gt;</span> FALSE</span></span>
<span><span class='c'>#&gt; $ color              <span style='color: #555555; font-style: italic;'>&lt;lgl&gt;</span> FALSE</span></span>
<span><span class='c'>#&gt; $ scalable           <span style='color: #555555; font-style: italic;'>&lt;lgl&gt;</span> TRUE</span></span>
<span><span class='c'>#&gt; $ vertical           <span style='color: #555555; font-style: italic;'>&lt;lgl&gt;</span> FALSE</span></span>
<span><span class='c'>#&gt; $ n_glyphs           <span style='color: #555555; font-style: italic;'>&lt;int&gt;</span> 2252</span></span>
<span><span class='c'>#&gt; $ n_sizes            <span style='color: #555555; font-style: italic;'>&lt;int&gt;</span> 0</span></span>
<span><span class='c'>#&gt; $ n_charmaps         <span style='color: #555555; font-style: italic;'>&lt;int&gt;</span> 10</span></span>
<span><span class='c'>#&gt; $ bbox               <span style='color: #555555; font-style: italic;'>&lt;list&gt;</span> &lt;-11.406250, 17.343750, -5.765625, 13.453125&gt;</span></span>
<span><span class='c'>#&gt; $ max_ascend         <span style='color: #555555; font-style: italic;'>&lt;dbl&gt;</span> 9.234375</span></span>
<span><span class='c'>#&gt; $ max_descend        <span style='color: #555555; font-style: italic;'>&lt;dbl&gt;</span> -2.765625</span></span>
<span><span class='c'>#&gt; $ max_advance_width  <span style='color: #555555; font-style: italic;'>&lt;dbl&gt;</span> 18</span></span>
<span><span class='c'>#&gt; $ max_advance_height <span style='color: #555555; font-style: italic;'>&lt;dbl&gt;</span> 12</span></span>
<span><span class='c'>#&gt; $ lineheight         <span style='color: #555555; font-style: italic;'>&lt;dbl&gt;</span> 12</span></span>
<span><span class='c'>#&gt; $ underline_pos      <span style='color: #555555; font-style: italic;'>&lt;dbl&gt;</span> -1.203125</span></span>
<span><span class='c'>#&gt; $ underline_size     <span style='color: #555555; font-style: italic;'>&lt;dbl&gt;</span> 0.59375</span></span>
<span></span></code></pre>
</div>
<p>Further, for each glyph there is a range of information in addition to its shape:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nf'>systemfonts</span><span class='nf'>::</span><span class='nf'><a href='https://systemfonts.r-lib.org/reference/glyph_info.html'>glyph_info</a></span><span class='o'>(</span><span class='s'>"j"</span>, family <span class='o'>=</span> <span class='s'>"Helvetica"</span>, size <span class='o'>=</span> <span class='m'>30</span><span class='o'>)</span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'># A tibble: 1 × 9</span></span></span>
<span><span class='c'>#&gt;   glyph index width height x_bearing y_bearing x_advance y_advance bbox     </span></span>
<span><span class='c'>#&gt;   <span style='color: #555555; font-style: italic;'>&lt;chr&gt;</span> <span style='color: #555555; font-style: italic;'>&lt;int&gt;</span> <span style='color: #555555; font-style: italic;'>&lt;dbl&gt;</span>  <span style='color: #555555; font-style: italic;'>&lt;dbl&gt;</span>     <span style='color: #555555; font-style: italic;'>&lt;dbl&gt;</span>     <span style='color: #555555; font-style: italic;'>&lt;dbl&gt;</span>     <span style='color: #555555; font-style: italic;'>&lt;dbl&gt;</span>     <span style='color: #555555; font-style: italic;'>&lt;dbl&gt;</span> <span style='color: #555555; font-style: italic;'>&lt;list&gt;</span>   </span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>1</span> j        77     6     27        -<span style='color: #BB0000;'>1</span>        21         7         0 <span style='color: #555555;'>&lt;dbl [4]&gt;</span></span></span>
<span></span></code></pre>
</div>
<p>These terms are more easily understood with a diagram:</p>
<div class="highlight">
<img src="https://opensource.posit.co/blog/2025-05-12_fonts-in-r/figs/unnamed-chunk-6-1.png" width="700px" style="display: block; margin: auto;" />
</div>
<p>The <code>x_advance</code> in particular is important when rendering text because it tells you how far to move to the right before rendering the next glyph (ignoring for a bit the concept of kerning)</p>
<h3 id="text-shaping">Text shaping
</h3>
<p>The next important concept to understand is <strong>text shaping</strong>, which, in the simplest of terms, is to convert a succession of characters into a sequence of glyphs along with their locations. Important here is the distinction between <strong>characters</strong>, the things you think of as letters, and <strong>glyphs</strong>, which is what the font will draw. For example, think of the character &ldquo;f&rdquo;, which is often tricky to draw because the &ldquo;hook&rdquo; of the f can interfere with other characters. To solve this problem, many typefaces include <strong>ligatures</strong>, like &ldquo;ﬁ&rdquo;, which are used for specific pairs of characaters. Ligatures are extremely important for languages like Arabic.</p>
<p>A few of the challenges of text shaping include kerning, bidirectional text, and font substitution. <strong>Kerning</strong> is the adjustment of distance between specific pairs of characters. For example, you can put &ldquo;VM&rdquo; a little closer together but &ldquo;OO&rdquo; needs to be a little further apart. Kerning is an integral part of all modern text rendering and you will almost solemnly notice it when it is absent (or worse, 






<a href="https://www.fastcompany.com/91324550/kerning-on-pope-francis-tomb-is-a-travesty" target="_blank" rel="noopener">wrongly applied</a>
).</p>
<p>Not every language writes text in the same direction, but regardless of your native script, you are likely to use arabic numerals which are always written left-to-right. This gives rise to the challenge of <strong>bidirectional</strong> (or bidi) text, which mixes text flowing in different directions. This imposes a whole new range of challenges!</p>
<p>Finally, you might request a character that a font doesn&rsquo;t contain. One way to deal with this is to render a glyph representing a missing glyph, usually an empty box or a question mark. But it&rsquo;s typically more useful to use the correct glyph from a different font. This is called <strong>font fallback</strong> and happens all the time for emojis, but can also happen when you suddenly change script without bothering to pick a new font. Font fallback is an imprecise science, typically relying on an operating system font that has a very large number of characters, but might look very different from your existing font.</p>
<p>Once you have determined the order and location of glyphs, you are still not done. Text often needs to be wrapped to fit into a specific width, it may need a specific justification, perhaps, indentation or tracking must be applied, etc. Thankfully, all of this is generally a matter of (often gnarly) math that you just have to get right. That is, all except text wrapping which should happen at the right boundaries, and may need to break up a word and inserting a hyphen etc.</p>
<p>Like I said, the pit of despair is bottomless&hellip;</p>
<h2 id="font-handling-in-r">Font handling in R
</h2>
<p>You hopefully arrive at this section with an appreciation of the horrors that goes into rendering text. If not, maybe this 






<a href="https://faultlore.com/blah/text-hates-you/" target="_blank" rel="noopener">blog post</a>
 will convince you.</p>
<p>Are you still here? Good.</p>
<p>Now that you understand the basics of what goes into handling fonts and text, we can now discuss the details of fonts in R specifically.</p>
<h3 id="fonts-and-text-from-a-user-perspective">Fonts and text from a user perspective
</h3>
<p>The users perception of working with fonts in R is largely shaped by plots. This means using either base or grid graphics or one of the packages that have been build on top of it, like 






<a href="https://ggplot2.tidyverse.org" target="_blank" rel="noopener">ggplot2</a>
. While the choice of tool will affect <em>where</em> you specify the font to use, they generally agree on how to specify it.</p>
<table style="width:99%;">
<colgroup>
<col style="width: 37%" />
<col style="width: 5%" />
<col style="width: 18%" />
<col style="width: 37%" />
</colgroup>
<thead>
<tr>
<th>Graphic system</th>
<th>Argument</th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
<tr>
<td></td>
<td><em>Typeface</em></td>
<td><em>Font</em></td>
<td><em>Size</em></td>
</tr>
<tr>
<td><p><strong>Base</strong></p>
<p><em>Arguments are passed to <code>par()</code> to set globally or directly to the call that renders text (e.g. <code>text()</code>)</em></p></td>
<td><code>family</code></td>
<td><code>font</code></td>
<td><code>cra</code> (pixels) or <code>cin</code> (inches) multiplied by <code>cex</code></td>
</tr>
<tr>
<td><p><strong>Grid</strong></p>
<p>Arguments are passed to the <code>gp</code> argument of relevant grobs using the <code>gpar()</code> constructor</p></td>
<td><code>fontfamily</code></td>
<td><code>fontface</code></td>
<td><code>fontsize</code> (points) multiplied by <code>cex</code></td>
</tr>
<tr>
<td><p><strong>ggplot2</strong></p>
<p>Arguments are set in <code>element_text()</code> to alter theme fonts or directly in the geom call to alter geom fonts</p></td>
<td><code>family</code></td>
<td><code>face</code> (in <code>element_text()</code>) or <code>fontface</code> (in geoms)</td>
<td><code>size</code> (points when used in <code>element_text()</code>, depends on the value of <code>size.unit</code> argument when used in geom)</td>
</tr>
</tbody>
</table>
<p>From the table it is clear that in R <code>fontfamily</code>/<code>family</code> is used to describe the typeface and <code>font</code>/<code>fontface</code>/<code>face</code> is used to select a font from the typeface. Size settings is just a plain mess.</p>
<p>The major limitation in <code>fontface</code> (and friends) is that it takes a number, not a string, and you can only select from four options: <code>1</code>: plain, <code>2</code>: bold, <code>3</code>: italic, and <code>4</code>: bold-italic. This means, for example, that there&rsquo;s no way to select Futura Condensed Extra Bold. Another limitation is that it&rsquo;s not possible to specify any font variations such as using tabular numbers or stylistic ligatures.</p>
<h3 id="fonts-and-text-from-a-graphics-device-perspective">Fonts and text from a graphics device perspective
</h3>
<p>In R, a graphics device is the part responsible for doing the rendering you request and put it on your screen or in a file. When you call 






<a href="https://rdrr.io/r/grDevices/png.html" target="_blank" rel="noopener"><code>png()</code></a>
 or 






<a href="https://ragg.r-lib.org/reference/agg_png.html" target="_blank" rel="noopener"><code>ragg::agg_png()</code></a>
 you open up a graphics device that will receive all the plotting instructions from R. Both graphics devices will ultimately produce the same file type (PNG), but how they choose to handle and respond to the plotting instructions may differ (greatly). Nowhere is this difference more true than when it comes to text rendering.</p>
<p>After a user has made a call that renders some text, it is funneled through the graphic system (base or grid), handed off to the graphics engine, which ultimately asks the graphics device to render the text. From the perspective of the graphics device it is much the same information that the user provided which are presented to it. The 






<a href="https://rdrr.io/r/graphics/text.html" target="_blank" rel="noopener"><code>text()</code></a>
 method of the device are given an array of characters, the typeface, the size in points, and an integer denoting if the style is regular, bold, italic, or bold-italic.</p>
<figure>
<img src="https://opensource.posit.co/blog/2025-05-12_fonts-in-r/text_call_flow.svg" data-fig-alt="A diagram showing the flow of text rendering instructions from ggplot2, grid, the graphics engine, and down to the graphics device. Very little changes in the available information about the font during the flow" alt="Flow of font information through the R rendering stack" />
<figcaption aria-hidden="true">Flow of font information through the R rendering stack</figcaption>
</figure>
<p>This means that it is up to the graphics device to find the approprate font file (using the provided typeface and font style) and shape the text with all that that entails. This is a lot of work, which is why text is handled so inconsistently between graphics devices. Issues can range from not being able to find fonts installed on the computer, to not providing font fallback mechanisms, or even handling right-to-left text. It may also be that certain font file formats are not well supported so that e.g. color emojis are not rendered correctly.</p>
<p>There have been a number of efforts to resolve these problems over the years:</p>
<ul>
<li>
<p><strong>extrafont</strong>: Developed by Winston Chang, 






<a href="https://github.com/wch/extrafont" target="_blank" rel="noopener">extrafont</a>
 sought to mainly improve the situation for the 






<a href="https://rdrr.io/r/grDevices/pdf.html" target="_blank" rel="noopener"><code>pdf()</code></a>
 device which generally only had access to the postscript fonts that comes with R. The package allows the 






<a href="https://rdrr.io/r/grDevices/pdf.html" target="_blank" rel="noopener"><code>pdf()</code></a>
 device to get access to TrueType fonts installed on the computer, as well as provide means for embedding the font into the PDF so that it can be opened on systems where the font is not installed. (It also provides the capabilities to the Windows 






<a href="https://rdrr.io/r/grDevices/png.html" target="_blank" rel="noopener"><code>png()</code></a>
 device).</p>
</li>
<li>
<p><strong>sysfonts</strong> and <strong>showtext</strong>. These packages are developed by Yixuan Qiu and provide support for system fonts to all graphics devices, by hijacking the 






<a href="https://rdrr.io/r/graphics/text.html" target="_blank" rel="noopener"><code>text()</code></a>
 method of the graphics device to treat text as polygons or raster images. This guarantees your plots will look the same on every device, but it doesn&rsquo;t do advanced text shaping, so there&rsquo;s no support for ligatures or font substitution. Additionally, it produces large files with inaccessible text when used to produce pdf and svg outputs.</p>
</li>
<li>
<p><strong>systemfonts</strong> and <strong>textshaping</strong>. These packages are developed by me to provide a soup-to-nuts solution to text rendering for graphics devices. 






<a href="https://systemfonts.r-lib.org" target="_blank" rel="noopener">systemfonts</a>
 provides access to fonts installed on the system along with font fallback mechanisms, registration of non-system fonts, reading of font files etc. 






<a href="https://github.com/r-lib/textshaping" target="_blank" rel="noopener">textshaping</a>
 builds on top of systemfonts and provides a fully modern engine for shaping text. The functionality is exposed both at the R level and at the C level, so that graphics devices can directly access to font lookup and shaping.</p>
</li>
</ul>
<p>We will fosus on systemfonts, because it&rsquo;s designed to give R a modern text rendering stack. That&rsquo;s unfortunately impossible without coordination with the graphics device, which means that to use all these features you need a supported graphics device. There are currently two options:</p>
<ul>
<li>The 






<a href="https://ragg.r-lib.org" target="_blank" rel="noopener">ragg</a>
 package provides graphics devices for rendering raster graphics in a variety of formats (PNG, JPEG, TIFF) and uses systemfonts and textshaping extensively.</li>
<li>The 






<a href="https://svglite.r-lib.org" target="_blank" rel="noopener">svglite</a>
 package provides a graphic device for rendering vector graphics to SVG using systemfonts and textshaping for text.</li>
</ul>
<p>You might notice there&rsquo;s currently a big hole in this workflow: PDFs. This is something we plan to work on in the future.</p>
<h2 id="a-systemfonts-based-workflow">A systemfonts based workflow
</h2>
<p>With all that said, how do you actually use systemfonts to use custom fonts in your plots? First, you&rsquo;ll need to use ragg or svglite.</p>
<h3 id="using-ragg">Using ragg
</h3>
<p>While there is no way to unilaterally make 






<a href="https://ragg.r-lib.org/reference/agg_png.html" target="_blank" rel="noopener"><code>ragg::agg_png()</code></a>
 the default everywhere, it&rsquo;s possible to get close:</p>
<ul>
<li>
<p>Positron: recent versions automatically use ragg for the plot pane if it&rsquo;s installed.</p>
</li>
<li>
<p>RStudio IDE: set &ldquo;AGG&rdquo; as the backend under Global Options &gt; General &gt; Graphics.</p>
</li>
<li>
<p>






<a href="https://ggplot2.tidyverse.org/reference/ggsave.html" target="_blank" rel="noopener"><code>ggplot2::ggsave()</code></a>
: ragg will be automatically used for raster output if installed.</p>
</li>
<li>
<p>R Markdown and Quarto: you need to set the <code>dev</code> option to <code>&quot;ragg_png&quot;</code>. You can either do this with 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></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">#| include: false</span>
</span></span><span class="line"><span class="cl"><span class="n">knitr</span><span class="o">::</span><span class="n">opts_chunk</span><span class="o">$</span><span class="nf">set</span><span class="p">(</span><span class="n">dev</span> <span class="o">=</span> <span class="s">&#34;ragg_png&#34;</span><span class="p">)</span></span></span></code></pre></td></tr></table>
</div>
</div></div>
<p>Or in Quarto, you can set it in the yaml metadata:</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-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nn">---</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">title</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;My Document&#34;</span><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">html</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">knitr</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">opts_chunk</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">dev</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;ragg_png&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nn">---</span></span></span></code></pre></td></tr></table>
</div>
</div></div>
</li>
</ul>
<p>If you want to use a font installed on your computer, you&rsquo;re done!</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nf'>grid</span><span class='nf'>::</span><span class='nf'><a href='https://rdrr.io/r/grid/grid.text.html'>grid.text</a></span><span class='o'>(</span></span>
<span>  <span class='s'>"FUTURA 🎉"</span>,</span>
<span>  gp <span class='o'>=</span> <span class='nf'>grid</span><span class='nf'>::</span><span class='nf'><a href='https://rdrr.io/r/grid/gpar.html'>gpar</a></span><span class='o'>(</span>fontfamily <span class='o'>=</span> <span class='s'>"Futura"</span>, fontface <span class='o'>=</span> <span class='m'>3</span>, fontsize <span class='o'>=</span> <span class='m'>30</span><span class='o'>)</span></span>
<span><span class='o'>)</span></span>
</code></pre>
<img src="https://opensource.posit.co/blog/2025-05-12_fonts-in-r/figs/unnamed-chunk-7-1.png" width="700px" style="display: block; margin: auto;" />
</div>
<p>Or, if using ggplot2</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nf'><a href='https://ggplot2.tidyverse.org/reference/ggplot.html'>ggplot</a></span><span class='o'>(</span><span class='nf'><a href='https://rdrr.io/r/stats/na.fail.html'>na.omit</a></span><span class='o'>(</span><span class='nv'>penguins</span><span class='o'>)</span><span class='o'>)</span> <span class='o'>+</span></span>
<span>  <span class='nf'><a href='https://ggplot2.tidyverse.org/reference/geom_point.html'>geom_point</a></span><span class='o'>(</span><span class='nf'><a href='https://ggplot2.tidyverse.org/reference/aes.html'>aes</a></span><span class='o'>(</span>x <span class='o'>=</span> <span class='nv'>bill_len</span>, y <span class='o'>=</span> <span class='nv'>body_mass</span>, colour <span class='o'>=</span> <span class='nv'>species</span><span class='o'>)</span><span class='o'>)</span> <span class='o'>+</span></span>
<span>  <span class='nf'><a href='https://ggplot2.tidyverse.org/reference/labs.html'>labs</a></span><span class='o'>(</span>x <span class='o'>=</span> <span class='s'>"Bill Length"</span>, y <span class='o'>=</span> <span class='s'>"Body Mass"</span>, colour <span class='o'>=</span> <span class='s'>"Species"</span><span class='o'>)</span> <span class='o'>+</span></span>
<span>  <span class='nf'><a href='https://ggplot2.tidyverse.org/reference/ggtheme.html'>theme_minimal</a></span><span class='o'>(</span>base_family <span class='o'>=</span> <span class='s'>"Futura"</span><span class='o'>)</span></span>
</code></pre>
<img src="https://opensource.posit.co/blog/2025-05-12_fonts-in-r/figs/unnamed-chunk-8-1.png" width="700px" style="display: block; margin: auto;" />
</div>
<p>If the results don&rsquo;t look as you expect, you can use various systemfonts helpers to diagnose the problem:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nf'>systemfonts</span><span class='nf'>::</span><span class='nf'><a href='https://systemfonts.r-lib.org/reference/match_fonts.html'>match_fonts</a></span><span class='o'>(</span><span class='s'>"Futura"</span>, weight <span class='o'>=</span> <span class='s'>"bold"</span><span class='o'>)</span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'># A tibble: 1 × 3</span></span></span>
<span><span class='c'>#&gt;   path                                          index features  </span></span>
<span><span class='c'>#&gt;   <span style='color: #555555; font-style: italic;'>&lt;chr&gt;</span>                                         <span style='color: #555555; font-style: italic;'>&lt;int&gt;</span> <span style='color: #555555; font-style: italic;'>&lt;list&gt;</span>    </span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>1</span> /System/Library/Fonts/Supplemental/Futura.ttc     2 <span style='color: #555555;'>&lt;font_ftr&gt;</span></span></span>
<span></span><span><span class='nf'>systemfonts</span><span class='nf'>::</span><span class='nf'><a href='https://systemfonts.r-lib.org/reference/font_fallback.html'>font_fallback</a></span><span class='o'>(</span><span class='s'>"🎉"</span>, family <span class='o'>=</span> <span class='s'>"Futura"</span>, weight <span class='o'>=</span> <span class='s'>"bold"</span><span class='o'>)</span></span>
<span><span class='c'>#&gt;                                          path index</span></span>
<span><span class='c'>#&gt; 1 /System/Library/Fonts/Apple Color Emoji.ttc     0</span></span>
<span></span></code></pre>
</div>
<p>If you want to see all the fonts that are available for use, you can use 






<a href="https://systemfonts.r-lib.org/reference/system_fonts.html" target="_blank" rel="noopener"><code>systemfonts::system_fonts()</code></a>
</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nf'>systemfonts</span><span class='nf'>::</span><span class='nf'><a href='https://systemfonts.r-lib.org/reference/system_fonts.html'>system_fonts</a></span><span class='o'>(</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='c'>#&gt; <span style='color: #555555;'># A tibble: 570 × 9</span></span></span>
<span><span class='c'>#&gt;    path                                         index name  family style weight width italic monospace</span></span>
<span><span class='c'>#&gt;    <span style='color: #555555; font-style: italic;'>&lt;chr&gt;</span>                                        <span style='color: #555555; font-style: italic;'>&lt;int&gt;</span> <span style='color: #555555; font-style: italic;'>&lt;chr&gt;</span> <span style='color: #555555; font-style: italic;'>&lt;chr&gt;</span>  <span style='color: #555555; font-style: italic;'>&lt;chr&gt;</span> <span style='color: #555555; font-style: italic;'>&lt;ord&gt;</span>  <span style='color: #555555; font-style: italic;'>&lt;ord&gt;</span> <span style='color: #555555; font-style: italic;'>&lt;lgl&gt;</span>  <span style='color: #555555; font-style: italic;'>&lt;lgl&gt;</span>    </span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'> 1</span> /System/Library/Fonts/Supplemental/Rockwell…     2 Rock… Rockw… Bold  bold   norm… FALSE  FALSE    </span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'> 2</span> /System/Library/Fonts/Noteworthy.ttc             0 Note… Notew… Light normal norm… FALSE  FALSE    </span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'> 3</span> /System/Library/Fonts/Supplemental/Devanaga…     1 Deva… Devan… Bold  bold   norm… FALSE  FALSE    </span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'> 4</span> /System/Library/Fonts/Supplemental/Kannada …     0 Kann… Kanna… Regu… normal norm… FALSE  FALSE    </span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'> 5</span> /System/Library/Fonts/Supplemental/Verdana …     0 Verd… Verda… Bold  bold   norm… FALSE  FALSE    </span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'> 6</span> /System/Library/Fonts/ArialHB.ttc                8 Aria… Arial… Light light  norm… FALSE  FALSE    </span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'> 7</span> /System/Library/Fonts/AppleSDGothicNeo.ttc      10 Appl… Apple… Thin  thin   norm… FALSE  FALSE    </span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'> 8</span> /System/Library/Fonts/Supplemental/DecoType…     0 Deco… DecoT… Regu… normal norm… FALSE  FALSE    </span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'> 9</span> /System/Library/Fonts/Supplemental/Trebuche…     0 Treb… Trebu… Ital… normal norm… TRUE   FALSE    </span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>10</span> /System/Library/Fonts/Supplemental/Khmer MN…     0 Khme… Khmer… Regu… normal norm… FALSE  FALSE    </span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'># ℹ 560 more rows</span></span></span>
<span></span></code></pre>
</div>
<h3 id="extra-font-styles">Extra font styles
</h3>
<p>As we discussed above, the R interface only allows you to select between four styles: plain, italic, bold, and bold-italic. If you want to use a thin font, you have no way of communicating this wish to the device. To overcome this, systemfonts provides <code>register_variant()</code> which allows you to register a font with a new typeface name. For example, to use the thin font from the Avenir Next typeface you can register it as follows:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nf'>systemfonts</span><span class='nf'>::</span><span class='nf'><a href='https://systemfonts.r-lib.org/reference/register_variant.html'>register_variant</a></span><span class='o'>(</span></span>
<span>  name <span class='o'>=</span> <span class='s'>"Avenir Thin"</span>,</span>
<span>  family <span class='o'>=</span> <span class='s'>"Avenir Next"</span>,</span>
<span>  weight <span class='o'>=</span> <span class='s'>"thin"</span></span>
<span><span class='o'>)</span></span></code></pre>
</div>
<p>Now you can use Avenir Thin where you would otherwise specify the typeface:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nf'>grid</span><span class='nf'>::</span><span class='nf'><a href='https://rdrr.io/r/grid/grid.text.html'>grid.text</a></span><span class='o'>(</span></span>
<span>  <span class='s'>"Thin weight is soo classy"</span>,</span>
<span>  gp <span class='o'>=</span> <span class='nf'>grid</span><span class='nf'>::</span><span class='nf'><a href='https://rdrr.io/r/grid/gpar.html'>gpar</a></span><span class='o'>(</span>fontfamily <span class='o'>=</span> <span class='s'>"Avenir Thin"</span>, fontsize <span class='o'>=</span> <span class='m'>30</span><span class='o'>)</span></span>
<span><span class='o'>)</span></span>
</code></pre>
<img src="https://opensource.posit.co/blog/2025-05-12_fonts-in-r/figs/unnamed-chunk-13-1.png" width="700px" style="display: block; margin: auto;" />
</div>
<p><code>register_variant()</code> also allows you to turn on font features otherwise hidden away:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nf'>systemfonts</span><span class='nf'>::</span><span class='nf'><a href='https://systemfonts.r-lib.org/reference/register_variant.html'>register_variant</a></span><span class='o'>(</span></span>
<span>  name <span class='o'>=</span> <span class='s'>"Avenir Small Caps"</span>,</span>
<span>  family <span class='o'>=</span> <span class='s'>"Avenir Next"</span>,</span>
<span>  features <span class='o'>=</span> <span class='nf'>systemfonts</span><span class='nf'>::</span><span class='nf'><a href='https://systemfonts.r-lib.org/reference/font_feature.html'>font_feature</a></span><span class='o'>(</span></span>
<span>    letters <span class='o'>=</span> <span class='s'>"small_caps"</span></span>
<span>  <span class='o'>)</span></span>
<span><span class='o'>)</span></span>
<span><span class='nf'>grid</span><span class='nf'>::</span><span class='nf'><a href='https://rdrr.io/r/grid/grid.text.html'>grid.text</a></span><span class='o'>(</span></span>
<span>  <span class='s'>"All caps — Small caps"</span>,</span>
<span>  gp <span class='o'>=</span> <span class='nf'>grid</span><span class='nf'>::</span><span class='nf'><a href='https://rdrr.io/r/grid/gpar.html'>gpar</a></span><span class='o'>(</span>fontfamily <span class='o'>=</span> <span class='s'>"Avenir Small Caps"</span>, fontsize <span class='o'>=</span> <span class='m'>30</span><span class='o'>)</span></span>
<span><span class='o'>)</span></span>
</code></pre>
<img src="https://opensource.posit.co/blog/2025-05-12_fonts-in-r/figs/unnamed-chunk-14-1.png" width="700px" style="display: block; margin: auto;" />
</div>
<h3 id="fonts-from-other-places">Fonts from other places
</h3>
<p>Historically, systemfonts primary role was to access the font installed on your computer, the <strong>system fonts</strong>. But what if you&rsquo;re using a computer where you don&rsquo;t have the rights to install new fonts, or you don&rsquo;t want the hassle of installing a font just to use it for a single plot? That&rsquo;s the problem solved by <code>systemfonts::add_font()</code> which makes it easy to use a font based on a path. But in many cases you don&rsquo;t even need that as systemfont now scans <code>./fonts</code> and <code>~/fonts</code> and adds any font files it find. This means that you can put personal fonts in a fonts folder in your home directory, and project fonts in a fonts directory at the root of the project. This is a great way to ensure that specific fonts are available when you deploy some code to a server.</p>
<p>And you don&rsquo;t even need to leave R to populate these folders. 






<a href="https://systemfonts.r-lib.org/reference/web-fonts.html" target="_blank" rel="noopener"><code>systemfonts::get_from_google_fonts()</code></a>
 will download and install a google font in <code>~/fonts</code>:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nf'>systemfonts</span><span class='nf'>::</span><span class='nf'><a href='https://systemfonts.r-lib.org/reference/web-fonts.html'>get_from_google_fonts</a></span><span class='o'>(</span><span class='s'>"Barrio"</span><span class='o'>)</span></span>
<span></span>
<span><span class='nf'>grid</span><span class='nf'>::</span><span class='nf'><a href='https://rdrr.io/r/grid/grid.text.html'>grid.text</a></span><span class='o'>(</span></span>
<span>  <span class='s'>"A new font a day keeps Tufte away"</span>,</span>
<span>  gp <span class='o'>=</span> <span class='nf'>grid</span><span class='nf'>::</span><span class='nf'><a href='https://rdrr.io/r/grid/gpar.html'>gpar</a></span><span class='o'>(</span>fontfamily <span class='o'>=</span> <span class='s'>"Barrio"</span>, fontsize <span class='o'>=</span> <span class='m'>30</span><span class='o'>)</span></span>
<span><span class='o'>)</span></span>
</code></pre>
<img src="https://opensource.posit.co/blog/2025-05-12_fonts-in-r/figs/unnamed-chunk-15-1.png" width="700px" style="display: block; margin: auto;" />
</div>
<p>And if you want to make sure this code works for anyone using your code (regardless of whether or not they already have the font installed), you can use 






<a href="https://systemfonts.r-lib.org/reference/require_font.html" target="_blank" rel="noopener"><code>systemfonts::require_font()</code></a>
. If the font isn&rsquo;t already installed, this function download it from one of the repositories it knows about. If it can&rsquo;t find it it will either throw an error (the default) or remap the name to another font so that plotting will still succeed.</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nf'>systemfonts</span><span class='nf'>::</span><span class='nf'><a href='https://systemfonts.r-lib.org/reference/require_font.html'>require_font</a></span><span class='o'>(</span><span class='s'>"Rubik Distressed"</span><span class='o'>)</span></span>
<span><span class='c'>#&gt; Trying Google Fonts... Found! Downloading font to /var/folders/l4/tvfrd0ps4dqdr2z7kvnl9xh40000gn/T//Rtmp2qw4bE</span></span>
<span></span><span></span>
<span><span class='nf'>grid</span><span class='nf'>::</span><span class='nf'><a href='https://rdrr.io/r/grid/grid.text.html'>grid.text</a></span><span class='o'>(</span></span>
<span>  <span class='s'>"There are no bad fonts\nonly bad text"</span>,</span>
<span>  gp <span class='o'>=</span> <span class='nf'>grid</span><span class='nf'>::</span><span class='nf'><a href='https://rdrr.io/r/grid/gpar.html'>gpar</a></span><span class='o'>(</span>fontfamily <span class='o'>=</span> <span class='s'>"Rubik Distressed"</span>, fontsize <span class='o'>=</span> <span class='m'>30</span><span class='o'>)</span></span>
<span><span class='o'>)</span></span>
</code></pre>
<img src="https://opensource.posit.co/blog/2025-05-12_fonts-in-r/figs/unnamed-chunk-16-1.png" width="700px" style="display: block; margin: auto;" />
</div>
<p>By default, <code>require_font()</code> places new fonts in a temporary folder so it doesn&rsquo;t pollute your carefully curated collection of fonts.</p>
<h3 id="font-embedding-in-svg">Font embedding in SVG
</h3>
<p>Fonts work a little differently in vector formats like SVG. These formats include the raw text and only render the font when you open the file. This makes for small, accessible files with crisp text at every level of zoom. But it comes with a price: since the text is rendered when it&rsquo;s opened, it relies on the font in use being available on the viewer&rsquo;s computer. This obviously puts you at the mercy of their font selection, so if you want consistent outputs you&rsquo;ll need to <strong>embed</strong> the font.</p>
<p>In SVG, you can embed fonts using an <code>@import</code> statement in the stylesheet, and can point to a web resource so the SVG doesn&rsquo;t need to contain the entire font. systemfonts provides facilities to generate URLs for import statements and can provide them in a variety of formats:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nf'>systemfonts</span><span class='nf'>::</span><span class='nf'><a href='https://systemfonts.r-lib.org/reference/fonts_as_import.html'>fonts_as_import</a></span><span class='o'>(</span><span class='s'>"Barrio"</span><span class='o'>)</span></span>
<span><span class='c'>#&gt; [1] "https://fonts.bunny.net/css2?family=Barrio&amp;display=swap"</span></span>
<span></span><span><span class='nf'>systemfonts</span><span class='nf'>::</span><span class='nf'><a href='https://systemfonts.r-lib.org/reference/fonts_as_import.html'>fonts_as_import</a></span><span class='o'>(</span><span class='s'>"Rubik Distressed"</span>, type <span class='o'>=</span> <span class='s'>"link"</span><span class='o'>)</span></span>
<span><span class='c'>#&gt; [1] "&lt;link rel=\"stylesheet\" href=\"https://fonts.bunny.net/css2?family=Rubik+Distressed&amp;display=swap\"/&gt;"</span></span>
<span></span></code></pre>
</div>
<p>Further, if the font is not available from an online repository, it can embed the font data directly into the URL:</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/substr.html'>substr</a></span><span class='o'>(</span><span class='nf'>systemfonts</span><span class='nf'>::</span><span class='nf'><a href='https://systemfonts.r-lib.org/reference/fonts_as_import.html'>fonts_as_import</a></span><span class='o'>(</span><span class='s'>"Chalkduster"</span><span class='o'>)</span>, <span class='m'>1</span>, <span class='m'>200</span><span class='o'>)</span></span>
<span><span class='c'>#&gt; [1] "data:text/css,@font-face%20%7B%0A%20%20font-family:%20%22Chalkduster%22;%0A%20%20src:%20url(data:font/ttf;charset=utf-8;base64,AAEAAAAMAIAAAwC4T1MvMmk8+wsAAAFIAAAAYGNtYXBJhgfNAAAEOAAACspnbHlmLDPYGwAAf"</span></span>
<span></span></code></pre>
</div>
<p>svglite uses this feature to allow seamless font embedding with the <code>web_fonts</code> argument. It can take a URL as returned by <code>fonts_as_import()</code> or just the name of the typeface and the URL will automatically be resolved. Look at line 6 in the SVG generated below</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nv'>svg</span> <span class='o'>&lt;-</span> <span class='nf'>svglite</span><span class='nf'>::</span><span class='nf'><a href='https://svglite.r-lib.org/reference/svgstring.html'>svgstring</a></span><span class='o'>(</span>web_fonts <span class='o'>=</span> <span class='s'>"Barrio"</span><span class='o'>)</span></span>
<span><span class='nf'>grid</span><span class='nf'>::</span><span class='nf'><a href='https://rdrr.io/r/grid/grid.text.html'>grid.text</a></span><span class='o'>(</span><span class='s'>"Example"</span>, gp <span class='o'>=</span> <span class='nf'>grid</span><span class='nf'>::</span><span class='nf'><a href='https://rdrr.io/r/grid/gpar.html'>gpar</a></span><span class='o'>(</span>fontfamily <span class='o'>=</span> <span class='s'>"Barrio"</span><span class='o'>)</span><span class='o'>)</span></span>
<span><span class='nf'><a href='https://rdrr.io/r/base/invisible.html'>invisible</a></span><span class='o'>(</span><span class='nf'><a href='https://rdrr.io/r/grDevices/dev.html'>dev.off</a></span><span class='o'>(</span><span class='o'>)</span><span class='o'>)</span></span>
<span><span class='nf'><a href='https://rdrr.io/r/grDevices/cairo.html'>svg</a></span><span class='o'>(</span><span class='o'>)</span></span>
<span><span class='c'>#&gt; &lt;?xml version='1.0' encoding='UTF-8' ?&gt;</span></span>
<span><span class='c'>#&gt; &lt;svg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' width='720.00pt' height='576.00pt' viewBox='0 0 720.00 576.00'&gt;</span></span>
<span><span class='c'>#&gt; &lt;g class='svglite'&gt;</span></span>
<span><span class='c'>#&gt; &lt;defs&gt;</span></span>
<span><span class='c'>#&gt;   &lt;style type='text/css'&gt;&lt;![CDATA[</span></span>
<span><span class='c'>#&gt;     @import url('https://fonts.bunny.net/css2?family=Barrio&amp;display=swap');</span></span>
<span><span class='c'>#&gt;     .svglite line, .svglite polyline, .svglite polygon, .svglite path, .svglite rect, .svglite circle &#123;</span></span>
<span><span class='c'>#&gt;       fill: none;</span></span>
<span><span class='c'>#&gt;       stroke: #000000;</span></span>
<span><span class='c'>#&gt;       stroke-linecap: round;</span></span>
<span><span class='c'>#&gt;       stroke-linejoin: round;</span></span>
<span><span class='c'>#&gt;       stroke-miterlimit: 10.00;</span></span>
<span><span class='c'>#&gt;     &#125;</span></span>
<span><span class='c'>#&gt;     .svglite text &#123;</span></span>
<span><span class='c'>#&gt;       white-space: pre;</span></span>
<span><span class='c'>#&gt;     &#125;</span></span>
<span><span class='c'>#&gt;     .svglite g.glyphgroup path &#123;</span></span>
<span><span class='c'>#&gt;       fill: inherit;</span></span>
<span><span class='c'>#&gt;       stroke: none;</span></span>
<span><span class='c'>#&gt;     &#125;</span></span>
<span><span class='c'>#&gt;   ]]&gt;&lt;/style&gt;</span></span>
<span><span class='c'>#&gt; &lt;/defs&gt;</span></span>
<span><span class='c'>#&gt; &lt;rect width='100%' height='100%' style='stroke: none; fill: #FFFFFF;'/&gt;</span></span>
<span><span class='c'>#&gt; &lt;defs&gt;</span></span>
<span><span class='c'>#&gt;   &lt;clipPath id='cpMC4wMHw3MjAuMDB8MC4wMHw1NzYuMDA='&gt;</span></span>
<span><span class='c'>#&gt;     &lt;rect x='0.00' y='0.00' width='720.00' height='576.00' /&gt;</span></span>
<span><span class='c'>#&gt;   &lt;/clipPath&gt;</span></span>
<span><span class='c'>#&gt; &lt;/defs&gt;</span></span>
<span><span class='c'>#&gt; &lt;g clip-path='url(#cpMC4wMHw3MjAuMDB8MC4wMHw1NzYuMDA=)'&gt;</span></span>
<span><span class='c'>#&gt; &lt;text x='360.00' y='292.32' text-anchor='middle' style='font-size: 12.00px; font-family: "Barrio";' textLength='48.12px' lengthAdjust='spacingAndGlyphs'&gt;Example&lt;/text&gt;</span></span>
<span><span class='c'>#&gt; &lt;/g&gt;</span></span>
<span><span class='c'>#&gt; &lt;/g&gt;</span></span>
<span><span class='c'>#&gt; &lt;/svg&gt;</span></span>
<span></span></code></pre>
</div>
<h2 id="want-more">Want more?
</h2>
<p>This document has mainly focused on how to use the fonts you desire from within R. R has other limitations when it comes to text rendering specifically how to render text that consists of a mix of fonts. This has been solved by 






<a href="https://marquee.r-lib.org" target="_blank" rel="noopener">marquee</a>
 and the curious soul can continue there in order to up their skills in rendering text with R</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nf'>grid</span><span class='nf'>::</span><span class='nf'><a href='https://rdrr.io/r/grid/grid.draw.html'>grid.draw</a></span><span class='o'>(</span></span>
<span>  <span class='nf'>marquee</span><span class='nf'>::</span><span class='nf'><a href='https://marquee.r-lib.org/reference/marquee_grob.html'>marquee_grob</a></span><span class='o'>(</span></span>
<span>    <span class='s'>"_This_ **is** the &#123;.red end&#125;"</span>,</span>
<span>    <span class='nf'>marquee</span><span class='nf'>::</span><span class='nf'><a href='https://marquee.r-lib.org/reference/classic_style.html'>classic_style</a></span><span class='o'>(</span>base_size <span class='o'>=</span> <span class='m'>30</span><span class='o'>)</span></span>
<span>  <span class='o'>)</span></span>
<span><span class='o'>)</span></span>
</code></pre>
<img src="https://opensource.posit.co/blog/2025-05-12_fonts-in-r/figs/unnamed-chunk-20-1.png" width="700px" style="display: block; margin: auto;" />
</div>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>Be aware that the style name is at the discretion of the developer of the typeface. It is very common to see discrepancies between the style name and e.g. the weight reported by the font (e.g. Avenir Next Ultra Light is a <em>thin</em> weight font).&#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/2025-05-12_fonts-in-r/thumbnail-wd.jpg" length="324850" type="image/jpeg" />
    </item>
    <item>
      <title>S7 0.2.0</title>
      <link>https://opensource.posit.co/blog/2024-11-07_s7-0-2-0/</link>
      <pubDate>Thu, 07 Nov 2024 00:00:00 +0000</pubDate>
      <guid>https://opensource.posit.co/blog/2024-11-07_s7-0-2-0/</guid>
      <dc:creator>Tomasz Kalinowski</dc:creator>
      <dc:creator>Hadley Wickham</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()`](https://rdrr.io/pkg/hugodown/man/use_tidy_post.html))
* [ ] 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()`](https://rdrr.io/pkg/hugodown/man/use_tidy_post.html)
* [ ] Add intro sentence, e.g. the standard tagline for the package
* [ ] [`usethis::use_tidy_thanks()`](https://usethis.r-lib.org/reference/use_tidy_thanks.html)
-->
<p>We&rsquo;re excited to announce that 






<a href="https://rconsortium.github.io/S7/" target="_blank" rel="noopener">S7</a>
 v0.2.0 is now available on CRAN! S7 is a new object-oriented programming (OOP) system designed to supersede both S3 and S4. You might wonder why R needs a new OOP system when we already have two. The reason lies in the history of R&rsquo;s OOP journey: S3 is a simple and effective system for single dispatch, while S4 adds formal class definitions and multiple dispatch, but at the cost of complexity. This has forced developers to choose between the simplicity of S3 and the sophistication of S4.</p>
<p>The goal of S7 is to unify the OOP landscape by building on S3&rsquo;s existing dispatch system and incorporating the most useful features of S4 (along with some new ones), all with a simpler syntax. S7&rsquo;s design and implementation have been a collaborative effort by a working group from the 






<a href="https://www.r-consortium.org" target="_blank" rel="noopener">R Consortium</a>
, including representatives from R-Core, Bioconductor, tidyverse/Posit, ROpenSci, and the wider R community. Since S7 builds on S3, it is fully compatible with existing S3-based code. It&rsquo;s also been thoughtfully designed to work with S4, and as we learn more about the challenges of transitioning from S4 to S7, we&rsquo;ll continue to add features to ease this process.</p>
<p>Our long-term goal is to include S7 in base R, but for now, you can install it from CRAN:</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='s'>"S7"</span><span class='o'>)</span></span></code></pre>
</div>
<h2 id="whats-new-in-the-second-release">What&rsquo;s new in the second release
</h2>
<p>The second release of S7 brings refinements and bug fixes. Highlights include:</p>
<ul>
<li>Support for lazy property defaults, making class setup more flexible.</li>
<li>Custom property setters now run on object initialization.</li>
<li>Significant speed improvements for setting and getting properties with <code>@</code> and <code>@&lt;-</code>.</li>
<li>Expanded compatibility with base S3 classes.</li>
<li>






<a href="https://rconsortium.github.io/S7/reference/convert.html" target="_blank" rel="noopener"><code>convert()</code></a>
 now provides a default method for transforming a parent class into a subclass.</li>
</ul>
<p>Additionally, there are numerous bug fixes and quality-of-life improvements, such as better error messages, improved support for base Ops methods, and compatibility improvements for using <code>@</code> in R versions prior to 4.3. You can see a full list of changes in the 






<a href="https://github.com/RConsortium/S7/blob/main/NEWS.md" target="_blank" rel="noopener">release notes</a>
.</p>
<h2 id="who-should-use-s7">Who should use S7
</h2>
<p>S7 is a great fit for R users who like to try new things but don&rsquo;t need to be the first. It&rsquo;s already used in several CRAN packages, and the tidyverse team is applying it in new projects. While you may still run into a few issues, many early problems have been resolved.</p>
<h2 id="usage">Usage
</h2>
<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://rconsortium.github.io/S7/'>S7</a></span><span class='o'>)</span></span></code></pre>
</div>
<p>Let&rsquo;s dive into the basics of S7. To learn more, check out the package vignettes, including a more detailed introduction in 






<a href="https://rconsortium.github.io/S7/articles/S7.html" target="_blank" rel="noopener"><code>vignette(&quot;S7&quot;)</code></a>
, and coverage of generics and methods in 






<a href="https://rconsortium.github.io/S7/articles/generics-methods.html" target="_blank" rel="noopener"><code>vignette(&quot;generics-methods&quot;)</code></a>
, and classes and objects in 






<a href="https://rconsortium.github.io/S7/articles/classes-objects.html" target="_blank" rel="noopener"><code>vignette(&quot;classes-objects&quot;)</code></a>
.</p>
<h3 id="classes-and-objects">Classes and objects
</h3>
<p>S7 classes have formal definitions, specified by 






<a href="https://rconsortium.github.io/S7/reference/new_class.html" target="_blank" rel="noopener"><code>new_class()</code></a>
, which includes a list of properties and an optional validator. For example, the following code creates a <code>Range</code> class with <code>start</code> and <code>end</code> properties, and a validator to ensure that <code>start</code> is always less than <code>end</code>:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nv'>Range</span> <span class='o'>&lt;-</span> <span class='nf'><a href='https://rconsortium.github.io/S7/reference/new_class.html'>new_class</a></span><span class='o'>(</span><span class='s'>"Range"</span>,</span>
<span>  properties <span class='o'>=</span> <span class='nf'><a href='https://rdrr.io/r/base/list.html'>list</a></span><span class='o'>(</span></span>
<span>    start <span class='o'>=</span> <span class='nv'>class_double</span>,</span>
<span>    end <span class='o'>=</span> <span class='nv'>class_double</span></span>
<span>  <span class='o'>)</span>,</span>
<span>  validator <span class='o'>=</span> <span class='kr'>function</span><span class='o'>(</span><span class='nv'>self</span><span class='o'>)</span> <span class='o'>&#123;</span></span>
<span>    <span class='kr'>if</span> <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'>self</span><span class='o'>@</span><span class='nv'>start</span><span class='o'>)</span> <span class='o'>!=</span> <span class='m'>1</span><span class='o'>)</span> <span class='o'>&#123;</span></span>
<span>      <span class='s'>"@start must be length 1"</span></span>
<span>    <span class='o'>&#125;</span> <span class='kr'>else</span> <span class='kr'>if</span> <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'>self</span><span class='o'>@</span><span class='nv'>end</span><span class='o'>)</span> <span class='o'>!=</span> <span class='m'>1</span><span class='o'>)</span> <span class='o'>&#123;</span></span>
<span>      <span class='s'>"@end must be length 1"</span></span>
<span>    <span class='o'>&#125;</span> <span class='kr'>else</span> <span class='kr'>if</span> <span class='o'>(</span><span class='nv'>self</span><span class='o'>@</span><span class='nv'>end</span> <span class='o'>&lt;</span> <span class='nv'>self</span><span class='o'>@</span><span class='nv'>start</span><span class='o'>)</span> <span class='o'>&#123;</span></span>
<span>      <span class='s'>"@end must be greater than or equal to @start"</span></span>
<span>    <span class='o'>&#125;</span></span>
<span>  <span class='o'>&#125;</span></span>
<span><span class='o'>)</span></span></code></pre>
</div>
<p>






<a href="https://rconsortium.github.io/S7/reference/new_class.html" target="_blank" rel="noopener"><code>new_class()</code></a>
 returns the class object, which also serves as the constructor to create instances of the class:</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'>Range</span><span class='o'>(</span>start <span class='o'>=</span> <span class='m'>1</span>, end <span class='o'>=</span> <span class='m'>10</span><span class='o'>)</span></span>
<span><span class='nv'>x</span></span>
<span><span class='c'>#&gt; &lt;Range&gt;</span></span>
<span><span class='c'>#&gt;  @ start: num 1</span></span>
<span><span class='c'>#&gt;  @ end  : num 10</span></span>
<span></span></code></pre>
</div>
<h3 id="properties">Properties
</h3>
<p>The data an object holds are called its <strong>properties</strong>. Use <code>@</code> to get and set properties:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nv'>x</span><span class='o'>@</span><span class='nv'>start</span></span>
<span><span class='c'>#&gt; [1] 1</span></span>
<span></span><span><span class='nv'>x</span><span class='o'>@</span><span class='nv'>end</span> <span class='o'>&lt;-</span> <span class='m'>20</span></span>
<span><span class='nv'>x</span></span>
<span><span class='c'>#&gt; &lt;Range&gt;</span></span>
<span><span class='c'>#&gt;  @ start: num 1</span></span>
<span><span class='c'>#&gt;  @ end  : num 20</span></span>
<span></span></code></pre>
</div>
<p>Properties are automatically validated against the type declared in 






<a href="https://rconsortium.github.io/S7/reference/new_class.html" target="_blank" rel="noopener"><code>new_class()</code></a>
 (in this case, <code>double</code>) and checked by the class <strong>validator</strong>:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nv'>x</span><span class='o'>@</span><span class='nv'>end</span> <span class='o'>&lt;-</span> <span class='s'>"x"</span></span>
<span><span class='c'>#&gt; Error: &lt;Range&gt;@end must be &lt;double&gt;, not &lt;character&gt;</span></span>
<span></span><span><span class='nv'>x</span><span class='o'>@</span><span class='nv'>end</span> <span class='o'>&lt;-</span> <span class='o'>-</span><span class='m'>1</span></span>
<span><span class='c'>#&gt; Error: &lt;Range&gt; object is invalid:</span></span>
<span><span class='c'>#&gt; - @end must be greater than or equal to @start</span></span>
<span></span></code></pre>
</div>
<h3 id="generics-and-methods">Generics and methods
</h3>
<p>Like S3 and S4, S7 uses <strong>functional OOP</strong>, where methods belong to <strong>generic</strong> functions, and method calls look like regular function calls: <code>generic(object, arg2, arg3)</code>. A generic uses the types of its arguments to automatically pick the appropriate method implementation.</p>
<p>You can create a new generic with 






<a href="https://rconsortium.github.io/S7/reference/new_generic.html" target="_blank" rel="noopener"><code>new_generic()</code></a>
, specifying the arguments to dispatch on:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nv'>inside</span> <span class='o'>&lt;-</span> <span class='nf'><a href='https://rconsortium.github.io/S7/reference/new_generic.html'>new_generic</a></span><span class='o'>(</span><span class='s'>"inside"</span>, <span class='s'>"x"</span><span class='o'>)</span></span></code></pre>
</div>
<p>To define a method for a specific class, use <code>method(generic, class) &lt;- implementation</code>:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nf'><a href='https://rconsortium.github.io/S7/reference/method.html'>method</a></span><span class='o'>(</span><span class='nv'>inside</span>, <span class='nv'>Range</span><span class='o'>)</span> <span class='o'>&lt;-</span> <span class='kr'>function</span><span class='o'>(</span><span class='nv'>x</span>, <span class='nv'>y</span><span class='o'>)</span> <span class='o'>&#123;</span></span>
<span>  <span class='nv'>y</span> <span class='o'>&gt;=</span> <span class='nv'>x</span><span class='o'>@</span><span class='nv'>start</span> <span class='o'>&amp;</span> <span class='nv'>y</span> <span class='o'>&lt;=</span> <span class='nv'>x</span><span class='o'>@</span><span class='nv'>end</span></span>
<span><span class='o'>&#125;</span></span>
<span></span>
<span><span class='nf'>inside</span><span class='o'>(</span><span class='nv'>x</span>, <span class='nf'><a href='https://rdrr.io/r/base/c.html'>c</a></span><span class='o'>(</span><span class='m'>0</span>, <span class='m'>5</span>, <span class='m'>10</span>, <span class='m'>15</span><span class='o'>)</span><span class='o'>)</span></span>
<span><span class='c'>#&gt; [1] FALSE  TRUE  TRUE  TRUE</span></span>
<span></span></code></pre>
</div>
<p>Printing the generic shows its methods:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nv'>inside</span></span>
<span><span class='c'>#&gt; &lt;S7_generic&gt; inside(x, ...) with 1 methods:</span></span>
<span><span class='c'>#&gt; 1: method(inside, Range)</span></span>
<span></span></code></pre>
</div>
<p>And you can retrieve the method for a specific class:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nf'><a href='https://rconsortium.github.io/S7/reference/method.html'>method</a></span><span class='o'>(</span><span class='nv'>inside</span>, <span class='nv'>Range</span><span class='o'>)</span></span>
<span><span class='c'>#&gt; &lt;S7_method&gt; method(inside, Range)</span></span>
<span><span class='c'>#&gt; function (x, y) </span></span>
<span><span class='c'>#&gt; &#123;</span></span>
<span><span class='c'>#&gt;     y &gt;= x@start &amp; y &lt;= x@end</span></span>
<span><span class='c'>#&gt; &#125;</span></span>
<span></span></code></pre>
</div>
<h2 id="known-limitations">Known limitations
</h2>
<p>While we are pleased with S7&rsquo;s design, there are still some limitations:</p>
<ul>
<li>S7 objects can be serialized to disk (with 






<a href="https://rdrr.io/r/base/readRDS.html" target="_blank" rel="noopener"><code>saveRDS()</code></a>
), but the current implementation saves the entire class specification with each object. This may change in the future.</li>
<li>Support for implicit S3 classes <code>&quot;array&quot;</code> and <code>&quot;matrix&quot;</code> is still in development.</li>
</ul>
<p>We expect the community will uncover more issues as S7 is more widely adopted. If you encounter any problems, please file an issue at 






<a href="https://github.com/RConsortium/OOP-WG/issues" target="_blank" rel="noopener">https://github.com/RConsortium/OOP-WG/issues</a>
. We appreciate your feedback in helping us make S7 even better! 😃</p>
<h2 id="acknowledgements">Acknowledgements
</h2>
<p>Thank you to all people who have contributed issues, code, and comments to this release:</p>
<p>






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






<a href="https://github.com/waynelapierre" target="_blank" rel="noopener">@waynelapierre</a>
.</p>
]]></description>
      <enclosure url="https://opensource.posit.co/blog/2024-11-07_s7-0-2-0/thumbnail-wd.jpg" length="78819" type="image/jpeg" />
    </item>
    <item>
      <title>pkgdown 2.1.0</title>
      <link>https://opensource.posit.co/blog/2024-07-08_pkgdown-2-1-0/</link>
      <pubDate>Mon, 08 Jul 2024 00:00:00 +0000</pubDate>
      <guid>https://opensource.posit.co/blog/2024-07-08_pkgdown-2-1-0/</guid>
      <dc:creator>Hadley Wickham</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
* [s] 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 delighted to announce the release of 






<a href="http://pkgdown.r-lib.org/" target="_blank" rel="noopener">pkgdown</a>
 2.1.0. pkgdown is designed to make it quick and easy to build a beautiful and accessible website for your package.</p>
<p>You can install it 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='s'>"pkgdown"</span><span class='o'>)</span></span></code></pre>
</div>
<p>This is a massive release with a bunch of new features. I&rsquo;ll highlight the most important here, but as always, I highlight recommend skimming the 






<a href="https://github.com/r-lib/pkgdown/releases/tag/v2.1.0" target="_blank" rel="noopener">release notes</a>
 for other smaller improvements and bug fixes.</p>
<p>First, and most importantly, please join me in welcoming two new authors to pkgdown: 






<a href="https://github.com/olivroy" target="_blank" rel="noopener">Olivier Roy</a>
 and 






<a href="https://github.com/salim-b" target="_blank" rel="noopener">Salim Brüggemann</a>
. They have both contributed many improvements to the package and I&rsquo;m very happy to officially have them aboard as package authors.</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://pkgdown.r-lib.org/'>pkgdown</a></span><span class='o'>)</span></span></code></pre>
</div>
<h2 id="lifecycle-changes">Lifecycle changes
</h2>
<p>Let&rsquo;s get started with the important stuff, the 






  
  

<a href="https://opensource.posit.co/blog/2021-02-15_lifecycle-1-0-0/">lifecycle updates</a>
. Most important we&rsquo;ve decided to deprecate support for Bootstrap 3, which was superseded in December 2021. We&rsquo;re starting to more directly encourage folks to move away from it as maintaining two separate sets of site templates is a time sink. If you&rsquo;re still using BS3, now&rsquo;s the 


  
  
  





  
  

<a href="https://opensource.posit.co/blog/2021-12-03_pkgdown-2-0-0/#bootstrap-5">time to upgrade</a>
.</p>
<p>There are three other changes that are less likely to affect folks:</p>
<ul>
<li>
<p>The <code>document</code> argument to 






<a href="https://pkgdown.r-lib.org/reference/build_site.html" target="_blank" rel="noopener"><code>build_site()</code></a>
 and 






<a href="https://pkgdown.r-lib.org/reference/build_reference.html" target="_blank" rel="noopener"><code>build_reference()</code></a>
 has been removed after being deprecated in pkgdown 1.4.0; use the 


  
  
  





<a href="https://pkgdown.r-lib.org/reference/build_site.html#arg-devel" target="_blank" rel="noopener"><code>devel</code> argument</a>
 instead.</p>
</li>
<li>
<p>






<a href="https://pkgdown.r-lib.org/reference/autolink_html.html" target="_blank" rel="noopener"><code>autolink_html()</code></a>
 was deprecated in pkgdown 1.6.0 and now warns every time you use it; use 






<a href="https://downlit.r-lib.org/reference/downlit_html_path.html" target="_blank" rel="noopener"><code>downlit::downlit_html_path()</code></a>
 instead.</p>
</li>
<li>
<p>






<a href="https://pkgdown.r-lib.org/reference/preview_page.html" target="_blank" rel="noopener"><code>preview_page()</code></a>
 has been deprecated; use 






<a href="https://pkgdown.r-lib.org/reference/preview_site.html" target="_blank" rel="noopener"><code>preview_site()</code></a>
 instead.</p>
</li>
</ul>
<h2 id="major-new-features">Major new features
</h2>
<p>pkgdown 2.1.0 has two major new features: support for Quarto vignettes and a new light switch that toggles between light and dark modes.</p>
<h3 id="quarto-support">Quarto support
</h3>
<p>






<a href="https://pkgdown.r-lib.org/reference/build_articles.html" target="_blank" rel="noopener"><code>build_article()</code></a>
/






<a href="https://pkgdown.r-lib.org/reference/build_articles.html" target="_blank" rel="noopener"><code>build_articles()</code></a>
 now support articles and vignettes written with Quarto. To use it, make sure you have the the latest version of Quarto, 1.5, which was released last week. By and large you should be able to just write in Quarto and things will just work, but you will need to make a small change to your GitHub action. Learn more at 






<a href="https://pkgdown.r-lib.org/articles/quarto.html" target="_blank" rel="noopener"><code>vignette(&quot;quarto&quot;)</code></a>
.</p>
<p>Combining the individual quarto and pkgdown templating systems is a delicate art, so while I&rsquo;ve done my best to make it work, there may be some rough edges. Check out the current known limitations in 






<a href="https://pkgdown.r-lib.org/articles/quarto.html" target="_blank" rel="noopener"><code>vignette(&quot;quarto&quot;)</code></a>
, and please file an issue if you encounter a quarto feature that doesn&rsquo;t work quite right.</p>
<h3 id="light-switch">Light switch
</h3>
<p>pkgdown sites can now provide a &ldquo;light switch&rdquo; that allows the reader to switch between light and dark modes (based on work in bslib by 






<a href="https://github.com/gadenbuie" target="_blank" rel="noopener">@gadenbuie</a>
). You can try it out on 






<a href="https://pkgdown.r-lib.org" target="_blank" rel="noopener">https://pkgdown.r-lib.org</a>
: the light switch appears at the far right at the navbar and remembers the users choice between visits to your site.</p>
<p>(Note that the light switch works differently to quarto dark mode. In quarto, you can provide two completely different themes for light and dark mode. In pkgdown, dark mode is a relatively thin overlay that based on your light theme colours.)</p>
<p>For now, you&rsquo;ll need to opt-in to the light-switch by adding the following to your <code>_pkgdown.yml</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></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="l">template</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">light-switch</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>In the future we hope to turn it on automatically.</p>
<p>You can learn more about customising the light switch in 






<a href="https://pkgdown.r-lib.org/articles/customise.html" target="_blank" rel="noopener"><code>vignette(&quot;customise&quot;)</code></a>
: you can choose to select your own syntax highlighting scheme for dark mode, override dark-specific BS lib variables, and move its location in the navbar.</p>
<h2 id="user-experience">User experience
</h2>
<p>We&rsquo;ve made a bunch of small changes to enhance the user experience of pkgdown sites:</p>
<ul>
<li>
<p>We&rsquo;ve continued in our efforts to make pkgdown sites as accessible as possible by now warning if you&rsquo;ve forgotten to add alt text to images (including plots) in your articles. We&rsquo;ve also added a new 






<a href="https://pkgdown.r-lib.org/articles/accessibility.html" target="_blank" rel="noopener"><code>vignette(&quot;accessibility&quot;)</code></a>
 which describes additional manual tasks you can perform to make your site as accessible as possible.</p>
</li>
<li>
<p>






<a href="https://pkgdown.r-lib.org/reference/build_reference.html" target="_blank" rel="noopener"><code>build_reference()</code></a>
 adds anchors to arguments making it possible to link directly to an argument. This is very useful when you&rsquo;re trying to direct folks to the documentation for a specific argument, e.g. 


  
  
  





<a href="https://pkgdown.r-lib.org/reference/build_site.html#arg-devel" target="_blank" rel="noopener">https://pkgdown.r-lib.org/reference/build_site.html#arg-devel</a>
.</p>
</li>
<li>
<p>






<a href="https://pkgdown.r-lib.org/reference/build_reference.html" target="_blank" rel="noopener"><code>build_reference_index()</code></a>
 now displays function lifecycle badges 


  
  
  





<a href="https://pkgdown.r-lib.org/reference/index.html#deprecated-functions" target="_blank" rel="noopener">next to the function name</a>
. If you want to gather together (e.g.) all the deprecated function in one spot in the reference index, you can use the new topic selector <code>has_lifecycle(&quot;deprecated&quot;)</code>.</p>
</li>
<li>
<p>The new <code>template.math-rendering</code> option allows you to control how math is rendered on your site. The default uses <code>mathml</code> which is zero dependency but has the lowest fidelity. If you use a lot of math on your site, you can switch back to the previous method with <code>mathjax</code>, or try out <code>katex</code>, a faster alternative.</p>
</li>
<li>
<p>pkgdown sites no longer depend on external content distribution networks (CDN) for common javascript, CSS, and font files. CDNs no longer provide 






<a href="https://www.stefanjudis.com/notes/say-goodbye-to-resource-caching-across-sites-and-domains/" target="_blank" rel="noopener">any performance advantages</a>
 and make deployment harder inside certain locked-down corporate environments.</p>
</li>
<li>
<p>pkgdown includes translations for more terms including &ldquo;Abstract&rdquo; and &ldquo;Search site&rdquo;. A big thanks to @jplecavalier, @dieghernan, @krlmlr, @LDalby, @rich-iannone, @jmaspons, and @mine-cetinkaya-rundel for providing updated translations in French, Spanish, Portugese, Germna, Catalan, and Turkish!</p>
<p>I&rsquo;ve also written 






<a href="https://pkgdown.r-lib.org/articles/translations.html" target="_blank" rel="noopener"><code>vignette(&quot;translations&quot;)</code></a>
, a brief vignette that discusses how translation works for non-English sites, and includes how you can create translations for new languages. (This is a great way to contribute to pkgdown if you are multi-lingual!)</p>
</li>
</ul>
<h3 id="developer-experience">Developer experience
</h3>
<p>We&rsquo;ve also made a bunch of minor improvements to make improve the package developer experience:</p>
<ul>
<li>
<p>YAML validation has been substantially improved so you should get much clearer errors if you have made a mistake in your <code>_pkgdown.yml</code>. Please 






<a href="https://github.com/r-lib/pkgdown/issues/new" target="_blank" rel="noopener">file an issue</a>
 if you find a case where the error message is not helpful.</p>
</li>
<li>
<p>The <code>build_*()</code> functions (apart from 






<a href="https://pkgdown.r-lib.org/reference/build_site.html" target="_blank" rel="noopener"><code>build_site()</code></a>
) no longer automatically preview in interactive sessions since they all emit clickable links to any files that have changed. You can continue to use 






<a href="https://pkgdown.r-lib.org/reference/preview_site.html" target="_blank" rel="noopener"><code>preview_site()</code></a>
 to open the site in your browser.</p>
</li>
<li>
<p>The <code>build_*()</code> functions now work better if you&rsquo;re previewing just part of a site and haven&rsquo;t built the whole thing. It should no longer be necessary to run 






<a href="https://pkgdown.r-lib.org/reference/init_site.html" target="_blank" rel="noopener"><code>init_site()</code></a>
 in most cases, and you shouldn&rsquo;t be able to get into a state where you&rsquo;re told to run 






<a href="https://pkgdown.r-lib.org/reference/init_site.html" target="_blank" rel="noopener"><code>init_site()</code></a>
 and then it doesn&rsquo;t work.</p>
</li>
<li>
<p>We give more and clearer details of the site building process including reporting on exactly what is generated by bslib, what is copied from templates, and what redirects are generated.</p>
</li>
</ul>
<h2 id="acknowledgements">Acknowledgements
</h2>
<p>A big thanks to all 212 folks who contributed to this release! 






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






<a href="https://github.com/mccarthy-m-g" target="_blank" rel="noopener">@mccarthy-m-g</a>
, 






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






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






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






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






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






<a href="https://github.com/mine-cetinkaya-rundel" target="_blank" rel="noopener">@mine-cetinkaya-rundel</a>
, 






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






<a href="https://github.com/zkamvar" target="_blank" rel="noopener">@zkamvar</a>
.</p>
]]></description>
      <enclosure url="https://opensource.posit.co/blog/2024-07-08_pkgdown-2-1-0/thumbnail-wd.jpg" length="68610" type="image/jpeg" />
    </item>
    <item>
      <title>withr 3.0.0</title>
      <link>https://opensource.posit.co/blog/2024-01-18_withr-3-0-0/</link>
      <pubDate>Thu, 18 Jan 2024 00:00:00 +0000</pubDate>
      <guid>https://opensource.posit.co/blog/2024-01-18_withr-3-0-0/</guid>
      <dc:creator>Lionel Henry</dc:creator><description><![CDATA[<p>It&rsquo;s not without jubilant bearing that we announce the release of the 3.0.0 version of 






<a href="https://withr.r-lib.org/" target="_blank" rel="noopener">withr</a>
, the tidyverse solution for automatic cleanup of resources! In this release, the internals of withr were rewritten to improve the performance and increase the compatibility with base R&rsquo;s 






<a href="https://rdrr.io/r/base/on.exit.html" target="_blank" rel="noopener"><code>on.exit()</code></a>
 mechanism.</p>
<p>You can install it 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='s'>"withr"</span><span class='o'>)</span></span></code></pre>
</div>
<p>In this blog post we&rsquo;ll go over the changes that made this rewrite possible, but first we&rsquo;ll review the cleanup strategies made possible by withr.</p>
<p>You can see a full list of changes in the 


  
  
  





<a href="https://withr.r-lib.org/news/index.html#withr-300" target="_blank" rel="noopener">release notes</a>
.</p>
<div class="highlight">
</div>
<h2 id="cleaning-up-resources-with-base-r-and-with-withr">Cleaning up resources with base R and with withr
</h2>
<p>Traditionally, resource cleanup in R is done with 






<a href="https://rdrr.io/r/base/on.exit.html" target="_blank" rel="noopener"><code>base::on.exit()</code></a>
. Cleaning up in the on-exit hook ensures that the cleanup happens both in the normal case, when the code has finished running without error, and in the error case, when something went wrong and execution is interrupted.</p>
<p>






<a href="https://rdrr.io/r/base/on.exit.html" target="_blank" rel="noopener"><code>on.exit()</code></a>
 is meant to be used inside functions but it also works within 






<a href="https://rdrr.io/r/base/eval.html" target="_blank" rel="noopener"><code>local()</code></a>
, which we&rsquo;ll use here for our examples:</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/eval.html'>local</a></span><span class='o'>(</span><span class='o'>&#123;</span></span>
<span>  <span class='nf'><a href='https://rdrr.io/r/base/on.exit.html'>on.exit</a></span><span class='o'>(</span><span class='nf'><a href='https://rdrr.io/r/base/message.html'>message</a></span><span class='o'>(</span><span class='s'>"Cleaning time!"</span><span class='o'>)</span><span class='o'>)</span></span>
<span>  <span class='nf'><a href='https://rdrr.io/r/base/print.html'>print</a></span><span class='o'>(</span><span class='m'>1</span> <span class='o'>+</span> <span class='m'>2</span><span class='o'>)</span></span>
<span><span class='o'>&#125;</span><span class='o'>)</span></span>
<span><span class='c'>#&gt; [1] 3</span></span>
<span></span><span><span class='c'>#&gt; Cleaning time!</span></span>
<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/eval.html'>local</a></span><span class='o'>(</span><span class='o'>&#123;</span></span>
<span>  <span class='nf'><a href='https://rdrr.io/r/base/on.exit.html'>on.exit</a></span><span class='o'>(</span><span class='nf'><a href='https://rdrr.io/r/base/message.html'>message</a></span><span class='o'>(</span><span class='s'>"Cleaning time!"</span><span class='o'>)</span><span class='o'>)</span></span>
<span>  <span class='kr'><a href='https://rdrr.io/r/base/stop.html'>stop</a></span><span class='o'>(</span><span class='s'>"uh oh"</span><span class='o'>)</span></span>
<span>  <span class='nf'><a href='https://rdrr.io/r/base/print.html'>print</a></span><span class='o'>(</span><span class='m'>1</span> <span class='o'>+</span> <span class='m'>2</span><span class='o'>)</span></span>
<span><span class='o'>&#125;</span><span class='o'>)</span></span>
<span><span class='c'>#&gt; <span style='color: #BBBB00; font-weight: bold;'>Error</span><span style='font-weight: bold;'>:</span></span></span>
<span><span class='c'>#&gt; <span style='color: #BBBB00;'>!</span> uh oh</span></span>
<span></span><span><span class='c'>#&gt; Cleaning time!</span></span>
<span></span></code></pre>
</div>
<p>






<a href="https://rdrr.io/r/base/on.exit.html" target="_blank" rel="noopener"><code>on.exit()</code></a>
 is guaranteed to run no matter what and this property makes it invaluable for resource cleaning. No more accidental littering!</p>
<p>However the process of cleaning up this way can be a bit verbose and feel too manual. Here is how you&rsquo;d create and clean up a temporary file for instance:</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/eval.html'>local</a></span><span class='o'>(</span><span class='o'>&#123;</span></span>
<span>  <span class='nv'>my_file</span> <span class='o'>&lt;-</span> <span class='nf'><a href='https://rdrr.io/r/base/tempfile.html'>tempfile</a></span><span class='o'>(</span><span class='o'>)</span></span>
<span></span>
<span>  <span class='nf'><a href='https://rdrr.io/r/base/files.html'>file.create</a></span><span class='o'>(</span><span class='nv'>my_file</span><span class='o'>)</span></span>
<span>  <span class='nf'><a href='https://rdrr.io/r/base/on.exit.html'>on.exit</a></span><span class='o'>(</span><span class='nf'><a href='https://rdrr.io/r/base/files.html'>file.remove</a></span><span class='o'>(</span><span class='nv'>my_file</span><span class='o'>)</span><span class='o'>)</span></span>
<span></span>
<span>  <span class='nf'><a href='https://rdrr.io/r/base/writeLines.html'>writeLines</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'>"a"</span>, <span class='s'>"b"</span><span class='o'>)</span>, con <span class='o'>=</span> <span class='nv'>my_file</span><span class='o'>)</span></span>
<span><span class='o'>&#125;</span><span class='o'>)</span></span></code></pre>
</div>
<p>Wouldn&rsquo;t it be great if we could wrap this code up in a function? That&rsquo;s the goal of withr&rsquo;s <code>local_</code>-prefixed functions. They combine both the creation or modification of a resource and its (eventual) restoration to the original state into a single function:</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/eval.html'>local</a></span><span class='o'>(</span><span class='o'>&#123;</span></span>
<span>  <span class='nv'>my_file</span> <span class='o'>&lt;-</span> <span class='nf'>withr</span><span class='nf'>::</span><span class='nf'><a href='https://withr.r-lib.org/reference/with_tempfile.html'>local_tempfile</a></span><span class='o'>(</span><span class='o'>)</span></span>
<span></span>
<span>  <span class='nf'><a href='https://rdrr.io/r/base/writeLines.html'>writeLines</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'>"a"</span>, <span class='s'>"b"</span><span class='o'>)</span>, con <span class='o'>=</span> <span class='nv'>my_file</span><span class='o'>)</span></span>
<span><span class='o'>&#125;</span><span class='o'>)</span></span></code></pre>
</div>
<p>In this case we have created a resource (a file), but the same principle applies to modifying resources such as global options:</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/eval.html'>local</a></span><span class='o'>(</span><span class='o'>&#123;</span></span>
<span>  <span class='c'># Let's temporarily print with a single decimal place</span></span>
<span>  <span class='nf'>withr</span><span class='nf'>::</span><span class='nf'><a href='https://withr.r-lib.org/reference/with_options.html'>local_options</a></span><span class='o'>(</span>digits <span class='o'>=</span> <span class='m'>1</span><span class='o'>)</span></span>
<span>  <span class='nf'><a href='https://rdrr.io/r/base/print.html'>print</a></span><span class='o'>(</span><span class='m'>1</span><span class='o'>/</span><span class='m'>3</span><span class='o'>)</span></span>
<span><span class='o'>&#125;</span><span class='o'>)</span></span>
<span><span class='c'>#&gt; [1] 0.3</span></span>
<span></span><span></span>
<span><span class='c'># The original option value has been restored</span></span>
<span><span class='nf'><a href='https://rdrr.io/r/base/options.html'>getOption</a></span><span class='o'>(</span><span class='s'>"digits"</span><span class='o'>)</span></span>
<span><span class='c'>#&gt; [1] 7</span></span>
<span></span><span></span>
<span><span class='nf'><a href='https://rdrr.io/r/base/print.html'>print</a></span><span class='o'>(</span><span class='m'>1</span><span class='o'>/</span><span class='m'>3</span><span class='o'>)</span></span>
<span><span class='c'>#&gt; [1] 0.3333333</span></span>
<span></span></code></pre>
</div>
<p>And you can equivalently use the <code>with_</code>-prefixed variants (from which the package takes its name!), this way you don&rsquo;t need to wrap in 






<a href="https://rdrr.io/r/base/eval.html" target="_blank" rel="noopener"><code>local()</code></a>
:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nf'>withr</span><span class='nf'>::</span><span class='nf'><a href='https://withr.r-lib.org/reference/with_options.html'>with_options</a></span><span class='o'>(</span><span class='nf'><a href='https://rdrr.io/r/base/list.html'>list</a></span><span class='o'>(</span>digits <span class='o'>=</span> <span class='m'>1</span><span class='o'>)</span>, <span class='nf'><a href='https://rdrr.io/r/base/print.html'>print</a></span><span class='o'>(</span><span class='m'>1</span><span class='o'>/</span><span class='m'>3</span><span class='o'>)</span><span class='o'>)</span></span>
<span><span class='c'>#&gt; [1] 0.3</span></span>
<span></span></code></pre>
</div>
<p>The <code>with_</code> functions are useful for creating very small scopes for given resources, inside or outside a function.</p>
<h2 id="the-withr-300-rewrite">The withr 3.0.0 rewrite
</h2>
<p>Traditionally, withr implemented its own exit event system on top of 






<a href="https://rdrr.io/r/base/on.exit.html" target="_blank" rel="noopener"><code>on.exit()</code></a>
. We needed an extra layer because of a couple of missing features:</p>
<ul>
<li>
<p>When multiple resources are managed by a piece of code, the order in which these resources are restored or cleaned up sometimes matter. The most consistent order for cleanup is last-in first-out (LIFO). In other words the oldest resource, on which younger resources might depend, is cleaned up last. But historically R only supported first-in first-out (FIFO) order.</p>
</li>
<li>
<p>The other missing piece was being able to inspect the contents of the exit hook. The 






<a href="https://rdrr.io/r/base/sys.parent.html" target="_blank" rel="noopener"><code>sys.on.exit()</code></a>
 R helper was created for this purpose but was affected by a bug that prevented it from working inside functions.</p>
</li>
</ul>
<p>We contributed two changes to R 3.5.0 that filled these missing pieces, fixing the 






<a href="https://rdrr.io/r/base/sys.parent.html" target="_blank" rel="noopener"><code>sys.on.exit()</code></a>
 bug and adding an <code>after</code> argument to 






<a href="https://rdrr.io/r/base/on.exit.html" target="_blank" rel="noopener"><code>on.exit()</code></a>
 to allow last-in first-out ordering.</p>
<p>Until now, we haven&rsquo;t been able to leverage these contributions because of our policy of 






  
  

<a href="https://opensource.posit.co/blog/2019-04-01_r-version-support/">supporting the current and previous four versions of R</a>
. Now that enough time has passed, it was time for a rewrite! Our version of 






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






<a href="https://withr.r-lib.org/reference/defer.html" target="_blank" rel="noopener"><code>withr::defer()</code></a>
. Along with better default behaviour, 






<a href="https://withr.r-lib.org/reference/defer.html" target="_blank" rel="noopener"><code>withr::defer()</code></a>
 allows the clean up of resources non-locally (ironically an essential feature for implementing <code>local_</code> functions). Given the changes in R 3.5.0, 






<a href="https://withr.r-lib.org/reference/defer.html" target="_blank" rel="noopener"><code>withr::defer()</code></a>
 can now be implemented as a simple wrapper around 






<a href="https://rdrr.io/r/base/on.exit.html" target="_blank" rel="noopener"><code>on.exit()</code></a>
.</p>
<p>One benefit of the rewrite is that mixing withr tools and 






<a href="https://rdrr.io/r/base/on.exit.html" target="_blank" rel="noopener"><code>on.exit()</code></a>
 in the same function now correctly interleaves cleanup:</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/eval.html'>local</a></span><span class='o'>(</span><span class='o'>&#123;</span></span>
<span>  <span class='nf'><a href='https://rdrr.io/r/base/on.exit.html'>on.exit</a></span><span class='o'>(</span><span class='nf'><a href='https://rdrr.io/r/base/print.html'>print</a></span><span class='o'>(</span><span class='m'>1</span><span class='o'>)</span><span class='o'>)</span></span>
<span></span>
<span>  <span class='nf'>withr</span><span class='nf'>::</span><span class='nf'><a href='https://withr.r-lib.org/reference/defer.html'>defer</a></span><span class='o'>(</span><span class='nf'><a href='https://rdrr.io/r/base/print.html'>print</a></span><span class='o'>(</span><span class='m'>2</span><span class='o'>)</span><span class='o'>)</span></span>
<span></span>
<span>  <span class='nf'><a href='https://rdrr.io/r/base/on.exit.html'>on.exit</a></span><span class='o'>(</span><span class='nf'><a href='https://rdrr.io/r/base/print.html'>print</a></span><span class='o'>(</span><span class='m'>3</span><span class='o'>)</span>, add <span class='o'>=</span> <span class='kc'>TRUE</span>, after <span class='o'>=</span> <span class='kc'>FALSE</span><span class='o'>)</span></span>
<span></span>
<span>  <span class='nf'>withr</span><span class='nf'>::</span><span class='nf'><a href='https://withr.r-lib.org/reference/defer.html'>defer</a></span><span class='o'>(</span><span class='nf'><a href='https://rdrr.io/r/base/print.html'>print</a></span><span class='o'>(</span><span class='m'>4</span><span class='o'>)</span><span class='o'>)</span></span>
<span></span>
<span>  <span class='nf'><a href='https://rdrr.io/r/base/print.html'>print</a></span><span class='o'>(</span><span class='m'>5</span><span class='o'>)</span></span>
<span><span class='o'>&#125;</span><span class='o'>)</span></span>
<span><span class='c'>#&gt; [1] 5</span></span>
<span><span class='c'>#&gt; [1] 4</span></span>
<span><span class='c'>#&gt; [1] 3</span></span>
<span><span class='c'>#&gt; [1] 2</span></span>
<span><span class='c'>#&gt; [1] 1</span></span>
<span></span></code></pre>
</div>
<p>But the main benefit is increased performance. Here is how <code>defer()</code> compared to 






<a href="https://rdrr.io/r/base/on.exit.html" target="_blank" rel="noopener"><code>on.exit()</code></a>
 in the previous version:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nv'>base</span> <span class='o'>&lt;-</span> <span class='kr'>function</span><span class='o'>(</span><span class='o'>)</span> <span class='nf'><a href='https://rdrr.io/r/base/on.exit.html'>on.exit</a></span><span class='o'>(</span><span class='kc'>NULL</span><span class='o'>)</span></span>
<span><span class='nv'>withr</span> <span class='o'>&lt;-</span> <span class='kr'>function</span><span class='o'>(</span><span class='o'>)</span> <span class='nf'>defer</span><span class='o'>(</span><span class='kc'>NULL</span><span class='o'>)</span></span>
<span></span>
<span><span class='c'># withr 2.5.2</span></span>
<span><span class='nf'>bench</span><span class='nf'>::</span><span class='nf'><a href='http://bench.r-lib.org/reference/mark.html'>mark</a></span><span class='o'>(</span><span class='nf'>base</span><span class='o'>(</span><span class='o'>)</span>, <span class='nf'>withr</span><span class='o'>(</span><span class='o'>)</span>, check <span class='o'>=</span> <span class='kc'>FALSE</span><span class='o'>)</span><span class='o'>[</span><span class='m'>1</span><span class='o'>:</span><span class='m'>8</span><span class='o'>]</span></span>
<span><span class='c'>#&gt; # A tibble: 2 × 8</span></span>
<span><span class='c'>#&gt;   expression      min median `itr/sec` mem_alloc `gc/sec` n_itr  n_gc</span></span>
<span><span class='c'>#&gt;   &lt;bch:expr&gt; &lt;bch:tm&gt; &lt;bch:&gt;     &lt;dbl&gt; &lt;bch:byt&gt;    &lt;dbl&gt; &lt;int&gt; &lt;dbl&gt;</span></span>
<span><span class='c'>#&gt; 1 base()            0   82ns  6954952.        0B    696.   9999     1</span></span>
<span><span class='c'>#&gt; 2 withr()      26.2µs 27.9µs    35172.    88.4KB     52.8  9985    15</span></span></code></pre>
</div>
<p>withr 3.0.0 has now caught up to 






<a href="https://rdrr.io/r/base/on.exit.html" target="_blank" rel="noopener"><code>on.exit()</code></a>
 quite a bit:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='c'># withr 3.0.0</span></span>
<span><span class='nf'>bench</span><span class='nf'>::</span><span class='nf'><a href='http://bench.r-lib.org/reference/mark.html'>mark</a></span><span class='o'>(</span><span class='nf'>base</span><span class='o'>(</span><span class='o'>)</span>, <span class='nf'>withr</span><span class='o'>(</span><span class='o'>)</span>, check <span class='o'>=</span> <span class='kc'>FALSE</span><span class='o'>)</span><span class='o'>[</span><span class='m'>1</span><span class='o'>:</span><span class='m'>8</span><span class='o'>]</span></span>
<span><span class='c'>#&gt; # A tibble: 2 × 8</span></span>
<span><span class='c'>#&gt;   expression      min median `itr/sec` mem_alloc `gc/sec` n_itr  n_gc</span></span>
<span><span class='c'>#&gt;   &lt;bch:expr&gt; &lt;bch:tm&gt; &lt;bch:&gt;     &lt;dbl&gt; &lt;bch:byt&gt;    &lt;dbl&gt; &lt;int&gt; &lt;dbl&gt;</span></span>
<span><span class='c'>#&gt; 1 base()            0   82ns  7329829.        0B       0  10000     0</span></span>
<span><span class='c'>#&gt; 2 withr()      2.95µs  3.4µs   280858.        0B     225.  9992     8</span></span></code></pre>
</div>
<p>Of course 






<a href="https://rdrr.io/r/base/on.exit.html" target="_blank" rel="noopener"><code>on.exit()</code></a>
 is still much faster, in part because <code>defer()</code> supports more features (more on that below), but mostly because <code>on.exit</code> is a primitive function whereas <code>defer()</code> is implemented as a normal R function. That said, we hope that we now have made <code>defer()</code> (and the <code>local_</code> and <code>with_</code> functions that use it) sufficiently fast to be used even in performance-critical micro-tools.</p>
<h2 id="improved-withr-features">Improved withr features
</h2>
<p>Over the successive releases of withr we&rsquo;ve improved the behaviour of cleanup expressions interactively, in scripts executed with 






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






<a href="https://rdrr.io/r/base/on.exit.html" target="_blank" rel="noopener"><code>on.exit()</code></a>
 is a bit inconsistent when it is used outside of a function:</p>
<ul>
<li>Interactively, it doesn&rsquo;t do anything.</li>
<li>In 






<a href="https://rdrr.io/r/base/source.html" target="_blank" rel="noopener"><code>source()</code></a>
 and in knitr, it runs immediately instead of a the end of the script</li>
</ul>
<p>






<a href="https://withr.r-lib.org/reference/defer.html" target="_blank" rel="noopener"><code>withr::defer()</code></a>
 and the 






<a href="https://withr.r-lib.org/reference/with_.html" target="_blank" rel="noopener"><code>withr::local_</code></a>
 helpers try to be more helpful for these cases.</p>
<p>Interactively, it saves the cleanup action in a special global hook and you get information about how to actually perform the cleanup:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nv'>file</span> <span class='o'>&lt;-</span> <span class='nf'>withr</span><span class='nf'>::</span><span class='nf'><a href='https://withr.r-lib.org/reference/with_tempfile.html'>local_tempfile</a></span><span class='o'>(</span><span class='o'>)</span></span>
<span><span class='c'>#&gt; Setting global deferred event(s).</span></span>
<span><span class='c'>#&gt; i These will be run:</span></span>
<span><span class='c'>#&gt;   * Automatically, when the R session ends.</span></span>
<span><span class='c'>#&gt;   * On demand, if you call `withr::deferred_run()`.</span></span>
<span><span class='c'>#&gt; i Use `withr::deferred_clear()` to clear them without executing.</span></span>
<span></span>
<span><span class='c'># Clean up now</span></span>
<span><span class='nf'>withr</span><span class='nf'>::</span><span class='nf'><a href='https://withr.r-lib.org/reference/defer.html'>deferred_run</a></span><span class='o'>(</span><span class='o'>)</span></span>
<span><span class='c'>#&gt; Ran 1/1 deferred expressions</span></span></code></pre>
</div>
<p>In knitr or 






<a href="https://rdrr.io/r/base/source.html" target="_blank" rel="noopener"><code>source()</code></a>
<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup>, the cleanup is performed at the end of the document or of the script. If you need chunk-level cleanup, use 






<a href="https://rdrr.io/r/base/eval.html" target="_blank" rel="noopener"><code>local()</code></a>
 as we&rsquo;ve been doing in the examples of this blog post:</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-md" data-lang="md"><span class="line"><span class="cl">Cleaning up at the end of the document:
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="s">```r
</span></span></span><span class="line"><span class="cl"><span class="n">document_wide_file</span> <span class="o">&lt;-</span> <span class="n">withr</span><span class="o">::</span><span class="nf">local_tempfile</span><span class="p">()</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">Cleaning up at the end of the chunk:
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="s">```r
</span></span></span><span class="line"><span class="cl"><span class="nf">local</span><span class="p">({</span>
</span></span><span class="line"><span class="cl">  <span class="n">local_file</span> <span class="o">&lt;-</span> <span class="n">withr</span><span class="o">::</span><span class="nf">local_tempfile</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="s">```</span></span></span></code></pre></td></tr></table>
</div>
</div></div>
<p>Starting from withr 3.0.0, you can also run <code>deferred_run()</code> inside of a chunk:</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-md" data-lang="md"><span class="line"><span class="cl"><span class="s">```r
</span></span></span><span class="line"><span class="cl"><span class="n">withr</span><span class="o">::</span><span class="nf">deferred_run</span><span class="p">()</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; Ran 1/1 deferred expressions</span>
</span></span><span class="line"><span class="cl"><span class="s">```</span></span></span></code></pre></td></tr></table>
</div>
</div></div>
<h2 id="acknowledgements">Acknowledgements
</h2>
<p>Thanks to the github contributors who helped us with this release!</p>
<p>






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






<a href="https://github.com/vanhry" target="_blank" rel="noopener">@vanhry</a>
.</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>






<a href="https://rdrr.io/r/base/source.html" target="_blank" rel="noopener"><code>source()</code></a>
 is only supported by default when running in the global environment, which is usually the case. For the special case of sourcing in a local environment, you need to set <code>options(withr.hook_source = TRUE)</code> first.&#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/2024-01-18_withr-3-0-0/thumbnail-wd.jpg" length="251649" type="image/jpeg" />
    </item>
    <item>
      <title>roxygen2 7.3.0</title>
      <link>https://opensource.posit.co/blog/2024-01-11_roxygen2-7-3-0/</link>
      <pubDate>Thu, 11 Jan 2024 00:00:00 +0000</pubDate>
      <guid>https://opensource.posit.co/blog/2024-01-11_roxygen2-7-3-0/</guid>
      <dc:creator>Hadley Wickham</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 well pleased to announce the release of 






<a href="http://roxygen2.r-lib.org/" target="_blank" rel="noopener">roxygen2</a>
 7.3.0. roxygen2 allows you to write specially formatted R comments that generate R documentation files (<code>man/*.Rd</code>) and the <code>NAMESPACE</code> file. roxygen2 is used by over 13,000 CRAN packages.</p>
<p>You can install it 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='s'>"roxygen2"</span><span class='o'>)</span></span></code></pre>
</div>
<p>There are four major improvements in this release:</p>
<ul>
<li>
<p>The <code>NAMESPACE</code> roclet now reports if you have S3 methods that are missing an <code>@export</code> tag. All S3 methods need to be <code>@export</code>ed even if the generic is not. This avoids rare, but hard to debug, problems. If you think this is giving a false positive, 






<a href="https://github.com/r-lib/roxygen2/issues/new" target="_blank" rel="noopener">please file an issue</a>
 and suppress the warning with <code>@exportS3Method NULL</code>.</p>
<p>I&rsquo;ve also considerably revamped the documentation for S3 methods in 


  
  
  





<a href="https://roxygen2.r-lib.org/dev/articles/namespace.html#s3" target="_blank" rel="noopener"><code>vignette(&quot;namespace&quot;)</code></a>
. The docs now discuss what exporting an S3 method really means, and why it would be technically better to call it <em>registering</em> the method.</p>
</li>
<li>
<p>Finally, the <code>NAMESPACE</code> roclet once again regenerates imports <em>before</em> loading package code and parsing roxygen blocks. This has been the goal for a 






<a href="https://github.com/r-lib/roxygen2/issues/372" target="_blank" rel="noopener">long time</a>
, but we accidentally broke it when adding support for code execution in markdown blocks. This change resolves a family of problems where you somehow bork your <code>NAMESPACE</code> and can&rsquo;t easily get fix it because you can&rsquo;t re-document the package because you can&rsquo;t load your package because your <code>NAMESPACE</code> is borked.</p>
</li>
<li>
<p><code>@docType package</code> now works like 


  
  
  





<a href="https://roxygen2.r-lib.org/articles/rd-other.html#packages" target="_blank" rel="noopener"><code>&quot;_PACKAGE&quot;</code></a>
, including creating a <code>{packagename}-package</code> alias automatically. This resolves a bug introduced in roxygen2 7.0.0 that meant that many packages lacked the correct alias for their package documentation topic.</p>
</li>
<li>
<p><code>&quot;_PACKAGE&quot;</code> does a better job of automatically generating aliases. In particular, it will no longer generate a duplicate alias if you have a function with the same name as your package (like 






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






<a href="https://reprex.tidyverse.org/reference/reprex.html" target="_blank" rel="noopener"><code>reprex::reprex()</code></a>
). If you&rsquo;ve previously had to hack around this bug, you can now delete any custom <code>@aliases</code> tags associated with the <code>&quot;_PACKAGE&quot;</code> docs.</p>
</li>
</ul>
<p>You can see a full list of other minor improvements and bug fixes in the 






<a href="https://github.com/r-lib/roxygen2/releases/tag/v7.3.0" target="_blank" rel="noopener">release notes</a>
.</p>
<h2 id="acknowledgements">Acknowledgements
</h2>
<p>A big thanks to the 46 folks who helped make this release possible through their thoughtful questions and carefully crafted code! 






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






<a href="https://github.com/trusch139" target="_blank" rel="noopener">@trusch139</a>
.</p>
]]></description>
      <enclosure url="https://opensource.posit.co/blog/2024-01-11_roxygen2-7-3-0/thumbnail-wd.jpg" length="161750" type="image/jpeg" />
    </item>
    <item>
      <title>Three ways errors are about to get better in tidymodels</title>
      <link>https://opensource.posit.co/blog/2023-11-10_tidymodels-errors-q4/</link>
      <pubDate>Fri, 10 Nov 2023 00:00:00 +0000</pubDate>
      <guid>https://opensource.posit.co/blog/2023-11-10_tidymodels-errors-q4/</guid>
      <dc:creator>Simon Couch</dc:creator><description><![CDATA[<p>Twice a year, the tidymodels team comes together for &ldquo;spring cleaning,&rdquo; a week-long project devoted to package maintenance. Ahead of the week, we come up with a list of maintenance tasks that we&rsquo;d like to see consistently implemented across our packages. Many of these tasks can be completed by running one usethis function, while others are much more involved, like issue triage.<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup> In tidymodels, triaging issues in our core packages helps us to better understand common ways that users struggle to wrap their heads around an API choice we&rsquo;ve made or find the information they need. So, among other things, refinements to the wording of our error messages is a common output of our spring cleanings. This blog post will call out three kinds of changes to our erroring that came out of this spring cleaning:</p>
<ul>
<li>Improving existing errors: 


  
  
  





<a href="#outcome">The outcome went missing</a>
</li>
<li>Do something where we once did nothing: 


  
  
  





<a href="#predict">Predicting with things that can&rsquo;t predict</a>
</li>
<li>Make a place and point to it: 


  
  
  





<a href="#model">Model formulas</a>
</li>
</ul>
<p>To demonstrate, we&rsquo;ll walk through some examples using the tidymodels packages:</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://tidymodels.tidymodels.org'>tidymodels</a></span><span class='o'>)</span></span>
<span><span class='c'>#&gt; ── <span style='font-weight: bold;'>Attaching packages</span> ──────────────────────────── tidymodels 1.1.1 ──</span></span>
<span></span><span><span class='c'>#&gt; <span style='color: #00BB00;'>✔</span> <span style='color: #0000BB;'>broom       </span> 1.0.5          <span style='color: #00BB00;'>✔</span> <span style='color: #0000BB;'>recipes     </span> 1.0.8.<span style='color: #BB0000;'>9000</span></span></span>
<span><span class='c'>#&gt; <span style='color: #00BB00;'>✔</span> <span style='color: #0000BB;'>dials       </span> 1.2.0          <span style='color: #00BB00;'>✔</span> <span style='color: #0000BB;'>rsample     </span> 1.2.0     </span></span>
<span><span class='c'>#&gt; <span style='color: #00BB00;'>✔</span> <span style='color: #0000BB;'>dplyr       </span> 1.1.3          <span style='color: #00BB00;'>✔</span> <span style='color: #0000BB;'>tibble      </span> 3.2.1     </span></span>
<span><span class='c'>#&gt; <span style='color: #00BB00;'>✔</span> <span style='color: #0000BB;'>ggplot2     </span> 3.4.4          <span style='color: #00BB00;'>✔</span> <span style='color: #0000BB;'>tidyr       </span> 1.3.0     </span></span>
<span><span class='c'>#&gt; <span style='color: #00BB00;'>✔</span> <span style='color: #0000BB;'>infer       </span> 1.0.5          <span style='color: #00BB00;'>✔</span> <span style='color: #0000BB;'>tune        </span> 1.1.2.<span style='color: #BB0000;'>9000</span></span></span>
<span><span class='c'>#&gt; <span style='color: #00BB00;'>✔</span> <span style='color: #0000BB;'>modeldata   </span> 1.2.0          <span style='color: #00BB00;'>✔</span> <span style='color: #0000BB;'>workflows   </span> 1.1.3     </span></span>
<span><span class='c'>#&gt; <span style='color: #00BB00;'>✔</span> <span style='color: #0000BB;'>parsnip     </span> 1.1.1.<span style='color: #BB0000;'>9001</span>     <span style='color: #00BB00;'>✔</span> <span style='color: #0000BB;'>workflowsets</span> 1.0.1     </span></span>
<span><span class='c'>#&gt; <span style='color: #00BB00;'>✔</span> <span style='color: #0000BB;'>purrr       </span> 1.0.2          <span style='color: #00BB00;'>✔</span> <span style='color: #0000BB;'>yardstick   </span> 1.2.0</span></span>
<span></span><span><span class='c'>#&gt; ── <span style='font-weight: bold;'>Conflicts</span> ─────────────────────────────── tidymodels_conflicts() ──</span></span>
<span><span class='c'>#&gt; <span style='color: #BB0000;'>✖</span> <span style='color: #0000BB;'>purrr</span>::<span style='color: #00BB00;'>discard()</span> masks <span style='color: #0000BB;'>scales</span>::discard()</span></span>
<span><span class='c'>#&gt; <span style='color: #BB0000;'>✖</span> <span style='color: #0000BB;'>dplyr</span>::<span style='color: #00BB00;'>filter()</span>  masks <span style='color: #0000BB;'>stats</span>::filter()</span></span>
<span><span class='c'>#&gt; <span style='color: #BB0000;'>✖</span> <span style='color: #0000BB;'>dplyr</span>::<span style='color: #00BB00;'>lag()</span>     masks <span style='color: #0000BB;'>stats</span>::lag()</span></span>
<span><span class='c'>#&gt; <span style='color: #BB0000;'>✖</span> <span style='color: #0000BB;'>recipes</span>::<span style='color: #00BB00;'>step()</span>  masks <span style='color: #0000BB;'>stats</span>::step()</span></span>
<span><span class='c'>#&gt; <span style='color: #0000BB;'>•</span> Use suppressPackageStartupMessages() to eliminate package startup messages</span></span>
<span></span></code></pre>
</div>
<p>Note that my installed versions include the current dev version of a few tidymodels packages. You can install those versions with:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nf'>pak</span><span class='nf'>::</span><span class='nf'><a href='https://pak.r-lib.org/reference/pak.html'>pak</a></span><span class='o'>(</span><span class='nf'><a href='https://rdrr.io/r/base/paste.html'>paste0</a></span><span class='o'>(</span><span class='s'>"tidymodels/"</span>, <span class='nf'><a href='https://rdrr.io/r/base/c.html'>c</a></span><span class='o'>(</span><span class='s'>"tune"</span>, <span class='s'>"parsnip"</span>, <span class='s'>"recipes"</span><span class='o'>)</span><span class='o'>)</span><span class='o'>)</span></span></code></pre>
</div>
<h2 id="the-outcome-went-missing-">The outcome went missing 👻
</h2>
<p>The tidymodels packages focus on <em>supervised</em> machine learning problems, predicting the value of an outcome using predictors.<sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup> For example, in the code:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nv'>linear_spec</span> <span class='o'>&lt;-</span> <span class='nf'>linear_reg</span><span class='o'>(</span><span class='o'>)</span></span>
<span></span>
<span><span class='nv'>linear_fit</span> <span class='o'>&lt;-</span> <span class='nf'>fit</span><span class='o'>(</span><span class='nv'>linear_spec</span>, <span class='nv'>mpg</span> <span class='o'>~</span> <span class='nv'>hp</span>, <span class='nv'>mtcars</span><span class='o'>)</span></span></code></pre>
</div>
<p>The <code>mpg</code> variable is the outcome. There are many ways that an analyst may mistakenly fail to pass an outcome. In the most straightforward case, they might omit the outcome on the LHS of the formula:</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">fit</span><span class="p">(</span><span class="n">linear_spec</span><span class="p">,</span> <span class="o">~</span> <span class="n">hp</span><span class="p">,</span> <span class="n">mtcars</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; Error in lm.fit(x, y, offset = offset, singular.ok = singular.ok, ...) : </span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;   incompatible dimensions</span></span></span></code></pre></td></tr></table>
</div>
</div></div>
<p>In this case, parsnip used to defer to the modeling engine to raise an error, which may or may not be informative.</p>
<p>There are many less obvious ways an analyst may mistakenly supply no outcome variable. For example, try spotting the issue in the following code, defining a recipe to perform principal component analysis (PCA) on the numeric variables in the data before fitting 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><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="n">mtcars_rec</span> <span class="o">&lt;-</span>
</span></span><span class="line"><span class="cl">  <span class="nf">recipe</span><span class="p">(</span><span class="n">mpg</span> <span class="o">~</span> <span class="n">.,</span> <span class="n">mtcars</span><span class="p">)</span> <span class="o">%&gt;%</span>
</span></span><span class="line"><span class="cl">  <span class="nf">step_pca</span><span class="p">(</span><span class="nf">all_numeric</span><span class="p">())</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">workflow</span><span class="p">(</span><span class="n">mtcars_rec</span><span class="p">,</span> <span class="n">linear_spec</span><span class="p">)</span> <span class="o">%&gt;%</span> <span class="nf">fit</span><span class="p">(</span><span class="n">mtcars</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; Error: object &#39;.&#39; not found</span></span></span></code></pre></td></tr></table>
</div>
</div></div>
<p>A head-scratcher! To help diagnose what&rsquo;s happening here, we could first try seeing what data is actually being passed to the model.</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nv'>mtcars_rec_trained</span> <span class='o'>&lt;-</span></span>
<span>  <span class='nv'>mtcars_rec</span> <span class='o'>%&gt;%</span> </span>
<span>  <span class='nf'>prep</span><span class='o'>(</span><span class='nv'>mtcars</span><span class='o'>)</span> </span>
<span></span>
<span><span class='nv'>mtcars_rec_trained</span> <span class='o'>%&gt;%</span> <span class='nf'>bake</span><span class='o'>(</span><span class='kc'>NULL</span><span class='o'>)</span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'># A tibble: 32 × 5</span></span></span>
<span><span class='c'>#&gt;      PC1   PC2    PC3     PC4    PC5</span></span>
<span><span class='c'>#&gt;    <span style='color: #555555; font-style: italic;'>&lt;dbl&gt;</span> <span style='color: #555555; font-style: italic;'>&lt;dbl&gt;</span>  <span style='color: #555555; font-style: italic;'>&lt;dbl&gt;</span>   <span style='color: #555555; font-style: italic;'>&lt;dbl&gt;</span>  <span style='color: #555555; font-style: italic;'>&lt;dbl&gt;</span></span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'> 1</span> -<span style='color: #BB0000;'>195.</span>  12.8 -<span style='color: #BB0000;'>11.4</span>   0.016<span style='text-decoration: underline;'>4</span>  2.17 </span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'> 2</span> -<span style='color: #BB0000;'>195.</span>  12.9 -<span style='color: #BB0000;'>11.7</span>  -<span style='color: #BB0000;'>0.479</span>   2.11 </span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'> 3</span> -<span style='color: #BB0000;'>142.</span>  25.9 -<span style='color: #BB0000;'>16.0</span>  -<span style='color: #BB0000;'>1.34</span>   -<span style='color: #BB0000;'>1.18</span> </span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'> 4</span> -<span style='color: #BB0000;'>279.</span> -<span style='color: #BB0000;'>38.3</span> -<span style='color: #BB0000;'>14.0</span>   0.157  -<span style='color: #BB0000;'>0.817</span></span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'> 5</span> -<span style='color: #BB0000;'>399.</span> -<span style='color: #BB0000;'>37.3</span>  -<span style='color: #BB0000;'>1.38</span>  2.56   -<span style='color: #BB0000;'>0.444</span></span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'> 6</span> -<span style='color: #BB0000;'>248.</span> -<span style='color: #BB0000;'>25.6</span> -<span style='color: #BB0000;'>12.2</span>  -<span style='color: #BB0000;'>3.01</span>   -<span style='color: #BB0000;'>1.08</span> </span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'> 7</span> -<span style='color: #BB0000;'>435.</span>  20.9  13.9   0.801  -<span style='color: #BB0000;'>0.916</span></span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'> 8</span> -<span style='color: #BB0000;'>160.</span> -<span style='color: #BB0000;'>20.0</span> -<span style='color: #BB0000;'>23.3</span>  -<span style='color: #BB0000;'>1.06</span>    0.787</span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'> 9</span> -<span style='color: #BB0000;'>172.</span>  10.8 -<span style='color: #BB0000;'>18.3</span>  -<span style='color: #BB0000;'>4.40</span>   -<span style='color: #BB0000;'>0.836</span></span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>10</span> -<span style='color: #BB0000;'>209.</span>  19.7  -<span style='color: #BB0000;'>8.94</span> -<span style='color: #BB0000;'>2.58</span>    1.33 </span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'># ℹ 22 more rows</span></span></span>
<span></span></code></pre>
</div>
<p>Mmm. What happened to <code>mpg</code>? We mistakenly told <code>step_pca()</code> to perform PCA on <em>all</em> of the numeric variables, not just the numeric <em>predictors</em>! As a result, it incorporated <code>mpg</code> into the principal components, removing each of the original numeric variables after the fact. Rewriting using the correct tidyselect specification <code>all_numeric_predictors()</code>:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nv'>mtcars_rec_new</span> <span class='o'>&lt;-</span> </span>
<span>  <span class='nf'>recipe</span><span class='o'>(</span><span class='nv'>mpg</span> <span class='o'>~</span> <span class='nv'>.</span>, <span class='nv'>mtcars</span><span class='o'>)</span> <span class='o'>%&gt;%</span></span>
<span>  <span class='nf'>step_pca</span><span class='o'>(</span><span class='nf'>all_numeric_predictors</span><span class='o'>(</span><span class='o'>)</span><span class='o'>)</span></span>
<span></span>
<span><span class='nf'>workflow</span><span class='o'>(</span><span class='nv'>mtcars_rec_new</span>, <span class='nv'>linear_spec</span><span class='o'>)</span> <span class='o'>%&gt;%</span> <span class='nf'>fit</span><span class='o'>(</span><span class='nv'>mtcars</span><span class='o'>)</span></span>
<span><span class='c'>#&gt; ══ Workflow [trained] ════════════════════════════════════════════════</span></span>
<span><span class='c'>#&gt; <span style='font-style: italic;'>Preprocessor:</span> Recipe</span></span>
<span><span class='c'>#&gt; <span style='font-style: italic;'>Model:</span> linear_reg()</span></span>
<span><span class='c'>#&gt; </span></span>
<span><span class='c'>#&gt; ── Preprocessor ──────────────────────────────────────────────────────</span></span>
<span><span class='c'>#&gt; 1 Recipe Step</span></span>
<span><span class='c'>#&gt; </span></span>
<span><span class='c'>#&gt; • step_pca()</span></span>
<span><span class='c'>#&gt; </span></span>
<span><span class='c'>#&gt; ── Model ─────────────────────────────────────────────────────────────</span></span>
<span><span class='c'>#&gt; </span></span>
<span><span class='c'>#&gt; Call:</span></span>
<span><span class='c'>#&gt; stats::lm(formula = ..y ~ ., data = data)</span></span>
<span><span class='c'>#&gt; </span></span>
<span><span class='c'>#&gt; Coefficients:</span></span>
<span><span class='c'>#&gt; (Intercept)          PC1          PC2          PC3          PC4  </span></span>
<span><span class='c'>#&gt;    43.39293      0.07609     -0.05266      0.57892      0.94890  </span></span>
<span><span class='c'>#&gt;         PC5  </span></span>
<span><span class='c'>#&gt;    -1.72569</span></span>
<span></span></code></pre>
</div>
<p>Works like a charm. That error we saw previously could be much more helpful, though. With the current developmental version of parsnip, this looks like:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nf'>fit</span><span class='o'>(</span><span class='nv'>linear_spec</span>, <span class='o'>~</span> <span class='nv'>hp</span>, <span class='nv'>mtcars</span><span class='o'>)</span></span>
<span><span class='c'>#&gt; <span style='color: #BBBB00; font-weight: bold;'>Error</span><span style='font-weight: bold;'>:</span></span></span>
<span><span class='c'>#&gt; <span style='color: #BBBB00;'>!</span> `linear_reg()` was unable to find an outcome.</span></span>
<span><span class='c'>#&gt; <span style='color: #00BBBB;'>ℹ</span> Ensure that you have specified an outcome column and that it hasn't</span></span>
<span><span class='c'>#&gt;   been removed in pre-processing.</span></span>
<span></span></code></pre>
</div>
<p>Or, with workflows:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nf'>workflow</span><span class='o'>(</span><span class='nv'>mtcars_rec</span>, <span class='nv'>linear_spec</span><span class='o'>)</span> <span class='o'>%&gt;%</span> <span class='nf'>fit</span><span class='o'>(</span><span class='nv'>mtcars</span><span class='o'>)</span></span>
<span><span class='c'>#&gt; <span style='color: #BBBB00; font-weight: bold;'>Error</span><span style='font-weight: bold;'>:</span></span></span>
<span><span class='c'>#&gt; <span style='color: #BBBB00;'>!</span> `linear_reg()` was unable to find an outcome.</span></span>
<span><span class='c'>#&gt; <span style='color: #00BBBB;'>ℹ</span> Ensure that you have specified an outcome column and that it hasn't</span></span>
<span><span class='c'>#&gt;   been removed in pre-processing.</span></span>
<span></span></code></pre>
</div>
<p>Much better.</p>
<h2 id="predicting-with-things-that-cant-predict">Predicting with things that can&rsquo;t predict
</h2>
<p>Earlier this year, Dr. Louise E. Sinks put out a 






<a href="https://lsinks.github.io/posts/2023-04-10-tidymodels/tidymodels_tutorial.html" target="_blank" rel="noopener">wonderful blog post</a>
 documenting what it felt like to approach the various object types defined in the tidymodels as a newcomer to the collection of packages. They wrote:</p>
<blockquote>
<p>I found it confusing that <code>fit</code>, <code>last_fit</code>, <code>fit_resamples</code>, etc., did not all produce objects that contained the same information and could be acted on by the same functions.</p>
</blockquote>
<p>This makes sense. While we try to forefront the intended mental model for fitting and predicting with tidymodels in our APIs and documentation, we also need to be proactive in anticipating common challenges in constructing that mental model.</p>
<p>For example, we&rsquo;ve found that it&rsquo;s sometimes not clear to users which outputs they can call 






<a href="https://rdrr.io/r/stats/predict.html" target="_blank" rel="noopener"><code>predict()</code></a>
 on. One such situation, as Louise points out, is with <code>fit_resamples()</code>:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='c'># fit a linear regression model to bootstrap resamples of mtcars</span></span>
<span><span class='nv'>mtcars_res</span> <span class='o'>&lt;-</span> <span class='nf'>fit_resamples</span><span class='o'>(</span><span class='nf'>linear_reg</span><span class='o'>(</span><span class='o'>)</span>, <span class='nv'>mpg</span> <span class='o'>~</span> <span class='nv'>.</span>, <span class='nf'>bootstraps</span><span class='o'>(</span><span class='nv'>mtcars</span><span class='o'>)</span><span class='o'>)</span></span>
<span></span>
<span><span class='nv'>mtcars_res</span></span>
<span><span class='c'>#&gt; # Resampling results</span></span>
<span><span class='c'>#&gt; # Bootstrap sampling </span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'># A tibble: 25 × 4</span></span></span>
<span><span class='c'>#&gt;    splits          id          .metrics         .notes          </span></span>
<span><span class='c'>#&gt;    <span style='color: #555555; font-style: italic;'>&lt;list&gt;</span>          <span style='color: #555555; font-style: italic;'>&lt;chr&gt;</span>       <span style='color: #555555; font-style: italic;'>&lt;list&gt;</span>           <span style='color: #555555; font-style: italic;'>&lt;list&gt;</span>          </span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'> 1</span> <span style='color: #555555;'>&lt;split [32/11]&gt;</span> Bootstrap01 <span style='color: #555555;'>&lt;tibble [2 × 4]&gt;</span> <span style='color: #555555;'>&lt;tibble [0 × 3]&gt;</span></span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'> 2</span> <span style='color: #555555;'>&lt;split [32/10]&gt;</span> Bootstrap02 <span style='color: #555555;'>&lt;tibble [2 × 4]&gt;</span> <span style='color: #555555;'>&lt;tibble [0 × 3]&gt;</span></span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'> 3</span> <span style='color: #555555;'>&lt;split [32/16]&gt;</span> Bootstrap03 <span style='color: #555555;'>&lt;tibble [2 × 4]&gt;</span> <span style='color: #555555;'>&lt;tibble [0 × 3]&gt;</span></span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'> 4</span> <span style='color: #555555;'>&lt;split [32/11]&gt;</span> Bootstrap04 <span style='color: #555555;'>&lt;tibble [2 × 4]&gt;</span> <span style='color: #555555;'>&lt;tibble [0 × 3]&gt;</span></span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'> 5</span> <span style='color: #555555;'>&lt;split [32/10]&gt;</span> Bootstrap05 <span style='color: #555555;'>&lt;tibble [2 × 4]&gt;</span> <span style='color: #555555;'>&lt;tibble [0 × 3]&gt;</span></span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'> 6</span> <span style='color: #555555;'>&lt;split [32/13]&gt;</span> Bootstrap06 <span style='color: #555555;'>&lt;tibble [2 × 4]&gt;</span> <span style='color: #555555;'>&lt;tibble [0 × 3]&gt;</span></span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'> 7</span> <span style='color: #555555;'>&lt;split [32/16]&gt;</span> Bootstrap07 <span style='color: #555555;'>&lt;tibble [2 × 4]&gt;</span> <span style='color: #555555;'>&lt;tibble [0 × 3]&gt;</span></span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'> 8</span> <span style='color: #555555;'>&lt;split [32/11]&gt;</span> Bootstrap08 <span style='color: #555555;'>&lt;tibble [2 × 4]&gt;</span> <span style='color: #555555;'>&lt;tibble [0 × 3]&gt;</span></span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'> 9</span> <span style='color: #555555;'>&lt;split [32/11]&gt;</span> Bootstrap09 <span style='color: #555555;'>&lt;tibble [2 × 4]&gt;</span> <span style='color: #555555;'>&lt;tibble [0 × 3]&gt;</span></span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>10</span> <span style='color: #555555;'>&lt;split [32/10]&gt;</span> Bootstrap10 <span style='color: #555555;'>&lt;tibble [2 × 4]&gt;</span> <span style='color: #555555;'>&lt;tibble [0 × 3]&gt;</span></span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'># ℹ 15 more rows</span></span></span>
<span></span></code></pre>
</div>
<p>With previous tidymodels versions, mistakenly trying to predict with this object resulted in the following output:</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-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">predict</span><span class="p">(</span><span class="n">mtcars_res</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; Error in UseMethod(&#34;predict&#34;) : </span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;   no applicable method for &#39;predict&#39; applied to an object of class</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;   &#34;c(&#39;resample_results&#39;, &#39;tune_results&#39;, &#39;tbl_df&#39;, &#39;tbl&#39;, &#39;data.frame&#39;)&#34;</span></span></span></code></pre></td></tr></table>
</div>
</div></div>
<p>Some R developers may recognize this error as what results when we didn&rsquo;t define any 






<a href="https://rdrr.io/r/stats/predict.html" target="_blank" rel="noopener"><code>predict()</code></a>
 method for <code>tune_results</code> objects. We didn&rsquo;t do so because prediction isn&rsquo;t well-defined for tuning results. <em>But</em>, this error message does little to help a user understand why that&rsquo;s the case.</p>
<p>We&rsquo;ve recently made some changes to error more informatively in this case. We do so by defining a &ldquo;dummy&rdquo; 






<a href="https://rdrr.io/r/stats/predict.html" target="_blank" rel="noopener"><code>predict()</code></a>
 method for tuning results, implemented only for the sake of erroring more informatively. The same code will now give the following output:</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">predict</span><span class="p">(</span><span class="n">mtcars_res</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; Error in `predict()`:</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; ! `predict()` is not well-defined for tuning results.</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; ℹ To predict with the optimal model configuration from tuning</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;   results, ensure that the tuning result was generated with the</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;   control option `save_workflow = TRUE`, run `fit_best()`, and</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;   then predict using `predict()` on its output.</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; ℹ To collect predictions from tuning results, ensure that the</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;   tuning result was generated with the control option `save_pred</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;   = TRUE` and run `collect_predictions()`.</span></span></span></code></pre></td></tr></table>
</div>
</div></div>
<p>References to important concepts or functions, like 






<a href="https://tune.tidymodels.org/reference/control_grid.html" target="_blank" rel="noopener">control options</a>
, 






<a href="https://tune.tidymodels.org/reference/fit_best.html?q=fit_best" target="_blank" rel="noopener"><code>fit_best()</code></a>
, and 






<a href="https://tune.tidymodels.org/reference/collect_predictions.html?q=collect" target="_blank" rel="noopener"><code>collect_predictions()</code></a>
, link to the help-files for those functions using 






<a href="https://cli.r-lib.org/reference/cli_abort.html" target="_blank" rel="noopener">cli&rsquo;s erroring tools</a>
.</p>
<p>We hope new error messages like this will help to get folks back on track.</p>
<h2 id="model-formulas">Model formulas
</h2>
<p>In R, formulas provide a compact, symbolic notation to specify model terms. Many modeling functions in R make use of &ldquo;specials,&rdquo; or nonstandard notations used in formulas. Specials are defined and handled as a special case by a given modeling package. parsnip defers to engine packages to handle specials, so you can work with them as usual. For example, the mgcv package provides support for generalized additive models in R, and defines a special called <code>s()</code> to indicate smoothing terms. You can interface with it via tidymodels like so:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='c'># define a generalized additive model specification</span></span>
<span><span class='nv'>gam_spec</span> <span class='o'>&lt;-</span> <span class='nf'>gen_additive_mod</span><span class='o'>(</span><span class='s'>"regression"</span><span class='o'>)</span></span>
<span></span>
<span><span class='c'># fit the specification using a formula with specials</span></span>
<span><span class='nf'>fit</span><span class='o'>(</span><span class='nv'>gam_spec</span>, <span class='nv'>mpg</span> <span class='o'>~</span> <span class='nv'>cyl</span> <span class='o'>+</span> <span class='nf'>s</span><span class='o'>(</span><span class='nv'>disp</span>, k <span class='o'>=</span> <span class='m'>5</span><span class='o'>)</span>, <span class='nv'>mtcars</span><span class='o'>)</span></span>
<span><span class='c'>#&gt; parsnip model object</span></span>
<span><span class='c'>#&gt; </span></span>
<span><span class='c'>#&gt; </span></span>
<span><span class='c'>#&gt; Family: gaussian </span></span>
<span><span class='c'>#&gt; Link function: identity </span></span>
<span><span class='c'>#&gt; </span></span>
<span><span class='c'>#&gt; Formula:</span></span>
<span><span class='c'>#&gt; mpg ~ cyl + s(disp, k = 5)</span></span>
<span><span class='c'>#&gt; </span></span>
<span><span class='c'>#&gt; Estimated degrees of freedom:</span></span>
<span><span class='c'>#&gt; 3.39  total = 5.39 </span></span>
<span><span class='c'>#&gt; </span></span>
<span><span class='c'>#&gt; GCV score: 6.380152</span></span>
<span></span></code></pre>
</div>
<p>While parsnip can handle specials just fine, the package is often used in conjunction with the greater tidymodels package ecosystem, which defines its own pre-processing infrastructure and functionality via packages like hardhat and recipes. The specials defined in many modeling packages introduce conflicts with that infrastructure. To support specials while also maintaining consistent syntax elsewhere in the ecosystem, <strong>tidymodels delineates between two types of formulas: preprocessing formulas and model formulas</strong>. Preprocessing formulas determine the input variables, while model formulas determine the model structure.</p>
<p>This is a tricky abstraction, and one that users have tripped up on in the past. Users could generate all sorts of different errors by 1) mistakenly passing model formulas where preprocessing formulas were expected, or 2) forgetting to pass a model formula where it&rsquo;s needed. For an example of 1), we could pass recipes the same formula we passed to parsnip:</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-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">recipe</span><span class="p">(</span><span class="n">mpg</span> <span class="o">~</span> <span class="n">cyl</span> <span class="o">+</span> <span class="nf">s</span><span class="p">(</span><span class="n">disp</span><span class="p">,</span> <span class="n">k</span> <span class="o">=</span> <span class="m">5</span><span class="p">),</span> <span class="n">mtcars</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; Error in `inline_check()`:</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; ! No in-line functions should be used here; use steps to </span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;   define baking actions.</span></span></span></code></pre></td></tr></table>
</div>
</div></div>
<p>But we <em>just</em> used a special with another tidymodels function! Rude!</p>
<p>Or, to demonstrate 2), we pass the preprocessing formula as we ought to but forget to provide the model formula:</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="n">gam_wflow</span> <span class="o">&lt;-</span> 
</span></span><span class="line"><span class="cl">  <span class="nf">workflow</span><span class="p">()</span> <span class="o">%&gt;%</span>
</span></span><span class="line"><span class="cl">  <span class="nf">add_formula</span><span class="p">(</span><span class="n">mpg</span> <span class="o">~</span> <span class="n">.)</span> <span class="o">%&gt;%</span>
</span></span><span class="line"><span class="cl">  <span class="nf">add_model</span><span class="p">(</span><span class="n">gam_spec</span><span class="p">)</span> 
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">gam_wflow</span> <span class="o">%&gt;%</span> <span class="nf">fit</span><span class="p">(</span><span class="n">mtcars</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; Error in `fit_xy()`:</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; ! `fit()` must be used with GAM models (due to its use of formulas).</span></span></span></code></pre></td></tr></table>
</div>
</div></div>
<p>Uh, but I <em>did</em> just use <code>fit()</code>!</p>
<p>Since the distinction between model formulas and preprocessor formulas comes up in functions across tidymodels, we decide to create a 






<a href="https://parsnip.tidymodels.org/dev/reference/model_formula.html" target="_blank" rel="noopener">central page</a>
 that documents the concept itself, hopefully making the syntax associated with it come more easily to users. Then, we link to it <em>all over the place</em>. For example, those errors now look like:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nf'>recipe</span><span class='o'>(</span><span class='nv'>mpg</span> <span class='o'>~</span> <span class='nv'>cyl</span> <span class='o'>+</span> <span class='nf'>s</span><span class='o'>(</span><span class='nv'>disp</span>, k <span class='o'>=</span> <span class='m'>5</span><span class='o'>)</span>, <span class='nv'>mtcars</span><span class='o'>)</span></span>
<span><span class='c'>#&gt; <span style='color: #BBBB00; font-weight: bold;'>Error</span><span style='font-weight: bold;'> in `inline_check()`:</span></span></span>
<span><span class='c'>#&gt; <span style='color: #BB0000;'>✖</span> No in-line functions should be used here.</span></span>
<span><span class='c'>#&gt; <span style='color: #00BBBB;'>ℹ</span> The following function was found: `s`.</span></span>
<span><span class='c'>#&gt; <span style='color: #00BBBB;'>ℹ</span> Use steps to do transformations instead.</span></span>
<span><span class='c'>#&gt; <span style='color: #00BBBB;'>ℹ</span> If your modeling engine uses special terms in formulas, pass that</span></span>
<span><span class='c'>#&gt;   formula to workflows as a model formula</span></span>
<span><span class='c'>#&gt;   (`?parsnip::model_formula()`).</span></span>
<span></span></code></pre>
</div>
<p>Or:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nv'>gam_wflow</span> <span class='o'>%&gt;%</span> <span class='nf'>fit</span><span class='o'>(</span><span class='nv'>mtcars</span><span class='o'>)</span></span>
<span><span class='c'>#&gt; <span style='color: #BBBB00; font-weight: bold;'>Error</span><span style='font-weight: bold;'>:</span></span></span>
<span><span class='c'>#&gt; <span style='color: #BBBB00;'>!</span> When working with generalized additive models, please supply</span></span>
<span><span class='c'>#&gt;   the model specification to `workflows::add_model()` along with a</span></span>
<span><span class='c'>#&gt;   `formula` argument.</span></span>
<span><span class='c'>#&gt; <span style='color: #00BBBB;'>ℹ</span> See `?parsnip::model_formula()` to learn more.</span></span>
<span></span></code></pre>
</div>
<p>While I&rsquo;ve only outlined three, there are all sorts of improvements to error messages on their way to the tidymodels packages in upcoming releases. If you happen to stumble across them, we hope they quickly set you back on the right path. 🗺</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>Issue triage consists of categorizing, prioritizing, and consolidating issues in a repository&rsquo;s issue tracker.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:2">
<p>See the 






<a href="https://tidyclust.tidymodels.org" target="_blank" rel="noopener">tidyclust</a>
 package for unsupervised learning with tidymodels!&#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/2023-11-10_tidymodels-errors-q4/thumbnail-wd.jpg" length="650472" type="image/jpeg" />
    </item>
    <item>
      <title>testthat 3.2.0</title>
      <link>https://opensource.posit.co/blog/2023-10-08_testthat-3-2-0/</link>
      <pubDate>Sun, 08 Oct 2023 00:00:00 +0000</pubDate>
      <guid>https://opensource.posit.co/blog/2023-10-08_testthat-3-2-0/</guid>
      <dc:creator>Hadley Wickham</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 chuffed to announce the release of 






<a href="http://testthat.r-lib.org/" target="_blank" rel="noopener">testthat</a>
 3.2.0. testthat makes it easy to turn your existing informal tests into formal, automated tests that you can rerun quickly and easily. testthat is the most popular unit-testing package for R, and is used by almost 9,000 CRAN and Bioconductor packages. You can learn more about unit testing at 






<a href="https://r-pkgs.org/tests.html" target="_blank" rel="noopener">https://r-pkgs.org/tests.html</a>
.</p>
<p>You can install it 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='s'>"testthat"</span><span class='o'>)</span></span></code></pre>
</div>
<p>testthat 3.2.0 includes relatively few new features but there have been nine patch releases since testthat 3.1.0. These patch releases contained a bunch of experiments that we now believe are ready for the world. So this blog post summarises the changes in 






<a href="https://github.com/r-lib/testthat/releases/tag/v3.1.1" target="_blank" rel="noopener">3.1.1</a>
, 






<a href="https://github.com/r-lib/testthat/releases/tag/v3.1.2" target="_blank" rel="noopener">3.1.2</a>
, 






<a href="https://github.com/r-lib/testthat/releases/tag/v3.1.3" target="_blank" rel="noopener">3.1.3</a>
, 






<a href="https://github.com/r-lib/testthat/releases/tag/v3.1.4" target="_blank" rel="noopener">3.1.4</a>
, 






<a href="https://github.com/r-lib/testthat/releases/tag/v3.1.5" target="_blank" rel="noopener">3.1.5</a>
, 






<a href="https://github.com/r-lib/testthat/releases/tag/v3.1.6" target="_blank" rel="noopener">3.1.6</a>
, 






<a href="https://github.com/r-lib/testthat/releases/tag/v3.1.7" target="_blank" rel="noopener">3.1.7</a>
, 






<a href="https://github.com/r-lib/testthat/releases/tag/v3.1.8" target="_blank" rel="noopener">3.1.8</a>
, 






<a href="https://github.com/r-lib/testthat/releases/tag/v3.1.9" target="_blank" rel="noopener">3.1.9</a>
, and 






<a href="https://github.com/r-lib/testthat/releases/tag/v3.1.10" target="_blank" rel="noopener">3.1.10</a>
 over the last two years.</p>
<p>Here we&rsquo;ll focus on the biggest news: new expectations, tweaks to the way that error snapshots are reported, support for mocking, a new way to detect if a test has changed global state, and a bunch of smaller UI improvements.</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://testthat.r-lib.org'>testthat</a></span><span class='o'>)</span></span></code></pre>
</div>
<h2 id="documentation">Documentation
</h2>
<p>The first and most important thing to point out is that the second edition of 






<a href="https://r-pkgs.org" target="_blank" rel="noopener">R Packages</a>
 contains updated and much expanded coverage of testing. Coverage of testing is now split up over three chapters:</p>
<ul>
<li>






<a href="https://r-pkgs.org/testing-basics.html" target="_blank" rel="noopener">Testing basics</a>
</li>
<li>






<a href="https://r-pkgs.org/testing-design.html" target="_blank" rel="noopener">Designing your test suite</a>
</li>
<li>






<a href="https://r-pkgs.org/testing-advanced.html" target="_blank" rel="noopener">Advanced testing techniques</a>
</li>
</ul>
<p>There&rsquo;s also a new vignette about special files (






<a href="https://testthat.r-lib.org/articles/special-files.html" target="_blank" rel="noopener"><code>vignette(&quot;special-files&quot;)</code></a>
) which describes the various special files that you find in <code>tests/testthat</code> and when you might need to use them.</p>
<h2 id="new-expectations">New expectations
</h2>
<p>There are a handful of notable new expectations. 






<a href="https://testthat.r-lib.org/reference/expect_setequal.html" target="_blank" rel="noopener"><code>expect_contains()</code></a>
 and 






<a href="https://testthat.r-lib.org/reference/expect_setequal.html" target="_blank" rel="noopener"><code>expect_in()</code></a>
 work similarly to <code>expect_true(all(expected %in% object))</code> or <code>expect_true(all(object %in% expected))</code> but give more informative failure messages:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nv'>fruits</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'>"apple"</span>, <span class='s'>"banana"</span>, <span class='s'>"pear"</span><span class='o'>)</span></span>
<span><span class='nf'><a href='https://testthat.r-lib.org/reference/expect_setequal.html'>expect_contains</a></span><span class='o'>(</span><span class='nv'>fruits</span>, <span class='s'>"apple"</span><span class='o'>)</span></span>
<span><span class='nf'><a href='https://testthat.r-lib.org/reference/expect_setequal.html'>expect_contains</a></span><span class='o'>(</span><span class='nv'>fruits</span>, <span class='s'>"pineapple"</span><span class='o'>)</span></span>
<span><span class='c'>#&gt; Error: `fruits` (`actual`) doesn't fully contain all the values in "pineapple" (`expected`).</span></span>
<span><span class='c'>#&gt; * Missing from `actual`: "pineapple"</span></span>
<span><span class='c'>#&gt; * Present in `actual`:   "apple", "banana", "pear"</span></span>
<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/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='o'>)</span></span>
<span><span class='nf'><a href='https://testthat.r-lib.org/reference/expect_setequal.html'>expect_in</a></span><span class='o'>(</span><span class='nv'>x</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='o'>)</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/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'>NA</span>, <span class='kc'>FALSE</span><span class='o'>)</span></span>
<span><span class='nf'><a href='https://testthat.r-lib.org/reference/expect_setequal.html'>expect_in</a></span><span class='o'>(</span><span class='nv'>x</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='o'>)</span><span class='o'>)</span></span>
<span><span class='c'>#&gt; Error: `x` (`actual`) isn't fully contained within c(TRUE, FALSE) (`expected`).</span></span>
<span><span class='c'>#&gt; * Missing from `expected`: NA</span></span>
<span><span class='c'>#&gt; * Present in `expected`:   TRUE, FALSE</span></span>
<span></span></code></pre>
</div>
<p>






<a href="https://testthat.r-lib.org/reference/expect_no_error.html" target="_blank" rel="noopener"><code>expect_no_error()</code></a>
, 






<a href="https://testthat.r-lib.org/reference/expect_no_error.html" target="_blank" rel="noopener"><code>expect_no_warning()</code></a>
, and 






<a href="https://testthat.r-lib.org/reference/expect_no_error.html" target="_blank" rel="noopener"><code>expect_no_message()</code></a>
 make it easier (and clearer) to confirm that code runs without errors, warnings, or messages. The default fails if there is any error/warning/message, but you can optionally supply either the <code>message</code> or <code>class</code> arguments to confirm the absence of a specific error/warning/message.</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nv'>foo</span> <span class='o'>&lt;-</span> <span class='kr'>function</span><span class='o'>(</span><span class='nv'>x</span><span class='o'>)</span> <span class='o'>&#123;</span></span>
<span>  <span class='kr'>if</span> <span class='o'>(</span><span class='nv'>x</span> <span class='o'>&lt;</span> <span class='m'>0</span><span class='o'>)</span> <span class='o'>&#123;</span></span>
<span>    <span class='nv'>x</span> <span class='o'>+</span> <span class='s'>"10"</span></span>
<span>  <span class='o'>&#125;</span> <span class='kr'>else</span> <span class='o'>&#123;</span></span>
<span>    <span class='nv'>x</span> <span class='o'>=</span> <span class='m'>20</span></span>
<span>  <span class='o'>&#125;</span></span>
<span><span class='o'>&#125;</span></span>
<span></span>
<span><span class='nf'><a href='https://testthat.r-lib.org/reference/expect_no_error.html'>expect_no_error</a></span><span class='o'>(</span><span class='nf'>foo</span><span class='o'>(</span><span class='o'>-</span><span class='m'>10</span><span class='o'>)</span><span class='o'>)</span></span>
<span><span class='c'>#&gt; Error: Expected `foo(-10)` to run without any errors.</span></span>
<span><span class='c'>#&gt; <span style='color: #0000BB;'>ℹ</span> Actually got a &lt;simpleError&gt; with text:</span></span>
<span><span class='c'>#&gt;   non-numeric argument to binary operator</span></span>
<span></span><span></span>
<span><span class='c'># No difference here but will lead to a better failure later</span></span>
<span><span class='c'># once you've fixed this problem and later introduce a new one</span></span>
<span><span class='nf'><a href='https://testthat.r-lib.org/reference/expect_no_error.html'>expect_no_error</a></span><span class='o'>(</span><span class='nf'>foo</span><span class='o'>(</span><span class='o'>-</span><span class='m'>10</span><span class='o'>)</span>, message <span class='o'>=</span> <span class='s'>"non-numeric argument"</span><span class='o'>)</span></span>
<span><span class='c'>#&gt; Error: Expected `foo(-10)` to run without any errors matching pattern 'non-numeric argument'.</span></span>
<span><span class='c'>#&gt; <span style='color: #0000BB;'>ℹ</span> Actually got a &lt;simpleError&gt; with text:</span></span>
<span><span class='c'>#&gt;   non-numeric argument to binary operator</span></span>
<span></span></code></pre>
</div>
<h2 id="snapshotting-changes">Snapshotting changes
</h2>
<p><code>expect_snapshot(error = TRUE)</code> has a new display of error messages that strives to be closer to what you see interactively. In particular, you&rsquo;ll no longer see the error class and you will now see the error call.</p>
<ul>
<li>
<p>Old display:</p>
<pre><code>Code
  f()
Error &lt;simpleError&gt;
  baz
</code></pre>
</li>
<li>
<p>New display:</p>
<pre><code>Code
  f()
Condition
  Error in `f()`:
  ! baz
</code></pre>
</li>
</ul>
<p>If you have used <code>expect_snapshot(error = TRUE)</code> in your package, this means that you will need to re-run and approve your snapshots. We hope this is not too annoying and we believe it is worth it given the more accurate reflection of generated error messages. This will not affect checks on CRAN because, by default, snapshot tests are not run on CRAN.</p>
<h2 id="mocking">Mocking
</h2>
<p>Mocking<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup> is a tool for temporarily replacing the implementation of a function in order to make testing easier. Sometimes when testing a function, one part of it is challenging to run in your test environment (maybe it requires human interaction, a live database connection, or maybe it just takes a long time to run). For example, take the following imaginary function. It has a bunch of straightforward computation that would be easy to test but right in the middle of the function it calls <code>complicated()</code> which is hard to test:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nv'>my_function</span> <span class='o'>&lt;-</span> <span class='kr'>function</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 class='o'>&#123;</span></span>
<span>  <span class='nv'>a</span> <span class='o'>&lt;-</span> <span class='nf'>f</span><span class='o'>(</span><span class='nv'>x</span>, <span class='nv'>y</span><span class='o'>)</span></span>
<span>  <span class='nv'>b</span> <span class='o'>&lt;-</span> <span class='nf'>g</span><span class='o'>(</span><span class='nv'>y</span>, <span class='nv'>z</span><span class='o'>)</span></span>
<span>  <span class='nv'>c</span> <span class='o'>&lt;-</span> <span class='nf'>h</span><span class='o'>(</span><span class='nv'>a</span>, <span class='nv'>b</span><span class='o'>)</span></span>
<span>  </span>
<span>  <span class='nv'>d</span> <span class='o'>&lt;-</span> <span class='nf'>complicated</span><span class='o'>(</span><span class='nv'>c</span><span class='o'>)</span></span>
<span>  </span>
<span>  <span class='nf'>i</span><span class='o'>(</span><span class='nv'>d</span>, <span class='m'>1</span>, <span class='kc'>TRUE</span><span class='o'>)</span></span>
<span><span class='o'>&#125;</span></span></code></pre>
</div>
<p>Mocking allows you to temporarily replace <code>complicated()</code> with something simpler, allowing you to test the rest of the function. testthat now supports mocking with 






<a href="https://testthat.r-lib.org/reference/local_mocked_bindings.html" target="_blank" rel="noopener"><code>local_mocked_bindings()</code></a>
, which temporarily replaces the implementation of a function. For example, to test <code>my_function()</code> you might write something like this:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nf'><a href='https://testthat.r-lib.org/reference/test_that.html'>test_that</a></span><span class='o'>(</span><span class='s'>"my_function() returns expected result"</span>, <span class='o'>&#123;</span></span>
<span>  <span class='nf'><a href='https://testthat.r-lib.org/reference/local_mocked_bindings.html'>local_mocked_bindings</a></span><span class='o'>(</span></span>
<span>    complicated <span class='o'>=</span> <span class='kr'>function</span><span class='o'>(</span><span class='nv'>x</span><span class='o'>)</span> <span class='kc'>TRUE</span></span>
<span>  <span class='o'>)</span></span>
<span>  <span class='nv'>...</span></span>
<span><span class='o'>&#125;</span><span class='o'>)</span></span></code></pre>
</div>
<p>testthat has a complicated past with mocking. testthat introduced 






<a href="https://testthat.r-lib.org/reference/with_mock.html" target="_blank" rel="noopener"><code>with_mock()</code></a>
 in v0.9 (way back in 2014), but we started discovering problems with the implementation in v2.0.0 (2017) leading to its deprecation in v3.0.0 (2020). A few packages arose to fill the gap (like 






<a href="https://github.com/r-lib/mockery" target="_blank" rel="noopener">mockery</a>
, 






<a href="https://krlmlr.github.io/mockr/" target="_blank" rel="noopener">mockr</a>
, and 






<a href="https://nbenn.github.io/mockthat/" target="_blank" rel="noopener">mockthat</a>
) but none of their implementations were completely satisfactory. Earlier this year a new approach occurred to me that avoids many of the problems of the previous approaches. This is now implemented in 






<a href="https://testthat.r-lib.org/reference/local_mocked_bindings.html" target="_blank" rel="noopener"><code>with_mocked_bindings()</code></a>
 and 






<a href="https://testthat.r-lib.org/reference/local_mocked_bindings.html" target="_blank" rel="noopener"><code>local_mocked_bindings()</code></a>
; we&rsquo;ve been using these new functions for a few months now without problems, and it feels like time to announce to the world.</p>
<h2 id="state-inspector">State inspector
</h2>
<p>In times gone by it was very easy to accidentally change the state of the world in a test:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nf'><a href='https://testthat.r-lib.org/reference/test_that.html'>test_that</a></span><span class='o'>(</span><span class='s'>"side-by-side diffs work"</span>, <span class='o'>&#123;</span></span>
<span>  <span class='nf'><a href='https://rdrr.io/r/base/options.html'>options</a></span><span class='o'>(</span>width <span class='o'>=</span> <span class='m'>20</span><span class='o'>)</span></span>
<span>  <span class='nf'><a href='https://testthat.r-lib.org/reference/expect_snapshot.html'>expect_snapshot</a></span><span class='o'>(</span></span>
<span>    <span class='nf'>waldo</span><span class='nf'>::</span><span class='nf'><a href='https://waldo.r-lib.org/reference/compare.html'>compare</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'>"X"</span>, <span class='nv'>letters</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='nv'>letters</span>, <span class='s'>"X"</span><span class='o'>)</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>
<p>When you look at a single test it&rsquo;s easy to spot the problem, and switch to a more appropriate way of temporarily changing the options, like 






<a href="https://withr.r-lib.org/reference/with_options.html" target="_blank" rel="noopener"><code>withr::local_options()</code></a>
. But sometimes this mistake crept in a long time ago and is now hiding amongst hundreds or thousands of tests.</p>
<p>In earlier versions of testthat, finding tests that accidentally changed the world was painful: the only way was to painstakingly review each test. Now you can use 






<a href="https://testthat.r-lib.org/reference/set_state_inspector.html" target="_blank" rel="noopener"><code>set_state_inspector()</code></a>
 to register a function that&rsquo;s called before and after every test. If the function returns different values, testthat will let you know. You&rsquo;ll typically do this either in <code>tests/testhat/setup.R</code> or an existing helper file.</p>
<p>So, for example, to detect if any of your tests have modified options you could use this state inspector:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nf'><a href='https://testthat.r-lib.org/reference/set_state_inspector.html'>set_state_inspector</a></span><span class='o'>(</span><span class='kr'>function</span><span class='o'>(</span><span class='o'>)</span> <span class='o'>&#123;</span></span>
<span>  <span class='nf'><a href='https://rdrr.io/r/base/list.html'>list</a></span><span class='o'>(</span>options <span class='o'>=</span> <span class='nf'><a href='https://rdrr.io/r/base/options.html'>options</a></span><span class='o'>(</span><span class='o'>)</span><span class='o'>)</span></span>
<span><span class='o'>&#125;</span><span class='o'>)</span></span></code></pre>
</div>
<p>Or maybe you&rsquo;ve seen an <code>R CMD check</code> warning that you&rsquo;ve forgotten to close a connection:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nf'><a href='https://testthat.r-lib.org/reference/set_state_inspector.html'>set_state_inspector</a></span><span class='o'>(</span><span class='kr'>function</span><span class='o'>(</span><span class='o'>)</span> <span class='o'>&#123;</span></span>
<span>  <span class='nf'><a href='https://rdrr.io/r/base/list.html'>list</a></span><span class='o'>(</span>connections <span class='o'>=</span> <span class='nf'><a href='https://rdrr.io/r/base/nrow.html'>nrow</a></span><span class='o'>(</span><span class='nf'><a href='https://rdrr.io/r/base/showConnections.html'>showConnections</a></span><span class='o'>(</span><span class='o'>)</span><span class='o'>)</span><span class='o'>)</span></span>
<span><span class='o'>&#125;</span><span class='o'>)</span></span></code></pre>
</div>
<p>And you can of course combine multiple checks just by returning a more complicated list.</p>
<h2 id="ui-improvements">UI improvements
</h2>
<p>testthat 3.2.0 includes a bunch of minor user interface improvements that should make day-to-day use of testthat more enjoyable. Some of our favourite highlights are:</p>
<ul>
<li>Parallel testing now works much better with snapshot tests. (And updates to the processx package means that testthat no longer leaves processes around if you terminate a test process early.)</li>
<li>We use an improved algorithm to find the source reference associated with an expectation/error/warning/skip. We now look for the most recent call (within inside 






<a href="https://testthat.r-lib.org/reference/test_that.html" target="_blank" rel="noopener"><code>test_that()</code></a>
 that has known source. This generally gives more specific locations than the previous approach and gives much better locations if an error occurs in an exit handler.</li>
<li>Tracebacks are no longer truncated and we use rlang&rsquo;s default tree display; this should make it easier to track down problems when testing in non-interactive contexts.</li>
<li>Assuming you have a recent RStudio, test failures are now clickable, taking you to the line where the problem occurred. Similarly, when a snapshot test changes, you can now click that suggested code to run the appropriate 






<a href="https://testthat.r-lib.org/reference/snapshot_accept.html" target="_blank" rel="noopener"><code>snapshot_accept()</code></a>
 call.</li>
<li>Skips are now only shown at the end of reporter summaries, not as tests are run. This makes them less intrusive in interactive tests while still allowing you to verify that the correct tests are skipped.</li>
</ul>
<h2 id="acknowledgements">Acknowledgements
</h2>
<p>A big thanks to all 127 contributors who helped make these last 10 release of testthat happen, whether it be through contributed code or filing issues: 






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






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






<a href="https://github.com/zsigmas" target="_blank" rel="noopener">@zsigmas</a>
.</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>Think mimicking, like a mockingbird, not making fun of.&#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/2023-10-08_testthat-3-2-0/thumbnail-wd.jpg" length="273742" type="image/jpeg" />
    </item>
  </channel>
</rss>
