R Programming • Syntax and Core Types

R Data Types — Work with numeric, integer, character, logical types

Understanding R's atomic type system is foundational to writing correct, efficient statistical programs.

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

Every programming language must decide how to represent and manipulate data in memory, and the choices a language's designers make about type systems profoundly shape its expressiveness, safety, and performance characteristics. R's type system descends from a lineage of statistical computing languages that prioritized interactive data analysis over systems-level concerns like manual memory management. Unlike C or Java, where a programmer explicitly declares every variable's type before use, R employs dynamic typing — the interpreter infers a value's type at runtime, and a single variable can be reassigned to hold a different type entirely. This design choice emerged directly from R's heritage: a language built by statisticians for statisticians, where rapid prototyping mattered more than compile-time guarantees.

1976 • S Language Created at Bell Labs

John Chambers and colleagues at Bell Labs created the S language for statistical computing. S introduced the concept of treating all data as vectors by default, with basic types including numeric, character, and logical — a design that R would later inherit wholesale.

1993 • R Development Begins

Ross Ihaka and Robert Gentleman at the University of Auckland began developing R as a free, open-source implementation of S. They preserved S's atomic type system but implemented it atop a Lisp-like interpreter with garbage collection, formalizing the six atomic vector types that persist today.

2000 • R 1.0.0 Released

The first stable release of R codified the type hierarchy: logical, integer, double (numeric), complex, character, and raw. The CRAN package repository launched the same year, and the type system's consistency across packages became a critical interoperability guarantee.

2011–present • Tidyverse and Type-Aware Tooling

Hadley Wickham's tidyverse ecosystem introduced stricter conventions around types — for instance, tibbles enforce column types more predictably than base data frames. Packages like vctrs formalized type coercion rules, reflecting the community's growing attention to type safety in production R code.

The central question this lesson addresses is deceptively simple: when you assign x <- 42 in R, what exactly is x? Is it an integer, a floating-point number, something else? Understanding how R categorizes, stores, and implicitly converts between its atomic types is essential for debugging subtle errors — the kind where TRUE + 1 yields 2 and "3" + 1 throws an error, even though both seem like they should work.

SECTION 2

Core Principles & Definitions

R's type system rests on a few foundational ideas that distinguish it from most general-purpose languages. The most important is that R has no true scalar types — even a single value like 5L is actually a vector of length one. Every atomic value in R belongs to one of six atomic vector types, four of which are the most commonly encountered in day-to-day programming: numeric (double), integer, character, and logical. The remaining two — complex and raw — serve specialized purposes and are less frequently used in typical data analysis workflows.

1

Everything Is a Vector

R has no scalar types. The expression x <- 3.14 creates a numeric vector of length 1. This vectorization pervades the language: operations automatically broadcast across elements, eliminating explicit loops for elementwise computation.
2

Atomic Homogeneity

An atomic vector can hold elements of only one type. If you attempt to combine different types in c(), R silently coerces all elements to the most general type. This implicit coercion follows a strict hierarchy: logical → integer → double → character.
3

Dynamic Typing, Strong Semantics

Variables are not declared with types; R infers them at runtime. However, R is not weakly typed in the JavaScript sense — adding a character to a number produces an error, not silent concatenation. Type discipline is enforced at the operator level.
4

typeof() vs. class()

The function typeof() returns the internal storage mode (e.g., "double", "integer"), while class() returns the object-oriented class (e.g., "numeric", "factor"). Distinguishing these two queries is critical for debugging type-related issues.
✦

KEY TAKEAWAY

Think of R's type coercion hierarchy like a series of increasingly capacious containers. A logical value (TRUE/FALSE) fits inside an integer container (1/0), which fits inside a double container (1.0/0.0), which fits inside a character container ("1"/"0"). Mixing types in a vector is like pouring liquids into the smallest container that holds them all — R always upsizes, never downsizes, and the result is always homogeneous.

SECTION 3

Visual Explanation — The Type Hierarchy

