Tibbles (Modern Data Frames)
Reviewed & published by Brayan K
A tibble is the tidyverse's modern reimagining of the data frame — a rectangular table that prints compactly, never silently changes your data's type, and never partial-matches a column name.
Part of the free R course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
In this lesson you'll build tibbles with tibble(), see how they differ from data.frame(), convert existing data with as_tibble(), and meet list columns — the feature that powers advanced tidyverse workflows.
What You'll Learn in This Lesson
- How to build a tibble with tibble() and reference columns as you go
- Why tibbles print column types and only the first 10 rows
- How tibbles avoid factor conversion and stringsAsFactors surprises
- Why partial column-name matching is disabled (and safer)
- How to convert with as_tibble() and back with as.data.frame()
- What a list column is and when it's useful
1️⃣ Building a Tibble
You create a tibble with tibble(), passing one named argument per column. Each column is an ordinary vector, and all columns must be the same length (length-1 values are recycled). When you print it, a tibble shows the dimensions, the column names, and — crucially — the type of each column (<chr> for character, <dbl> for double).
# A tibble is the tidyverse's modern take on a data frame.
# Load it from the tibble package (part of the tidyverse).
library(tibble)
# Build one column-by-column with tibble():
people <- tibble(
name = c("Ada", "Linus", "Grace"),
age = c(36, 28, 45),
city = c("London", "Helsinki", "New York")
)
people
# tibble() prints a compact summary with the dimensions and the
# column TYPE under each name (<chr>, <dbl>) — far more informative
# than the plain output of a base data.frame.A neat bonus: tibble() evaluates its arguments in order, so you can build a column from columns defined earlier in the same call.
# Unlike data.frame(), tibble() lets you REFER to a column you just
# defined while building the next one — handy for derived values.
library(tibble)
sales <- tibble(
units = c(10, 20, 30),
price = c(5, 5, 4),
total = units * price # <- uses 'units' and 'price' defined above
)
sales
# total is 50, 100, 120. With base data.frame() this line would error
# because 'units' and 'price' aren't visible yet.2️⃣ No Surprises: Types & Names
Two long-standing data-frame gotchas disappear with tibbles. First, tibbles never coerce a character column into a factor — what you type is what you get. (Old R turned strings into factors unless you set stringsAsFactors = FALSE; this was the default until R 4.0.) Second, tibbles disable partial matching of column names, so a typo returns NULL with a warning instead of silently handing you the wrong column.
# Tibbles remove two classic data.frame "surprises".
library(tibble)
# 1) No automatic factor conversion. In old R, data.frame() turned
# character columns into factors unless you set stringsAsFactors.
df <- data.frame(grade = c("A", "B", "A")) # may become a factor (older R)
tib <- tibble(grade = c("A", "B", "A")) # ALWAYS stays character
class(df$grade) # "factor" on R < 4.0, "character" on R >= 4.0
class(tib$grade) # always "character"
# 2) No partial matching of column names. A typo fails loudly instead
# of silently returning the wrong column.
tib$gr # NULL + a warning (does NOT match 'grade')
# A base data.frame would have quietly returned the 'grade' column.These guarantees matter most in scripts you run unattended: a silent factor conversion or a partial-match typo can corrupt an analysis without raising an error. Tibbles fail loudly so you catch problems early.
3️⃣ Converting & List Columns
You rarely have to start from scratch. as_tibble() upgrades any existing data frame, and because tibbles don't use row names, you can capture them into a real column with the rownames = argument.
# Convert an existing data frame (or matrix) with as_tibble().
library(tibble)
# mtcars is a built-in base data.frame with row names.
cars_tbl <- as_tibble(mtcars, rownames = "model")
cars_tbl
# as_tibble() keeps the row names as a real 'model' column (tibbles
# don't use row names), and prints the first 10 rows with column types.
# Going back to a plain data frame is just as easy:
back <- as.data.frame(cars_tbl)The most distinctive tibble feature is the list column: a column whose cells each hold a list element — a vector, a data frame, even a model. Tibbles print these cleanly, which is why they underpin nested, "many models" workflows.
# A tibble column can itself hold a LIST — one list per row. This
# "list column" is impossible to print cleanly in a base data.frame
# but is central to advanced tidyverse workflows (e.g. nested models).
library(tibble)
nested <- tibble(
group = c("a", "b"),
values = list(c(1, 2, 3), c(10, 20)) # each cell holds a vector
)
nested
# The 'values' column shows <dbl [3]> and <dbl [2]> — the type and
# length of the list element in each row. You can later map() over it.Your turn. Fill in the ___ blank, run it, and confirm the column types print.
# 🎯 YOUR TURN — replace each ___ then run it.
library(tibble)
# 1) Build a tibble of 3 products with name, price, and quantity.
inventory <- tibble(
name = ___, # 👉 replace ___ with c("Pen", "Mug", "Book")
price = c(2, 8, 15),
quantity = c(40, 12, 7)
)
# 2) Add a 'value' column = price * quantity by referencing the
# columns you just made (rebuild with tibble() or use transform()):
inventory <- tibble(
name = inventory$name,
price = inventory$price,
quantity = inventory$quantity,
value = price * quantity # references price & quantity above
)
# 3) Print it. Confirm the <dbl> type tags appear and value is correct.
inventoryMini-Challenge: tibble vs data.frame
Build the same dataset two ways and compare. This is the fastest way to feel the difference in printing, type handling, and strictness for yourself.
# 🎯 MINI-CHALLENGE: tibble vs data.frame, side by side.
#
# 1. Create the SAME small dataset twice: once with data.frame()
# and once with tibble() (columns: city = chars, temp = numbers).
# 2. Use class() on the character column of each — show how a tibble
# guarantees "character" while old data.frame() might give "factor".
# 3. Convert the data.frame version to a tibble with as_tibble().
# 4. Print both and describe (in a comment) two differences you see
# in how they print (column types shown? rows truncated?).
#
# Write it yourself, then run it in RStudio or rdrr.io/snippets.
# your code hereCommon Errors (and the fix)
- ❌ could not find function "tibble" — ✅ run library(tibble) (or library(tidyverse)) first; the package must be loaded.
- ❌ Columns of different lengths error — ✅ every column must be the same length, except length-1 values, which are recycled. Check with lengths().
- ❌ A function complains it can't handle the tibble — ✅ a few older base functions expect row names; wrap with as.data.frame().
- ❌ tib$nam returns NULL — ✅ that's the strict-name feature working. Spell the column name in full; tibbles don't partial-match.
Pro Tips
- 💡 See more rows: print(tib, n = 50) overrides the 10-row default; View(tib) opens the full table in RStudio.
- 💡 Row-by-row entry: tribble() lets you type a small tibble in a readable, spreadsheet-like layout.
- 💡 Glimpse instead of print: glimpse(tib) shows every column with its type and first few values — great for wide tables.
📋 Quick Reference — Tibbles
| Task | R Syntax |
|---|---|
| Create a tibble | tibble(a = 1:3, b = c("x","y","z")) |
| Convert a data frame | as_tibble(df) |
| Keep row names | as_tibble(df, rownames = "id") |
| Back to data frame | as.data.frame(tib) |
| Print more rows | print(tib, n = 50) |
| Type-by-type summary | glimpse(tib) |
🎉 Lesson Complete!
- ✅ A tibble is a modern data frame from the tibble package
- ✅ tibble() prints column types and only the first 10 rows
- ✅ No silent factor conversion and no partial name matching
- ✅ as_tibble() upgrades a data frame; as.data.frame() goes back
- ✅ List columns let each cell hold a vector, frame, or model
Practice quiz
What is a tibble?
- A base R matrix
- A named list only
- The tidyverse's modern data frame
- A plotting function
Answer: The tidyverse's modern data frame. A tibble is the tidyverse's enhanced data frame from the tibble package.
What class does a tibble have?
- Only "data.frame"
- Only "matrix"
- "list" alone
- c("tbl_df", "tbl", "data.frame")
Answer: c("tbl_df", "tbl", "data.frame"). A tibble IS a data frame with extra classes layered on top.
How many rows does a tibble print by default?
- The first 10
- All of them
- Just 1
- 100
Answer: The first 10. A tibble prints only the first 10 rows plus a summary, to avoid flooding the console.
What does a tibble show under each column name when printed?
- The row count
- The column type, e.g. <chr> or <dbl>
- Nothing
- The mean
Answer: The column type, e.g. <chr> or <dbl>. Tibbles print the type tag (<chr>, <dbl>) beneath each column name.
How do tibbles handle character columns compared to old data.frame()?
- They drop them
- They sort them
- They never coerce them to factors
- They convert them to numbers
Answer: They never coerce them to factors. Tibbles keep characters as characters; no silent factor conversion.
What happens with partial column-name matching on a tibble (e.g. tib$gr for 'grade')?
- It returns 'grade'
- It errors fatally
- It renames the column
- It returns NULL with a warning (no partial match)
Answer: It returns NULL with a warning (no partial match). Tibbles disable partial matching, so a typo returns NULL with a warning.
Which function converts an existing data frame into a tibble?
- as_tibble()
- make.tibble()
- tibble.cast()
- to_tbl()
Answer: as_tibble(). as_tibble() upgrades a data frame (or matrix) to a tibble.
How do you convert a tibble back to a plain data frame?
- unframe()
- as.data.frame()
- untibble()
- base()
Answer: as.data.frame(). as.data.frame() turns a tibble back into a base data frame.
What is a list column in a tibble?
- A column of column names
- A factor column
- A column of NA values
- A column whose cells each hold a list element (vector, frame, or model)
Answer: A column whose cells each hold a list element (vector, frame, or model). List columns store one list element per row, powering nested workflows.
Can you reference a column you just defined within the same tibble() call?
- Yes — tibble() evaluates arguments in order
- No, never
- Only with data.frame()
- Only for numeric columns
Answer: Yes — tibble() evaluates arguments in order. tibble() lets later columns use earlier ones; base data.frame() cannot.
Continue this course
- Previous: Checkpoint: R Basics
- Next: Reshaping: pivot_longer & pivot_wider — Move between wide and long (tidy) data with tidyr
- Quick reference: R cheat sheet
Frequently asked questions
Is a tibble a different object from a data frame?
Not really — a tibble IS a data frame, just with extra classes layered on top. Its class is c("tbl_df", "tbl", "data.frame"), so every function that accepts a data frame also accepts a tibble. The tibble simply overrides a few behaviours (printing, subsetting, type handling) to be safer and more readable. You can convert back and forth freely with as_tibble() and as.data.frame().
Why does my tibble only print 10 rows?
By design. A tibble prints just the first 10 rows and only as many columns as fit your console, then summarises the rest — so accidentally printing a million-row object won't flood your screen. The columns that don't fit are listed by name and type at the bottom. To see more, use print(tib, n = 50) for more rows, or View(tib) in RStudio for the full interactive table.
Do I have to switch everything to tibbles?
No. Base data frames still work everywhere and aren't going away. Tibbles shine in tidyverse pipelines (dplyr, tidyr, ggplot2 all return and accept them happily) and when you want predictable printing and type handling. If a non-tidyverse function complains about a tibble, wrap it in as.data.frame(). Many people use tibbles throughout an analysis and only drop to a plain data frame when a specific function requires it.
What is a list column and when would I use one?
A list column stores a list — one element per row — so each cell can hold an entire vector, data frame, or even a fitted model. It's the foundation of 'many models' workflows: you group your data, nest each group into its own tibble inside a column, fit a model per row with purrr::map(), and keep everything in one tidy table. Base data frames technically allow list columns too, but tibbles print them cleanly and the tidyverse is built to work with them.