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

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

stringsAsFactors = FALSE is the default

data.frame() and read.table() turned strings into factors

Strings stay character; stringsAsFactors = TRUE asks for the old behaviour

Data Frames

4.0.0

Raw strings

Backslashes and quotes escaped one by one

r"(C:\shelf)"

Syntax and Lexical Structure

4.0.0

Matrices also inherit from "array"

class(m) had length one

c("matrix", "array"); ask with is.matrix() or inherits()

Matrices and Arrays

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

Memory and Copy Semantics

4.0.0

New default palette()

The old, more saturated colours

Less saturated colours; the old ones are palette("R3"); palette.pals() and palette.colors() are new

Base Graphics and Devices

4.0.0

list2DF()

data.frame() or as.data.frame() on a list, which check and rewrite the names

list2DF() builds the data frame from a list of columns as given

Lists

4.1.0

Native pipe

The %>% of the magrittr package

|>, a syntax transformation

Pipes and the Native Pipe

4.1.0

Function shorthand

function(x) x + 1

\(x) x + 1

Operators and Precedence

4.1.0

...names()

names(list(...)), which evaluates every argument

...names()

Functions

4.2.0

if and while with a condition of length greater than one

A warning; the first element was used

An error

Control Flow

4.2.0

&& and || with an operand of length greater than one

The first element was used without a word

A warning in 4.2.0; an error since 4.3.0 (next rows)

Operators and Precedence

4.2.0

Pipe placeholder _

The magrittr dot, data = .

_ as a named argument, once: data = _

Pipes and the Native Pipe

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)

Encodings, Locales and Internationalization

4.2.0

Sys.setLanguage()

Sys.setenv(LANGUAGE = ...), which does not flush messages already looked up

Sys.setLanguage(); the NEWS calls it partly experimental

Encodings, Locales and Internationalization

4.3.0

&& and || with an operand of length greater than one

A warning (R 4.2.x)

An error: 'length = 2' in coercion to 'logical(1)'

Control Flow

4.3.0

_ may start an extraction chain (experimental)

coef(fit)[[2]], or an anonymous function at the end of the pipe

_$coefficients[[2]], labelled "an experimental feature" in the NEWS

Pipes and the Native Pipe

4.4.0

%||% is in base R

A helper defined in every project, or the one from rlang

x %||% y

Operators and Precedence

4.4.0

Tailcall() and Exec() (experimental)

Deep recursion, ending in a stack error

A tail call that reuses the stack frame

Function Factories and Higher-Order Functions

4.4.0

sort_by()

df[order(df$a, df$b), ]

sort_by(df, ~ a + b)

Data Frames

4.4.0

is.atomic(NULL) is FALSE

TRUE

FALSE; is.null(x) || is.atomic(x) keeps the old answer

Atomic Vectors and Types

4.4.0

declare() (experimental)

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

S4SXP is renamed OBJSXP

One type name for S4 objects

typeof() is "object" for an object that is not S4, as an S7 object is

OOP Overview and Base Types

4.4.0

use() is available (announced in the 4.5.0 NEWS)

library() attaches every export

use("pkg", "name") attaches the names given; experimental

Packages, Libraries and Namespaces

4.5.0

grepv()

grep(value = TRUE)

grepv()

Strings and Regular Expressions

4.5.0

penguins and penguins_raw

Not in R’s datasets: a data package or a CSV file was needed

Both are in the datasets package

Reading and Writing Text Data

4.5.0

use() is listed in the NEWS

(see 4.4.0)

The entry says "actually already available since R 4.4.0"

Packages, Libraries and Namespaces

4.5.0

tools::sha256sum()

tools::md5sum(), or a package

tools::sha256sum()

Connections and the File System (md5sum() of an extracted file), this page

4.5.0

is.R() is defunct (deprecated in 4.4.0)

if (exists("is.R") && is.R())

An error; delete the test

Defensive Programming and Input Validation

4.5.0