R Atomic Type Coercion HierarchylogicalTRUE / FALSEinteger1L, 42L, −7Ldouble3.14, 1e5, NaNcharacter"hello", "42"as.integeras.doubleas.character (any type)Implicit Coercion Direction (narrowest → widest)logicalintegerdoublecharacter→→→Example:c(TRUE, 3L, 2.5)→ all coerced todoublec(1.0, 3.0, 2.5)Example:c(1L, "a")→ all coerced tocharacterc("1", "a")
The top row shows the three numeric-compatible atomic types (logical, integer, double) with solid arrows indicating explicit coercion functions. Dashed arrows show that any type can be coerced to character. The spectrum bar at the bottom illustrates implicit coercion: when types are mixed in a single vector via c(), R always coerces to the widest (rightmost) type present.

The diagram above captures R's single most important typing rule: implicit coercion always moves toward the most general type. A logical value can be losslessly represented as an integer (TRUE becomes 1, FALSE becomes 0), an integer can be losslessly represented as a double (42L becomes 42.0), and any value can be losslessly represented as a character string. The reverse is not true — as.integer("hello") produces NA with a warning, because the string has no sensible integer representation. This asymmetry is precisely why coercion is one-directional: information can be preserved when widening, but may be lost when narrowing.

SECTION 4

How R Stores Types Internally

Understanding R's type system at a deeper level requires examining how each atomic type maps to underlying C representations in R's interpreter. When you create a variable, R allocates a SEXP (S-expression pointer) — a C-level structure that contains a type tag (called SEXPTYPE), metadata (length, attributes), and a pointer to the data payload. The four primary SEXPTYPE values for our atomic types are LGLSXP (logical), INTSXP (integer), REALSXP (double/numeric), and STRSXP (character).

MEMORY LAYOUT — NUMERIC VECTOR

SEXP→[SEXPTYPE=REALSXP∣length=n∣attributes∣data:double[n]]SEXP → [ SEXPTYPE=REALSXP | length=n | attributes | data: double[n] ]

Each element in a numeric (double) vector occupies 8 bytes (64-bit IEEE 754). An integer vector uses 4 bytes per element (32-bit signed). Logical vectors also use 4 bytes per element internally (stored as int). Character vectors store pointers to a global string pool (CHARSXP cache), so identical strings share memory.

MEMORY SIZE FORMULA

bytes(v)=headeroverhead+n×sizeof(elementtype)bytes(v) = header_overhead + n × sizeof(element_type)

Where n is the vector length, sizeof(double) = 8, sizeof(integer) = 4, sizeof(logical) = 4. You can verify with object.size() in R. For a 1-million-element vector, using integer instead of double saves approximately 4 MB.

⚠

Why 42 is a double, not an integer

A common source of confusion: by default, R stores bare numeric literals like 42 as doubles (REALSXP), not integers. To create an integer, you must append the L suffix: 42L. This is a deliberate design choice — since R is primarily used for statistical computing, and most statistical operations produce real-valued results, defaulting to double avoids unnecessary integer-to-double coercions in the common case. Verify: typeof(42) returns "double", while typeof(42L) returns "integer".

The practical implication of these storage details becomes significant at scale. If you are working with a dataset containing millions of rows and a column of small whole numbers (e.g., counts from 0 to 1000), storing them as integer rather than double halves the memory footprint for that column. Furthermore, integer arithmetic can be marginally faster on some architectures because the CPU can use integer-specific instructions. The object.size() function and the lobstr::obj_size() function from the lobstr package are your primary tools for profiling these memory differences.

SECTION 5

Detailed Breakdown of Each Type

Propertylogicalintegerdouble (numeric)character
typeof()"logical""integer""double""character"
class()"logical""integer""numeric""character"
Literal syntaxTRUE, FALSE, NA1L, 0L, NA_integer_3.14, 1e−3, Inf, NaN"abc", 'xyz', NA_character_
Storage (bytes/elem)4 (int internally)4 (32-bit signed)8 (64-bit IEEE 754)8 (pointer to CHARSXP)
NA variantNANA_integer_NA_real_NA_character_
Test functionis.logical()is.integer()is.double()is.character()

Comparison of R's four primary atomic vector types

