R Programming • Debugging and Testing

print() & str() — Use print/str() to inspect objects and their structure

Master R's fundamental introspection functions to diagnose bugs by revealing what your objects actually contain.

A statistical scatter plot with code braces and a variable symbol for R programming.
Inside this lesson 10 sections 5 questions 7 figures

SECTION 1

Historical Context & Motivation

Debugging is as old as programming itself, and the simplest debugging technique—printing values to the console—predates formal debuggers by decades. In R, the language's roots in S (developed at Bell Labs in the 1970s) established a tradition of interactive, REPL-driven exploration where inspecting objects at the console was the primary means of understanding program state. The functions print() and str() evolved as the two complementary lenses for this task: one shows you what an object looks like (its rendered representation), while the other reveals what it is (its internal structure). Understanding the distinction between these two perspectives is fundamental to effective debugging in R.

1976 • S Language at Bell Labs

John Chambers and colleagues create S, an interactive statistical computing language with a REPL that encourages object inspection via console output—laying the groundwork for R's print-based debugging culture.

1993 • R Is Born

Ross Ihaka and Robert Gentleman at the University of Auckland begin developing R as an open-source implementation of S. The print() generic and str() utility function are included from the earliest versions, reflecting S's inspection philosophy.

2000 • R 1.0.0 Released

R's first stable release solidifies the S3 method dispatch system that makes print() polymorphic—calling print() on a linear model yields a different output than calling it on a data frame, because each class defines its own print method.

2014–Present • Tidyverse & Tibble Printing

The tidyverse introduces tibbles with enhanced print methods that display data dimensions and column types inline. Packages like lobstr and pillar extend the str() philosophy into richer structural visualizations, but print() and str() remain the universal first resort.