A C23 compiler is selected by default

C17, the compiler’s default standard

C23 where the compiler supports it; USE_C17 asks for the old standard

Interfacing C, C++ and Fortran

4.6.0

%notin%

!(x %in% y)

x %notin% y

Operators and Precedence

4.6.0

messageCondition()

A condition assembled with structure()

messageCondition(), like errorCondition() and warningCondition()

Conditions and Error Handling

4.6.0

bitstring()

intToBits() and paste()

bitstring()

Numbers and Arithmetic

4.6.0

match.arg(several.ok = "all")

several.ok = TRUE and a check written by hand

Several values, every one of which must match

Functions and Defensive Programming and Input Validation

4.6.0

substr(i2 = NULL)

substr(x, i, nchar(x))

substr(x, i, NULL) means "to the end"

Strings and Regular Expressions

4.6.0

panel.first and panel.last for hist(), barplot() and boxplot()

grid() drawn on top of the data

The grid is drawn first

Base Graphics and Devices

4.6.0

C++20 is the default C++ standard

C++17

C++20 where available, otherwise C++17

Interfacing C, C++ and Fortran

4.6.0

The citation of R has the DOI 10.32614/R.manuals

A citation without a DOI

citation() includes the DOI

Getting Help and Documentation

4.6.1

Bug fixes only

(see the R 4.6.1 section)

cut(), all(), zapsmall() and as.matrix() of POSIXlt among them

this page

R-devel (4.7.0, upcoming)

Compose(), Funcall(), Partial(); cumvar(); objectNotFoundError and functionNotFoundError; a warning for x[logi] with a non-multiple length

Not released

Prose only, may change before 4.7.0

this page; Function Factories and Higher-Order Functions

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\")"

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

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"

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

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"

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

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

Packages, Libraries and Namespaces

plot() generic

The S3 generic plot() moved from graphics to base, and is re-exported from graphics

Base Graphics and Devices

proportions() and marginSums()

New names for prop.table() and margin.table(), which stay

Matrices and Arrays

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)"

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

...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"

Other changes in R 4.1.0

Change Detail Page

get(), exists() and get0()

An error when the first argument has length greater than one; before, the extra elements were silently ignored

Assignment, Names and Values

The base environment is locked

Bindings can no longer be added to the base environment and namespace, or removed from them

Testing with testthat

all.equal() of functions

Also compares their environments, so a comparison of two functions with different environments can now be FALSE

Operators and Precedence

apply() gains simplify

simplify = FALSE returns a list

The apply Family and Functionals

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"

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)"

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"

Other changes in R 4.2.0

Change Detail Page

&& and ||

A warning when an operand has length greater than one; an error since R 4.3.0

Operators and Precedence

default.stringsAsFactors()

Formally deprecated; defunct in R 4.3.0

Data Frames

list2DF()

Checks that its arguments have the same length instead of recycling

Lists

Stack overflow errors

Signalled with the condition class stackOverflowError

Function Factories and Higher-Order Functions

rbind() of data frames

Warns when binding vectors whose lengths do not recycle wholly

Data Frames

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

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

Other changes in R 4.3.0

Change Detail Page

as.Date() of a number

No longer needs origin

Dates and Times

tapply()

Accepts a data frame as X and a formula as INDEX

The apply Family and Functionals

chooseOpsMethod() and the matrixOps group

A new generic for choosing between two methods of an operator, and %*% joins a new group generic

S3 Group Generics and Operator Overloading

default.stringsAsFactors()

Defunct

Data Frames

C++17 default

The default C++ standard is C++17 where available

Interfacing C, C++ and Fortran

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

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"

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"

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"

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

Other changes in R 4.4.0

Change Detail Page

UseMethod()

No longer forwards the local variables of the generic into the method (shown in the section on removals)

S3 Classes and Generics

<, >, <=, >=

Signal an error when an operand is a symbol or a call

Operators and Precedence

Rprof()

Gains the event argument and elapsed-time profiling on Unix

