# readxl: getting Excel data into R without installing anything else

> 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.

**tidyverse/readxl** — Read excel files (.xls and .xlsx) into R 🖇

- Repository: https://github.com/tidyverse/readxl
- Website: https://readxl.tidyverse.org
- Stars: 754 · Forks: 196
- Language: C++
- License: NOASSERTION
- Published: 2026-10-21 · Updated: 2026-10-21 · Language: en
- Canonical page: https://hysenlabs.com/projects/tidyverse-readxl

## 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:

```r
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:

```r
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 rows
```

The 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:

```r
excel_sheets(xlsx_example)
#> [1] "mtcars"   "chickwts" "quakes"
```

```r
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 rows
```

The 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.

```r
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     1
```

```r
read_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.85
```

This 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.

```r
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 rows
```

The 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:

```r
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:

```r
install.packages("readxl")
```

And for the development version straight from GitHub, the pak route is:

```r
#install.packages("pak")
pak::pak("tidyverse/readxl")
```

The first run of a new session then needs the library call:

```r
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.

## 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.

## FAQ

### 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().

## Sources

- [Issues](https://github.com/tidyverse/readxl/issues)
- [Project website](https://readxl.tidyverse.org)
- [README](https://github.com/tidyverse/readxl/blob/main/README.md)
- [Releases](https://github.com/tidyverse/readxl/releases)
- [tidyverse/readxl on GitHub](https://github.com/tidyverse/readxl)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/tidyverse-readxl
