R Pipe Operator: History and Practical Guide
Follow the R pipe operator from an early Stack Overflow answer to base R, with a practical comparison of |> and %>% and guidance on when to use each.

From Stack Overflow to Base R: The Story of the Pipe in R
At first glance, the pipe operator looks almost too simple: take a value, send it into a function, then pass the result along to the next function. In R, that small notation change has a big effect on how people write and read data analysis code. Instead of unpacking a deeply nested expression from the inside out, you can often follow a chain of operations from top to bottom.
But the pipe didn't land in R as one neat, final feature. It arrived through a mix of experiments, package design, language-design debates, and - most importantly - years of everyday use. The familiar %>% traces back to magrittr and became closely associated with dplyr. In 2021, R added a native pipe, |>. The two operators look similar, but they're not interchangeable in every situation.
This article walks through that history and the practical differences that actually matter when you're writing or maintaining R code. You'll see where each operator came from, how the native pipe behaves, and how to choose between them without turning a style preference into a hard-and-fast rule.
Why piping changed how R code reads
A pipe connects operations by taking the result on the left and using it as the input to an expression on the right. That sounds like a small syntactic convenience - but it changes the direction you read a data transformation.
Say you have a series of steps on a data frame: group the rows, pick columns, compute summaries, then filter the output. As nested function calls, the first operation ends up buried inside the last one:
filter(
summarise(
select(
group_by(data, group_a, group_b),
group_a, group_b, value
),
average = mean(value, na.rm = TRUE)
),
average > 10
)
Execution (and reading) runs from the inside out. To understand what you get at the end, you have to hunt for the initial data, work your way through the parentheses, and keep an eye on how the object changes after each call.
A pipe writes the same sequence in the order the work happens:
data |>
group_by(group_a, group_b) |>
select(group_a, group_b, value) |>
summarise(average = mean(value, na.rm = TRUE)) |>
filter(average > 10)
A pipe doesn't magically make every transformation clear. If you stack ambiguous names, rely on hidden side effects, or mix unrelated steps, you'll still have a hard time maintaining the code. What it changes is the default reading flow: each line receives the result produced by the lines above it.
A useful mental model
For a straightforward pipe expression, treat the left-hand side as the input and the right-hand side as the next operation. In base R, this:
1:10 |> mean()
is essentially the same idea as:
mean(1:10)
The left value becomes the first argument to the function call on the right. That's a good fit for many transformations, especially when the steps naturally read like "do one thing, then do the next."
It's less helpful when the value needs to go into a different argument position, when the operation includes important side effects, or when your chain gets so long that intermediate results are difficult to inspect. Pipes can improve one kind of code organization - but they don't replace naming intermediate values or breaking a messy process into functions when that's what you actually need.
If you remember one thing, make it this: the advantage isn't "fewer characters." It's that the flow of data is easier to see without hiding what each step is doing.
From an online answer to magrittr and dplyr
The story of the R pipe starts with a practical question. In January 2012, an anonymous Stack Overflow user asked about implementing the forward pipe used in languages like F#. The basic idea was to write code like data |> foo |> bar instead of nesting function calls.
Ben Bolker posted a compact implementation using %>%:
`%>%` <- function(x, f) do.call(f, list(x))
pi %>% sin %>% cos
That was a small answer to a specific problem, not the pipe most R users would later recognize. Still, it showed the core pattern: take the result of one operation and feed it into the next.
In 2012, Peter Meilstrup began an early package-based approach. He started ptools, which included a chain function. The package was later renamed vadr, and the chain function stayed available. These were among several efforts to make sequential data operations easier to write.
dplyr makes the pipeline familiar
Hadley Wickham began developing dplyr in October 2012, building on earlier work on plyr and shifting toward a new approach to data manipulation. Early versions of dplyr didn't include a pipe. People wrote nested calls, which worked - but became awkward as soon as a transformation involved multiple verbs.
In 2013, dplyr added chain, and then introduced its own pipe operator, %.%. Instead of wrestling with nested calls, you could flow a data frame through a sequence of operations. It made multi-step transformations easier to scan: group, select, summarize, filter.
It also forced a real design choice. A pipe isn't just a cosmetic operator; its behavior determines how functions get data, how errors are expressed, and what users have to learn to read pipelines safely. The dplyr pipe was built for a data-manipulation workflow, but a general-purpose pipe could be useful well beyond dplyr.
Magrittr becomes the common pipe
In December 2013, Stefan Bache returned to the original Stack Overflow discussion with a more flexible implementation. His operator could insert the left-hand value into an expression using a dot placeholder. The package started as plumbr and was renamed magrittr, referencing Belgian artist René Magritte and his painting The Treachery of Images.
From there, %>% became tightly associated with dplyr. The projects converged: dplyr deprecated its own %.% operator and switched to magrittr's %>% in 2014. RStudio added a shortcut for inserting the pipe, later standardizing on Ctrl+Shift+M. That doesn't sound dramatic, but it matters: features that are fast to type usually end up being used every day.
Magrittr's influence went beyond the operator name. It helped make data analysis code more readable and gave users a way to place the piped value into arguments beyond the first one. That flexibility is still one reason some people stick with %>%.
Other pipe experiments
The history isn't a straight line from one operator to the next. R users tried multiple ways to express data flow, often balancing flexibility against a particular coding style.
pipeR: more ways to place the input
Kun Ren published pipeR in 2014. It introduced a %>>% operator that supported several piping styles, including inserting the left-hand value into an expression, using formulas, and saving intermediate results. For example, you could fit a model, save it, summarize it, and query it later in one sequence.
That kind of flexibility is useful when you need behavior beyond "pass this value as the first argument." The trade-off is complexity: more expressive operators come with more rules for readers. A package can enable an expressive workflow, but every extra form adds to what people need to understand to maintain the code. pipeR still exists, though it hasn't had an active release cycle in years.
wrapr and the dot-arrow pipe
The wrapr package added the dot-arrow pipe, %.>%, in 2018. Its design makes the left-hand value available as a dot in the expression on the right, and it also keeps that value bound to . in the environment. For example:
1:10 %.>% mean(.)
This approach targets a different goal than the base R pipe: it's a convenient way to work inside an expression while still retaining access to the original input. As with other alternatives, the real question isn't "can it do more?" - it's whether those extra semantics help your team keep code understandable.
Not every chaining syntax is a general pipe
Some R tools handle sequential work in their own ways. For instance, data.table often uses chained expressions with [ rather than offering a general-purpose pipe operator. That syntax is built around data.table's own operations and evaluation model.
So it's worth keeping expectations straight: a pipe is one way to express a workflow, not a universal requirement for composable code. The best choice depends on the language features and conventions of the package you're using. Mixing multiple pipe styles in one codebase can create more friction than it removes, so consistency tends to beat "use everything."
The native R pipe and how to choose
People have discussed adding a native pipe to base R for years. In 2016, Lionel Henry explored what a native pipe could look like and argued that language-level handling could improve error recovery compared with relying entirely on an external package. After that, the R-devel mailing list picked up more discussion. At UseR! 2020, Luke Tierney and Martin Mächler discussed native pipe work, drawing on ideas from Henry and Jim Hester.
R 4.1.0, released in May 2021, introduced the base pipe operator |>. It gives R users a built-in option without requiring magrittr to be attached. In other words, you can write a simple dplyr pipeline with the native operator:
flights |>
dplyr::group_by(year, month, day) |>
dplyr::summarise(
average_delay = mean(arr_delay, na.rm = TRUE),
.groups = "drop"
) |>
dplyr::filter(average_delay > 30)
Here, the pipe handles the flow between function calls; dplyr still provides the verbs. The native pipe is part of R itself, but it doesn't replace dplyr (or any other package).
The practical differences between |> and %>%
For a standard pipeline - where the input is the first argument - |> and %>% often look almost identical:
1:10 |> mean()
library(magrittr)
1:10 %>% mean()
The native pipe is intentionally limited. The right-hand side has to be a function call. So 1:10 |> mean is an error, while 1:10 |> mean() is valid. magrittr also supports shorthand forms, including omitting parentheses when you use a function name directly.
The bigger difference is how the left-hand value is placed. magrittr's dot placeholder can move that value around inside the right-hand expression:
penguins %>%
rpart::rpart(species ~ ., method = "class", data = .)
In contrast, the native pipe normally supplies the value as the first argument and doesn't treat magrittr's . as a general replacement marker. When you need to pass the value to a non-first argument, R 4.2.0 and later support the native placeholder _ in a named argument:
penguins |>
rpart::rpart(species ~ ., method = "class", data = _)
This placeholder has rules: it's used inside a function call, and in this form it must appear on the right-hand side of a named argument. For other transformations, you can use an anonymous function. R 4.1.0 introduced shorthand function notation \(x):
penguins |>
(\(x) rpart::rpart(species ~ ., method = "class", data = x))()
That's explicit, but it isn't always as compact as magrittr. The overall trade-off is clear: base R keeps the pipe's behavior narrower, while magrittr gives you more substitution options.
Magrittr also offers related operators like %T>%, %$%, and %<>%, which cover teeing, exposing names, and compound assignment patterns. Those can be handy in existing code, but they add package-specific syntax. If you're starting fresh, it's worth asking whether an intermediate variable would make the intent clearer.
A decision framework for real projects
Use the native |> when you want a built-in operator, your supported R versions include it, and your pipelines mostly pass values as first arguments. It's a reasonable default for new code that doesn't already depend on magrittr.
Use %>% when your project already relies on tidyverse conventions and magrittr semantics, when the dot placeholder meaningfully improves clarity, or when you need related magrittr operators. Rewriting a working codebase just to swap one pipe symbol for another usually doesn't pay off. In practice, compatibility, team familiarity, and readability across the whole expression matter more than operator minimalism.
Before you decide, check three things:
- R version:
|>requires R 4.1.0 or later; the native_placeholder requires R 4.2.0 or later. - Argument position: Does the input naturally belong as the first argument, or does the pipeline depend on placeholder behavior?
- Codebase convention: Will the choice fit the packages, tooling, and style the team already uses?
One more practical tip: if a chain is hard to debug, name an intermediate result. Pipes are great for reading a sequence, but they're not meant to hide every intermediate state. In real production analysis code, being able to inspect and test a step is often worth a few extra lines.
Conclusion
The R pipe didn't come from one single design decision. It grew out of practical experiments: an early Stack Overflow answer, package-level chaining approaches, magrittr's flexible operator, and eventually a native feature in base R. That history is why |> and %>% are similar, but not the same.
For new work, start with the native pipe when it fits your R-version requirements and argument flow. Keep magrittr when its features solve a real readability problem - or when your existing project depends on them. Either way, use pipes to make the sequence of transformations clear, not to make complex code shorter than it really is.
For more detail, see the R language documentation, the magrittr reference, and the original history of the R pipe.