Performance, Profiling and Benchmarking

pictex() and xfig()

Deprecated; xfig() was removed in R 4.5.0

Base Graphics and Devices

is.R()

Deprecated; defunct in R 4.5.0

Defensive Programming and Input Validation

as.data.frame.<class>() methods

Twelve of them formally deprecated in favour of calling as.data.frame() or as.data.frame.vector()

Data Frames

C++ compiler

No default C++ compiler is set if no C++17 compiler is available

Interfacing C, C++ and Fortran

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"

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

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)

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"

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

xfig()

The device, deprecated in R 4.4.0, is removed; the error names the replacement (shown in the section on removals)

Base Graphics and Devices

install.packages() and download.packages()

Download several packages at once with libcurl

Packages, Libraries and Namespaces

methods:::.requirePackage()

Calls requireNamespace() instead of require(), so a workflow that relied on the packages being attached needs an explicit library()

Packages, Libraries and Namespaces

S4 MethodsList

linearizeMlist(), listFromMlist(), showMlist() and the "MethodsList" class, deprecated for more than nine years, are removed from methods, and several other deprecated functions of the same family are defunct

S4 Classes and Methods

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

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"

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

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”"

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"

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())

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/},
#> }

Other changes in R 4.6.0

Change Detail Page

structure(NULL, ...)

Defunct (deprecated in R 3.4.0); NULL cannot have attributes (shown in the section on removals)

Attributes and Names

Sweave drivers

Rtangle no longer puts empty lines at the end of tangled scripts

Reports with Quarto and R Markdown

Non-API entry points

More non-API functions are reported by R CMD check and some internal entry points are hidden

Interfacing C, C++ and Fortran

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

cut()

cut(1:11, {}) signals an error for the invalid breaks

all()

all(<symbol>) signals an error; before, the symbol was coerced to NULL internally, with a warning, and could access illegal memory

zapsmall()

zapsmall(x, digits = Inf) no longer returns all NaN when x contains an Inf

as.matrix()

as.matrix(<POSIXlt>) again produces a numeric matrix, and the c() and subassignment methods for POSIXlt work correctly more often

wilcox.test()

The p-value computation of exact = FALSE, correct = k had a missing dnorm(z) factor, as did the two-sample case, so the results of such calls change

stl()

Checks its tuning parameters at the R level; s.window = 0 no longer dumps core; the routine is now C code translated from Fortran

abbreviate()

Works better for non-ASCII input

dyn.load()

Overly long error messages from the system no longer corrupt its state

getGenerics()

The data part lists generic function names instead of package names

as()

Coerces S4 objects to S4 superclasses that extend old-style classes correctly, instead of returning only the S3 part

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

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

if (class(m) == "matrix"), if (returned) with a vector

the condition has length > 1

is.matrix(), inherits(), any(), all()

4.3.0

returned && TRUE with a vector operand

'length = 60' in coercion to 'logical(1)'

A single TRUE or FALSE, or & and |

4.3.0

default.stringsAsFactors()

'default.stringsAsFactors' is defunct. followed by the advice to use stringsAsFactors = FALSE

Delete the call

4.4.0

A method that reads a local variable of its generic

object 'loan_days' not found

Pass the value as an argument of the generic and the method

4.4.0

quote(pages) < 100

comparison (<) is not possible for language types

Evaluate the expression first, or compare with identical()

4.5.0

is.R()

'is.R' is defunct.

Delete the test

4.5.0

xfig()

'xfig' was removed in R 4.5.0. followed by "Consider an SVG device instead."

svg() or another device

4.6.0

structure(NULL, class = "book")

attempt to set an attribute on NULL

structure(list(), class = "book")

4.6.1

all(quote(books))

