Written and maintained by CASRAI Editorial Board
Last updated
R’s base t.test() function runs all three t-test designs — one-sample, two-sample independent, and paired — through one function, accepts data either as a formula (outcome ~ group) or as separate vectors, and defaults to Welch’s t-test rather than the equal-variances (pooled) version most other stats packages start from. This page is the R-specific procedure: both calling conventions, what the var.equal argument actually changes, and how to pull the numbers back out of the returned object for a write-up instead of re-typing them by hand.
For the statistical logic behind the test itself — why the three designs exist, the assumptions, and when a t-test is the wrong tool — see CASRAI’s guide to the t-test first and come back here to run it in R specifically. Running the same test in another package? See Stata or SPSS.
Formula interface vs. vector interface
t.test() is a generic function with two methods you’ll actually use:
- Formula interface —
t.test(outcome ~ group, data = df).outcomeis a numeric column,groupis a factor (or a column R can coerce to one) with exactly two levels. This is the natural call when your data is already in one long-format data frame — one row per observation, one column marking which group it belongs to. - Vector (default) interface —
t.test(x, y), wherexandyare two separate numeric vectors. This is the natural call for a one-sample test (justx) or when your two groups already live in two separate columns/objects rather than one long column plus a grouping factor.
The formula method only accepts a two-level grouping factor — a third level, or a numeric/character column with more than two distinct values, fails with grouping factor must have exactly 2 levels rather than silently comparing the first two.
One-sample t-test
t.test(x, mu = 0)
mu is the fixed value you’re testing the sample mean against (it defaults to 0, so set it explicitly for anything else). Example — testing whether a set of survey satisfaction scores differs from a scale midpoint of 3:
t.test(scores, mu = 3)
Independent two-sample t-test
Formula interface, with data in one long-format data frame:
t.test(score ~ group, data = df)
Vector interface, with the two groups already in separate objects:
t.test(treatment, control)
Both return the same test — t.test() doesn’t care which interface produced the two samples it’s comparing, only what they contain.
Paired t-test
Set paired = TRUE and pass the two measurement vectors in matching order (row 1 of before pairs with row 1 of after, and so on):
t.test(before, after, paired = TRUE)
Passing unpaired data with paired = TRUE — vectors of different length, or the wrong row order — either errors immediately (unequal lengths) or silently pairs the wrong observations (matching lengths, wrong order), so confirm the row correspondence before running it, not after reading the output.
The var.equal argument: why R defaults to Welch’s test
t.test()‘s var.equal argument defaults to FALSE. That default matters more than it looks: with var.equal = FALSE, R runs Welch’s t-test — it does not assume the two groups have equal population variances, and it adjusts the degrees of freedom downward (via the Welch–Satterthwaite equation) to compensate, which is why a Welch result’s degrees of freedom is usually a non-integer like 27.19 rather than a round number. Set var.equal = TRUE to run the classic Student’s pooled-variance t-test instead — the version that assumes both groups share one population variance and uses the simpler n1 + n2 - 2 degrees of freedom.
This is a real, load-bearing difference from the other packages this same guide series covers: Stata’s ttest command pools by default and needs the explicit unequal option to switch to Welch’s version, and SPSS runs Levene’s Test for Equality of Variances alongside the t-test and expects you to pick the correct output row based on that result, rather than picking the test up front. R instead defaults to the option that doesn’t require the equal-variances assumption at all — Welch’s test is valid whether or not the variances are actually equal, which is also why current statistical guidance increasingly treats Welch’s as the safer default rather than a special case (see CASRAI’s z-test vs. t-test comparison for related test-selection reasoning). If you have a specific reason to assume equal variances — a study design that guarantees it, or a requirement to match a pooled-variance result reported elsewhere — set var.equal = TRUE explicitly rather than relying on the default doing something else.
Reading the object t.test() returns
t.test() doesn’t just print output — it returns a list (class "htest") you can assign and query directly, which is what makes it usable inside a script or an R Markdown/Quarto report instead of copy-typed from the console:
result <- t.test(score ~ group, data = df)
result$statistic # the t value
result$parameter # degrees of freedom
result$p.value # the p-value
result$estimate # group mean(s)
result$conf.int # confidence interval for the difference (or mean)
result$method # which test actually ran, e.g. "Welch Two Sample t-test"
result$method is worth checking explicitly the first time you run a new comparison — it states in plain text whether R ran the Welch or the Student version, which is a fast way to confirm var.equal did what you expected without re-deriving it from the degrees of freedom.
Extracting results for a report
For inline reporting, pull the specific values out of the list rather than re-typing console output by hand — this is both less error-prone and reproducible if the underlying data changes:
sprintf("t(%.2f) = %.2f, p = %.3f",
result$parameter, result$statistic, result$p.value)
The broom package’s tidy() function does the same job for a whole table of results at once, which is useful when a script runs the same test across several outcome variables in a loop: broom::tidy(result) returns a one-row tibble with estimate (the mean difference, or group means as estimate1/estimate2 for the two-sample case), statistic, p.value, parameter (df), and conf.low/conf.high as plain columns — row-bindable across multiple tests instead of extracting each field by hand every time. See CASRAI’s guide to reporting confidence intervals and what a p-value means for how to phrase the surrounding sentence once you have the numbers, and Cohen’s d for the effect-size figure a t-test result alone doesn’t give you — t.test() reports significance, not effect size, and a complete report needs both.
Common errors
data are essentially constant— one of the groups (or the single sample, for a one-sample test) has zero variance, so the standard error is zero and the t-statistic is undefined. Check for a data-entry problem (a column that didn’t actually vary, or got recoded to one repeated value) before assuming the test is broken.grouping factor must have exactly 2 levels— the formula interface’s grouping column has one level (nothing to compare) or three-plus (not what a t-test does — that’s ANOVA’s job). Subset to the two groups you actually want, or rundroplevels()if an unused factor level survived a filter.- Not enough observations —
t.test()needs at least 2 observations per group to compute a variance at all; fewer produces anot enough 'x' observationserror rather than a result.
Frequently asked questions
Does t.test() require normally distributed data?
The t-test’s validity rests on the sampling distribution of the mean being approximately normal, not the raw data itself — by the Central Limit Theorem, that holds increasingly well as sample size grows, even when the underlying data is skewed. For small samples with visibly non-normal data, check a QQ plot (qqnorm()/qqline()) or consider wilcox.test(), R’s nonparametric alternative, instead.
How do I run a one-tailed t-test in R?
Set the alternative argument to "less" or "greater" instead of the default "two.sided" — e.g. t.test(x, y, alternative = "greater") tests whether x‘s mean exceeds y‘s, one direction only.
What’s the difference between t.test() in R and Excel’s T.TEST function?
Excel’s T.TEST returns only a p-value as a single number, with the test type and tails selected via numeric argument codes; R’s t.test() returns the full test object — statistic, degrees of freedom, confidence interval, and group estimates together — so nothing extra needs to be computed separately once you’ve run it.
Do I need a package to run a t-test in R?
No — t.test() ships in the stats package, which loads automatically with every R session. No library() call or CRAN install is needed for anything covered on this page; packages like broom are only needed for the optional tidy-output step above.
See also CASRAI’s guide to logistic regression in R for a second common inferential procedure using the same extract-and-report pattern, and the research tools hub for CASRAI’s full coverage of statistical software.