Memory Representation of a Mixed-Type AssignmentTracing what happens when you run: x <- c(TRUE, 3L, 2.5, "R")Step 1: Before coercion (conceptual)TRUElogical3Linteger2.5double"R"character⬇ Widest type is character → all coerced to characterStep 2: After coercion (actual storage)"TRUE"character"3"character"2.5"character"R"characterSEXP structure (character vector, length=4)STRSXPlen=4ptr→"TRUE"ptr→"3"ptr→"2.5"ptr→"R"Each character element is a pointer into R's global CHARSXP string cache.Identical strings across the session share the same cached CHARSXP.
This diagram traces the internal representation of c(TRUE, 3L, 2.5, "R"). Because character is the widest type present, all four elements are coerced to character strings before the vector is assembled. The bottom row shows the resulting SEXP structure: a STRSXP header followed by four pointers into R's global string cache.

The detailed type comparison table above highlights several subtleties worth internalizing. First, note the asymmetry between typeof() and class() for doubles: typeof(3.14) returns "double" but class(3.14) returns "numeric". This is because "numeric" is an S3 class name that encompasses both doubles and integers in certain contexts, while "double" is the precise internal storage type. Second, each type has a typed NA variant: using NA_integer_ or NA_character_ ensures that a missing value does not inadvertently trigger coercion when inserted into a typed vector.

SECTION 6

Worked Example — Type Inspection and Coercion

Suppose you are cleaning a dataset imported from a CSV file and need to verify that each column has the expected type before performing computations. The following worked example demonstrates a realistic type-checking and coercion workflow.

Diagnosing and fixing types in an imported dataset column

  1. 1

    Step 1 — Create sample data and inspect types

    After importing a CSV, numeric columns are sometimes read as character because of stray non-numeric entries. Let's simulate this scenario: ages <- c("21", "34", "28", "N/A", "45") Now inspect the type:typeof(ages) # "character" class(ages) # "character" is.character(ages) # TRUE
    The vector is character — we cannot compute mean(ages) without coercion.
  2. 2

    Step 2 — Attempt numeric coercion

    We use as.numeric() (equivalent to as.double()) to convert: ages_num <- as.numeric(ages) # Warning: NAs introduced by coercion ages_num # 21 34 28 NA 45 The string "N/A" has no valid numeric representation, so R converts it to NA and emits a warning.
    typeof(ages_num) returns "double". The fourth element is now NA_real_.
  3. 3

    Step 3 — Convert to integer for storage efficiency

    Ages are whole numbers, so integer storage is appropriate: ages_int <- as.integer(ages_num) ages_int # 21 34 28 NA 45 typeof(ages_int) # "integer" We verify memory savings:object.size(ages_num) # 96 bytes object.size(ages_int) # 72 bytes
    For this small vector the savings are modest, but at 10⁶ elements the difference is ≈ 4 MB.
  4. 4

    Step 4 — Create a logical mask for missing data

    We often need to identify which entries are missing: mask <- is.na(ages_int) mask # FALSE FALSE FALSE TRUE FALSE typeof(mask) # "logical" Now leverage logical-to-integer coercion to count NAs: sum(mask) # 1 The sum() function coerces TRUE → 1 and FALSE → 0, then sums.
    There is exactly 1 missing value in the vector. The idiom sum(is.na(x)) is one of the most common uses of logical-to-integer coercion in practice.
  5. 5

    Step 5 — Compute the mean, excluding NAs

    mean(ages_int, na.rm = TRUE) # 32 The na.rm = TRUE argument removes NA values before computation. Without it, mean(ages_int) returns NA because R propagates missingness by default — a principled design choice that forces the analyst to make explicit decisions about missing data.
    The mean age is 32, computed as (21 + 34 + 28 + 45) / 4 = 128 / 4 = 32.

SECTION 7

Strengths, Pitfalls, and Common Gotchas