'symbol' object cannot be coerced to type 'logical', after a warning

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.

  1. 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.

  2. 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; Rscript prints 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.

  3. 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(), T and F, 1:length(x) and the magrittr pipe; they do not know is.R(), which a plain search finds. The page Style, Linting and Static Analysis explains the default set and how to enable the others.

  4. Fix what the table of removals shows, then the habits in the last table of this page.

  5. State the oldest R the code needs. A package says so in Depends, and R CMD build adds Depends: 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.

  6. 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 check on 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

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 columns Version, Date, Category, Text and HTML, 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() and nameOfClass(), 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.

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

Compose(), Funcall(), Partial()

New higher-order functions (see Function Factories and Higher-Order Functions)

cumvar()

A new function that calculates cumulative variances

objectNotFoundError, functionNotFoundError

"Object not found" errors signal conditions of class objectNotFoundError, and "function not found" errors the subclass functionNotFoundError; the condition classes used internally for some errors are documented in ?conditions

x[logi]

A logical index now warns if length(x) is not a multiple of length(logi)

use()

Gets an argument pos

.OBJSXP()

A new low-level function that enables deparsing of non-S4 objects of type "object"

read.dcf() and write.dcf()

Treat DESCRIPTION and PACKAGES files as UTF-8, as the Debian control file format requires

rbinom()

A fix that changes results; to reproduce old results RNGkind() gets a new binom.kind argument, and RNGversion("4.6") keeps the previous behaviour

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

stringsAsFactors = TRUE was the default

FALSE is the default; ask for a factor with factor() or stringsAsFactors = TRUE

4.0

Data Frames

Anonymous functions

Only function(x)

The shorthand \(x)

4.1

Operators and Precedence

Pipes

The magrittr %>% everywhere

The native |> with _ as a named argument (4.2) and, experimentally, as the head of an extraction chain (4.3); the differences (no ., no implicit braces, the rewrite happens at parse time) are explained on the page

4.1

Pipes and the Native Pipe

Conditions

if with a condition of length greater than one warned, and && with vectors silently used the first element

Both are errors

4.2 and 4.3

Control Flow

Null default

A hand-written %||% helper, or the one from rlang

The %||% of base R

4.4

Operators and Precedence

Loading packages in scripts

library() attaching everything, and require() used for control flow

library(), requireNamespace(), pkg::fun, and use() (announced in the 4.5.0 NEWS, available since 4.4.0, experimental)

4.4

Packages, Libraries and Namespaces

Iteration and data access

Ad-hoc sapply(); attach()

vapply() with a declared type; with() or an explicit $

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 methods; R6 and S7 are packages

always

OOP Overview and Base Types, Reference Classes and R6 and The S7 Object System

Package tooling

package.skeleton(), a hand-written NAMESPACE, and the R-hub v1 builder

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

Reproducible Environments with renv

Literate programming

Sweave

R Markdown or Quarto; Quarto is preferred for new projects and R Markdown is still maintained

always

Reports with Quarto and R Markdown

Web APIs

httr

httr2; plumber 1.x is stable and plumber2 is pre-1.0

always

HTTP and Web APIs and Shiny, plumber and Web APIs

Parallelism

The snow and multicore packages

The base parallel package, and the future and mirai packages

always

Parallel and Asynchronous Computing

Profiling

The dead rstudio.github.io/profvis address

The current site, https://profvis.r-lib.org/

always

Performance, Profiling and Benchmarking

is.atomic(NULL)

TRUE

FALSE

4.4

Atomic Vectors and Types

is.R()

Used to test for R rather than S-PLUS

Deprecated in 4.4 and defunct in 4.5; delete the test

4.5

Defensive Programming and Input Validation

Version notes

The facts of this page that are most often quoted wrongly, with what the NEWS says.

Claim What the NEWS says Where

use() arrived in R 4.5.0

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

Tailcall(), Exec() and declare() are stable

All three are called experimental in the R 4.4.0 NEWS

R 4.4.0

&& with a vector is an error since R 4.2.0

A warning in R 4.2.0; an error since R 4.3.0

R 4.2.0 and 4.3.0

is.R() was removed in R 4.4.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