readxl: getting Excel data into R without installing anything else
Read excel files (.xls and .xlsx) into R 🖇
At a glance
- What is it?
- readxl is the tidyverse package for reading .xls and .xlsx files into R, built on libxls and RapidXML with no external dependencies, which makes it easy to install on any operating system. It suits anyone who needs tabular data out of a spreadsheet, and it deliberately does not cover writing, formatting or non-tabular layouts.
- Who is it for?
- readxl is the right package for the common case of moving a rectangular block of cells out of an Excel file into R, because it has no external dependencies, reads both the legacy and the modern format through the same call, and returns a tibble with typed columns rather than a matrix of strings. It is the wrong package for writing spreadsheets, for formulas and formatting, and for a workbook whose layout is not tabular, since the design targets tabular data specifically.
- Can I use it commercially?
- Check first. The repository uses a licence we do not classify automatically, so read its LICENSE file before any commercial use.
- Is it still maintained?
- Yes. The repository last received commits 6 days ago.
- What is it written in?
- Mainly C++, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 25, 2026, and from our analysis. They are not legal advice.
Editorial analysis
No external dependencies, and that is the design goal
The readxl package makes it easy to get data out of Excel and into R, and the comparison the project makes is with other packages such as gdata, xlsx and xlsReadWrite, all of which need external dependencies. readxl has none, which is why it is easy to install and use on all operating systems.
The package is written in C++ and does the parsing itself. The legacy .xls format is handled by the libxls C library, which abstracts away many of the complexities of the underlying binary format, and the modern xml-based .xlsx format is parsed with the RapidXML C++ library. Both are vendored into the source tree, which is what makes the no-dependency claim possible.
The design goal is narrower than a general spreadsheet library, and the project says so: readxl is designed to work with tabular data. That single sentence eliminates a large class of use cases. There is no cell formatting, no formula evaluation, no chart reading, and no writing of any kind. What you get is rows and columns, typed.
The lifecycle badge in the readme marks the package as stable, and the site for the project is readxl.tidyverse.org, with a data import cheatsheet that covers readxl alongside the related packages readr and googlesheets4.
read_excel detects the format and returns a tibble
The entry point is read_excel(), and it reads both xls and xlsx files, detecting the format from the file extension. The result is a tibble with typed columns, which is the difference from a spreadsheet-as-grid view. In the worked example from the documentation, the same dataset read from datasets.xlsx and from datasets.xls comes back as a 32 by 11 tibble of double columns, and both produce identical output.
The package ships example files that the documentation uses throughout. The helper readxl_example() with no arguments lists them, and calling it with a filename returns the path to that file:
readxl_example()
#> [1] "clippy.xls" "clippy.xlsx" "datasets.xls" "datasets.xlsx"
#> [5] "deaths.xls" "deaths.xlsx" "geometry.xls" "geometry.xlsx"
#> [9] "type-me.xls" "type-me.xlsx"A single sheet is read with the same function, and the result is the same shape of object:
read_excel(xlsx_example)
#> # A tibble: 32 × 11
#> mpg cyl disp hp drat wt qsec vs am gear carb
#> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
#> 1 21 6 160 110 3.9 2.62 16.5 0 1 4 4
#> 2 21 6 160 110 3.9 2.88 17.0 0 1 4 4
#> 3 22.8 4 108 93 3.85 2.32 18.6 1 1 4 1
#> # ℹ 29 more rowsThe library is loaded the usual way, and note the documentation's warning that even after installing the whole tidyverse you still need to load readxl explicitly, because it is not a core tidyverse package loaded by library(tidyverse).
Sheets, cell ranges and the arguments that narrow the read
Workbooks hold several sheets and readxl lets you address them. excel_sheets() lists the names, and read_excel() takes a sheet by name or by number:
excel_sheets(xlsx_example)
#> [1] "mtcars" "chickwts" "quakes"read_excel(xlsx_example, sheet = "chickwts")
#> # A tibble: 71 × 2
#> weight feed
#> <dbl> <chr>
#> 1 179 horsebean
#> 2 160 horsebean
#> 3 136 horsebean
#> # ℹ 68 more rowsThe other axis of control is how much of the sheet to read. n_max limits the rows taken, and range selects cells, either with an Excel-style cell range, with the helper functions cell_rows() and cell_cols(), or with a sheet-qualified address such as "mtcars!B1:D5". The documentation notes you can even specify the sheet inside the range argument when providing an Excel-style range.
read_excel(xlsx_example, n_max = 3)
#> # A tibble: 3 × 11
#> mpg cyl disp hp drat wt qsec vs am gear carb
#> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
#> 1 21 6 160 110 3.9 2.62 16.5 0 1 4 4
#> 2 21 6 160 110 3.9 2.88 17.0 0 1 4 4
#> 3 22.8 4 108 93 3.85 2.32 18.6 1 1 4 1read_excel(xlsx_example, range = "C1:E4")
#> # A tibble: 3 × 3
#> disp hp drat
#> <dbl> <dbl> <dbl>
#> 1 160 110 3.9
#> 2 160 110 3.9
#> 3 108 93 3.85This matters more than it first appears. A large workbook can hold a million rows of formatting and a few hundred rows of data, and reading only the block you need is the difference between a quick call and a slow one. The range arguments also let you skip the preamble that spreadsheets always have, the title rows above the real header.
Missing values are not always blank cells
One argument deserves its own attention because it is where silent data errors come from. If NAs are represented by something other than blank cells, the na argument tells readxl what to treat as missing. The example in the documentation passes na = "0" and the effect is visible in the result: the vs column, which previously read 0, becomes NA for the first rows, because zero was in fact the marker for a missing value in that file.
read_excel(xlsx_example, na = "0")
#> # A tibble: 32 × 11
#> mpg cyl disp hp drat wt qsec vs am gear carb
#> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
#> 1 21 6 160 110 3.9 2.62 16.5 NA 1 4 4
#> 2 21 6 160 110 3.9 2.88 17.0 NA 1 4 4
#> 3 22.8 4 108 93 3.85 2.32 18.6 1 1 4 1
#> # ℹ 29 more rowsThe point is that the default is to trust the file. A blank cell is missing, and anything else is data. If a spreadsheet encodes missing values as the string NA, or as a dash, or as a sentinel number, readxl will hand you that value as ordinary data unless you tell it otherwise. That is a defensible default, but it puts the responsibility for knowing your source file on you, and it is a real source of the kind of quiet mistake that only shows up three months later in a summary table.
The column types are also inferred, so a column of mostly numbers with a stray text cell will not come back as a clean double column. The documentation does not walk through the type inference rules, so if a column comes back as character when you expected numeric, the fastest check is to look at the raw cell.
Installation is three one-liners, and the package is not loaded by tidyverse
The easiest install is the whole tidyverse from CRAN:
install.packages("tidyverse")The documentation then adds a note that is easy to miss and costs people a confused afternoon: you will still need to load readxl explicitly, because it is not a core tidyverse package loaded via library(tidyverse). So installing the tidyverse gets you the package on disk, and a separate library(readxl) is still needed in the session that uses it.
If you want only this package, that is:
install.packages("readxl")And for the development version straight from GitHub, the pak route is:
#install.packages("pak")
pak::pak("tidyverse/readxl")The first run of a new session then needs the library call:
library(readxl)Everything here is a CRAN install or a GitHub development install, so there is no system package to install, no compiler to set up, and no Java runtime, which is exactly the property the package advertises.
A read-only package, and the licence files behind it
It is worth being explicit about what readxl does not do, because the name invites assumptions. There is no write function in the package. You cannot use it to save a data frame back to a workbook, to write a formula, to apply a number format, to build a chart, or to preserve the styling of a source file. The comparison packages named in the readme, gdata, xlsx and xlsReadWrite, are described as existing packages for the same problem rather than as feature-complete alternatives, and the project points to readr and googlesheets4 as the related packages it is becoming more consistent with.
The other limitation is structural. Excel workbooks in the wild are frequently not rectangular data with a header row. They have a title block, a note explaining the columns, merged cells, blank separator rows, and a second table further down. readxl will read a range, but a range assumes a clean block, and the package does not describe heuristics for inferring where a table starts and stops inside a messy sheet. For a hand-made report, that is a real limit.
On the licence side the repository is listed with a custom classification rather than a standard open source identifier, and the tree holds three separate licence files: LICENSE, LICENSE.md and LICENSE.note. That arrangement is unusual enough to be deliberate, and the terms are in those files rather than in the readme. A team shipping a product that depends on readxl should read them; this article does not interpret them.
The maintenance picture is clear. The current release is 1.6.0, published on 2026-10-17, after 1.5.0 on 2026-05-16 and 1.4.5 on 2025-03-07, and the last push was on 2026-10-19. The readme is generated from README.Rmd, the lifecycle badge says stable, and the repository carries the usual tidyverse scaffolding: DESCRIPTION, NAMESPACE, NEWS.md, R/, src/, man/, tests/, vignettes/, revdep/ and a pkgdown/ directory for the site.
Editorial conclusion
readxl is the right package for the common case of moving a rectangular block of cells out of an Excel file into R, because it has no external dependencies, reads both the legacy and the modern format through the same call, and returns a tibble with typed columns rather than a matrix of strings. It is the wrong package for writing spreadsheets, for formulas and formatting, and for a workbook whose layout is not tabular, since the design targets tabular data specifically. Before you adopt it, check the three things that decide whether it fits: that your data really is tabular, because the cell range arguments assume a rectangular block with a header row; that the na argument covers whatever your source uses for a missing value, since Excel represents blanks in several ways; and the licence, because the repository carries LICENSE, LICENSE.md and LICENSE.note files, a custom arrangement that your organisation's licence review needs to look at rather than assume from the package's popularity.
Frequently asked questions
How do I install readxl in R?
Install it from CRAN with install.packages("readxl"), or install the whole tidyverse with install.packages("tidyverse"), in which case you still need to load readxl explicitly because it is not a core tidyverse package. The development version is installed with pak::pak("tidyverse/readxl") after installing pak.
Can readxl read both .xls and .xlsx files?
Yes. read_excel() reads both formats and detects which one you have from the file extension. The .xls format is parsed with the libxls C library and the xml-based .xlsx format with the RapidXML C++ library, both vendored so the package has no external dependencies.
How do I read a specific sheet or range of cells with readxl?
Use excel_sheets() to list the sheet names, then pass sheet to read_excel() by name or by number, and use n_max or the range argument to limit what is read. The range argument accepts an Excel-style range such as "C1:E4", a sheet-qualified range such as "mtcars!B1:D5", or the helpers cell_rows() and cell_cols().
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/tidyverse-readxl)Was this useful?