FeatureStrengthPitfall
Implicit coercionEnables elegant idioms like sum(x > 0) to count TRUE values without explicit casting.Silently converts types when combining vectors, potentially producing unexpected character vectors from mixed inputs.
Default doubleAvoids integer overflow (R integers are 32-bit, max ≈ 2.1 × 10⁹) in most statistical operations.Floating-point comparison issues: 0.1 + 0.2 == 0.3 returns FALSE. Use all.equal() instead.
NA propagationForces explicit handling of missing data, preventing silent computation on incomplete datasets.Forgetting na.rm = TRUE causes entire summaries to return NA. Beginners often spend debugging time on this.
Dynamic typingRapid prototyping — no type declarations, fast iterative analysis at the REPL.Type errors surface at runtime, not compile time. A function expecting numeric may silently receive character and fail deep in a pipeline.
Character string poolIdentical strings share memory via CHARSXP caching, making character vectors of repeated values very memory-efficient.Unique strings (e.g., UUIDs) each allocate separate cache entries, which can cause unexpected memory bloat with millions of distinct strings.

Strengths and pitfalls of R's type system design choices

✦

KEY TAKEAWAY

R's type system is optimized for the interactive data analysis use case — it prioritizes convenience and expressiveness over compile-time safety. This is analogous to the engineering trade-off between a dynamically typed scripting language like Python and a statically typed systems language like Rust. In R, the equivalent of type annotations is defensive use of stopifnot(is.numeric(x)) assertions at function boundaries — a practice that becomes increasingly important as your R code moves from exploratory analysis to production pipelines.

SECTION 8

Connection to Advanced Type Constructs

The four atomic types covered in this lesson are the foundation upon which R builds its more complex data structures. Understanding this foundation makes the higher-level abstractions — factors, dates, data frames, tibbles — far less mysterious, because each of these is ultimately an atomic vector with attributes attached. A factor, for instance, is an integer vector with a levels attribute and a class attribute set to "factor". A Date is a double vector with class = "Date", storing the number of days since 1970-01-01.

ConceptThis Lesson (Atomic Types)Advanced Topic
Homogeneous collectionAtomic vectors — all elements same typeMatrices & arrays — multi-dimensional atomic vectors
Heterogeneous collectionNot possible in a single atomic vectorLists — each element can be any type; data frames are lists of equal-length vectors
Categorical dataStored as character vectorsFactors — integer-backed with level labels, enabling ordered categories and efficient storage
Type safetyRuntime checks via is.*() functionsThe vctrs package provides formal type hierarchies; R7/S7 classes add method dispatch with type contracts
Missing dataTyped NAs: NA_integer_, etc.tidyr and data.table provide structured missing-data imputation workflows built on typed NAs

Mapping atomic type concepts to advanced R constructs

As you move into subsequent lessons on lists, data frames, and the S3/S4 object systems, you will see that mastering atomic types is not merely an academic exercise — it is the prerequisite for understanding why df$column behaves the way it does, why certain joins fail in dplyr, and why Rcpp interfaces require you to declare exact SEXP types when passing data between R and C++.

SECTION 9

Practice Problems

Question 1: CONCEPTUAL

Explain why typeof(42) returns "double" rather than "integer", even though 42 is a whole number. What design rationale justifies this default, and how would you create an integer 42 instead?
Show answer
R defaults all bare numeric literals to double (64-bit floating-point) because the language is designed for statistical computing, where most operations produce real-valued results. If R defaulted to integer, nearly every arithmetic expression would trigger an implicit integer-to-double coercion (e.g., division, square roots, means), adding overhead in the common case. Furthermore, 32-bit integers overflow at ≈ 2.1 × 10⁹, which is easily exceeded in combinatorics or large-dataset row counts. To explicitly create an integer, append the L suffix: 42L. Alternatively, use as.integer(42).

Question 2: BASIC CALCULATION

Predict the output type and value of each of the following expressions. Verify your predictions by stating what typeof() would return for each: (a) TRUE + FALSE + TRUE (b) c(1L, 2.5) (c) paste(42, "is the answer")
Show answer
(a) Value: 2, type: "integer". The + operator coerces logicals to integers (TRUE → 1, FALSE → 0), yielding 1 + 0 + 1 = 2. Since all operands coerce to integer (no double present), the result remains integer. (b) Value: c(1.0, 2.5), type: "double". Mixing integer (1L) and double (2.5) in c() triggers coercion to the wider type (double), so 1L becomes 1.0. (c) Value: "42 is the answer", type: "character". The paste() function always coerces all arguments to character before concatenation, regardless of input types.

Question 3: INTERMEDIATE