Despite the proliferation of IDE-integrated debuggers (RStudio's Environment pane, breakpoints, and traceback tools), print() and str() remain indispensable because they work everywhere—inside functions, inside loops, in non-interactive batch scripts, and in remote sessions where no GUI is available. The central question they address is: What does this object actually contain, and what is its type? Mastering these two functions is the first step toward systematic debugging in R.

SECTION 2

Core Principles & Definitions

Before diving into code, it is essential to grasp the conceptual difference between value inspection and structural inspection. The print() function performs value inspection: it renders the object in a human-friendly format determined by the object's class. The str() function performs structural inspection: it compactly displays the internal type hierarchy, dimensions, and a preview of the data contained within the object. These two perspectives are complementary and together provide a complete picture of any R object.

1

print() Is Polymorphic

print() is an S3 generic function. When you call print(x), R dispatches to the appropriate method (e.g., print.lm, print.data.frame) based on the class of x. This means different object types render differently.
2

str() Is Uniform

str() ignores class-specific formatting and reveals the raw internal structure: types (int, num, chr, logi), dimensions, nesting depth for lists, and abbreviated data previews—giving you a type-system-level view.
3

Auto-Printing vs. Explicit

Typing an object name at the REPL triggers auto-printing (implicit print()). Inside functions or loops, you must call print() explicitly—auto-printing does not occur within non-interactive evaluation contexts.
4

Invisible Returns

Many R functions return values invisibly via invisible(). Assigning x <- plot(...) returns the result silently. Wrapping in print() forces output: print(x <- plot(...)), a critical debugging trick.
5

Recursive Structures

R lists and model objects can be deeply nested. str() accepts a max.level argument to control recursion depth, preventing console flooding when inspecting complex objects like lm or ggplot objects.
✦

KEY TAKEAWAY

Think of print() as reading the label on a shipping box—it tells you the product name and a summary. Think of str() as opening the box and examining the packing list—it reveals the exact contents, their types, and how they are organized inside. Effective debugging requires both: you need to know what the object says it is and what it actually is.

SECTION 3

Visual Explanation — How print() and str() See Objects

The following diagram illustrates how print() and str() provide different views of the same R object. Consider a simple data frame with three columns: a character vector, a numeric vector, and a logical vector. The print() path dispatches through the S3 method system to produce a tabular rendering, while the str() path bypasses class-specific formatting to expose the underlying list-of-vectors structure.

How print() and str() View the Same Objectdf <- data.frame(...)R object in memoryprint(df)str(df)S3 Dispatch: print.data.frame()class(df) → "data.frame" → method lookupBypass class → inspect typeof()Walk internal structure recursivelyOUTPUT (Value Inspection) name score pass1 Alice 95 TRUE2 Bob 82 TRUE3 Carol 67 FALSE↑ Human-readable table format Types are NOT shownOUTPUT (Structural Inspection)'data.frame': 3 obs. of 3 vars: $ name : chr "Alice" "Bob" ... $ score: num 95 82 67 $ pass : logi TRUE TRUE FALSE↑ Compact structural summary Types, dims, and preview shownDebugging Workflow: Use print() to verify values → Use str() to verify typesMost bugs in R are type mismatches that print() alone will not reveal
The diagram shows the dual-path inspection model. The left path (cyan) traces print() through S3 dispatch to a formatted table. The right path (violet) traces str() directly to the internal structure, revealing types and dimensions that print() hides.

Notice that the print() output on the left looks clean and readable, but it does not tell you whether score is stored as an integer or a double, or whether name is a character vector or a factor. The str() output on the right immediately answers these questions. In debugging scenarios, type mismatches are among the most common sources of errors—a column that appears numeric in print output may actually be stored as character due to a stray non-numeric value during data import. Only str() reveals this directly.

SECTION 4

How It Works — S3 Dispatch and Internal Representation

Understanding why print() and str() behave differently requires understanding R's S3 object-oriented system. R uses a dispatch mechanism where calling a generic function like print(x) causes R to look up the class attribute of x, then search for a method named print.<class>. If no class-specific method exists, R falls back to print.default(). This is why calling print() on a linear model object produces a concise coefficient summary rather than dumping the full internal list structure.

The print() Dispatch Chain

S3 METHOD RESOLUTION

print(x)→print.<class(x)[1]>(x)→print.<class(x)[2]>(x)→...→print.default(x)print(x) → print.<class(x)[1]>(x) → print.<class(x)[2]>(x) → ... → print.default(x)

R walks the class vector of x from left to right, attempting each method name. class(x)[1] is the most specific class; print.default() is the fallback. You can inspect which method will be called with methods(print) or getAnywhere(print.lm).

The str() Inspection Algorithm

In contrast, str() operates by recursively traversing the internal SEXP (S-expression) structure of the object. For each element, it reports the typeof() (the C-level storage type: integer, double, character, logical, list, etc.), the length or dimensions, and a truncated preview of the first few values. The key parameters controlling str()'s behavior are max.level (recursion depth, default is NA meaning unlimited), vec.len (number of values previewed per vector), and list.len (maximum list elements shown).

str() KEY PARAMETERS

str(object,max.level=NA,vec.len=4,list.len=99,give.attr=TRUE)str(object, max.level = NA, vec.len = 4, list.len = 99, give.attr = TRUE)

max.level: maximum nesting depth to display (NA = all). vec.len: how many elements of each vector to show. list.len: maximum number of list components. give.attr: whether to show attributes (class, names, etc.).

⚠

Auto-Printing Gotcha

A common source of confusion: in R's REPL, typing x at the prompt implicitly calls print(x). But inside a function body, a bare x on its own line produces no output. You must explicitly write print(x) or cat(x) to produce console output during function execution. This is one of the most frequent surprises for newcomers to R debugging.

SECTION 5

Object Types and Their print() / str() Signatures

Different R object types produce characteristic signatures under print() and str(). Recognizing these patterns quickly is a skill that accelerates debugging. The following diagram and table catalog the most common R objects and their inspection signatures, serving as a reference you can consult when encountering unfamiliar output.

R Object Hierarchy — str() Reveals Internal StructureR Object (SEXP)Atomic VectorsRecursive (Lists)integerint [1:n]doublenum [1:n]characterchr [1:n]listList of ndata.framen obs. × m varslm / glmList of 12+str() Output ExamplesAtomic:num [1:5] 3.14 2.72 1.41 1.62 0.58List:List of 3 $ x: num [1:3] 1 2 3 $ y: chr "hello"data.frame:'data.frame': 150 obs. of 5 variables:lm:List of 12 $ coefficients: Named num [1:2] ...Key pattern: str() always shows typeof, dimensions, then a data preview.$ prefix indicates named list elements; [1:n] indicates vector length.
This hierarchy shows R's two fundamental object families: atomic vectors (homogeneous, flat) and recursive structures (heterogeneous, nested). The bottom panel shows characteristic str() output patterns for each type.
Object Typeprint() Showsstr() ShowsKey Debugging Use
numeric vectorFormatted numbers with [1] index markersnum [1:n] followed by first few valuesCheck length and whether int vs. double
factorLevel labels (looks like character)Factor w/ k levels: internal integer codesReveals factor vs. character (a top bug source)
data.frameTabular rows and columnsn obs. of m variables, with per-column typesIdentify mistyped columns after CSV import
listEach element printed in sequence with $nameList of n with nested type treeNavigate complex nested API responses
lm (model)Call + coefficients onlyList of 12+ elements: residuals, fitted, etc.Discover all extractable model components

Comparison of print() and str() output for common R object types

SECTION 6

Worked Example — Debugging a Data Import Issue

Consider the following scenario: you read a CSV file and attempt to compute the mean of a column, but R returns NA with a warning about a non-numeric argument. This is one of the most common debugging situations in R, and it illustrates exactly why print() alone is insufficient while str() immediately reveals the problem.

Finding a Hidden Type Bug with str()

  1. 1

    Step 1 — Reproduce the Bug

    You load a dataset and compute a mean: df <- read.csv("scores.csv") then mean(df$score). R returns NA with warning: "argument is not numeric or logical: returning NA". The error seems puzzling—you expect score to be numeric.
  2. 2

    Step 2 — Try print() First

    You run print(head(df)) and see a clean table with values like 95, 82, 67, N/A, 91, 78. The numbers look numeric. The print output does not reveal the issue because the formatted display renders both numeric values and the string "N/A" identically in a plain text table.
  3. 3

    Step 3 — Use str() to Reveal the Type

    You run str(df) and see: 'data.frame': 100 obs. of 3 variables: $ name: chr "Alice" "Bob" ... $ score: chr "95" "82" "67" "N/A" ... $ pass: logi TRUE TRUE FALSE .... The score column is chr (character), not numeric! The string "N/A" in the CSV forced R to read the entire column as character.
    Bug found: score column is character, not numeric, due to a non-standard missing value marker.
  4. 4

    Step 4 — Fix the Issue

    Re-import with df <- read.csv("scores.csv", na.strings = c("NA", "N/A")) to tell R that "N/A" should be treated as a missing value. Then verify with str(df) — now score shows as num.
  5. 5

    Step 5 — Confirm the Fix

    Run mean(df$score, na.rm = TRUE) and get 82.3. Use print(summary(df$score)) to verify the distribution looks reasonable and that the NA count matches expectations. The combined use of str() for diagnosis and print() for verification confirms the fix is correct.
    mean(df$score, na.rm = TRUE) = 82.3 ✓

SECTION 7

print() vs. str() — Strengths and Limitations

Neither print() nor str() is universally superior; they serve different purposes and each has limitations that the other compensates for. A mature R programmer develops the habit of reaching for one or the other depending on the specific diagnostic question at hand. The following comparison table codifies when to prefer each function, and when to look beyond both to more specialized inspection tools.

Dimensionprint()str()
Primary Question"What does this value look like?""What type/structure does this have?"
Method DispatchS3 generic — output varies by classMostly uniform — bypasses class formatting
Type VisibilityTypes are hidden (factor looks like character)Types are explicit (int, num, chr, logi, Factor)
Large ObjectsCan flood console with thousands of rowsAlways compact; controllable via max.level
Nested ListsPrints each element sequentially (verbose)Shows tree structure with indentation (concise)
Best ForVerifying data values, quick sanity checksDiagnosing type bugs, understanding unfamiliar objects
LimitationCan mislead about types (implicit coercion)Does not show full data—only a preview

When to use print() vs. str() in debugging workflows

✦

KEY TAKEAWAY

Think of print() and str() as analogous to two diagnostic instruments in engineering. print() is like a voltmeter reading: it tells you the measured value, but not whether your circuit is wired correctly. str() is like a schematic diagram: it reveals the wiring, component types, and connections. A skilled engineer uses both tools iteratively—and so should a skilled R programmer.

SECTION 8

Beyond print() and str() — Advanced Inspection

While print() and str() form the foundation of object inspection in R, several more specialized functions extend their capabilities. Understanding how these advanced tools relate to the fundamental pair helps you select the right instrument for each debugging scenario. The table below compares print() and str() against their more advanced counterparts, situating them within R's broader inspection ecosystem.

FunctionPurposeRelationship to print()/str()
class() / typeof()Returns the S3 class or C-level type as a stringstr() calls these internally; use standalone for quick type checks
summary()Statistical summary (min, max, mean, quartiles for numerics)Complements print()—shows distributional properties, not raw values
dput() / dump()Outputs R code that would recreate the objectMore verbose than str(); useful for creating reproducible examples
lobstr::obj_str()Enhanced str() with memory addresses and reference countsExtends str() for advanced memory and copy-on-modify debugging
glimpse() (dplyr)Transposed data frame preview optimized for wide datasetsTidyverse-flavored str(); shows types + data in a more readable layout
traceback() / browser()Interactive debugger and call stack inspectionUse print()/str() inside browser() to inspect local variables at breakpoints

R inspection ecosystem—from fundamental to advanced

A productive debugging workflow typically begins with str() for structural diagnosis, proceeds to print() or head() for value verification, and escalates to browser() or traceback() only when the simpler tools fail to localize the issue. As you advance in R, you will find that print() and str() remain the first two functions you call in virtually every debugging session, regardless of your experience level.

SECTION 9

Practice Problems

Question 1: CONCEPTUAL

Explain why typing x at the R console produces output, but placing x on a line inside a function body does not. What is the mechanism responsible for this difference, and how do you force output inside a function?
Show answer
R's REPL (Read-Eval-Print Loop) has an implicit auto-printing step: after evaluating an expression, it calls print() on the result if the result is visible (not returned via invisible()). Inside a function body, R evaluates expressions but does not auto-print intermediate results—only the final return value is subject to auto-printing when the function is called interactively. To force output inside a function, you must explicitly call print(x) or cat(x). This distinction is rooted in R's evaluation semantics: the REPL's 'P' step is not part of R's general expression evaluation; it is a feature of the interactive loop only.

Question 2: BASIC CALCULATION

Given x <- c(1L, 2L, 3L) and y <- c(1, 2, 3), the output of print(x) and print(y) looks identical: [1] 1 2 3. Write the exact str() output for each and explain how str() distinguishes them.
Show answer
For x: str(x) outputs 'int [1:3] 1 2 3', indicating the vector stores 32-bit integers. For y: str(y) outputs 'num [1:3] 1 2 3', indicating the vector stores 64-bit doubles. Although print() renders both identically because R's default number formatting does not distinguish integers from doubles, str() reveals the underlying C-level storage type. This distinction matters in practice because integer overflow occurs at 2³¹ − 1 ≈ 2.1 × 10⁹, while doubles can represent much larger values. Functions expecting integer input (e.g., sequence indexing, bitwise operations) may behave differently depending on the storage type.

Question 3: INTERMEDIATE

You have a function that returns a list but the caller sees no output: my_func <- function() { result <- list(a = 1, b = "hello"); invisible(result) }. Calling my_func() at the console prints nothing. Describe two different ways to inspect the return value, and explain what str() would show for the result.
Show answer
Two approaches: (1) Capture the result and print it explicitly: out <- my_func(); print(out) or str(out). (2) Wrap the call in print() directly: print(my_func()). The invisible() function sets a visibility flag on the return value that suppresses auto-printing, but print() overrides this flag by explicitly requesting console output. For str(out), the output would be: List of 2 $ a: num 1 $ b: chr "hello". This reveals that the result is a two-element named list with a numeric element and a character element—information that helps the caller understand the function's contract and extract the right components.

Question 4: APPLIED

After fitting a linear model with model <- lm(mpg ~ wt + hp, data = mtcars), you call print(model) and only see coefficients. You need to extract the residuals and R-squared. Use str() with max.level = 1 to discover the component names, then explain how to extract both quantities.
Show answer
Running str(model, max.level = 1) reveals that the lm object is a List of 12 with named components including $coefficients, $residuals, $fitted.values, $rank, model,andothers.Theprint.lm()methoddeliberatelyshowsonlythecallandcoefficientsforbrevity—thisisacasewhereprint()′sclass−specificformattinghidesmostoftheobject′scontents.Toextractresiduals:modelmodel, and others. The print.lm() method deliberately shows only the call and coefficients for brevity—this is a case where print()'s class-specific formatting hides most of the object's contents. To extract residuals: modelresiduals or residuals(model). For R-squared, it is not stored directly in the lm object; instead, call summary(model)$r.squared. Using str(summary(model), max.level = 1) reveals that summary.lm produces a different list with additional computed statistics including r.squared and adj.r.squared. This example illustrates a key principle: print() on model objects is designed for quick interpretation, but str() is necessary to discover what information is actually available.

Question 5: CRITICAL THINKING

Design a custom S3 class called "student" (a named list with fields name, grades, and gpa) and write a custom print.student() method that displays a formatted summary. Then explain why str() on a student object would still show the raw list structure despite your custom print method. Under what circumstances could relying solely on your custom print() method lead you astray during debugging?
Show answer
Implementation: student <- function(name, grades, gpa) { obj <- list(name = name, grades = grades, gpa = gpa); class(obj) <- 'student'; obj }. Then: print.student <- function(x, ...) { cat(sprintf('Student: %s | GPA: %.2f ', xname,xname, xgpa)) }. When you call print() on a student object, R dispatches to print.student(), showing only the formatted name and GPA. However, str() ignores the class attribute for display purposes and recursively walks the underlying list, showing: List of 3 $ name: chr "Alice" $ grades: num [1:4] 95 88 76 91 $ gpa: num 3.75 - attr(*, 'class') = chr 'student'. Relying solely on print.student() could be misleading in several ways: (1) if grades accidentally becomes character instead of numeric due to a data import error, print.student() would not reveal this since it only displays name and GPA; (2) if someone adds extra fields to the list (e.g., $email), print.student() would silently ignore them; (3) if gpa is NA due to a computation error, a naive print method might display 'GPA: NA' without explaining why, whereas str() would help you trace back to the grades vector to identify the source of the NA propagation.

SUMMARY

Summary — print() & str() for Object Inspection

Summary

The functions print() and str() are R's foundational object inspection tools, serving complementary roles in debugging and exploratory programming. print() performs value inspection by dispatching through R's S3 method system to produce class-specific formatted output—human-readable but potentially misleading about types. str() performs structural inspection by bypassing class formatting to reveal the internal type hierarchy, dimensions, and a compact data preview.

Key principles to remember: auto-printing only works at the REPL—inside functions, you must call print() explicitly. invisible() return values suppress auto-printing and require explicit print() or str() to inspect. Type mismatches (e.g., character masquerading as numeric) are among the most common R bugs, and str() is the fastest way to detect them. For complex objects like model fits, use str(object, max.level = 1) to survey top-level components without console flooding. Together, these two functions form the bedrock of R debugging—master them before reaching for more sophisticated tools like browser() or traceback().

Varsity Tutors • R Programming • print() & str() — Use print/str() to inspect objects and their structure