Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.
The goodpractice package has been recommended by rOpenSci since it was first started just over 10 years ago by Gábor Csárdi. We used to ask our editors to manually run goodpractice on all packages submitted to software peer-review, and then to ask authors to fix any notable issues flagged by the package. It is now integrated within our own pkgcheck system, and used to automatically identify any goodpractice issues with all new submissions. The package changed maintainers several times before the previous maintainers gave the green light for us to take over maintenance of the package two years ago (28th May 2024).
We’re really pleased to share that we’ve recently rolled out a host of updates and extensions to the package. These make it both easier to use, and more powerful. This was a collaborative effort between new package author, Athanasia Mo Mowinckel, current maintainer, Mark Padgham, and the generative AI tool Claude. We describe the process at the end of this tech note.
Easier control of checks
The goodpractice package is a convenience wrapper around several other packages including rcmdcheck, lintr, cyclocomp, and desc, along with a few hand-coded checks within the package itself.
The previous version of goodpractice had a total of 230 checks, generally prefixed with the name of the package which defined each check.
Checks were identifiable with the single function, all_checks().
The only way to control which checks were run was to pass a checks parameter to the main function as a character vector of the names of checks you wanted to run.
So, for example, if you wanted to skip the rcmdcheck checks, you had to do something like this:
mychecks <- grepv("^rcmdcheck", all_checks(), invert = TRUE)
gp(checks = mychecks)
That wasn’t easy! And honestly, most people just didn’t bother.
This update finally makes it easier to control checks, by defining them in “groups”, which generally correspond to the separate packages that run the actual checks.
The groups can be seen with the new all_check_groups() function, and controlled with the checks_by_group() function.
Checks can be controlled by simply naming the check groups you want to run, like this:
gp(checks = checks_by_group("description", "lintr"))
New check groups
The documentation for the all_check_groups() function shows all the new check groups added in this update, including:
| Group name | Run by default? | Description |
|---|---|---|
| description | yes | Check common issues with DESCRIPTION files, including formatting issues with URLs, DOIs, package names, and author and contributor roles, along with other common DESCRIPTION issues. |
| rd | yes | Check whether or not man/*.Rd function documentation includes both example code and return values (regardless of whether or not documentation files are generated by roxygen2). |
| roxygen2 | yes | Check for problems with Roxygen2 generated documentation. |
| revdep | no | Reverse-dependency checks. |
| code_structure | yes | Checks for common issues like duplicated or unused functions. |
| package_structure | yes | Generic checks like whether a package has a README, a NEWS file, or whether all files use a .R extension, and not .r. |
| spelling | yes | Check spelling. |
| tidyverse | no | Check compliance with the Tidyverse style guide, mostly lintr checks. |
| urlchecker | yes | Check whether all URLs are valid. |
| vignette | yes | Check that vignette code does not use either rm() or setwd(). |
Many of these checks are powered by treesitter, the extremely efficient syntax parsing library used by GitHub — which is genuinely neat to have working in R.
The number of checks has been increased from 230 to 338, with all listed in the output of the all_checks() function.
Improved check reporting
The goodpractice package has always been designed for console output. This updated version now provides immediate and consistent detail on check progress while running. The cli package is also used to give consistently formatted output for all check groups.
The large increase in checks also means that directly printing goodpractice results may fill several screens of output.
This update also adds a groups parameter to the print() method, to enable printing check results only for specified groups.
This means you can run the main gp() function once, then step through each check group by printing results only for that group, fixing those, and then moving on to the next.
We’ve found this turns what could feel like an overwhelming wall of output into a manageable to-do list.
For example:
x <- gp() print(x, "description") #> Aww! Shining package! Keep up the priceless work! print(x, "namespace") #> ── It is good practice to ───────────────────────────────────────────────────────────────────────────────────────────────────────────── #> #> ✖ remove or use internal functions that are defined but never called. Dead code increases maintenance burden. #> #> R/utils.R:3 #> R/utils.R:73 #> #> ✖ define exported (user-facing) functions before internal helper functions within each R source file. #> #> R/api.R:85 #> #> ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Notes on the development process
From Mo (Athanasia)
I’ll be honest with you: a lot of this contribution was done with an AI assistant (Claude Code using Opus 4.5) at my elbow. I want to say something about that, because I think the honest version is more useful than the marketing version. The way Mark approached reviewing this work was very thoughtful and helpful.
Based on his feedback and how it was structured, I’ve learned a lot about what a good code review looks like, and it’s been absolutely wonderful. As a person who has mainly worked on their own code-bases alone, I have adopted a style of working that often creates diff monsters (hundreds of files changed), which can make reviewing very hard. Indeed, my first PR to this project was such a monster, and Mark pushed back asking for several smaller PRs he could actually manage to review. This is also where I learned about git worktrees (Thank you Maëlle!) and asking Claude to split the work up into several worktrees for easier review, was actually the first substantial change to how I now approach working with an AI assistant. Through this collaboration I learned a lot about splitting my work into better sizes and chunks.
The shape of it was something like this:
I’d locate an issue I thought was meaningful to tackle, and provide Claude Code with the issue context and possibly an idea of how to solve it. Once it had a solution, I’d review it, push back where it felt off, and we’d iterate. Mark would then review the PR and point out the things I’d missed. Three-way collaboration, more or less, with the AI doing the typing and me doing the deciding.
What worked well was scaffolding. Drafting a new check, wiring up the prep step that goes with it, generating the test cases for the edge paths I’d otherwise forget — that kind of work compresses really nicely with an AI. I would often ask it to generate some tests first, then create code that would pass the tests. This way we had a clear idea of what we wanted the new code to do, and then solve it.
What worked less well was anything involving judgement I hadn’t articulated yet. The first few times Claude opened a PR for me, it left the test-plan checkboxes unticked — even when the tests already passed. That’s not wrong exactly, but it’s misleading to a reviewer. I had to say “no, check the box if the test exists and passes; an unchecked box reads as TODO.” Similarly, when Mark left inline code suggestions on a PR, Claude tried to helpfully re-implement them locally — which would have stripped Mark’s attribution off his own contribution. We had to agree: suggestions get accepted on GitHub, not retyped.
And then there’s the unglamorous part. It would often forget instructions, despite having them documented in agents.md and local memory for the project. These were mostly trivial, but at times fairly bad. Some of the first PRs we made, Mark pointed out that the solutions were using regexp rather than AST (Abstrat Syntax Trees). I have to admit, I didn’t really know about AST before starting this project, and I am so glad I now know of it. Claude, despite being told very clearly that we wanted AST solutions, would often forget and implement regexp based solutions. It would also often revert to using for-loops rather than vectorization, and creating nested for-loops into the 3rd or 4th level — which is just horrible to follow as a human.
If you take one thing from this aside, take this: AI-assisted contribution didn’t mean handing the package over to a machine. It meant I could move faster on the parts I already understood, explore more confidently on the parts I didn’t, and spend my actual attention on the decisions that mattered — what to check for, how to group it, what to call it, and what to leave out. The friction taught me where to watch closely. The speed-ups taught me where I could trust the loop.
It’s a different way of working, and I’m still figuring out the shape of it, but it has enabled me to contribute to this project despite some very severe health issues making it hard for me to work as normal.
And one thing Claude always does better than me: writing good commit messages. I have found myself writing my own code, but asking Claude to commit them (making it look like Claude wrote the code, but I don’t care) because the commit messages are just so much better than what I write.
From Mark
The entire idea for this major update was initiated and driven by Mo. It was also my first real foray into using generative AI tools directly in rOpenSci packages, and I learnt a lot. After I rejected the initial monster PR, things settled down into much smaller and manageable PRs that were always focussed on one specific thing.
From that point, Mo was effectively the coder (assisted by Claude), and I was the reviewer. I never knew how much of the actual code was typed versus machine-generated, but was generally pleasantly surprised that I felt no need to inquire. Nearly every PR was obviously going to improve the package, and many of them were genuinely creative ideas. (That’s where I disagree slightly with Mo’s general description of the process: I am convinced a lot of the work behind this update came directly from her brilliant insights and experience, rather than her passive description of looking over pre-existing PRs.)
The process started in Feb 2026, and involved 70 pull requests. So many of these really were independent and new ideas. That enabled me to view each one with fresh eyes, and to think about whether I might approach anything in different ways. Over so many PRs, there was a lot of back-and-forth, from pushing back on Claude insisting on nested loops, to figuring out whether code was best parsed and analysed as text, or as a syntax tree. As Mo indicated, a lot of those discussions ultimately converged towards us bringing the power of treesitter into the package, and even helping to replace previous text-based code checks with more efficient and accurate AST approaches.
Our collaboration was very pleasant and enriching, and really felt like a direct cooperation between Mo as the ideas factory, me as the reviewer, and Claude as nothing more than a mediator of our ideas. The key role of Claude throughout the process was in handling all of the fiddly, technical details. We both looked carefully at every single line of code, but the use of Claude enabled our conversations to remain at higher, conceptual levels than what would have happened if we had to actually do all the technical implementation ourselves. I think that is the aspect that I found most surprising: That the use of Claude made our collaboration feel less technical, and therefore somehow even more human. And that gave us the ability to work though 70 pull requests representing over 100 new checks, all ready for everybody to use.
Let us know what you think
Like all rOpenSci packages, goodpractice is a community effort that lives through community use and feedback. We’d love to hear what you think, via email, or through issues on the GitHub repository.
R-bloggers.com offers daily e-mail updates about R news and tutorials about learning R and many other topics. Click here if you're looking to post or find an R/data-science job.
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.