Consider the vector v <- c(TRUE, 3L, 4.5, "hello"). (a) What is typeof(v)? (b) What are the exact string values stored in v? (c) Now suppose you run as.numeric(v). What result do you get and why?
Show answer
(a) typeof(v) returns "character" because character is the widest type in the coercion hierarchy, and the vector contains a character element ("hello"). (b) The stored values are c("TRUE", "3", "4.5", "hello"). Note that TRUE becomes the string "TRUE" (not "1"), and 3L becomes "3" (not "3.0"). (c) as.numeric(v) returns c(NA, 3, 4.5, NA) with a warning about NAs introduced by coercion. The string "TRUE" cannot be parsed as a number by as.numeric() (unlike as.logical() which would recognize it), and "hello" has no numeric representation. This demonstrates that coercion to character is information-lossy in practice — the original types are not recoverable.

Question 4: APPLIED

You import a CSV with 500,000 rows. One column, status, should contain only TRUE or FALSE but was imported as character ("TRUE", "FALSE", and some "Yes"/"No" entries). Write R code that: (1) converts the column to logical, correctly handling "Yes" → TRUE and "No" → FALSE, (2) reports how many entries could not be converted, and (3) verifies the final type.
Show answer
A robust approach first normalizes the non-standard entries, then converts: # Normalize Yes/No to TRUE/FALSE strings status <- ifelse(status == "Yes", "TRUE", ifelse(status == "No", "FALSE", status)) # Convert to logical status_logical <- as.logical(status) # Count unconvertible entries original_na <- sum(is.na(as.logical(original_status))) # NAs before new_na <- sum(is.na(status_logical)) cat("Entries that could not be converted:", new_na - original_na, "\n") # Verify type stopifnot(is.logical(status_logical)) typeof(status_logical) # "logical" The key insight is that as.logical() recognizes "TRUE"/"FALSE" (case-insensitive) but not "Yes"/"No", so preprocessing is required. In a production pipeline, you would use dplyr::case_when() for more readable multi-condition logic.

Question 5: CRITICAL THINKING

R's coercion hierarchy is logical → integer → double → character. Propose and justify an alternative design where the hierarchy were different — for example, what if R did not implicitly coerce logical to integer? Discuss at least two idioms from real R code that would break and how the language would need to compensate. Conversely, identify one class of bug that such a change would prevent.
Show answer
If R removed implicit logical-to-integer coercion, two fundamental idioms would break. First, sum(x > 0) — used ubiquitously to count elements satisfying a condition — would require explicit casting: sum(as.integer(x > 0)). Second, logical indexing in arithmetic contexts like weights * (group == "A") — a common masking pattern — would fail because multiplication of double by logical would be disallowed. The language would need to introduce explicit conversion functions or operator overloads for logical operands. However, the change would prevent a subtle class of bug: accidental arithmetic on Boolean columns. For instance, mean(is_valid) currently returns the proportion of TRUE values, but a programmer might intend this column to be used only for filtering, not arithmetic. Without implicit coercion, this would raise a type error, forcing the programmer to clarify intent. This trade-off exemplifies the broader tension between convenience and safety in dynamic type systems — the same tension that motivates Python's rejection of implicit bool-to-int coercion in certain contexts (e.g., type-checking with mypy).

SUMMARY

Summary — R Data Types

Summary

R's type system is built on six atomic vector types, four of which are central to everyday programming: logical (TRUE/FALSE), integer (suffixed with L), double (the default for all numeric literals), and character (strings). R has no true scalars — every value is a vector of length one. When types are mixed in a single vector, R applies implicit coercion following the hierarchy logical → integer → double → character, always widening to the most general type present.

Use typeof() to query internal storage mode and class() for the S3 class; these can differ (e.g., typeof returns "double" while class returns "numeric"). Each type has a typed NA variant (NA_integer_, NA_real_, NA_character_) to prevent unintended coercion. Explicit coercion functions — as.integer(), as.double(), as.character(), as.logical() — give you precise control. Mastering these atomic types is the prerequisite for understanding every higher-level R data structure: factors, matrices, lists, and data frames are all built on this atomic vector foundation.

Varsity Tutors • R Programming • R Data Types — Work with numeric, integer, character, logical types