I keep a small R project that runs nightly, processes about 40,000 rows of CSV data, and ships the cleaned output to a Postgres database (an open-source relational database management system). Last month I opened the file after six months away and immediately found three style violations that would have failed our internal review. That is the moment a linter earns its keep. A linter (a static analysis tool that reads your code without running it and flags style, syntax, and probable bugs) catches the dumb stuff the day after you write it, not six months later when you cannot remember what you were thinking.
R (a free software environment for statistical computing and graphics) does not have a single dominant linter like Python has Ruff or JavaScript has ESLint. The community has settled on a handful of tools, each with a different opinion about what “clean R” looks like. Here are the six I would actually install on a fresh R project today, in the order I would install them.
Start with lintr because it is the closest thing to a default
lintr is the package most R users mean when they say “the R linter.” It is mature, it ships on CRAN (the Comprehensive R Archive Network, R’s official package repository), and it integrates with RStudio out of the box. The defaults follow the tidyverse style guide, which is the de facto style standard for most R code in 2026.
The package lints individual files or whole directories. A typical setup looks like this:
# install once
install.packages("lintr")
# lint a single file
lintr::lint("R/clean_data.R")
# lint a whole directory
lintr::lint_dir("R/")
The output is a list of issues with file, line, and a short explanation. RStudio shows the same issues in the marker gutter (the vertical strip on the left edge of the editor that displays warning and error icons). If you use GitHub Actions, lintr has a ready-made workflow that runs on every pull request.
The defaults are opinionated. If your team uses snake_case (variable names written in lowercase with underscores, like clean_data) and the linter wants dot.case (variable names with dots, like clean.data), you will get hundreds of complaints on day one. The fix is to configure .lintr in your project root with the rules you actually want. Do not skip this step. Skipping it is the difference between a linter that helps and a linter that everybody disables after one PR.
Add styler for the opposite job
lintr reads code and complains. styler reads code and fixes it. It applies the tidyverse style guide non-destructively and writes the result back. It is the tool you run before you commit, not the tool you run in CI (continuous integration, the automated pipeline that runs every time you push code).
install.packages("styler")
styler::style_dir("R/")
The first time you run styler on a mature codebase, it touches every file. That is fine. Commit the changes as a single “apply styler” commit, then never touch formatting by hand again. The team rule is simple: lint in CI, format locally, never argue about style in a pull request.
styler is also useful as a teaching tool. New R users who read auto-formatted code pick up the conventions faster than new R users who read unformatted code. The output is what tidyverse style is supposed to look like. Reading it is practice.
Use goodpractice for the higher-level review
goodpractice runs lintr and a handful of additional checks: are you using 1:nrow(x) instead of seq_len(nrow(x)) (the off-by-one trap when nrow(x) is zero), are you saving .RData to the working directory (a habit that pollutes fresh sessions), are you using absolute paths in scripts (a portability bug that breaks on every other machine). It gives you a yes-or-no report with a handful of “this is fine in your case” exceptions.
install.packages("goodpractice")
goodpractice::gp()
I treat goodpractice as a sanity check, not a gate. The output is long, and many of the warnings are project-specific. The useful ones are the ones that flag real portability bugs: hard-coded paths, missing DESCRIPTION fields, missing package versions. Run it once on a mature project and read the report. Skip the issues that do not apply. Run it again when the project changes shape.
Use fletcher for tidyverse idiom checks
fletcher is a small linter that focuses on the tidyverse (a collection of R packages designed for data science that share a common grammar and philosophy). It checks whether you are using mutate() instead of transform(), whether you are using the pipe operator %>% (a way to chain operations so the output of one function flows into the next) consistently, whether you are using modern column types. It is narrower than lintr and more useful for teams that have committed to the tidyverse.
install.packages("fletcher")
fletcher::lint("R/clean_data.R")
fletcher does not replace lintr. It runs alongside it. The combination catches the things a general-purpose linter misses: you can write perfectly formatted R code that uses apply() everywhere instead of purrr::map(), and lintr will not blink. fletcher will.
Use parsnip or recipes as a sanity check for ML code
This one is not a linter in the strict sense. tidymodels (a collection of R packages for machine learning) has a habit of failing silently when you set up a model with a hyperparameter that does not apply to the engine. The error shows up three hours into a training run, not at setup. The fix is to dry-run the recipe.
library(recipes)
rec <- recipe(price ~ ., data = train_data) |>
step_normalize(all_numeric_predictors())
prep(rec)
If prep() does not error, the recipe is at least structurally valid. It is a one-line sanity check that has saved me from at least four wasted training runs in the last year.
Add a project-level pre-commit hook
The cheapest way to keep all six tools honest is a pre-commit hook (a script that runs automatically before each git commit and blocks the commit if the checks fail). The precommit R package wraps all of the above and gives you a YAML configuration file:
repos:
- https://github.com/lorenzwalthert/precommit
hooks:
- id: lintr
- id: styler
- id: readme-rmd-rendered
- id: parsable-R
- id: no-browser-statement
Install it once with precommit::use_precommit(), then every git commit runs the linters and blocks the commit if anything fails. The first week of using it is annoying. The second week is the last week you ever commit unlinted code.
Trade-offs
The six tools together are not free. lintr on a large codebase takes 30-60 seconds, which is fine on a developer’s machine but painful in CI. The fix is to lint only changed files in CI and the whole project on a nightly schedule. styler rewrites code non-deterministically if your team disagrees on config, which leads to noisy diffs (a confusing change list where formatting changes dominate the real edits). The fix is one canonical .lintr and one canonical styler config in the repo, with no overrides per developer. goodpractice gives you 50+ warnings per project, most of which do not apply. The fix is to suppress the ones you do not care about with a goodpractice config file, not to silence the whole tool.
For a solo R project, lintr and styler are enough. For a team of two or more, add goodpractice. For a tidyverse-heavy codebase, add fletcher. For ML-heavy work, add the prep() sanity check. The pre-commit hook is worth it from day one of any project that lives longer than a weekend.
What I would tell past me
If I could send a message back to the version of me that wrote the original 40,000-row nightly job, I would say three things.
- Install
lintrandstylerbefore you write the second file, not after you finish the project. The formatting cost compounds. Retrofitting style on 50 files is a full day. Running it from line one is invisible. - A linter is not a substitute for tests, and tests are not a substitute for a linter. They catch different bugs.
lintrcatches the bug where you wrote=instead of==and R silently assigned instead of compared. Tests catch the bug where your logic is wrong. Run both. - Do not skip the
.lintrconfig. The defaults are good for a fresh project. The defaults are not good for a project with a team and a history. Spend an hour on the config. Save a hundred hours of complaints.
Bottom line: If you write R for a living, install lintr and styler this week. Add goodpractice when the team grows past one. Run prep() on every ML recipe before training. Wire the pre-commit hook before the second PR. The rest is discipline.