The Problem Pipes Solve
A pipe takes the value before it and feeds it into the function call after it - x |> f() means f(x). That sounds like a trivial rewrite until you chain three or four steps. Without a pipe, multi-step transformations nest, and nested calls read backwards: the first thing that happens sits at the innermost position.
To read round(sqrt(x), 1) you start in the middle (x), work outwards (sqrt, then round), and keep track of which trailing , 1) belongs to which call. Three levels deep this is annoying; five levels deep, with data-frame verbs and multiple arguments each, it's genuinely hard to follow. The alternative people reach for - saving every intermediate step into tmp1, tmp2, tmp3 - clutters the workspace with names nobody needs.
A pipe rewrites the chain in the order the steps actually happen: take x, then square-root it, then round it.
The Native Pipe |>
Since R 4.1, the pipe is built into the language itself - no package required. |> inserts the previous value as the first argument of the next call:
c(1, 4, 9, 16) |> sqrt() |> round(1) is exactly round(sqrt(c(1, 4, 9, 16)), 1) - R literally rewrites one into the other while parsing, so there is zero runtime overhead. But now the code reads as a recipe, top to bottom: the data, then each transformation in order. This works with any function, not just data tools - mtcars |> head(3) is plain base R end to end.
Notice how well pipes fit functions whose first parameter is "the data": head, sort, unique, summary, and every dplyr verb are all designed that way, which is why pipelines feel so natural in R.
The Rules of |>
The native pipe is deliberately strict. Three rules cover almost everything:
1. The next step must be a call, with parentheses. x |> sqrt is a syntax error; you must write x |> sqrt():
c(1, 4, 9) |> sqrt # Error: The pipe operator requires a function call as RHS
c(1, 4, 9) |> sqrt() # correct
2. The value lands in the first argument position. Extra arguments you write stay in place after it - x |> round(1) is round(x, 1).
3. To land anywhere else, use the _ placeholder with a named argument (R 4.2+). Classic case: lm takes a formula first and the data second, so piping a data frame into it needs data = _:
The _ may appear once, and only as a named argument. When that's too restrictive, pipe into an anonymous function - it can put the value wherever it likes:
The lambda is wrapped in parentheses and then called with () - that keeps rule 1 satisfied. If you find yourself doing this often in one pipeline, that step probably wants to be a named function.
The magrittr Pipe %>%
Before the language had a pipe, the magrittr package provided one, and %>% became the signature of tidyverse code - loading dplyr gives it to you automatically. You will see it in nearly every dplyr tutorial and script written in the last decade:
library(dplyr)
mtcars %>%
filter(mpg > 30) %>%
select(mpg, wt)
Because %>% comes from a package, these examples aren't runnable in a bare R session - Error: could not find function "%>%" is exactly what you get if you forget library(dplyr) (or library(magrittr)).
Behavior for the common case is identical to |>: feed the previous value into the first argument of the next call. But magrittr is more permissive:
c(1, 4, 9) %>% sqrt # bare function name - no parentheses needed
mtcars %>% lm(mpg ~ wt, data = .) # . placeholder, usable anywhere
c(-2, 3) %>% { max(abs(.)) } # . can even appear several times, in nested calls
|> vs %>%: The Differences That Matter
For a first-argument pipeline - which is the vast majority of real code - the two are interchangeable. The differences live at the edges:
- Availability:
|>is base R 4.1+;%>%needs magrittr (usually via dplyr). - Parentheses:
|>demandssqrt();%>%accepts baresqrt. - Placeholder:
|>uses_, once, as a named argument only (4.2+);%>%uses., anywhere, any number of times, including inside nested calls. - Speed:
|>is rewritten at parse time into the nested call - zero cost.%>%is a regular function call with a small (in practice negligible) overhead.
Nothing here makes %>% wrong - it's battle-tested and slightly more flexible. But everything it does beyond |> can be done with a lambda, and the base pipe needs no dependency.
Which One Should You Use?
For new code: use |>. It's part of the language, works everywhere R 4.1+ runs, needs no package, and its strictness reads as a feature - pipelines stay simple or get refactored into named functions.
But you must be able to read %>% fluently, because tidyverse codebases, Stack Overflow answers, and most R books written since 2014 are full of it. Teams with an existing dplyr style often keep %>% for consistency, and that's a reasonable call - the worst choice is mixing both operators in one file. Whichever you write, one convention carries over: in long pipelines, put each step on its own line with the pipe at the end of the line, so the recipe reads as a list of steps.
The same taste applies here as with the apply family: pipes are for linear, step-after-step transformations. When a computation branches or needs intermediate results twice, assign a well-named variable instead of forcing everything through one chain.
What You Take Away
- A pipe feeds the previous value into the next call:
x |> f() |> g()isg(f(x)), written in the order it happens. |>is base R (4.1+): requires parentheses, targets the first argument,_placeholder with a named argument (4.2+).%>%is magrittr/tidyverse: bare names allowed,.placeholder anywhere - but it needs a package.- Prefer
|>in new code; read%>%everywhere else without blinking. - Pipes suit linear recipes; branching logic deserves named variables.
Next up: packages - where %>%, dplyr, and the other 20,000 extensions of R actually come from.
Frequently Asked Questions
What does %>% mean in R?
%>% is the magrittr pipe: it takes the value before it and feeds it as the first argument of the call after it, so x %>% f() %>% g() means g(f(x)). It comes from the magrittr package and is loaded automatically with dplyr and the tidyverse - it is not part of base R.
Do I need a package to use |> in R?
No. |> is the native pipe, built into base R since version 4.1 - it works in a plain R session with nothing installed. %>% is the one that needs a package (magrittr, or anything that re-exports it, like dplyr).
What is the difference between |> and %>% in R?
Both feed the previous value into the next call. |> is base R, requires explicit parentheses (x |> sqrt()), and uses the _ placeholder only with a named argument. %>% needs magrittr, accepts bare function names (x %>% sqrt), and its . placeholder can go anywhere, any number of times. Behavior is identical for the common first-argument case.