Output & Running
Hello, World
print("Hello, World!") cat("Hello, World!
") R's
cat() writes its arguments as plain text with no quotes and no index marker, but it does not add a trailing newline — you supply "\n" yourself. A top-level statement is a complete program, as in Python; there is no main.print() vs cat()
print(42)
print("hi") print(42)
print("hi") R has two output functions.
print() shows a value the way the console does — with a [1] position marker and quotes around strings ([1] "hi") — while cat() writes plain text. Python's print() is closer to R's cat().Building output strings
name = "Alice"
age = 30
print(f"{name} is {age}") name <- "Alice"
age <- 30L
cat(sprintf("%s is %d
", name, age)) R has no f-strings. The closest tools are
sprintf() (C-style format) and paste(). Note that %d requires an integer, so age is written 30L; a plain 30 is a double and would error with %d.Comments
# a single-line comment
print("done") # trailing comment # a single-line comment
print("done") # trailing comment R uses
# for comments exactly like Python. Unlike Python's triple-quoted strings, R has no block-comment syntax — every line needs its own #.Running a script
# Run a Python file from the shell:
python3 script.py # Run an R file from the shell:
Rscript script.R An R script is run with
Rscript, the counterpart to invoking python3. These are shell commands, not language statements, so they cannot run in the browser.Variables & Types
Assignment
count = 5
count = count + 1
print(count) count <- 5
count <- count + 1
print(count) The idiomatic assignment operator in R is
<-. Plain = also works for assignment, but convention reserves = for passing named arguments to functions, so <- is what you will see everywhere.Dynamic typing
value = 5
value = "now a string"
print(value) value <- 5
value <- "now a string"
print(value) Like Python, R is dynamically typed: a variable can be rebound to a value of any type at any time. No declaration is needed.
Numbers are doubles
value = 5
print(type(value)) value <- 5
print(class(value))
print(class(5L)) A bare numeric literal in R is a double, even when it looks like an integer —
class(5) is "numeric". To get a true integer you append L, as in 5L. Python instead infers int from 5 and float from 5.0.Booleans
flag = True
print(flag and False)
print(not flag) flag <- TRUE
print(flag && FALSE)
print(!flag) R's boolean literals are the fully capitalised
TRUE and FALSE (the abbreviations T and F also work but are discouraged). The operators are &&, ||, and ! rather than the words and, or, not.NULL and NA
missing = None
print(missing is None) absent <- NULL
unknown <- NA
print(is.null(absent))
print(is.na(unknown)) Where Python has a single
None, R has two distinct ideas: NULL is the absence of a value (an empty object), while NA is a present-but-unknown value used for missing data. Statistical code relies on this distinction constantly.Vectors & Vectorization
Creating a vector
numbers = [1, 2, 3, 4]
print(numbers) numbers <- c(1, 2, 3, 4)
print(numbers) The everyday R collection is the atomic vector, built with
c() ("combine"). All elements must share one type. It is closer to a typed array than to a Python list, and it is the foundation of nearly everything in R.There are no scalars
value = 42
# len(42) is a TypeError in Python
print(value) value <- 42
print(length(value))
print(value[1]) In R there is no separate scalar type —
42 is simply a numeric vector of length 1. You can take its length() (1) and index its first element with [1]. This is why operations generalise so naturally to whole vectors.Vectorized arithmetic
numbers = [1, 2, 3, 4]
doubled = [x * 2 for x in numbers]
print(doubled) numbers <- c(1, 2, 3, 4)
doubled <- numbers * 2
print(doubled) Arithmetic in R applies element-wise to a whole vector, so
numbers * 2 needs no loop or comprehension. This is the single biggest day-to-day difference from Python: you describe the operation on the whole vector, not on each element.Indexing is 1-based
letters = ["a", "b", "c"]
print(letters[0])
print(letters[2]) letters <- c("a", "b", "c")
print(letters[1])
print(letters[3]) R indexes from 1, not 0, so the first element is
letters[1] and the last of three is letters[3]. This matches mathematical convention and is shared with Julia, Fortran, and MATLAB.Negative indexing differs
numbers = [10, 20, 30]
# In Python, -1 means the LAST element
print(numbers[-1]) numbers <- c(10, 20, 30)
# In R, -1 REMOVES the first element
print(numbers[-1]) This is a classic trap. In Python a negative index counts from the end (
-1 is the last element). In R a negative index excludes those positions, so numbers[-1] returns everything except the first element. To get the last element in R, use numbers[length(numbers)] or tail(numbers, 1).Slicing and ranges
numbers = [10, 20, 30, 40, 50]
print(numbers[1:3])
print(list(range(1, 6))) numbers <- c(10, 20, 30, 40, 50)
print(numbers[2:3])
print(1:5) R ranges are written
start:end and are inclusive on both ends, so 2:3 selects the 2nd and 3rd elements and 1:5 is 1, 2, 3, 4, 5. Python slices are half-open and 0-based, so numbers[1:3] yields two elements and range(1, 6) stops before 6.Recycling
# Python has no direct equivalent — lengths
# must match or you loop / zip explicitly.
a = [1, 2, 3, 4]
b = [10, 20]
print([a[i] + b[i % 2] for i in range(4)]) a <- c(1, 2, 3, 4)
b <- c(10, 20)
print(a + b) When two vectors differ in length, R recycles the shorter one to match the longer, so
c(1,2,3,4) + c(10,20) gives 11 22 13 24. This is powerful but can hide bugs; R warns when the longer length is not a multiple of the shorter. Python has no built-in recycling.Logical indexing
numbers = [5, 12, 3, 20, 8]
big = [n for n in numbers if n > 10]
print(big) numbers <- c(5, 12, 3, 20, 8)
big <- numbers[numbers > 10]
print(big) Indexing a vector with a logical vector keeps the elements where the condition is
TRUE. Here numbers > 10 produces a logical mask, and numbers[mask] selects the matching values — the idiomatic R replacement for a filtering comprehension.Built-in statistics
import statistics
numbers = [4, 8, 15, 16, 23, 42]
print(sum(numbers))
print(max(numbers))
print(statistics.mean(numbers)) numbers <- c(4, 8, 15, 16, 23, 42)
print(sum(numbers))
print(max(numbers))
print(mean(numbers)) R's reason for existing is statistics, so
mean(), median(), sd(), var(), and quantile() are all in the base language with no import. Python needs the statistics module (or NumPy) for the mean.Numbers
Arithmetic and power
print(7 + 2)
print(7 * 2)
print(2 ** 10) print(7 + 2)
print(7 * 2)
print(2 ^ 10) The operators line up except for exponentiation: R writes powers with
^ (and also accepts **), whereas Python uses ** only.Integer division and modulo
print(17 // 5)
print(17 % 5) print(17 %/% 5)
print(17 %% 5) R spells integer (floor) division
%/% and the remainder %%. Python uses // and %. R's percent-delimited operators are a general mechanism — you can even define your own.Math functions
import math
print(math.sqrt(16))
print(round(math.exp(1), 4)) print(sqrt(16))
print(round(exp(1), 4)) Math functions such as
sqrt(), exp(), log(), sin(), and abs() are part of base R — no import math equivalent is needed because they live in the global namespace.Rounding
import math
print(round(3.567, 1))
print(math.floor(3.9))
print(math.ceil(3.1)) print(round(3.567, 1))
print(floor(3.9))
print(ceiling(3.1)) R rounds with
round(), takes the floor with floor(), and rounds up with ceiling() — note the full spelling ceiling, not ceil. All are base functions.Random numbers
import random
random.seed(42)
print(random.randint(1, 6)) set.seed(42)
print(sample(1:6, 1)) R seeds its generator with
set.seed() and draws with sample() (for discrete draws), runif() (uniform), or rnorm() (normal). The drawn value differs from Python's because the two languages use different generators, but each is reproducible once seeded.Infinity and NaN
print(float("inf"))
print(1.0 / float("inf"))
print(float("nan")) print(Inf)
print(1 / Inf)
print(NaN) R has the built-in literals
Inf, -Inf, and NaN (not-a-number), alongside NA for missing data. Python expresses infinity and NaN through float("inf") and float("nan").Strings
Concatenation
first = "Ada"
last = "Lovelace"
print(first + " " + last) first <- "Ada"
last <- "Lovelace"
print(paste(first, last))
print(paste0(first, last)) R does not concatenate strings with
+. Use paste(), which joins with a space by default, or paste0(), which joins with no separator. Both are vectorised, joining corresponding elements of multiple vectors.String length
text = "hello"
print(len(text)) text <- "hello"
print(nchar(text))
print(length(text)) Use
nchar() for the number of characters. A common surprise: length() of a single string is 1, because that string is a character vector of length 1 — length() counts elements, not characters.Changing case
text = "Hello"
print(text.upper())
print(text.lower()) text <- "Hello"
print(toupper(text))
print(tolower(text)) R changes case with the standalone functions
toupper() and tolower(), rather than the methods .upper()/.lower(). Almost everything in R is a function call on a value, not a method on an object.Substrings
text = "programming"
print(text[0:4]) text <- "programming"
print(substr(text, 1, 4)) A substring comes from
substr(text, start, stop), with 1-based, inclusive positions — so 1, 4 takes the first four characters. You cannot slice a string with [] in R the way you can in Python; [] indexes vector elements, and a string is a single element.Splitting
csv = "a,b,c"
print(csv.split(",")) csv <- "a,b,c"
print(strsplit(csv, ",")[[1]]) R splits with
strsplit(). Because it is vectorised over many strings at once, it returns a list of character vectors; [[1]] pulls out the result for the first (here only) string.Find and replace
text = "one,two,three"
print(text.replace(",", ";")) text <- "one,two,three"
print(gsub(",", ";", text)) R replaces with
gsub() (all matches) or sub() (first match only). The pattern is a regular expression by default; pass fixed = TRUE to match literally, the way Python's .replace() always does.Contains / matching
text = "hello world"
print("world" in text) text <- "hello world"
print(grepl("world", text)) To test whether a string contains a pattern, R uses
grepl() (read "grep logical"), which returns TRUE/FALSE. As with gsub(), the pattern is a regular expression unless you pass fixed = TRUE.Joining a vector
words = ["a", "b", "c"]
print(",".join(words)) words <- c("a", "b", "c")
print(paste(words, collapse = ",")) To collapse a character vector into one string, give
paste() a collapse argument. Python instead calls .join() on the separator string. The R form keeps the separator and the data as separate arguments.Lists & Names
A dict is a named list
person = {"name": "Alice", "age": 30}
print(person) person <- list(name = "Alice", age = 30)
print(person$name)
print(person$age) R's
list() is the heterogeneous container — it can hold values of different types, each optionally named. A named list is the nearest thing to a Python dict, though its keys are positions-with-names rather than a hash map.Accessing elements
person = {"name": "Alice", "age": 30}
print(person["name"]) person <- list(name = "Alice", age = 30)
print(person$name)
print(person[["name"]]) Two equivalent ways reach a named element:
person$name (concise, for a literal name) and person[["name"]] (works when the name is in a variable). Single brackets person["name"] return a one-element sub-list, not the value — a frequent source of confusion.Adding and modifying
person = {"name": "Alice"}
person["age"] = 30
print(person) person <- list(name = "Alice")
person$age <- 30
print(person$age) Assigning to a name that does not yet exist adds it; assigning to one that does replaces it — the same as Python dicts. R uses
person$age <- 30 or person[["age"]] <- 30.Keys and values
person = {"name": "Alice", "age": 30}
print(list(person.keys()))
print(list(person.values())) person <- list(name = "Alice", age = 30)
print(names(person))
print(unlist(person)) The names of a list come from
names(), the counterpart to dict.keys(). There is no single .values(); unlist() flattens the values into a vector (coercing to a common type), which is the usual way to see them together.Iterating a named list
person = {"name": "Alice", "age": 30}
for key, value in person.items():
print(key, value) person <- list(name = "Alice", age = 30)
for (key in names(person)) {
cat(key, ":", as.character(person[[key]]), "
")
} R has no direct
.items(); the idiom is to loop over names() and look each value up with [[ ]]. (For transforming a list, the apply family is usually preferred over an explicit loop.)Vector vs list
# Python lists hold mixed types freely:
mixed = [1, "two", 3.0]
print(mixed) # c() coerces to one type:
print(c(1, "two", 3.0))
# list() preserves each type:
print(list(1, "two", 3.0)[[1]] + 1) The key R distinction Python does not make:
c() builds an atomic vector and forces every element to one type (here everything becomes a string), while list() keeps each element's own type. A Python list behaves like R's list(), not its vector.Data Frames
Creating a data frame
import pandas as pd
df = pd.DataFrame({
"name": ["Alice", "Bob", "Carol"],
"age": [30, 25, 35],
})
print(df) df <- data.frame(
name = c("Alice", "Bob", "Carol"),
age = c(30, 25, 35)
)
print(df) The
data.frame is built into base R — it is the structure that inspired pandas, so a column-per-named-vector layout feels immediately familiar. The Python column needs pandas (pip install pandas) and so does not run here; the R column runs live with no package install.Selecting a column
import pandas as pd
df = pd.DataFrame({"name": ["Alice", "Bob"], "age": [30, 25]})
print(df["age"]) df <- data.frame(name = c("Alice", "Bob"), age = c(30, 25))
print(df$age) A column is just a named element of the data frame, reached with
df$age — the same $ used on lists, because a data frame is a list of equal-length columns. The result is an ordinary vector you can compute on directly.Filtering rows
import pandas as pd
df = pd.DataFrame({"name": ["Alice", "Bob", "Carol"], "age": [30, 25, 35]})
print(df[df["age"] > 28]) df <- data.frame(name = c("Alice", "Bob", "Carol"), age = c(30, 25, 35))
print(df[df$age > 28, ]) Data frames are indexed
df[rows, columns]. A logical vector in the row slot keeps matching rows; the empty column slot (after the comma) keeps every column. The trailing comma is required — df[df$age > 28] without it selects columns, not rows.Adding a column
import pandas as pd
df = pd.DataFrame({"name": ["Alice", "Bob"], "age": [30, 25]})
df["age_in_months"] = df["age"] * 12
print(df) df <- data.frame(name = c("Alice", "Bob"), age = c(30, 25))
df$age_in_months <- df$age * 12
print(df) Assigning to a new name with
$ adds a column, and because column arithmetic is vectorised, df$age * 12 computes the whole new column at once — no row loop.Summary statistics
import pandas as pd
df = pd.DataFrame({"age": [30, 25, 35, 40]})
print(df.describe()) df <- data.frame(age = c(30, 25, 35, 40))
print(summary(df)) Calling
summary() on a data frame reports the min, quartiles, median, and mean of every column — the base-R analogue of pandas' .describe(). summary() is a generic that adapts its output to whatever it is given.Group and aggregate
import pandas as pd
df = pd.DataFrame({"team": ["A", "A", "B"], "score": [10, 20, 30]})
print(df.groupby("team")["score"].mean()) df <- data.frame(team = c("A", "A", "B"), score = c(10, 20, 30))
print(aggregate(score ~ team, data = df, FUN = mean)) Base R groups with
aggregate() using a formula: score ~ team reads as "score by team". The ~ formula notation, which expresses a relationship between variables, is pervasive in R's modelling functions and has no Python equivalent.Control Flow
if / else if / else
score = 75
if score >= 90:
print("A")
elif score >= 70:
print("B")
else:
print("C") score <- 75
if (score >= 90) {
print("A")
} else if (score >= 70) {
print("B")
} else {
print("C")
} R's conditional uses parentheses around the test and braces around each block, and spells the middle branch
else if (two words) rather than Python's elif. The else must sit on the same line as the closing brace of the previous block.Scalar vs vectorized logic
a = [True, False, True]
b = [True, True, False]
# element-wise needs zip or a comprehension:
print([x and y for x, y in zip(a, b)]) a <- c(TRUE, FALSE, TRUE)
b <- c(TRUE, TRUE, FALSE)
print(a & b)
print(a[1] && b[1]) R has two pairs of logical operators. The single forms
& and | are vectorised, combining vectors element-wise. The double forms && and || take only single values and are what you use in an if condition.Vectorized conditional
numbers = [1, 2, 3, 4]
labels = ["even" if n % 2 == 0 else "odd" for n in numbers]
print(labels) numbers <- c(1, 2, 3, 4)
labels <- ifelse(numbers %% 2 == 0, "even", "odd")
print(labels) For a choice applied across a whole vector, R provides
ifelse(test, yes, no), which evaluates the condition element-wise and picks from yes or no position by position. A plain if is for a single decision; ifelse() is for a vector of them.switch
def describe(color):
match color:
case "red":
return "stop"
case "green":
return "go"
case _:
return "unknown"
print(describe("green")) describe <- function(color) {
switch(color,
red = "stop",
green = "go",
"unknown")
}
print(describe("green")) R's
switch() matches a string against named branches; a final unnamed argument acts as the default. It is an expression that returns a value, closer to Python's match used for simple value dispatch than to a C-style switch.No general truthiness
items = []
# An empty list is falsy in Python:
if not items:
print("empty") items <- c()
if (length(items) == 0) {
print("empty")
}
# A vector condition is an error in R:
message <- tryCatch(
if (c(TRUE, FALSE)) "ok",
error = function(e) conditionMessage(e)
)
print(message) R has no truthiness for containers: you cannot write
if (items) for an empty vector — test length(items) == 0 instead. An if condition must be a single logical value, and supplying a longer vector is an error, as the caught message shows.Functions
Defining a function
def double(x):
return x * 2
print(double(21)) double <- function(x) {
x * 2
}
print(double(21)) A function in R is a value created with
function(...) and bound to a name like any other variable. The value of the last expression is returned automatically, so an explicit return() is usually omitted.Default arguments
def greet(name, greeting="Hello"):
return f"{greeting}, {name}"
print(greet("Sam")) greet <- function(name, greeting = "Hello") {
paste0(greeting, ", ", name)
}
print(greet("Sam")) Default argument values work just as in Python, written with
= in the parameter list. R goes further: a default can even reference other arguments, because defaults are evaluated lazily, only when the argument is first used.Anonymous functions
square = lambda x: x ** 2
print(square(5)) square <- \(x) x ^ 2
print(square(5)) R's lambda is the backslash shorthand
\(x) x ^ 2 (added in R 4.1), equivalent to the longer function(x) x ^ 2. Unlike a Python lambda, the body is an ordinary expression and can be any single expression, including a braced block.Variable arguments
def total(*numbers):
return sum(numbers)
print(total(1, 2, 3, 4)) total <- function(...) {
sum(...)
}
print(total(1, 2, 3, 4)) R collects extra arguments with
... (the "dots"), the counterpart to Python's *args. The dots can be passed straight through to another function, which is how wrappers forward arguments they do not name themselves.Returning several values
def min_max(numbers):
return min(numbers), max(numbers)
low, high = min_max([3, 1, 4, 1, 5])
print(low, high) min_max <- function(numbers) {
list(low = min(numbers), high = max(numbers))
}
result <- min_max(c(3, 1, 4, 1, 5))
cat(result$low, result$high, "
") R functions return a single object, so to hand back several values you wrap them in a (usually named) list and unpack them afterward with
$. Python instead returns a tuple and destructures it on assignment.Closures
def make_counter():
count = 0
def increment():
nonlocal count
count += 1
return count
return increment
counter = make_counter()
print(counter(), counter(), counter()) make_counter <- function() {
count <- 0
function() {
count <<- count + 1
count
}
}
counter <- make_counter()
cat(counter(), counter(), counter(), "
") A nested function captures its enclosing environment, exactly like a Python closure. To assign to a captured variable, R uses the super-assignment operator
<<-, which walks outward to find an existing binding — the role Python's nonlocal plays.Apply & Iteration
for loop
for fruit in ["apple", "pear", "plum"]:
print(fruit) for (fruit in c("apple", "pear", "plum")) {
print(fruit)
} R's
for loop iterates directly over the elements of a vector, like Python's. It works, but R programmers reach for the vectorised apply family below far more often than for explicit loops.sapply (map to a vector)
numbers = [1, 2, 3, 4, 5]
squares = list(map(lambda x: x ** 2, numbers))
print(squares) numbers <- c(1, 2, 3, 4, 5)
squares <- sapply(numbers, function(x) x ^ 2)
print(squares) When you do need to apply a function element by element,
sapply() maps it over a vector and simplifies the result back to a vector — the closest match to Python's map() or a list comprehension.lapply (map to a list)
numbers = [1, 2, 3]
results = [[x] * x for x in numbers]
print(results[2]) numbers <- c(1, 2, 3)
results <- lapply(numbers, function(x) rep(x, x))
print(results[[3]]) When results have differing shapes, use
lapply(), which always returns a list (one element per input) without trying to simplify. Here each element is a vector of a different length, so a list is the right container.Reduce
from functools import reduce
numbers = [1, 2, 3, 4, 5]
print(reduce(lambda a, b: a + b, numbers)) numbers <- c(1, 2, 3, 4, 5)
print(Reduce(function(a, b) a + b, numbers)) R's
Reduce() folds a binary function across a vector, the same idea as Python's functools.reduce. (For a plain sum you would just call sum(); Reduce() is for accumulations with no dedicated function.)mapply (zip two vectors)
a = [1, 2, 3]
b = [10, 20, 30]
print([x + y for x, y in zip(a, b)]) a <- c(1, 2, 3)
b <- c(10, 20, 30)
print(mapply(function(x, y) x + y, a, b)) mapply() applies a function to corresponding elements of several vectors at once — the role Python fills by pairing zip() with a comprehension. (For simple addition, recycling already gives a + b directly.)while loop
count = 1
while count <= 3:
print(count)
count += 1 count <- 1
while (count <= 3) {
print(count)
count <- count + 1
} The
while loop is nearly identical; R wraps the condition in parentheses and the body in braces. R has no +=, so the counter is advanced with count <- count + 1.Factors & Missing Data
NA propagates
# Python has no built-in missing-number type;
# None cannot be added to a number.
values = [1, None, 3]
print([v for v in values if v is not None]) values <- c(1, NA, 3)
print(values + 1)
print(sum(values)) R's
NA is a first-class missing value that lives inside numeric vectors. Any arithmetic touching it yields NA — so sum(c(1, NA, 3)) is NA — which forces you to decide explicitly how to handle missingness rather than silently ignoring it.Detecting missing values
values = [1, None, 3]
print([v is None for v in values]) values <- c(1, NA, 3)
print(is.na(values)) Test for missing values with
is.na(), which returns a logical vector marking each NA. Crucially, you cannot compare with == NA — that itself returns NA — so is.na() is the only correct check.Ignoring NA in statistics
values = [4, None, 8]
present = [v for v in values if v is not None]
print(sum(present) / len(present)) values <- c(4, NA, 8)
print(mean(values, na.rm = TRUE)) Most R summary functions accept
na.rm = TRUE to drop missing values before computing — mean(), sum(), sd(), and so on. This built-in switch is far tidier than filtering the data first, as Python requires.Factors (categoricals)
# Plain Python uses strings for categories;
# pandas adds a Categorical type.
sizes = ["small", "large", "small", "medium"]
print(set(sizes)) sizes <- factor(c("small", "large", "small", "medium"))
print(sizes)
print(levels(sizes)) A
factor is R's type for categorical data: it stores the values as codes plus a set of levels (the distinct categories). Factors are how R represents groups in models and tables — a concept base Python leaves to plain strings.Ordered factors
# Python would compare via an explicit ranking:
order = {"low": 0, "medium": 1, "high": 2}
ratings = ["low", "high", "medium"]
print([order[r] < order["high"] for r in ratings]) ratings <- factor(
c("low", "high", "medium"),
levels = c("low", "medium", "high"),
ordered = TRUE
)
print(ratings < "high") Declaring a factor
ordered = TRUE with explicit levels gives the categories a rank, so comparisons like ratings < "high" work directly. In Python you would maintain a lookup table of ranks yourself.Objects (S3)
Giving a value a class
class Dog:
def __init__(self, name):
self.name = name
rex = Dog("Rex")
print(type(rex).__name__) dog <- list(name = "Rex")
class(dog) <- "Dog"
print(class(dog)) R's simplest object system, S3, is remarkably lightweight: an object is just an ordinary value (here a list) with a
class attribute attached. There is no class declaration — you label the data and that label drives method dispatch.Methods via generics
class Dog:
def __init__(self, name):
self.name = name
def speak(self):
return f"{self.name} says Woof"
print(Dog("Rex").speak()) speak <- function(x) UseMethod("speak")
speak.Dog <- function(x) paste(x$name, "says Woof")
rex <- structure(list(name = "Rex"), class = "Dog")
print(speak(rex)) S3 methods live outside the object. A generic calls
UseMethod("speak"), and R dispatches to speak.Dog based on the class of the first argument. So methods are named generic.class and belong to the function, not the data — the reverse of Python's object-owns-its-methods model.Customising print
class Point:
def __init__(self, x, y):
self.x, self.y = x, y
def __repr__(self):
return f"Point({self.x}, {self.y})"
print(Point(3, 4)) new_point <- function(x, y) {
structure(list(x = x, y = y), class = "Point")
}
print.Point <- function(x, ...) {
cat("Point(", x$x, ",", x$y, ")
")
}
print(new_point(3, 4)) Because
print() is itself a generic, defining print.Point customises how points display — the S3 counterpart to Python's __repr__. The same pattern extends built-in generics like summary() and format() to your own types.Error Handling
Raising an error
def withdraw(amount):
if amount < 0:
raise ValueError("amount must be positive")
return amount
try:
withdraw(-5)
except ValueError as error:
print(f"caught: {error}") withdraw <- function(amount) {
if (amount < 0) stop("amount must be positive")
amount
}
result <- tryCatch(
withdraw(-5),
error = function(e) paste("caught:", conditionMessage(e))
)
print(result) R signals an error with
stop(), the counterpart to Python's raise. The message text is read back from a caught error with conditionMessage().Catching errors
def safe_divide(a, b):
try:
return a / b
except ZeroDivisionError:
return None
print(safe_divide(10, 2))
print(safe_divide(10, 0)) safe_divide <- function(a, b) {
tryCatch(
if (b == 0) stop("divide by zero") else a / b,
error = function(e) NA
)
}
print(safe_divide(10, 2))
print(safe_divide(10, 0)) tryCatch() is R's try/except. You supply a handler keyed by condition class — error = function(e) ... — that runs if the expression raises that condition, and whose value becomes the result. (Note R divides by zero numerically to Inf, so the example raises its own error to illustrate handling.)Warnings
import warnings
def check(value):
if value < 0:
warnings.warn("negative input")
return abs(value)
with warnings.catch_warnings():
warnings.simplefilter("error")
try:
check(-5)
except UserWarning as warning:
print(f"warned: {warning}") check <- function(value) {
if (value < 0) warning("negative input")
abs(value)
}
result <- tryCatch(
check(-5),
warning = function(w) paste("warned:", conditionMessage(w))
)
print(result) A
warning() is a non-fatal signal that, unlike stop(), does not abort execution by default. tryCatch() can intercept warnings too by supplying a warning = handler, mirroring how Python can promote warnings to exceptions.Cleanup with finally
try:
print("trying")
result = 42
finally:
print("cleanup")
print(result) result <- tryCatch(
{
print("trying")
42
},
finally = print("cleanup")
)
print(result) tryCatch() takes a finally argument that always runs, whether or not an error occurred — the same guarantee as Python's finally block, useful for releasing resources.