What’s New and Migrating Older Code
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
Checked against R 4.6.1 (2026-06-24) on 2026-10-20; R 4.6.2 is scheduled for 2026-10-29. Re-checked on 2026-10-22: R 4.6.2 ("Diversified Portfolio") is at release-candidate stage and is scheduled for 2026-10-29, so nothing below is a claim about 4.6.2 itself; the section on upcoming changes only says what the R-devel NEWS lists for it. The page is base R, with three add-on packages used in one example each: magrittr 2.0.5 (the %>% pipe that older code uses), S7 0.2.2 (one example of the OBJSXP type) and lintr 3.4.0 (the linters that find old idioms, which also checked every script). R changes slowly and on purpose: a feature is announced in NEWS, usually deprecated before it is removed, and the manuals are updated with the release. This page puts the changes of seven releases, R 4.0.0 (2020) to R 4.6.1 (2026), in one place: a table of every language or base-R change with a link to the page of this reference that explains it, a section per release with the code that shows the difference, the removals that stop an old script, the table of habits that older tutorials still teach, the changes that R-devel lists for the next releases, and the ways to follow them.
Every example is a script that was run with Rscript on R 4.6.1 with warnings printed as they happen (options(warn = 1)), and the lines that start with #> are what it printed. The scripts also pass the default lintr linters; a # nolint: comment marks a line where the idiom on show is the one a default linter dislikes. The names books, loans, shelf_today, loan_days come from the running library-shelf scenario described on the section index: a script that needs them starts with source("scenario.R"). Because only R 4.6.1 was available, a "before" line is one of three kinds. It is an older idiom that still runs on 4.6.1 and is run (the stringsAsFactors = TRUE argument, a hand-written %||%, a magrittr pipe). Or it is an older form that now fails, caught with tryCatch() so that the script exits normally and the real 4.6.1 message is shown. Or it is a statement taken from NEWS and not run. No block claims output from R 4.0 to 4.5, which were not run for this page. Where a message contains quotation marks, they are the typographic quotes R prints in a UTF-8 locale.
This page does not repeat the pages it points to. Each row of the main table links to the page that documents the feature in depth, and most of those pages end with a == Version notes section that lists the NEWS entries of that topic; the facts here agree with them. The pitfalls that are not tied to a release (partial matching with $, 1:length(x), sapply() simplification, if (NA), growing vectors and the rest) are in Idioms and Common Pitfalls.
How R versions are numbered and released
R uses a major.minor.patchlevel numbering scheme, and the R FAQ says where to find the versions in use: r-release is the current release, r-patched is the current release plus bug fixes, and r-devel is the version heading for the next minor or major release, where new features are introduced (R FAQ 2.3: What is the current version of R?). The R Core release schedule is annual: a new x.y.0 release in spring, patch releases as needed, and a final patch release of the previous version shortly before the next x.y.0 (R Developer Page: release schedule). The dates in the next table are those of the source tarballs on CRAN, listed for the R 4 series at CRAN: source tarballs of R 4; R 4.6.1 is the version that every manual says it describes.
| Series | x.y.0 released | Last patch release |
|---|---|---|
R 4.0 |
2020-04-24 |
4.0.5 (2021-03-31) |
R 4.1 |
2021-05-18 |
4.1.3 (2022-03-10) |
R 4.2 |
2022-04-22 |
4.2.3 (2023-03-15) |
R 4.3 |
2023-04-21 |
4.3.3 (2024-02-29) |
R 4.4 |
2024-04-24 |
4.4.3 (2025-02-28) |
R 4.5 |
2025-04-11 |
4.5.3 (2026-03-11) |
R 4.6 |
2026-04-24 |
4.6.1 (2026-06-24), the current release; 4.6.2 is scheduled for 2026-10-29 |
Source: CRAN: source tarballs of R 4
Two habits of the project explain the shape of the changes below. First, a change that can break existing code is usually announced before it happens. The sequences that appear on this page are: && and || with an operand of length greater than one gave a warning in R 4.2.0 and are an error since R 4.3.0; default.stringsAsFactors() was deprecated in 4.2.0 and is defunct since 4.3.0; is.R() was deprecated in 4.4.0 and is defunct since 4.5.0; the xfig() device was deprecated in 4.4.0 and removed in 4.5.0; and structure(NULL, ...) was deprecated in R 3.4.0 and is defunct since 4.6.0. The rule for if and while followed the same path over a longer time: a warning in the earlier releases and an error since R 4.2.0. The functions .Deprecated() and .Defunct() that signal these conditions are in Defensive Programming and Input Validation. Second, a new feature that is not settled is labelled as experimental, in NEWS or on its help page, and this page repeats that label: NEWS calls the use of _ to start an extraction chain (R 4.3.0), Tailcall() and Exec() and declare() (R 4.4.0) experimental, and the help page of use() (available since R 4.4.0) says that its functionality is still experimental. Experimental features may be changed or dropped, so a package that has to run on many R versions should not build on them.
Every change at a glance
The table lists the changes of R 4.0.0 to R 4.6.1 that matter to code written in the language or with base R, each with the older form, the current form and the page of this reference that explains it. The entries come from the NEWS files (R NEWS), and three of them carry a qualification that the NEWS itself makes: the use of _ to start an extraction chain (R 4.3.0) is "an experimental feature", Tailcall(), Exec() and declare() (R 4.4.0) are experimental, and use() is listed in the R 4.5.0 NEWS with the remark that it was "actually already available since R 4.4.0". The "Before" column is what older code and older tutorials write; it does not say that the older form stopped working, and the sections that follow show which ones did.
| Version | Change | Before | Now | Explained on |
|---|---|---|---|---|
4.0.0 |
|
|
Strings stay character; |
|
4.0.0 |
Raw strings |
Backslashes and quotes escaped one by one |
|
|
4.0.0 |
Matrices also inherit from |
|
|
|
4.0.0 |
Reference counting replaces NAMED |
NAMED decided in base C code when an object could be changed in place |
Reference counting; R code sees no difference |
|
4.0.0 |
New default |
The old, more saturated colours |
Less saturated colours; the old ones are |
|
4.0.0 |
|
|
|
|
4.1.0 |
Native pipe |
The |
|
|
4.1.0 |
Function shorthand |
|
|
|
4.1.0 |
|
|
|
|
4.2.0 |
|
A warning; the first element was used |
An error |
|
4.2.0 |
|
The first element was used without a word |
A warning in 4.2.0; an error since 4.3.0 (next rows) |
|
4.2.0 |
Pipe placeholder |
The magrittr dot, |
|
|
4.2.0 |
UTF-8 is the native encoding on Windows (UCRT) |
A Windows code page, so scripts differed between Windows and Linux |
UTF-8 with the UCRT C runtime on recent Windows (not run here) |
|
4.2.0 |
|
|
|
|
4.3.0 |
|
A warning (R 4.2.x) |
An error: |
|
4.3.0 |
|
|
|
|
4.4.0 |
|
A helper defined in every project, or the one from rlang |
|
|
4.4.0 |
|
Deep recursion, ending in a stack error |
A tail call that reuses the stack frame |
|
4.4.0 |
|
|
|
|
4.4.0 |
|
|
|
|
4.4.0 |
|
Nothing |
A primitive that ignores its arguments; the syntax of declarations is still being developed |
this page; the byte compiler it is meant for is in Vectorisation and Iteration |
4.4.0 |
|
One type name for S4 objects |
|
|
4.4.0 |
|
|
|
|
4.5.0 |
|
|
|
|
4.5.0 |
|
Not in R’s |
Both are in the |
|
4.5.0 |
|
(see 4.4.0) |
The entry says "actually already available since R 4.4.0" |
|
4.5.0 |
|
|
|
Connections and the File System ( |
4.5.0 |
|
|
An error; delete the test |
|
4.5.0 |
A C23 compiler is selected by default |
C17, the compiler’s default standard |
C23 where the compiler supports it; |
|
4.6.0 |
|
|
|
|
4.6.0 |
|
A condition assembled with |
|
|
4.6.0 |
|
|
|
|
4.6.0 |
|
|
Several values, every one of which must match |
|
4.6.0 |
|
|
|
|
4.6.0 |
|
|
The grid is drawn first |
|
4.6.0 |
C++20 is the default C++ standard |
C++17 |
C++20 where available, otherwise C++17 |
|
4.6.0 |
The citation of R has the DOI |
A citation without a DOI |
|
|
4.6.1 |
Bug fixes only |
(see the R 4.6.1 section) |
|
|
R-devel (4.7.0, upcoming) |
|
Not released |
Prose only, may change before 4.7.0 |
R 4.0.0 (2020-04-24)
R 4.0.0 changed the defaults and the internals more than any later release of the series, and the NEWS opens it with the reminder that packages with compiled code needed to be reinstalled under the new version. For code written in R, five of its changes are visible every day: strings stay strings, raw string constants exist, a matrix has two classes, the default colours changed, and list2DF() is a new way to build a data frame. A sixth, reference counting, changed how R decides when to copy and is invisible from R code.
stringsAsFactors = FALSE is the default
The NEWS entry of R 4.0.0 says that R now uses a stringsAsFactors = FALSE default, "and hence by default no longer converts strings to factors" in calls to data.frame() and read.table(), and adds that a large number of packages had relied on the previous behaviour and needed updating. read.csv() is read.table() with different defaults, so it follows. The older behaviour is still one argument away, which is how the first block reproduces it: stringsAsFactors = TRUE gives the nine-level factor that every older tutorial printed. With factors, adding a value that is not a level is a warning and an NA, the error message of the old days that this change retired for most scripts. The function default.stringsAsFactors() that old code used to read the default was deprecated in R 4.2.0 and is defunct since R 4.3.0, and the last line of the block shows its real message. The page Data Frames explains the argument and Factors explains what a factor is for.
# Since R 4.0.0 strings stay strings.
books <- read.csv("books.csv")
class(books$genre)
#> [1] "character"
df <- data.frame(title = c("The Long Table", "Rivers of Glass"))
class(df$title)
#> [1] "character"
# Before R 4.0.0 the default was TRUE; the old behaviour is one argument away.
old <- read.csv("books.csv", stringsAsFactors = TRUE)
class(old$genre)
#> [1] "factor"
nlevels(old$genre)
#> [1] 9
# With factors, a value that is not a level is a warning and an NA.
tryCatch(
{
old$genre[1L] <- "Cookery"
old$genre[1L]
},
warning = \(w) conditionMessage(w)
)
#> [1] "invalid factor level, NA generated"
# default.stringsAsFactors() was deprecated in 4.2.0 and is defunct since 4.3.0.
tryCatch(default.stringsAsFactors(), error = \(e) conditionMessage(e))
#> [1] "'default.stringsAsFactors' is defunct.\nUse '`stringsAsFactors = FALSE`' instead.\nSee help(\"Defunct\")"
Source: R NEWS: Changes in R 4.0.0
Raw string constants
R 4.0.0 added a syntax for raw character constants similar to the one of C++: r"(...)", where the dots are any character sequence that does not contain the characters )". The NEWS says it makes it easier to write strings that contain backslashes or both single and double quotes, and refers to the help page of Quotes. The brackets may also be [] or {}, a capital R works as well as a small one, and a run of dashes between the r and the opening bracket (and again before the closing quote) lengthens the delimiter, so that a string may contain )" itself; the block shows the dashes. Raw strings matter most for Windows paths and regular expressions, where every backslash otherwise has to be doubled. They are a parser feature: the value is an ordinary character vector, as the identical() calls show. The syntax is explained in Syntax and Lexical Structure and used for patterns in Strings and Regular Expressions.
# A Windows path needs every backslash doubled in an ordinary string.
path_old <- "C:\\shelf\\books.csv"
path_raw <- r"(C:\shelf\books.csv)"
identical(path_old, path_raw)
#> [1] TRUE
nchar(path_raw)
#> [1] 18
# Both kinds of quote can appear without escapes.
quote_old <- "She said \"hello\" and 'goodbye'"
quote_raw <- r"(She said "hello" and 'goodbye')"
identical(quote_old, quote_raw)
#> [1] TRUE
# Dashes between the r and the bracket make a longer delimiter, so that the
# text may contain )" itself.
r"---(a )" inside a raw string)---"
#> [1] "a )\" inside a raw string"
# Regular expressions are where raw strings pay off.
iso_old <- "^\\d{4}-\\d{2}-\\d{2}$"
iso_raw <- r"(^\d{4}-\d{2}-\d{2}$)"
identical(iso_old, iso_raw)
#> [1] TRUE
grepl(iso_raw, c("2026-06-30", "30/06/2026"))
#> [1] TRUE FALSE
Source: R documentation: Quotes
Matrices also inherit from "array"
Since R 4.0.0 a matrix has two classes, c("matrix", "array"). The NEWS entry warns that this "invalidates code incorrectly assuming that" the class of a matrix has length one, and adds that S3 methods for class "array" are now dispatched for matrices. The classic casualty is a test such as if (class(m) == "matrix"): with a condition of length two it was a warning before R 4.2.0 and is an error since R 4.2.0, as the fourth line of output shows. The correct tests are is.matrix() for "is this a matrix" and inherits() for a class vector. The lintr linter class_equals_linter() finds the old form (see the migration section). The page Matrices and Arrays covers the class vector in detail.
m <- matrix(1:6, nrow = 2)
class(m)
#> [1] "matrix" "array"
inherits(m, "matrix")
#> [1] TRUE
is.matrix(m)
#> [1] TRUE
# Code written for R 3.x compared the whole class vector with a string.
tryCatch(
if (class(m) == "matrix") "a matrix",
error = \(e) conditionMessage(e)
)
#> [1] "the condition has length > 1"
# Ask the question directly instead.
if (is.matrix(m)) "a matrix"
#> [1] "a matrix"
# S3 methods for "array" are also dispatched for matrices.
describe <- function(x) UseMethod("describe")
describe.array <- function(x) "the array method"
describe.default <- function(x) "the default method"
describe(m)
#> [1] "the array method"
describe(1:3)
#> [1] "the default method"
Source: R documentation: class
Reference counting replaces NAMED
R 4.0.0 replaced the NAMED mechanism, with which R’s C code decided whether an object could safely be modified in place, by reference counting. The NEWS says it reduces the need for copying in some cases, should allow further optimisations, and is expected to have almost no impact on packages that use supported coding practices in their C and C++ code. For code written in R nothing changes: assignment never copies, and the copy is made when one of the two names is modified, which the block shows with the shelf’s pages column. What changed is how often R can skip the copy, and the way to observe it is tracemem(), which Memory and Copy Semantics and Assignment, Names and Values use. The header fields that hold the count are described in R Internals.
# Reference counting (R 4.0.0) is internal: what a script sees has not changed.
source("scenario.R")
pages <- books$pages
copy <- pages
copy[[1L]] <- 0L
# The copy is made at the modification; the original is untouched.
pages[[1L]]
#> [1] 387
copy[[1L]]
#> [1] 0
identical(pages[-1L], copy[-1L])
#> [1] TRUE
Source: R Internals: Rest of header
A new default palette()
The default palette() has a new set of colours that are less saturated and have better accessibility properties, according to the NEWS. The old default is kept under the name "R3", the new built-in palettes are listed by palette.pals(), and palette.colors() selects a subset of colours from any of them. Charts that use the numbers 1 to 8 as colours (col = 2) therefore changed their look in R 4.0.0 even though their code did not; code that needs the old look sets palette("R3"). The block changes the palette and sets it back, because the palette is session state. The page Base Graphics and Devices shows the colours.
# The default palette() changed in R 4.0.0; the old one is called "R3".
palette()
#> [1] "black" "#DF536B" "#61D04F" "#2297E6" "#28E2E5" "#CD0BBC" "#F5C710"
#> [8] "gray62"
palette("R3")
palette()
#> [1] "black" "red" "green3" "blue" "cyan" "magenta" "yellow"
#> [8] "gray"
palette("default")
# palette.pals() lists the named palettes that came with the change.
palette.pals()[1:5]
#> [1] "R3" "R4" "ggplot2" "Okabe-Ito" "Accent"
palette.colors(4L, palette = "Okabe-Ito")
#> [1] "#000000" "#E69F00" "#56B4E9" "#009E73"
Source: R documentation: palette
list2DF()
list2DF() creates a data frame from a list of variables. Unlike data.frame(), it does not check or rewrite the names: data.frame() turns the name loan date into loan.date and 2nd copy into X2nd.copy, and list2DF() keeps both as given. It also takes an nrow argument for the case of a list with no columns. The R 4.2.0 NEWS adds that it checks that its arguments have the same length instead of recycling. See Lists and Data Frames.
source("scenario.R")
cols <- list(title = trimws(books$title[1:3]), year = books$year[1:3])
# list2DF() (R 4.0.0) builds a data frame from a list of columns.
list2DF(cols)
#> title year
#> 1 The Dispossessed 1974
#> 2 Pride and Prejudice 1813
#> 3 the quiet streets 2019
# data.frame() checks and rewrites the names; list2DF() keeps them as given.
awkward <- list(`loan date` = 1:2, `2nd copy` = c("a", "b"))
names(data.frame(awkward))
#> [1] "loan.date" "X2nd.copy"
names(list2DF(awkward))
#> [1] "loan date" "2nd copy"
# An empty list needs the number of rows.
dim(list2DF(list(), nrow = 3L))
#> [1] 3 0
Source: R documentation: list2DF
Other changes in R 4.0.0
| Change | Detail | Page |
|---|---|---|
Reinstall packages |
Packages need to be (re-)installed under R 4.0.0, as under every x.y.0 release |
|
|
The S3 generic |
|
|
New names for |
Source: R NEWS: Changes in R 4.0.0
R 4.1.0 (2021-05-18)
R 4.1.0 added two pieces of syntax that changed how R code looks: the native pipe and the function shorthand. Both are parsed into ordinary calls, so neither adds a new kind of object, and a package that uses either one should declare Depends: R (>= 4.1.0) (R CMD build adds the field when it is missing and says so). A third change, ...names(), is a small helper for functions that take ....
The native pipe
The NEWS entry says that R now provides a simple native forward pipe syntax: the simple form inserts the left-hand side as the first argument of the right-hand side call. Before it, a pipe meant the magrittr package (2.0.5 here). The two look alike and differ in what they are. The magrittr %>% is an ordinary function that evaluates its right-hand side at run time and accepts a bare function name, an operator and the dot placeholder. The native pipe is a syntax transformation made by the parser: books |> head(3L) is read as head(books, 3L) and no pipe function is ever called, the right-hand side must be a call, and the placeholder is the underscore of the R 4.2.0 section below. The block runs both side by side and shows the transformation with quote() and the parse error for a right-hand side that is not a call. The magrittr lines carry a nolint comment because the linter pipe_consistency_linter() asks for one kind of pipe per file. Older code that works with magrittr keeps working; the page Pipes and the Native Pipe lists what has to change to move to the native pipe.
source("scenario.R")
library(magrittr)
# Before R 4.1.0 a pipe meant a package: magrittr 2.0.5 here.
books$pages %>% sum(na.rm = TRUE) # nolint: pipe_consistency_linter.
#> [1] 3755
# The native pipe is part of the language.
books$pages |> sum(na.rm = TRUE)
#> [1] 3755
# magrittr marks the place of the left-hand side with a dot; the native pipe
# uses a named placeholder (R 4.2.0).
books %>% # nolint: pipe_consistency_linter.
lm(pages ~ year, data = .) %>% # nolint: pipe_consistency_linter.
coef()
#> (Intercept) year
#> 2067.0926829 -0.8945014
books |> lm(pages ~ year, data = _) |> coef()
#> (Intercept) year
#> 2067.0926829 -0.8945014
# The native pipe is rewritten by the parser: no function is called.
quote(books |> head(3L))
#> head(books, 3L)
quote(books %>% head(3L)) # nolint: pipe_consistency_linter.
#> books %>% head(3L)
# The right-hand side must be a call.
tryCatch(parse(text = "books |> head"), error = \(e) conditionMessage(e))
#> [1] "The pipe operator requires a function call as RHS (<input>:1:10)"
Source: R documentation: pipeOp
The function shorthand
According to the NEWS, \(x) x + 1 is parsed as function(x) x + 1. The shorthand is not a new kind of function: the last identical() in the block compares the deparsed text of the two expressions and finds it equal. It is most useful where a function is passed as an argument and the word function is noise, as with vapply(). The block uses vapply() with a declared result type; the sapply() that older tutorials use for the same job can return a list or a matrix when the input changes, which is covered in Idioms and Common Pitfalls. See Operators and Precedence.
source("scenario.R")
titles <- trimws(books$title[1:4])
# Before R 4.1.0 every anonymous function was spelled out.
long_old <- vapply(titles, function(x) nchar(x) > 12L, logical(1L))
# The shorthand is parsed into the very same function call.
long_new <- vapply(titles, \(x) nchar(x) > 12L, logical(1L))
identical(long_old, long_new)
#> [1] TRUE
long_new
#> The Dispossessed Pride and Prejudice the quiet streets Loud Roofs
#> TRUE TRUE TRUE FALSE
deparse(quote(\(x) x + 1))
#> [1] "function(x) x + 1"
identical(deparse(quote(\(x) x + 1)), deparse(quote(function(x) x + 1)))
#> [1] TRUE
Source: R documentation: function
...names()
...names() returns the names of the arguments that were matched to ..., without evaluating them. Older code wrote names(list(...)), which evaluates every argument just to read the names, as the last two lines of the block show with an argument that fails when evaluated. The R 4.1.3 NEWS records that ...names() was changed to match names(list(...)) closely, so that the two agree on arguments without names (the empty string). See Functions.
# ...names() (R 4.1.0) reports the names of the arguments in `...` without
# evaluating them.
labels_new <- function(...) ...names()
labels_old <- function(...) names(list(...))
labels_new(title = "The Long Table", 1999L, genre = "Fiction")
#> [1] "title" "" "genre"
labels_old(title = "The Long Table", 1999L, genre = "Fiction")
#> [1] "title" "" "genre"
# The old idiom forces every argument; the new one does not.
tryCatch(labels_old(a = stop("evaluated")), error = \(e) conditionMessage(e))
#> [1] "evaluated"
labels_new(a = stop("evaluated"))
#> [1] "a"
Source: R documentation: dots
Other changes in R 4.1.0
| Change | Detail | Page |
|---|---|---|
|
An error when the first argument has length greater than one; before, the extra elements were silently ignored |
|
The base environment is locked |
Bindings can no longer be added to the base environment and namespace, or removed from them |
|
|
Also compares their environments, so a comparison of two functions with different environments can now be |
|
|
|
Source: R NEWS: Changes in R 4.1.0
R 4.2.0 (2022-04-22)
R 4.2.0 is the release that turned two sloppy habits of older code into errors: a condition of length greater than one in if and while, and a vector operand of && and || (a warning in R 4.2.0, an error since R 4.3.0). It also added the placeholder of the native pipe, made UTF-8 the native encoding on recent Windows, and added Sys.setLanguage().
if and while need a condition of length one
The NEWS of R 4.2.0 says that calling if() or while() with a condition of length greater than one gives an error rather than a warning, and that the environment variable _R_CHECK_LENGTH_1_CONDITION_, which made the check strict in R CMD check, no longer has any effect. Before, only the first element was used, which hid bugs: a whole column of the shelf’s loans was silently reduced to its first loan. The fix is to say what is meant, with any(), all() or isTRUE(), and to decide what an NA means; if (NA) is an error of its own (see Control Flow). The block shows the error for if and for while and the two usual repairs.
source("scenario.R")
returned <- !is.na(loans$return_date)
# R 4.2.0: if() and while() with a condition of length greater than one is an
# error. Before, it was a warning and only the first element was used.
tryCatch(
if (returned) "all returned",
error = \(e) conditionMessage(e)
)
#> [1] "the condition has length > 1"
# Say what you mean: any() or all(), with the NAs of the shelf dealt with.
if (all(returned)) "all returned" else "some loans are still out"
#> [1] "some loans are still out"
if (anyNA(loans$return_date)) "some return dates are missing"
#> [1] "some return dates are missing"
# The same rule holds for while().
tryCatch(
while (returned) break,
error = \(e) conditionMessage(e)
)
#> [1] "the condition has length > 1"
Source: R documentation: Control
The pipe placeholder
R 4.2.0 let a forward pipe use a named argument with the placeholder _ in the right-hand side call, to say where the left-hand side goes. The NEWS adds that the placeholder can appear only once, and the parser enforces both rules with messages of its own, shown in the block. The magrittr dot could be used several times and as .$name; the native placeholder is a named argument that appears once, and books |> lm(pages ~ year, data = _) is the shape to move magrittr code to. See Pipes and the Native Pipe.
source("scenario.R")
# R 4.2.0: the placeholder _ marks where the left-hand side goes. It must be
# a named argument, and it may appear once.
books |> lm(pages ~ year, data = _) |> coef()
#> (Intercept) year
#> 2067.0926829 -0.8945014
tryCatch(
parse(text = "books |> lm(pages ~ year, _)"),
error = \(e) conditionMessage(e)
)
#> [1] "pipe placeholder can only be used as a named argument (<input>:1:10)"
tryCatch(
parse(text = "books |> lm(pages ~ year, data = _, subset = _)"),
error = \(e) conditionMessage(e)
)
#> [1] "pipe placeholder may only appear once (<input>:1:10)"
Source: R documentation: pipeOp
UTF-8 on Windows
From R 4.2.0, R uses UTF-8 as the native encoding on recent Windows systems (Windows 10 version 1903 or Windows Server 2022 or later, according to the NEWS), and as part of this change it uses the UCRT as its C runtime. The effect for scripts is that most encoding differences between a script on Windows and on Linux disappear, and that packages with compiled code are built with a UCRT toolchain (Rtools42 or later). This cannot be run in the Linux container that checked this page, so it is described from the NEWS and the pages that cover it: Encodings, Locales and Internationalization, Scripts and the Command Line and Interfacing C, C++ and Fortran.
Sys.setLanguage()
Sys.setLanguage() is a new function that sets the language of messages and warnings, and the NEWS calls it "partly experimental". It sets LANGUAGE, flushes the cache of messages that have already been looked up, and returns the previous setting, which is why the block can restore it. Before, Sys.setenv(LANGUAGE = ...) was the idiom, and a message already looked up stayed in the old language. The container used for this page has the Spanish and German catalogues of R installed, so the two translated lines are real. See Encodings, Locales and Internationalization.
# R 4.2.0: Sys.setLanguage() sets the language of messages and flushes the
# translations that R has already looked up. It returns the previous language.
previous <- Sys.setLanguage("es")
tryCatch(log(-1), warning = \(w) conditionMessage(w))
#> [1] "Se han producido NaNs"
invisible(Sys.setLanguage("de"))
tryCatch(log(-1), warning = \(w) conditionMessage(w))
#> [1] "NaNs wurden erzeugt"
invisible(Sys.setLanguage(previous))
tryCatch(log(-1), warning = \(w) conditionMessage(w))
#> [1] "NaNs produced"
Source: R documentation: gettext
Other changes in R 4.2.0
| Change | Detail | Page |
|---|---|---|
|
A warning when an operand has length greater than one; an error since R 4.3.0 |
|
|
Formally deprecated; defunct in R 4.3.0 |
|
|
Checks that its arguments have the same length instead of recycling |
|
Stack overflow errors |
Signalled with the condition class |
|
|
Warns when binding vectors whose lengths do not recycle wholly |
Source: R NEWS: Changes in R 4.2.0
R 4.3.0 (2023-04-21)
R 4.3.0 finished the && and || story, extended the pipe placeholder, and made C++17 the default C++ standard. The last item matters to package authors only; the others matter to scripts.
&& and || need operands of length one
The NEWS of R 4.3.0 says that calling && or || with a left-hand side or, if it is evaluated, a right-hand side of length greater than one is now always an error, with a report of the form 'length = 4' in coercion to 'logical(1)', and that the environment variable _R_CHECK_LENGTH_1_LOGIC2_ no longer has any effect. In R 4.2.x the same call gave a warning and used the first element. The block uses the returned vector of 60 loans for the first two calls (so the message says length = 60), then a right operand of length two. The short-circuit rule did not change: when the left operand decides the result, the right one is not evaluated, so FALSE && c(TRUE, FALSE) is FALSE without an error. The vectorised operators are & and |. See Operators and Precedence and Control Flow.
source("scenario.R")
returned <- !is.na(loans$return_date)
# R 4.3.0: && and || with an operand of length greater than one is an error.
# In R 4.2.x the same call was a warning and the first element was used.
tryCatch(
returned && TRUE,
error = \(e) conditionMessage(e)
)
#> [1] "'length = 60' in coercion to 'logical(1)'"
tryCatch(
FALSE || returned,
error = \(e) conditionMessage(e)
)
#> [1] "'length = 60' in coercion to 'logical(1)'"
tryCatch(
TRUE && c(TRUE, FALSE),
error = \(e) conditionMessage(e)
)
#> [1] "'length = 2' in coercion to 'logical(1)'"
# The short-circuit rule still holds: the right operand is not evaluated here.
FALSE && c(TRUE, FALSE)
#> [1] FALSE
# && and || need a single TRUE or FALSE; & and | are the vectorised pair.
sum(returned & loans$loan_date < as.Date("2025-06-01"), na.rm = TRUE)
#> [1] 14
Source: R documentation: Logic
The placeholder may start an extraction chain (experimental)
R 4.3.0 let the placeholder be used as the first argument of an extraction call, such as _$coef, and, more generally, as the head of a chain of extractions such as _$coef[[2]]. The NEWS introduces it with the words "as an experimental feature", so it may change. It saves a name or an anonymous function at the end of a pipe, as the block shows, and until it is settled the safe form is the one in the second half of the block. The page Pipes and the Native Pipe covers the syntax.
source("scenario.R")
# R 4.3.0 (an experimental feature, says the NEWS): _ may be the head of a
# chain of extractions at the end of a pipe.
books |> lm(pages ~ year, data = _) |> _$coefficients[[2L]]
#> [1] -0.8945014
# The same thing without the experimental syntax.
fit <- lm(pages ~ year, data = books)
coef(fit)[[2L]]
#> [1] -0.8945014
Source: R documentation: pipeOp
Other changes in R 4.3.0
| Change | Detail | Page |
|---|---|---|
|
No longer needs |
|
|
Accepts a data frame as |
|
|
A new generic for choosing between two methods of an operator, and |
|
|
Defunct |
|
C++17 default |
The default C++ standard is C++17 where available |
Source: R NEWS: Changes in R 4.3.0
R 4.4.0 (2024-04-24)
R 4.4.0 added a handful of small tools that remove helper code from projects (%||%, sort_by()), two experimental language features (Tailcall() with Exec(), and declare()), a correction to is.atomic(), a new internal type name that prepares R for other object systems, and use(). It also deprecated is.R() and the xfig() device (section on removals).
%||% is in base R
The NEWS of R 4.4.0 describes x %||% y as an expressive idiom for if (!is.null(x)) x else y. Projects and packages defined their own copy (the rlang package has one too); a project that still defines it can delete the definition, because the base operator has the same meaning and is documented, rather surprisingly, on the help page of Control. Only NULL is replaced: NA, an empty string and a zero-length vector are values. The first block defines the old helper, uses it, removes it, and shows that the name now finds the operator in the base namespace. The operator lives in Operators and Precedence, and its use for optional arguments is in Defensive Programming and Input Validation. A script that must also run on R older than 4.4.0 keeps a guarded definition, shown in the section on keeping code portable.
source("scenario.R")
# Before R 4.4.0 every project carried its own copy of the operator.
`%||%` <- function(x, y) if (is.null(x)) y else x
NULL %||% "unknown"
#> [1] "unknown"
"known" %||% "unknown"
#> [1] "known"
rm(`%||%`)
# Now it is in base R, with the same meaning.
environmentName(environment(`%||%`))
#> [1] "base"
NULL %||% "unknown"
#> [1] "unknown"
"known" %||% "unknown"
#> [1] "known"
# A typical use: an optional field of a record.
record <- list(title = "The Long Table", year = NULL)
record$year %||% "n.d."
#> [1] "n.d."
record$title %||% "untitled"
#> [1] "The Long Table"
# Only NULL is replaced: NA, "" and character(0) are values.
NA %||% "unknown"
#> [1] NA
"" %||% "unknown"
#> [1] ""
length(character(0) %||% "unknown")
#> [1] 0
Source: R documentation: Control
Tailcall() and Exec() (experimental)
R 4.4.0 added the experimental functions Tailcall() and Exec() to support recursive functions that do not grow the stack. They are experimental: the help page says they may be changed or dropped in future releases. Plain recursion in R keeps one frame per call and stops with an error (node stack overflow here, evaluation nested too deeply in other cases); Tailcall() replaces the running call with the new one when it is the last thing the function does. The accumulator must be evaluated before the call, because an unevaluated argument would be passed as a chain of promises that is forced all at once at the end. The older workaround was a loop or a trampoline. See Function Factories and Higher-Order Functions.
# Tailcall() and Exec() (R 4.4.0) are experimental.
sum_plain <- function(n, acc = 0) {
if (n == 0) {
return(acc)
}
sum_plain(n - 1, acc + n)
}
tryCatch(sum_plain(1e5), error = \(e) conditionMessage(e))
#> [1] "node stack overflow"
sum_tail <- function(n, acc = 0) {
if (n == 0) {
return(acc)
}
acc <- acc + n # evaluate the accumulator now, not as a chain of promises
Tailcall(sum_tail, n - 1, acc)
}
sum_tail(1e5)
#> [1] 5000050000
sort_by()
sort_by() is a new generic function, "primarily useful for the data.frame method" that sorts the rows of a data frame by one or more columns. The columns are given as a formula (~ year + pages, later columns resolving ties in the earlier ones) or as a list, and further arguments such as decreasing = TRUE go on to order(). The result is the same as the older df[order(df$year, df$pages), ], as the first identical() shows, with row names that keep the original numbers. See Data Frames; the older idiom is the subject of R FAQ 7.23: How can I sort the rows of a data frame?.
source("scenario.R")
cols <- c("title", "year", "pages")
# Before R 4.4.0: order() inside a subscript.
old <- books[order(books$year, books$pages), cols]
# sort_by() takes the columns as a formula or as a list.
new <- sort_by(books[cols], ~ year + pages)
identical(old, new)
#> [1] TRUE
head(new, 4L)
#> title year pages
#> 2 Pride and Prejudice 1813 432
#> 1 The Dispossessed 1974 387
#> 8 The Lamp at Noon 1999 280
#> 11 Paper Moons 2005 88
# Further arguments go to order(), so decreasing = TRUE works.
head(sort_by(books[cols], ~year, decreasing = TRUE), 3L)
#> title year pages
#> 10 THE LONG TABLE 2023 256
#> 15 Ten Winters 2022 NA
#> 4 Loud Roofs 2021 198
# The rows keep their original row names, as with order().
rownames(head(new, 4L))
#> [1] "2" "1" "8" "11"
Source: R documentation: sort_by
is.atomic(NULL) is FALSE
R 4.4.0 made is.atomic(NULL) return FALSE, "as NULL is not an atomic vector" in the words of the NEWS. The entry adds that strict backward compatibility would replace is.atomic(foo) by is.null(foo) || is.atomic(foo), but that this should happen only sparingly. The code that is affected is the code that used is.atomic() as the test for "plain data" and let a missing field through as a NULL. The block shows the new answer, the old test written out, and a vapply() over a record with a NULL field. See Atomic Vectors and Types and Missing and Special Values.
# R 4.4.0: NULL is not an atomic vector.
is.atomic(NULL)
#> [1] FALSE
is.atomic(character(0))
#> [1] TRUE
# Code that wanted the old answer asks for it explicitly.
is_atomic_or_null <- function(x) is.null(x) || is.atomic(x)
is_atomic_or_null(NULL)
#> [1] TRUE
# A check that used is.atomic() to accept "plain data" now rejects NULL,
# which is what a missing field looks like.
record <- list(title = "The Long Table", isbn = NULL)
vapply(record, is.atomic, logical(1L))
#> title isbn
#> TRUE FALSE
vapply(list(title = "a", isbn = NULL), \(x) is.null(x) || is.atomic(x),
logical(1L))
#> title isbn
#> TRUE TRUE
declare() (experimental)
declare() is described in the NEWS as a new experimental primitive function "intended to eventually allow information about R code to be communicated to the interpreter, compiler, and code analysis tools", with a declaration syntax that is "still being developed". On R 4.6.1 it is a primitive that ignores its arguments and returns NULL invisibly, as the help page says, and the block confirms. There is nothing to migrate to and nothing to rely on: it is listed here so that the name is recognised when it is seen in newer code, and so that no script assigns a different meaning to it. The compiler that may one day use declarations is covered in Vectorisation and Iteration.
# declare() (R 4.4.0) is an experimental primitive: it ignores its arguments
# and returns NULL invisibly. The syntax of declarations is still being
# developed, so nothing in it is guaranteed to stay.
res <- withVisible(declare(type = "numeric"))
res$visible
#> [1] FALSE
res$value
#> NULL
typeof(declare)
#> [1] "special"
Source: R documentation: declare
S4SXP is renamed OBJSXP
At the C level, R 4.4.0 renamed the SEXP type S4SXP to OBJSXP, "to support experimenting with alternative object systems", with contributions from the R Consortium’s object-oriented programming working group (the NEWS also says the old name can still be used in C code but is deprecated). What an R user sees is the result of typeof(): an object of this type without the S4 bit reports "object", which is what an S7 object is, while an S4 instance still reports "S4" and a classed list is still a "list". The block uses S7 0.2.2 for the first line. S7 is a CRAN package, not part of base R; its stated goal is to be merged into base R eventually. The type is covered in OOP Overview and Base Types, The S7 Object System and S4 Classes and Methods; the C-level names are in R Internals.
library(S7)
# S4SXP was renamed OBJSXP in R 4.4.0 (the C-level type of objects that are
# neither S4 nor a vector). typeof() reports it as "object".
shelf_item <- new_class(
"shelf_item",
properties = list(title = class_character)
)
item <- shelf_item(title = "The Long Table")
typeof(item)
#> [1] "object"
isS4(item)
#> [1] FALSE
# S4 instances still report "S4", and a classed list is still a list.
setClass("shelf_entry", slots = c(title = "character"))
typeof(new("shelf_entry", title = "The Long Table"))
#> [1] "S4"
typeof(structure(list(title = "The Long Table"), class = "book"))
#> [1] "list"
Source: R Internals: SEXPTYPEs
use() (available since 4.4.0)
use() attaches only the exports that are named, and always with attach.required = FALSE, so that the packages in Depends are not attached. It is the script counterpart of importFrom in a NAMESPACE: the help page says that a script written with use() can be turned into package code by replacing the calls with the corresponding importFrom directives. The R 4.5.0 NEWS lists it as a new function and adds in parentheses that it was "actually already available since R 4.4.0", which is why this page lists it under both releases. The help page calls the functionality still experimental, with interfaces that may change. The block loads tools the old way and the new way, and checks what each attached; code for R 4.3 or older uses library(pkg, include.only = ), which use() wraps. See Packages, Libraries and Namespaces.
# Before: library() attaches every export of the package, in every script.
library(tools)
length(ls("package:tools")) > 100L
#> [1] TRUE
detach("package:tools")
# use() (available since R 4.4.0, announced in the R 4.5.0 NEWS) attaches only
# what is named. It is still experimental.
use("tools", "toTitleCase")
ls("package:tools")
#> [1] "toTitleCase"
toTitleCase("the long table")
#> [1] "The Long Table"
detach("package:tools")
# The rest of the code qualifies names, which is what a NAMESPACE would do.
tools::toTitleCase("the long table")
#> [1] "The Long Table"
isNamespaceLoaded("tools")
#> [1] TRUE
Source: R documentation: use
Other changes in R 4.4.0
| Change | Detail | Page |
|---|---|---|
|
No longer forwards the local variables of the generic into the method (shown in the section on removals) |
|
|
Signal an error when an operand is a symbol or a call |
|
|
Gains the |
|
|
Deprecated; |
|
|
Deprecated; defunct in R 4.5.0 |
|
|
Twelve of them formally deprecated in favour of calling |
|
C++ compiler |
No default C++ compiler is set if no C++17 compiler is available |
Source: R NEWS: Changes in R 4.4.0
R 4.5.0 (2025-04-11)
R 4.5.0 added grepv(), two data sets, tools::sha256sum() and the documentation of use(), made is.R() defunct, removed a graphics device, and changed the default C standard to C23. Strict R headers became the default for packages with compiled code.
grepv()
grepv() is grep() with value = TRUE as the default: the NEWS says it is identical to grep() except for the default of value. The older idiom grep(pattern, x, value = TRUE) keeps working, so there is nothing to migrate, only a shorter spelling; all other arguments (ignore.case, invert, fixed, perl) behave as in grep(). The block uses the trimmed shelf titles. See Strings and Regular Expressions.
source("scenario.R")
titles <- trimws(books$title)
# Before R 4.5.0: grep() with value = TRUE.
grep("^The ", titles, value = TRUE)
#> [1] "The Dispossessed" "The Lamp at Noon" "The Slow Comet"
# grepv() is grep() with value = TRUE as the default.
grepv("^The ", titles)
#> [1] "The Dispossessed" "The Lamp at Noon" "The Slow Comet"
identical(grepv("^The ", titles), grep("^The ", titles, value = TRUE))
#> [1] TRUE
# Everything else about grep() carries over.
grepv("^the ", titles, ignore.case = TRUE)
#> [1] "The Dispossessed" "the quiet streets" "The Lamp at Noon"
#> [4] "THE LONG TABLE" "The Slow Comet"
grepv("^The ", titles, invert = TRUE)[1:3]
#> [1] "Pride and Prejudice" "the quiet streets" "Loud Roofs"
Source: R documentation: grep
penguins and penguins_raw
R 4.5.0 added the data sets penguins and penguins_raw to the datasets package, credited in the NEWS to Ella Kaye, Heather Turner and Kristen Gorman. penguins is a clean table with 344 rows and eight columns (species, island, bill and flipper measurements, body_mass, sex, year), and penguins_raw keeps the 17 columns of the original source, with their original names. Tutorials written before R 4.5.0 needed a data package or a CSV file for the same data; since R 4.5.0 data(penguins) is not even needed, because the data set is lazy-loaded when datasets is on the search path. The statistics pages of this reference use penguins where the shelf scenario does not fit, for instance Descriptive Statistics and Tests. The data sets are described in Reading and Writing Text Data.
# R 4.5.0 added two data sets to the datasets package.
dim(penguins)
#> [1] 344 8
names(penguins)
#> [1] "species" "island" "bill_len" "bill_dep" "flipper_len"
#> [6] "body_mass" "sex" "year"
table(penguins$species)
#>
#> Adelie Chinstrap Gentoo
#> 152 68 124
# penguins_raw keeps the original column names of the source.
dim(penguins_raw)
#> [1] 344 17
names(penguins_raw)[1:5]
#> [1] "studyName" "Sample Number" "Species" "Region"
#> [5] "Island"
# Missing values are real NAs.
colSums(is.na(penguins))
#> species island bill_len bill_dep flipper_len body_mass
#> 0 0 2 2 2 2
#> sex year
#> 11 0
Source: R documentation: penguins
use() is documented in the NEWS
The NEWS of R 4.5.0 lists use() as a new function "to use packages in R scripts with full control over what gets added to the search path", and adds that it was actually already available since R 4.4.0. The behaviour, the block and the status (experimental) are in the R 4.4.0 section above; this entry is the reason the issue tables of other sources give R 4.5.0 for it. A script that uses use() needs R 4.4.0 or later, not 4.5.0.
tools::sha256sum()
The package tools gained sha256sum() in R 4.5.0, analogous to md5sum() and implementing the SHA-256 hashing algorithm. MD5 is still available, but a script that records a checksum to detect tampering, as opposed to accidental corruption, should use SHA-256. The block writes a six-byte file, prints both digests, and shows that the 64-character SHA-256 digest changes with the content. The names of the result are the file paths, which unname() removes here so that no path is printed. The checksum of an extracted file is used in Connections and the File System, and R CMD build --sha256 can add a SHA256 file to a tarball (Creating Packages).
# tools::sha256sum() (R 4.5.0) is the SHA-256 sibling of md5sum().
path <- tempfile(fileext = ".txt")
writeLines("shelf", path)
unname(tools::md5sum(path))
#> [1] "d4efc30e39a1a543c5f91beec3339feb"
unname(tools::sha256sum(path))
#> [1] "d2c57c181f994f276c319fa2dce1ea1c4d6c121ff63e8175bcdd5761f09ce54d"
nchar(unname(tools::sha256sum(path)))
#> [1] 64
# The checksum changes with the content.
writeLines("shelf2", path)
unname(tools::sha256sum(path))
#> [1] "e44dae711dc7d613a786884de97697590ac75a872b47efb1209d4aa0135fe6e1"
unlink(path)
Source: R documentation: sha256sum
is.R() is defunct
is.R() asked whether the code was running in R rather than in S-PLUS. The R 4.4.0 NEWS deprecated it "as no other S dialect is known to be in use", and the R 4.5.0 NEWS made it defunct and removed the effect of the environment variable _R_DEPRECATED_IS_R_. The name still exists, so a guard such as exists("is.R") && is.R() is true for the first operand and fails on the second; the block shows the real message. The repair is to delete the test, since the code runs on R. The R FAQ explains the relation between R and S in R FAQ 3.3: What are the differences between R and S?, and the deprecation process is described in Defensive Programming and Input Validation.
# is.R() asked "is this R rather than S-PLUS?". It was deprecated in R 4.4.0
# and is defunct since R 4.5.0: the name still exists, and calling it fails.
exists("is.R")
#> [1] TRUE
tryCatch(is.R(), error = \(e) conditionMessage(e))
#> [1] "'is.R' is defunct.\nSee help(\"Defunct\")"
# A guard like the following, common in 20-year-old scripts, now fails too.
tryCatch(
if (exists("is.R") && is.R()) "running R",
error = \(e) conditionMessage(e)
)
#> [1] "'is.R' is defunct.\nSee help(\"Defunct\")"
# The fix is to delete the test: the code runs on R.
R.version$language
#> [1] "R"
Source: R documentation: base-defunct
A C23 compiler by default
R 4.5.0 selects a C23 compiler by default, if one is available, for compiling R and packages. The NEWS lists the compilers known to select C23 (gcc 13 to 15 and recent clang releases, among them) and the ways out: a package can ask for the previous standard with USE_C17 in SystemRequirements, or an installer with R CMD INSTALL --use-C17, and R itself can be built with --without-C23 unless the compiler defaults to C23 anyway. C23 rejects implicit function declarations and K&R-style definitions, so old C code needs fixing or the request for C17. Strict R headers are also the default in R 4.5.0, which removes the legacy macros PI, Calloc, Realloc and Free; the replacements are M_PI, R_Calloc, R_Realloc and R_Free. This is a matter for packages with compiled code and cannot be shown in a script that runs under Rscript; Interfacing C, C++ and Fortran covers it with compiled examples.
Other changes in R 4.5.0
| Change | Detail | Page |
|---|---|---|
|
The device, deprecated in R 4.4.0, is removed; the error names the replacement (shown in the section on removals) |
|
|
Download several packages at once with libcurl |
|
|
Calls |
|
S4 |
|
Source: R NEWS: Changes in R 4.5.0
R 4.6.0 (2026-04-24)
R 4.6.0 is mostly additions: a negated membership operator, a constructor for message conditions, a bit-pattern printer, two options of existing functions, and drawing hooks for three more plot types. It also moved the default C++ standard to C++20, added a DOI to the citation of R, and made structure(NULL, ...) defunct.
%notin%
x %notin% table is the negation of x %in% table. The NEWS describes it as an idiom for !(x %in% table), "provided almost entirely for convenience and code readability" after many years of private definitions, mostly hidden in packages. Projects that define their own operator of the same name can delete the definition once they require R 4.6.0 or later. Because it is built on match(), NA is treated as a value, as with %in%. The block compares the three spellings on the shelf’s genres. See Operators and Precedence, where the membership operators are explained.
source("scenario.R")
wanted <- c("Fiction", "Poetry")
genre <- trimws(books$genre)
# Before R 4.6.0: negate %in% by hand, or define the operator in every project.
head(genre[!(genre %in% wanted)], 4L)
#> [1] "Science fiction" "Classic" "fiction" "Mystery"
# %notin% is the negation of %in%.
head(genre[genre %notin% wanted], 4L)
#> [1] "Science fiction" "Classic" "fiction" "Mystery"
identical(genre %notin% wanted, !(genre %in% wanted))
#> [1] TRUE
# Both use match(), so NA is a value like any other.
NA_character_ %notin% wanted
#> [1] TRUE
c(1, NA) %notin% c(NA, 2)
#> [1] TRUE FALSE
Source: R documentation: match
messageCondition()
messageCondition() creates a structured "message" condition object, analogous to errorCondition() and warningCondition() (R 3.6.0). The extra arguments become fields of the condition and class adds classes in front of c("message", "condition"), so a handler can select on the class and read the fields, which is how progress messages that carry data are written. Before, the object was assembled by hand with structure(), as the first lines of the block do; both produce the same classes. See Conditions and Error Handling.
# Before R 4.6.0 a classed message condition was assembled by hand.
old <- structure(
class = c("shelf_progress", "message", "condition"),
list(message = "loan 12 of 60\n", call = NULL, done = 12L)
)
# messageCondition() builds the same object, as errorCondition() and
# warningCondition() have done since R 3.6.0. Extra fields are named arguments.
new <- messageCondition(
"loan 12 of 60\n",
class = "shelf_progress", done = 12L
)
class(new)
#> [1] "shelf_progress" "message" "condition"
class(old)
#> [1] "shelf_progress" "message" "condition"
new$done
#> [1] 12
# message() accepts a condition, and a handler can pick the class out.
withCallingHandlers(
{
message(new)
"finished"
},
shelf_progress = \(m) {
cat("progress:", m$done, "\n")
invokeRestart("muffleMessage")
}
)
#> progress: 12
#> [1] "finished"
Source: R documentation: conditions
bitstring()
bitstring() is a user-friendly wrapper around intToBits() and its relatives, and its help page says it is inspired by the Julia function of the same name. It supports integer, numeric and raw vectors and prints the bits with a separator after the sign bit and, for doubles, between the exponent and the mantissa. The first lines of the block show the older way to print the low eight bits of an integer. See Numbers and Arithmetic.
# Before R 4.6.0 a bit pattern took a few steps.
bits_old <- paste(rev(as.integer(intToBits(5L))[1:8]), collapse = "")
bits_old
#> [1] "00000101"
# bitstring() wraps intToBits() and friends and prints the pattern.
bitstring(5L)
#> 0 0000000000000000000000000000101
class(bitstring(5L))
#> [1] "bitstring"
bitstring(-1L)
#> 1 1111111111111111111111111111111
bitstring(0.1)
#> 0 01111111011 1001100110011001100110011001100110011001100110011010
Source: R documentation: bitstring
match.arg(several.ok = "all")
match.arg() takes a new value for several.ok. A string that starts with "all" means that the argument may have more than one element and that every one of them must match choices. With several.ok = TRUE, the older option, elements that do not match are dropped without a word, which let typos through; the block shows both. The tests of valid values that people wrote by hand after several.ok = TRUE can be replaced. See Functions and Defensive Programming and Input Validation.
columns <- c("title", "author", "isbn", "year")
# several.ok = TRUE drops what does not match, without a word.
match.arg(c("ti", "oops"), columns, several.ok = TRUE)
#> [1] "title"
# R 4.6.0: several.ok = "all" also accepts several values, but every one of
# them must match.
match.arg(c("ti", "au"), columns, several.ok = "all")
#> [1] "title" "author"
tryCatch(
match.arg(c("ti", "oops"), columns, several.ok = "all"),
error = \(e) conditionMessage(e)
)
#> [1] "'arg' should be one of “title”, “author”, “isbn”, “year”"
Source: R documentation: match.arg
substr(i2 = NULL)
substring(txt, i1, i2), substr() and their replacement versions accept i2 = NULL to mean "to the end of the string". Before, substring(x, i) already ran to the end through its default, while substr() needed nchar(x); the block shows the older spelling and the new one, and a replacement. See Strings and Regular Expressions.
isbn <- "9780306406157"
# Before R 4.6.0 "to the end" was spelled out with nchar().
substr(isbn, 4L, nchar(isbn))
#> [1] "0306406157"
# i2 = NULL (R 4.6.0) means the end of the string.
substr(isbn, 4L, NULL)
#> [1] "0306406157"
substring(isbn, 4L, NULL)
#> [1] "0306406157"
# The replacement forms accept it too.
code <- "abcdef"
substr(code, 4L, NULL) <- "XYZ"
code
#> [1] "abcXYZ"
Source: R documentation: substr
panel.first and panel.last
hist() gains panel.first, and barplot() and boxplot() (through bxp()) gain panel.first and panel.last; hist() has no panel.last. They draw before and after the data, which is what a background grid needs, and before R 4.6.0 the usual place to find them was plot.default(). hist() and barplot() evaluate the argument as an expression, while boxplot() expects a function, which is why the block writes \() grid(). The block draws on a null PDF device so that nothing is written. See Base Graphics and Devices, where the real chart is shown.
source("scenario.R")
genre <- trimws(tolower(books$genre))
pdf(NULL)
# R 4.6.0: hist() has panel.first, and barplot() and boxplot() have
# panel.first and panel.last. Before, the grid was drawn on top of the data.
# hist() and barplot() take an expression; boxplot() takes a function.
h <- hist(books$pages, panel.first = grid())
class(h)
#> [1] "histogram"
b <- barplot(table(genre), panel.first = grid(), panel.last = box())
length(b)
#> [1] 6
bp <- boxplot(
pages ~ genre,
data = books,
panel.first = \() grid(),
panel.last = \() abline(h = 300, lty = 2L)
)
names(bp)[1:3]
#> [1] "stats" "n" "conf"
invisible(dev.off())
Source: R documentation: bxp
C++20 by default
The NEWS of R 4.6.0 says that the default C++ standard has changed to C++20 where available, which is the case on all known platforms from 2021 on; where it is not, C++17 is used if available, otherwise C++ is not supported, as before. Packages can request C++17 if essential. R 4.3.0 had made C++17 the default. This concerns only packages with C++ code: Interfacing C, C++ and Fortran.
A DOI in the citation of R
The citation of R now provides a DOI, 10.32614/R.manuals, and the NEWS asks authors to update their bibliographies accordingly. citation() shows it, as the block does, and toBibtex() gives the entry with the doi field. The page Getting Help and Documentation shows how to cite R and packages.
# R 4.6.0: the citation of R itself carries a DOI.
format(citation(), style = "text")
#> [1] "R Core Team (2026). _R: A Language and Environment for Statistical\nComputing_. R Foundation for Statistical Computing, Vienna, Austria.\ndoi:10.32614/R.manuals <https://doi.org/10.32614/R.manuals>.\n<https://www.R-project.org/>."
toBibtex(citation())
#> @Manual{,
#> title = {R: A Language and Environment for Statistical Computing},
#> author = {{R Core Team}},
#> organization = {R Foundation for Statistical Computing},
#> address = {Vienna, Austria},
#> year = {2026},
#> doi = {10.32614/R.manuals},
#> url = {https://www.R-project.org/},
#> }
Source: R documentation: citation
Other changes in R 4.6.0
| Change | Detail | Page |
|---|---|---|
|
Defunct (deprecated in R 3.4.0); NULL cannot have attributes (shown in the section on removals) |
|
Sweave drivers |
|
|
Non-API entry points |
More non-API functions are reported by |
Source: R NEWS: Changes in R 4.6.0
R 4.6.1 (2026-06-24)
R 4.6.1 is a bug-fix release: its NEWS has two entries under New Features (the LaTeX class jss bundled with R is updated to work with a newer hyperref, and the plain-text conversion of a simple Rd \eqn{} handles \geq, \leq, \neq and \ne) and ten under Bug Fixes. Nothing is added to the language, and code that ran on R 4.6.0 runs on R 4.6.1. The fixes that are visible from a script are collected in the table.
| Function | Fix |
|---|---|
|
|
|
|
|
|
|
|
|
The p-value computation of |
|
Checks its tuning parameters at the R level; |
|
Works better for non-ASCII input |
|
Overly long error messages from the system no longer corrupt its state |
|
The data part lists generic function names instead of package names |
|
Coerces S4 objects to S4 superclasses that extend old-style classes correctly, instead of returning only the S3 part |
Source: R NEWS: Changes in R 4.6.1
The block runs three of them on R 4.6.1. The all() call also prints the warning that comes before the error, which the handler captures; the zapsmall() call keeps the infinite entry and returns the vector; and as.matrix() of a POSIXlt object gives a numeric matrix of one row and ten fields.
# Some of the fixes of R 4.6.1 (bug-fix release, 2026-06-24), run on 4.6.1.
# cut() signals an error for invalid breaks.
tryCatch(cut(1:11, {}), error = \(e) conditionMessage(e))
#> [1] "invalid 'breaks' argument"
# all() of a symbol is an error; it used to coerce it to NULL internally. The
# warning that comes before the error is printed by the handler.
withCallingHandlers(
tryCatch(all(quote(books)), error = \(e) conditionMessage(e)),
warning = \(w) {
cat("warning:", conditionMessage(w), "\n")
invokeRestart("muffleWarning")
}
)
#> warning: coercing argument of type 'symbol' to logical
#> [1] "'symbol' object cannot be coerced to type 'logical'"
# zapsmall() with digits = Inf and an Inf entry keeps the vector.
zapsmall(c(1, 1e-20, Inf), digits = Inf)
#> [1] 1e+00 1e-20 Inf
# as.matrix() of a POSIXlt object is numeric again.
lt <- as.POSIXlt("2026-06-30 12:00:00", tz = "UTC")
m <- as.matrix(lt)
is.numeric(m)
#> [1] TRUE
dim(m)
#> [1] 1 10
Source: R NEWS: Changes in R 4.6.1
Removals and behaviour changes that stop old code
Most of the changes above add something and leave old code running. A few do the opposite: the old form fails, or quietly means something else. The next script runs four of them on R 4.6.1 and catches each error, so that the real message is shown. UseMethod() no longer forwards the local variables of the generic into the method (R 4.4.0): a method that read a variable the generic had defined, which older code did and which made method code hard to analyse, now fails with object not found, and the repair is to pass the value as an argument. The comparison operators signal an error for a symbol or a call (R 4.4.0). structure(NULL, ...) is defunct (R 4.6.0), because NULL cannot have attributes; it had been deprecated since R 3.4.0 and now fails, and structure(list(), ...) is the way to make an empty classed object. And xfig() was removed in R 4.5.0 after being deprecated in 4.4.0; the error names the replacement.
# R 4.4.0: UseMethod() no longer forwards the local variables of the generic
# to the method. Before, "loan_days" below was visible inside the method.
overdue_days <- function(x, ...) {
loan_days <- 21L # nolint: object_usage_linter.
UseMethod("overdue_days")
}
overdue_days.default <- function(x, ...) {
x - loan_days # nolint: object_usage_linter.
}
tryCatch(overdue_days(30L), error = \(e) conditionMessage(e))
#> [1] "object 'loan_days' not found"
# The fix: pass the value as an argument.
overdue_days2 <- function(x, loan_days = 21L, ...) UseMethod("overdue_days2")
overdue_days2.default <- function(x, loan_days = 21L, ...) x - loan_days
overdue_days2(30L)
#> [1] 9
# R 4.4.0: <, >, <= and >= signal an error for a symbol or a call.
tryCatch(quote(pages) < 100, error = \(e) conditionMessage(e))
#> [1] "comparison (<) is not possible for language types"
# R 4.6.0: structure(NULL, ...) is defunct, because NULL has no attributes.
tryCatch(structure(NULL, class = "book"), error = \(e) conditionMessage(e))
#> [1] "attempt to set an attribute on NULL"
class(structure(list(), class = "book"))
#> [1] "book"
# R 4.5.0 removed the xfig() device, deprecated since R 4.4.0.
tryCatch(xfig(), error = \(e) conditionMessage(e))
#> [1] "'xfig' was removed in R 4.5.0.\n Consider an SVG device instead."
The table collects every failure that this page has shown, with the message of R 4.6.1 as it was captured. The wording of messages changes between releases (the match.arg() help page warns about this), so a program should test the class of a condition rather than its text; see Conditions and Error Handling.
| Since | Old code | Message on R 4.6.1 | Fix |
|---|---|---|---|
4.2.0 |
|
|
|
4.3.0 |
|
|
A single |
4.3.0 |
|
|
Delete the call |
4.4.0 |
A method that reads a local variable of its generic |
|
Pass the value as an argument of the generic and the method |
4.4.0 |
|
|
Evaluate the expression first, or compare with |
4.5.0 |
|
|
Delete the test |
4.5.0 |
|
|
|
4.6.0 |
|
|
|
4.6.1 |
|
|
Evaluate the symbol first |
Moving older code to a current R
A script written for R 3.x or an early R 4.x usually runs unchanged on R 4.6.1. The changes that stop it are the ones in the table above, and the ones that change its meaning without stopping it are the new defaults (strings that stay strings, a matrix with two classes, a different is.atomic(NULL), a new palette). A practical order of work is the following.
-
Note the R version and the package versions the code was last known to work with. A lockfile from Reproducible Environments with renv records both, and is the way to compare an old and a new run package by package.
-
Run the script on the new R with warnings printed as they happen (
options(warn = 1)in a profile, as the scripts of this reference do;Rscriptprints warnings after the script otherwise). A warning is the cheapest signal: the&&and||warning of R 4.2.x was the notice for the error of R 4.3.0. -
Search the code for the old idioms. The linters of lintr know many of them, and a few lines of R run them over a string of code, as the next block does for seven lines of "tutorial" code. The linters find
class(x) == "matrix",sapply(),attach(),TandF,1:length(x)and the magrittr pipe; they do not knowis.R(), which a plain search finds. The page Style, Linting and Static Analysis explains the default set and how to enable the others. -
Fix what the table of removals shows, then the habits in the last table of this page.
-
State the oldest R the code needs. A package says so in
Depends, andR CMD buildaddsDepends: R (>= 4.1.0)itself when the code uses the native pipe or the lambda and the field is missing. The next section shows how a script checks for itself. -
Test on the oldest R that is supported, as well as on the newest; the community tools rig (rig on GitHub) and the Rocker project (Rocker) install and run several R versions side by side, and R-hub v2 runs
R CMD checkon a package in GitHub Actions (see Publishing to CRAN).
# Old idioms in a few lines of "tutorial" code, and what lintr 3.4.0 says.
old <- c(
'if (class(m) == "matrix") "a matrix"',
"x <- sapply(1:3, function(i) i + 1)",
"attach(books)",
"ok <- T",
"for (i in 1:length(x)) print(i)",
"books %>% head()",
"if (exists('is.R') && is.R()) 1"
)
linters <- list(
lintr::class_equals_linter(),
lintr::undesirable_function_linter(),
lintr::T_and_F_symbol_linter(),
lintr::seq_linter(),
lintr::pipe_consistency_linter()
)
for (code in old) {
res <- lintr::lint(text = code, linters = linters)
cat(sprintf("%s -> %d finding(s)\n", code, length(res)))
for (r in res) {
cat(" ", r$linter, ": ", r$message, "\n", sep = "")
}
}
#> if (class(m) == "matrix") "a matrix" -> 1 finding(s)
#> class_equals_linter: Use inherits(x, 'class-name'), is.<class> for S3 classes, or is(x, 'S4Class') for S4 classes, instead of comparing class(x) with ==.
#> x <- sapply(1:3, function(i) i + 1) -> 1 finding(s)
#> undesirable_function_linter: Avoid undesirable function "sapply". As an alternative, use vapply() with an appropriate `FUN.VALUE=` argument to obtain type-stable simplification.
#> attach(books) -> 1 finding(s)
#> undesirable_function_linter: Avoid undesirable function "attach". As an alternative, use roxygen2's @importFrom statement in packages, or `::` in scripts. attach() modifies the global search path.
#> ok <- T -> 1 finding(s)
#> T_and_F_symbol_linter: Use TRUE instead of the symbol T.
#> for (i in 1:length(x)) print(i) -> 1 finding(s)
#> seq_linter: Use seq_along(...) instead of 1:length(...), which is likely to be wrong in the empty edge case.
#> books %>% head() -> 1 finding(s)
#> pipe_consistency_linter: Use the |> pipe operator instead of the %>% pipe operator.
#> if (exists('is.R') && is.R()) 1 -> 0 finding(s)
# No default linter knows the defunct is.R(); search for it.
grepl("is\\.R\\(", old, perl = TRUE)
#> [1] FALSE FALSE FALSE FALSE FALSE FALSE TRUE
Each of the linters in the block has its own page on the lintr site: class_equals_linter, undesirable_function_linter, T_and_F_symbol_linter, seq_linter and pipe_consistency_linter. The messages are lintr 3.4.0’s; a later version may word them differently.
Keeping code portable across R versions
A script or a package that has to run on an older R than the one it was written on has three tools. The first is a version test: getRversion() returns an object of class numeric_version, which compares correctly with a string (getRversion() >= "4.1.0"), while comparing two version strings as text does not: "4.10.0" > "4.6.1" is FALSE. The second is a feature test, which asks for the thing the code needs and not for the release that introduced it: exists("grepv", envir = baseenv()). The third is a definition of the missing function that is made only when it is missing, so that the current R uses its own. The block shows all three with the operator of R 4.4.0. R.version.string and R.version$major and R.version$minor give the version of the running R as text.
R.version.string
#> [1] "R version 4.6.1 (2026-06-24)"
getRversion()
#> [1] ‘4.6.1’
getRversion() >= "4.1.0"
#> [1] TRUE
# Compare versions as versions, not as strings: "4.10.0" comes after "4.6.1".
"4.10.0" > "4.6.1"
#> [1] FALSE
numeric_version("4.10.0") > "4.6.1"
#> [1] TRUE
compareVersion("4.10.0", "4.6.1")
#> [1] 1
# A script that must also run on R older than 4.4.0 defines the operator only
# when base R lacks it.
if (getRversion() < "4.4.0") {
`%||%` <- function(x, y) if (is.null(x)) y else x
}
NULL %||% "unknown"
#> [1] "unknown"
# A feature test says what the code needs, not which release has it.
exists("grepv", envir = baseenv())
#> [1] TRUE
exists("no_such_function", envir = baseenv())
#> [1] FALSE
Source: R documentation: numeric_version
For a package, the supported minimum goes in DESCRIPTION. The shelf package of this reference needs R 4.1.0 because it uses the native pipe and \(x):
Depends:
R (>= 4.1.0)
The field is described in Creating Packages, and its relation to Imports and to the attaching of packages in Packages, Libraries and Namespaces. Two limits are worth knowing. A Depends: R (>= x.y.z) line only stops the installation on an older R; it does not make code written for the newer language run there. And testing on the oldest R that is supported, with a tool such as rig or the Rocker images, is the only way to find the calls that were added later.
How to keep up
R announces every change in NEWS, and there are several ways to read it.
-
The release NEWS (R NEWS) lists the changes from R 4.0.0 to the current release, newest first, in sections called Significant User-Visible Changes, New Features, Deprecated and Defunct, Bug Fixes and C-Level Facilities (the 4.6 series also has Graphics, Package Installation, Installation on a Unix-Alike, Utilities and Checking an Installation). The 3.x series has its own file (R NEWS for the 3.x series). The development NEWS (R-devel NEWS) starts with the changes of the next x.y.0 and also lists the next patch release, which is why 4.6.2 appears in it before it is out.
-
news()reads the NEWS of the installed R into a data frame with the columnsVersion,Date,Category,TextandHTML, which can be queried with an expression: the block below lists the entries of 4.6.1 by category and finds the entry that introduced%notin%. It reads the NEWS of the R that is running, so on an older R it does not know the newer entries. Its help page is news. -
The R Blog (The R Blog) carries posts by members of R Core and others about internals and infrastructure. Two that bear on this page, both read for it, are "Generalizing Support for Functional OOP in R" (2024-05-17), which explains the base R generics added for S7, among them
chooseOpsMethod()andnameOfClass(), and "New Tricks for the R Sweave Drivers" (2026-09-18), about the Sweave drivers whose help pages and options change in R 4.6.0 and R-devel. -
The developer page (R Developer Page) shows the release schedule, including the dates of the beta and the release candidate of the next patch release. The announcements of releases go to the r-announce mailing list (r-announce), and r-project.org shows the current release.
-
The R FAQ explains the numbering and where the versions are: R FAQ 2.3: What is the current version of R?.
# news() reads the NEWS of the running R into a data frame.
db <- news()
class(db)
#> [1] "news_db_from_Rd" "news_db" "data.frame"
names(db)
#> [1] "Version" "Date" "Category" "Text" "HTML"
range(db$Version)
#> [1] "4.0.0" "4.6.1"
# What 4.6.1 changed, by category.
table(news(Version == "4.6.1")$Category)
#>
#> Bug Fixes New Features
#> 10 2
# Search the text: which entry introduced %notin%?
hit <- news(Version == "4.6.0" & grepl("%notin%", Text, fixed = TRUE))
hit$Version
#> [1] "4.6.0"
hit$Category
#> [1] "New Features"
cat(strwrap(hit$Text[[1L]], width = 70L), sep = "\n")
#> x %notin% table newly in base is an idiom for !(x %in% table) and
#> provided almost entirely for convenience and code readability, from
#> an R-devel suggestion, after many years of private definitions mostly
#> hidden in packages, including in R's tools package.
Source: R documentation: news
Upcoming: R 4.6.2 and R-devel (4.7.0)
Nothing in this section is released, nothing is run, and everything may change before it is. It was checked against the live R-devel NEWS (R-devel NEWS) on 2026-10-22.
R 4.6.2. The R Developer Page lists the 4.6.2 beta from Monday 2026-10-19, the code freeze and release candidate for Thursday 2026-10-22, and the release for Thursday 2026-10-29; the version file of the 4.6 branch said "4.6.2 RC" on 2026-10-22, and the tarball directory of R 4 still ended at R-4.6.1 that day. The R-devel NEWS already has a section "Changes in R 4.6.2" which lists only bug fixes: among them an integer overflow in rhyper() when m + n exceeds .Machine$integer.max, a long-standing bug in nlminb() with constraints, seq.int() with a non-vector along.width, writeChar() in a multibyte locale, several fixes to strptime() and several crashes fixed in ksmooth(), besselI() and besselK(). If R 4.6.2 is out by the time this is read, the baseline of this page is still R 4.6.1 and the 4.6.2 NEWS is the place to look.
R 4.7.0. No release date is published for R 4.7.0; the schedule only says that x.y.0 releases are annual and in spring. The R-devel NEWS lists, among others, the following changes for it.
| Item | What the R-devel NEWS says |
|---|---|
|
New higher-order functions (see Function Factories and Higher-Order Functions) |
|
A new function that calculates cumulative variances |
|
"Object not found" errors signal conditions of class |
|
A logical index now warns if |
|
Gets an argument |
|
A new low-level function that enables deparsing of non-S4 objects of type "object" |
|
Treat |
|
A fix that changes results; to reproduce old results |
Source: R-devel NEWS: Changes in R-devel
The fix for rbinom() is an entry that can change the output of a seeded simulation, which is why the R-devel NEWS lists it among the significant user-visible changes; see Distributions and Random Numbers for set.seed() and RNGversion().
Where older tutorials differ
The table collects, in the order of the issue that defined this section, the habits that older tutorials still teach, each with the current form, the release that changed it and the page that explains it. The concept pages carry the same remark in their own sections; this is the whole list. Nothing in the first column is recommended code.
| Area | Older form | Current form | Since | Page |
|---|---|---|---|---|
Strings in data frames |
|
|
4.0 |
|
Anonymous functions |
Only |
The shorthand |
4.1 |
|
Pipes |
The magrittr |
The native |
4.1 |
|
Conditions |
|
Both are errors |
4.2 and 4.3 |
|
Null default |
A hand-written |
The |
4.4 |
|
Loading packages in scripts |
|
|
4.4 |
|
Iteration and data access |
Ad-hoc |
|
always |
The apply Family and Functionals, Data Frames and Idioms and Common Pitfalls |
Object systems |
"R5" as a separate thing |
Reference Classes are part of |
always |
OOP Overview and Base Types, Reference Classes and R6 and The S7 Object System |
Package tooling |
|
usethis, devtools and roxygen2; R-hub v2, which runs in GitHub Actions; the third edition of testthat |
always |
Creating Packages, Publishing to CRAN and Testing with testthat |
Reproducibility |
packrat |
renv |
always |
|
Literate programming |
Sweave |
R Markdown or Quarto; Quarto is preferred for new projects and R Markdown is still maintained |
always |
|
Web APIs |
httr |
httr2; plumber 1.x is stable and plumber2 is pre-1.0 |
always |
|
Parallelism |
The |
The base |
always |
|
Profiling |
The dead |
The current site, https://profvis.r-lib.org/ |
always |
|
|
|
|
4.4 |
|
|
Used to test for R rather than S-PLUS |
Deprecated in 4.4 and defunct in 4.5; delete the test |
4.5 |
Version notes
The facts of this page that are most often quoted wrongly, with what the NEWS says.
| Claim | What the NEWS says | Where |
|---|---|---|
|
Listed in the R 4.5.0 NEWS, with the remark that it was "actually already available since R 4.4.0"; the help page calls it experimental |
R 4.4.0 and R 4.5.0 |
The pipe placeholder can start an extraction chain |
Introduced in R 4.3.0 "as an experimental feature" |
R 4.3.0 |
|
All three are called experimental in the R 4.4.0 NEWS |
R 4.4.0 |
|
A warning in R 4.2.0; an error since R 4.3.0 |
R 4.2.0 and 4.3.0 |
|
Deprecated in R 4.4.0, defunct in R 4.5.0 |
R 4.4.0 and 4.5.0 |
The default C++ standard is C++17 |
C++17 since R 4.3.0, C++20 where available since R 4.6.0 |
R 4.3.0 and 4.6.0 |
R 4.6.2 is out |
Scheduled for 2026-10-29; the release candidate stage began on 2026-10-22 |
upcoming section |
The NEWS entries for R 3.4.0 (the deprecation of structure(NULL, ...)) are in the R 3.x NEWS (R NEWS for the 3.x series). The nickname of R 4.6.2, "Diversified Portfolio", is the one given on the R Developer Page; the nickname of R 4.6.0 is not published on r-project.org and is not used here.
Where to go next
-
Idioms and Common Pitfalls for the habits that are wrong in every release: partial matching with
$,1:length(x),sapply(),if (NA), growing vectors and the rest. -
Getting Started for the release cycle and version numbers.
-
Getting Help and Documentation for
news(),citation()andR.version. -
Pipes and the Native Pipe, Control Flow and Operators and Precedence for the language changes that need the most care when moving code.
-
Creating Packages and Publishing to CRAN for stating the minimum R version of a package and checking it on several versions.
-
Style, Linting and Static Analysis for finding old idioms with lintr.
-
R Reference for the whole section and its cheat sheet.
References
-
R NEWS: Changes in R 4.0.0 to 4.6.1; R 3.x NEWS; R-devel NEWS
-
R FAQ 2.3: What is the current version of R?
-
R FAQ 3.3: What are the differences between R and S?
-
R FAQ 7.23: How can I sort the rows of a data frame?
-
Writing R Extensions: Package Dependencies
-
R Internals: SEXPTYPEs; Rest of header
-
The R Blog: Generalizing Support for Functional OOP in R and New Tricks for the R Sweave Drivers