library(palaeoverse)
x <- tetrapods[1:3, c("collection_no", "collection_name", "lng")]
y <- tetrapods[1:3, c("collection_no", "collection_name", "formation")]
out <- merge(x, y, by = "collection_no")
out[["collection_name"]]Code review
install.packages(c("palaeoverse"))Motivation
We spend a lot of time writing code, but even more time reading it. This could be code that is part of a tutorial, a package, some analysis shared by a colleague, or even when we come back to our own code months later. Being able to read and understand code before running it is important, as we may not be able to immediately run code (for instance if it requires an internet connection), or we may have to wait for minutes or hours before the execution ends.
Being able to review code becomes even more important as the amount of AI-generated code keeps growing. As AI gets better at writing code, we spend more and more time reviewing code to ensure its correctness than writing it ourselves.
Finally, reviewing code is a good way to learn about new methods, new coding practices, and new functions that you might not have known existed.
How to review code
The main objectives of code review are to:
- verify that the code aligns with what we expect it to do;
- find potential mistakes;
- ensure that the code is maintainable, meaning that it follows good practices so that it can be understood, fixed, or improved in the future.
Both authors and reviewers should have realistic expectations of each other’s roles.
The code author is responsible for the code they are requesting to be reviewed, including enacting any change proposed during the process. They should do their best to follow good practices in order to facilitate code review and to ensure that other people can understand the code without asking them. This includes using informative variable names, providing clear documentation, and writing informative code comments to clarify why a piece of code is written how it is.
The reviewer’s role is to give opinions on the code from an alternative point of view to the author. This might require gathering some external knowledge that the author doesn’t necessarily have to provide. For example, if the reviewer is not familiar with a base R function that has been used, then they may need to learn this information on their own. The author is not responsible for teaching the reviewer about this function, but they should ensure that the reviewer can easily retrieve all the required information to understand the code.
This may add too much burden on the reviewer, in which case it might be better to transparently acknowledge that they cannot review some code (and, if possible, they may request a review from someone else). This is not ideal but it’s also not the end of the world. Also, finding and resolving bugs can be a beneficial part of the review process, but remember that bugs can persist in code that has been reviewed by many people!
Depending on the setting, the reviewer might start by checking that the code actually runs, but this is not always possible or needed (for instance if there are already automated tests that ensured that the code was giving the correct results).
The reviewer should then ask the following questions: what does this code do? What are the inputs and outputs? Do I have enough information (e.g. code comments, informative variable names) to understand that? Is there anything that is hard to get and not documented well enough? Is the code human-readable?
Once the reviewer is more familiar with the code, they can follow up with questions such as: what could go wrong? What are the assumptions, and what would happen if some of these break? Are these cases realistic? Are these cases properly handled?
A good way to assess your own understanding of the code is to prepare a list of expectations about what this code should do, and then compare these expectations to what the code actually does. This is not always convenient or possible, for example if the code to review is part of a pipeline that takes a long time to run, but it can uncover issues in many cases.
Note that the amount of tests or expectations to validate is also very context-dependent. In general, code written as part of a data analysis project is very specific to your use case and its purpose is not to run on completely different datasets. Code that is part of a data analysis project still needs to be properly reviewed and tested, but you have more control over how this code is used. If your project is a package meant to be used by other people, then you also need to think about all sorts of ways people might misuse your code in their own context.
Activity: Try out reviewing mini “bad code” examples that we provide
Without running these examples, do you see what is likely wrong or could be improved?
out[["collection_name"]] returns NULL because we have "collection_name.x" and "collection_name.y" in the output, because we forgot to merge on "collection_name" as well:
x <- tetrapods[1:3, c("collection_no", "collection_name", "lng")]
y <- tetrapods[1:3, c("collection_no", "collection_name", "formation")]
- out <- merge(x, y, by = "collection_no")
+ out <- merge(x, y, by = c("collection_no", "collection_name"))
out[["collection_name"]]x <- head(tetrapods$max_ma)
x_updated <- c()
for (i in seq_along(x)) {
if (x[i] > 270) {
x_updated[i] <- x[i] * 0.9
} else {
x_updated[i] <- x[i]
}
}In general, we should prefer vectorised code to for loops where possible. In this case, the function ifelse() is preferable to using an if condition in a loop because it can take the whole vector at once:
x_updated <- ifelse(x > 270, x * 0.9, x)x <- head(tetrapods$min_ma, 3)
idx_to_remove <- x[x < 0]
x[-idx_to_remove]In this case, we want to remove some values of x based on some index that we computed. The problem here is that if idx_to_remove is of length 0 because x doesn’t have negative values, then x[-idx_to_remove] returns an empty vector instead of returning the entire vector!
x[-idx_to_remove]numeric(0)
A safer way to do that is to use an if condition:
if (length(idx_to_remove) > 0) {
x[-idx_to_remove]
}Where to review code
Now that we know how to critically review code, we need to be able to communicate our findings. We could talk directly, or via email or a messaging service, but that quickly leads to information being lost, which increases the chances of these issues reappearing later. Instead, we should use tools that are dedicated to collaborative coding, such as Git and GitHub.
In this section, we assume that you are already familiar with the Git workflow to create and merge branches. We also assume that you have a GitHub account. The Git and GitHub module provides an introduction to these two tools.
How to use the GitHub interface to review pull requests (PRs)
- Go to the PR
- Go to the tab “Files changed”. All changes are displayed there. Some files are collapsed (e.g. Rd files), indicating that they are automatically generated and shouldn’t be reviewed.
- When you hover over a line, a blue cross appears on the left. This is how you can select the line for which you want to add a comment.
- Write your comment using the Markdown syntax. There is a button “Add a suggestion” that you can use to directly show the change you would like the author to apply.
- All comments you make are “Pending”, meaning that they are not visible to the author until you submit your review. They are still saved, so you can leave without submitting your review and come back to it later if needed.
- Once you have written all your comments, go to “Submit review”. There, you can decide whether you want to accept the PR, request changes (meaning that the author won’t be able to merge until you approve), or comment only. You can also write a general message regarding the PR.
- Once you are happy with your comments, you can submit your review.
Note that you can pull the branch locally to directly try it and explore more before writing your review. However, depending on the amount and on the complexity of changes, reviewing on GitHub only without actually running code is completely fine.
Self-reviewing your code
Before asking someone else to review the code, it is a good practice to review your code yourself. This largely overlaps with what a reviewer would do, meaning that you need to be able to answer the questions detailed in the section “How to review code?” above. Try to put yourself in the position of someone who knows less about your project than you do, is everything clearly explained?
You also need to ensure that your code is being tested. In an R package, this means that the test suite must not have any failures (you may also need to add new tests). In a data analysis project, this could be done via data validation (e.g. with validate), as well as with tests.
Finally, you should make sure your code to be reviewed is as self-contained as possible. It is good practice to have one pull request (PR) per new analysis, feature or bug fix: it is easier and faster to review (which benefits both the author and the reviewer) and it helps debugging in the future, since it is easier to narrow down the change that caused a problem. In many cases, PRs should contain code changes, updated documentation, and new tests, but this depends a lot on the context.
Review process
Reviewer side
Communicating comments to the author in a constructive way can be hard. At the outset, the reviewer should remember the overarching goal of the code they are reviewing. For example, a suggestion which improves performance at the expense of readability may conflict with the goals of the PR or project. This context will determine the scope and relevance of the comments needed.
It is important that the reviewer focuses on critiquing the code and not the author. Alienating the author has no benefit in terms of output and morale: either the discussion becomes more aggressive and the code isn’t properly written or reviewed anymore, or one of the parties involved simply stops spending time on the PR, and maybe will stop contributing altogether.
When the reviewer disagrees with the approach taken by the author in a part of the code, they should explain why they think another approach would be more suitable. You can also include code suggestions directly using the GitHub interface. Note, however, that reviewers do not have to specify exactly how they would do this and they don’t have to provide code directly. In general, it is up to the PR author to address comments.
For example:
- Bad: “You shouldn’t use a for loop here, this is obviously very slow.”
- Diminishes the author by using “obviously”
- Doesn’t suggest a solution.
- Better: “Can we use a vectorised function instead? This is often much faster than for loops.”
- Uses “we” to emphasise team work
- Gives the opportunity to the author to explain why they wrote the code as they did (maybe they made a mistake, maybe not).
- Gives a potential alternative (but the exploration still needs to be done by the author)
Note that positive comments are welcome to acknowledge good changes. Remember that even if you think the author did a good job, they will mostly see review comments that criticise their work, which can be demoralising.
Conclusion
Being able to critically review code, no matter who or what produced it, and to communicate feedback in a constructive way, are essential skills. In this module, we have seen the general principles of code review, how to review code in practice, and discussed how to facilitate communication between the author and the reviewer.
If you want to read more on the process, you can read the code review principles from the tidyverse team, themselves inspired from the Google code review principles.
Session info
─ Session info ───────────────────────────────────────────────────────────────
setting value
version R version 4.6.2 (2026-10-29)
os Ubuntu 26.04.1 LTS
system x86_64, linux-gnu
ui X11
language (EN)
collate C.UTF-8
ctype C.UTF-8
tz UTC
date 2026-11-11
pandoc 3.7.0.2 @ /usr/bin/pandoc
quarto 1.11.9 @ /usr/local/bin/quarto
─ Packages ───────────────────────────────────────────────────────────────────
! package * version date (UTC) lib source
ape 5.8-1 2024-12-16 [1] RSPM
askpass 1.2.1 2024-10-04 [1] RSPM
class 7.3-24 2026-08-03 [2] CRAN (R 4.6.2)
classInt 0.4-11 2025-01-08 [1] RSPM
cli 3.6.6 2026-04-09 [1] RSPM
R cpp11 <NA> <NA> [?] <NA>
curl 8.1.0 2026-10-14 [1] RSPM
DBI 1.3.0 2026-02-25 [1] RSPM
digest 0.6.39 2025-11-19 [1] RSPM
dplyr 1.2.1 2026-04-03 [1] RSPM
e1071 1.7-17 2025-12-18 [1] RSPM
generics 0.1.4 2025-05-09 [1] RSPM
geojsonsf 2.0.5 2025-11-26 [1] RSPM
R geometries <NA> <NA> [?] <NA>
geosphere 1.6-8 2026-04-05 [1] RSPM
glue 1.8.1 2026-04-17 [1] RSPM
h3jsr 1.3.1 2023-01-21 [1] RSPM
httr 1.4.9 2026-09-01 [1] RSPM
R jsonify <NA> <NA> [?] <NA>
jsonlite 2.0.0 2025-03-27 [1] RSPM
KernSmooth 2.23-27 2026-08-12 [2] CRAN (R 4.6.2)
lattice 0.23-1 2026-08-12 [2] CRAN (R 4.6.2)
lifecycle 1.0.5 2026-01-08 [1] RSPM
magrittr 2.0.5 2026-04-04 [1] RSPM
mime 0.13 2025-03-17 [1] RSPM
nlme 3.1-171 2026-09-01 [2] CRAN (R 4.6.2)
openssl 2.5.0 2026-11-02 [1] RSPM
palaeoverse 1.5.0 2026-06-11 [1] RSPM
pbapply 1.7-5 2026-09-01 [1] RSPM
pillar 1.11.1 2025-09-17 [1] RSPM
pkgconfig 2.0.3 2019-09-22 [1] RSPM
proxy 0.4-29 2025-12-29 [1] RSPM
purrr 1.2.2 2026-04-10 [1] RSPM
R6 2.6.1 2025-02-15 [1] RSPM
R rapidjsonr <NA> <NA> [?] <NA>
Rcpp 1.1.2 2026-07-05 [1] RSPM
rlang 1.3.0 2026-07-05 [1] RSPM
s2 1.1.13 2026-09-28 [1] RSPM
sf 1.1-3 2026-09-11 [1] RSPM
R sfheaders <NA> <NA> [?] <NA>
stringdist 0.9.17 2026-01-16 [1] RSPM
stringi 1.8.9 2026-08-04 [1] RSPM
stringr 1.6.0 2025-11-04 [1] RSPM
sys 3.4.3 2024-10-04 [1] RSPM
tibble 3.3.1 2026-01-11 [1] RSPM
tidyr 1.3.2 2025-12-19 [1] RSPM
tidyselect 1.2.1 2024-03-11 [1] RSPM
units 1.0-1 2026-03-11 [1] RSPM
utf8 1.2.6 2025-06-08 [1] RSPM
V8 8.2.1 2026-10-26 [1] RSPM
vctrs 0.7.3 2026-04-11 [1] RSPM
withr 3.0.3 2026-06-19 [1] RSPM
wk 0.9.5 2025-12-18 [1] RSPM
[1] /home/runner/work/_temp/Library
[2] /opt/R/4.6.2/lib/R/library
R ── Package was removed from disk.
──────────────────────────────────────────────────────────────────────────────