Program Structure and Lexical Elements
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Ruby 4.0 (4.0.7, checked on 2026-10-18); anything that arrived after Ruby 3.0 names the
version that introduced it. It describes what Ruby sees before any code runs: how a file is read and decoded, which parts
are comments and which comments are instructions, where one expression ends and the next begins, what counts as a name,
which words are reserved, and the handful of program-level forms (BEGIN, END, __END__, alias, undef,
defined?) that belong to the file rather than to a class. Everything about what the tokens mean (literals, operators,
control flow) lives on the pages that follow; this page is the grammar of the surface.
It builds on Getting Started. Later pages use it: Literals, Variables, Constants and Scope, Operators and Precedence and Control Expressions.
Every example was checked with ruby -wc, run with ruby -w on Ruby 4.0.7 and linted with RuboCop; the output shown is
the real output. Where an example is deliberately wrong (to show a warning or a syntax error) the text says so and the
message is copied from a real run. Error messages use the Ruby 3.4+ format (a straight quote instead of a backtick, and
for an instance of Integer instead of the old for 3:Integer).
How Ruby reads a file
A Ruby program is a text file, conventionally with the .rb extension and a snake_case name that matches the class or
module it defines (lib/shelf/isbn.rb defines Shelf::Isbn). The extension is only a convention: ruby runs whatever
file it is given, and require appends .rb itself.
The whole file is parsed before anything executes. Since Ruby 3.4 the parser is Prism (Ruby 3.3 and older used the
parse.y grammar, which is still available with --parser=parse.y); the result is compiled to YARV bytecode and then
run. Older descriptions of Ruby as an interpreter that "reads and executes the file line by line" are wrong in a way that
matters: a syntax error anywhere in the file, even on the last line, stops the program before its first puts, and
ruby -c file.rb can check the whole file without running it.
$ ruby -wc lib/shelf.rb
Syntax OK
$ ruby --parser=parse.y -wc lib/shelf.rb
Syntax OK
A file therefore has a lifecycle of four stages. The same stages apply to a file loaded with require, to a script run
with ruby script.rb and to code passed with -e.
| Stage | What happens |
|---|---|
Decode |
The bytes are read using the script encoding (UTF-8 unless a magic comment says otherwise). A UTF-8 byte order mark at the start is skipped. Bytes that are invalid in that encoding inside a literal are a syntax error. |
Tokenize and parse |
Prism turns the text into a syntax tree. Comments are discarded, except the magic comments, which are read first and
change how the rest of the file is parsed. Every syntax error and most warnings (unused variable, mismatched indentation,
|
Compile |
The tree becomes YARV instructions. |
Run |
Top-level statements execute in order. |
Source files and encoding
Ruby source is UTF-8 by default (this has been the default since Ruby 2.0). Inside a UTF-8 file you can write non-ASCII text in string literals, comments and even in identifiers, with no magic comment.
# frozen_string_literal: true
p __ENCODING__
p "Ñandú".encoding
p "Ñandú".bytesize
p "Ñandú".length
#<Encoding:UTF-8>
#<Encoding:UTF-8>
7
5
__ENCODING__ is a keyword that evaluates to the Encoding of the script; string, symbol and regexp literals are created
in that encoding. The default external encoding (Encoding.default_external) is a different thing: it is how File.read
and $stdin interpret bytes, and it comes from the locale (LANG, LC_ALL) or from -E/-U, not from the script
encoding. The distinction is covered in Encodings and Unicode.
Source: Encoding directive
Changing the script encoding
A magic comment on the first line (or on the second line when the first is a shebang) changes the script encoding. The
word coding is accepted in place of encoding, so the Emacs form # -*- coding: iso-8859-1 -*- also works.
# encoding: iso-8859-1
# frozen_string_literal: true
p __ENCODING__
p "abc".encoding
#<Encoding:ISO-8859-1>
#<Encoding:ISO-8859-1>
Source: Encoding directive
The position rule is stricter than for other magic comments. A # encoding: comment is honored only on line 1, or on
line 2 when line 1 is a #! shebang. Verified on 4.0.7: with the comment on line 2 after an ordinary comment, after a
blank line or after code, __ENCODING__ stayed UTF-8 and no warning was printed. Since UTF-8 is already the default,
# encoding: utf-8 is redundant in a modern file, and RuboCop’s Style/Encoding cop removes it.
Invalid bytes and the byte order mark
If a UTF-8 file contains bytes that are not valid UTF-8 (for example a Latin-1 é, byte 0xE9, saved by an old editor),
parsing fails before anything runs. A UTF-8 byte order mark (EF BB BF) at the start of the file is ignored, so files
saved by Windows editors still work.
$ ruby -w bad.rb
bad.rb: bad.rb:3: syntax error found (SyntaxError)
bad.rb:3: invalid multibyte character 0xE9
Source: Encoding directive
The fix is to re-save the file as UTF-8 or to declare the real encoding with # encoding:. Code passed with -e is
decoded with the locale encoding rather than UTF-8, which is why a literal with accents may fail under LC_ALL=C with the
same invalid multibyte character error.
Encoding command-line options
| Option | Effect |
|---|---|
|
Sets |
|
Sets |
|
Legacy: sets the script and external encoding to UTF-8, EUC-JP, Windows-31J or ASCII-8BIT. Prefer |
|
Skips everything before a |
Source: Ruby command-line options
The shebang line
On Unix-like systems a first line of #!/usr/bin/env ruby lets the operating system choose the interpreter, so
exe/shelf can be run as ./exe/shelf once it is executable (chmod +x). To Ruby the line is an ordinary comment, with
two exceptions: it counts as line 1 for the # encoding: rule (a magic comment may follow it), and ruby reads options
from it (for example #!/usr/bin/ruby -w). /usr/bin/env ruby is preferred over a hard-coded path because it finds the
Ruby selected by PATH (a version manager, a Bundler binstub, or the system Ruby). For the executables of a gem,
RubyGems generates the platform-specific launcher on install, so the shebang matters mostly during development.
Comments
Ruby has two comment forms.
# frozen_string_literal: true
# A full-line comment, indented like the code it describes.
module Shelf
Book = Data.define(:isbn, :title, :author) # a trailing comment
end
=begin
A block comment. =begin and =end must start in column 0,
and each must be alone on its line.
=end
=begin some_tag
Text after =begin on the same line is ignored by Ruby (tools use it as a label).
=end
p Shelf::Book.members
[:isbn, :title, :author]
Source: Code comments
The rules, from the official syntax guide:
-
#starts an inline comment that runs to the end of the line. It can be on its own line or after code, and it may be indented. -
=beginand=enddelimit a block comment. Both must start in the first column, and each must be at the start of its own line. An indented=beginis a syntax error. -
Anything after
=beginor=endon the same line is ignored by Ruby (it is conventionally a tag). -
Comments are ignored by the parser and are not kept in the syntax tree. Documentation tools (
rdoc,yard) and linters read the source text themselves, which is why a comment directly above adeforclassis treated as its documentation. See Documentation with RDoc and YARD. -
There is no nested or
/* */comment. To comment out a block of code in an editor, prefix each line with#;=begin/=endis rarely used in modern Ruby and RuboCop’sStyle/BlockCommentsflags it (the example above uses it on purpose). -
__END__on a line of its own ends the program text; see __END__ and DATA.
Some comments are instructions to tools rather than prose: # rubocop:disable Metrics/MethodLength (RuboCop),
# typed: strict (Sorbet), # :nodoc: (RDoc) and # steep:ignore (Steep). Ruby itself ignores them; the next section
covers the ones Ruby does read.
Magic comments
A magic comment is a comment that Ruby does read, in the form # name: value. It affects only the file in which it
appears, never files that require it or that it requires. Ruby 4.0 understands four.
| Directive | Values | Effect | Where it must be |
|---|---|---|---|
|
|
Makes plain string literals frozen ( |
In the first comment section, before any code. |
|
any |
Sets the script encoding, see above. |
Line 1, or line 2 after a shebang. |
|
|
Makes the values assigned to constants Ractor-shareable. |
Before the constants it should cover; applies to the rest of the scope. |
|
|
Turns the mismatched-indentation warning on or off. |
Any line; applies from there. |
Source: Magic comments
Several details are not in the table but were verified on 4.0.7:
-
The name is case-insensitive and accepts
-for_:# Frozen_String_Literal: trueand# frozen-string-literal: trueboth work, and a space after#is optional. Use the canonical lower-case form. -
The "first comment section" for
frozen_string_literalis the run of comments and blank lines at the top of the file; a blank line between comments does not end it, but the first line of code does. A directive after code is ignored, andruby -wsays so (see below). -
If the directive appears twice in that section, the last one wins.
-
An invalid value is ignored with a warning:
warning: invalid value for frozen_string_literal: maybe. -
Unknown names (
# foo: bar) are plain comments. -
Several directives can share one line in the Emacs-compatible form
# -*- coding: utf-8; frozen_string_literal: true -*-.
frozen_string_literal
By default a string literal creates a new mutable String every time it is evaluated. With
# frozen_string_literal: true every literal without interpolation is created once, at parse time, and frozen.
# frozen_string_literal: true
title = "Dune"
p title.frozen?
p "Dune #{1 + 1}".frozen?
p String.new("Dune").frozen?
p "Dune".dup.frozen?
copy = +"Dune"
p copy.frozen?
p "Dune".equal?("Dune")
begin
title << "!"
rescue FrozenError => e
puts e.message
end
true
false
false
false
false
true
can't modify frozen String: "Dune"
Source: frozen_string_literal directive
The rules:
-
Literals with interpolation (
"Dune #{x}") are never frozen and never shared (since Ruby 3.0).String.new,dup,+"..."(unary plus,String#+@) andString#*results are always new, mutable strings. -
The same literal text in the same file refers to the same object (
"Dune".equal?("Dune")istrue), which also saves allocations. Use-"text"(String#-@) to get the deduplicated frozen string for a runtime value. -
Mutating a frozen string raises
FrozenError(a subclass ofRuntimeError). -
The elements of
%w[...]and heredocs without interpolation are literals, so they are frozen too (verified:%w[x y].first.frozen?and a plain<<~TXTaretrue; a heredoc with#{...}isfalse). Strings that are not literals, such as:sym.to_sor"a" * 2, are unaffected, whileSymbol#namealways returns a frozen string. -
It is a per-file setting. A frozen file can call a library that has no directive, and the library’s own strings stay mutable.
Almost every file in a modern gem begins with this line, and RuboCop’s Style/FrozenStringLiteralComment enforces it;
the habit prevents accidental mutation of shared constants and lets Ruby avoid allocations.
Chilled strings (Ruby 3.4) and what the default is in 4.0
Frozen string literals are not the default in Ruby 4.0, and the claim "literals are frozen by default since 3.4" is
wrong. Ruby 3.4 introduced chilled strings as the migration step: in a file with no frozen_string_literal comment,
a literal is created not frozen (frozen? returns false), but the first attempt to mutate it emits a deprecation
warning that is shown only when deprecation warnings are enabled (-w, -W:deprecated, or Warning[:deprecated] = true).
The behavior is unchanged otherwise: the mutation succeeds.
This script deliberately has no magic comment (RuboCop reports Style/FrozenStringLiteralComment) so that it shows a
chilled literal.
title = "Dune"
p title.frozen?
title << " (1965)"
p title
$ ruby -w chill.rb
false
chill.rb:3: warning: literal string will be frozen in the future (run with --debug-frozen-string-literal for more information)
"Dune (1965)"
$ ruby chill.rb
false
"Dune (1965)"
$ ruby -W:deprecated --debug-frozen-string-literal chill.rb
false
chill.rb:3: warning: literal string will be frozen in the future
chill.rb:1: info: the string was created here
"Dune (1965)"
$ ruby --enable=frozen-string-literal chill.rb
true
chill.rb:3:in '<main>': can't modify frozen String: "Dune" (FrozenError)
Source: Ruby 3.4.0 release notes (chilled strings) and Ruby command-line options (--enable, --disable)
| Setting | Result for a literal in the file |
|---|---|
|
Frozen. Mutation raises |
|
Mutable, no warning (an explicit opt-out; the warning is not shown). |
no comment (Ruby 3.4 and 4.0) |
Chilled: |
no comment and |
Frozen everywhere, in every file that has no comment of its own; useful to find mutations in a test run. |
no comment and |
Mutable, never chilled, no warning. |
Verified on 4.0.7: RUBYOPT=--enable-frozen-string-literal freezes the literals of a script with no comment, and
RUBYOPT=-W:deprecated turns on the chilled-string warning for the whole process. The ticket that tracks making literals
frozen by default (Feature #20205) is still open, so a new file should state its choice explicitly.
A magic comment after code is ignored
puts "start"
# frozen_string_literal: true
p "x".frozen?
late.rb:2: warning: 'frozen_string_literal' is ignored after any tokens
start
false
Source: frozen_string_literal directive
The warning appears at parse time, before the first line prints; without -w it is silent, so a misplaced comment
silently has no effect. The Ruby 4.0 message uses straight quotes ('frozen_string_literal').
shareable_constant_value
A Ractor other than the main one can read a constant only if its value is shareable (deeply frozen or otherwise safe), so Ruby 3.0 added a
directive that makes constants deeply frozen. The documentation marks the directive as experimental, and two of its four
modes are still named experimental_.
| Mode | Effect on NAME = expression |
|---|---|
|
No change, no check. |
|
A literal (or a literal nested in a literal) assigned to a constant is deeply frozen. Anything else must already be
shareable or |
|
Every assigned value is made shareable with |
|
Every assigned value is deep-copied and the copy is made shareable, so the original stays mutable. |
# frozen_string_literal: true
# shareable_constant_value: literal
GENRES = %w[novel essay poetry]
LOCALES = { en: ["Dune"], es: ["Duna"] }
p GENRES.frozen?, GENRES.first.frozen?
p LOCALES[:en].frozen?, LOCALES[:en].first.frozen?
begin
OBJ = Object.new
rescue Ractor::IsolationError => e
puts "#{e.class}: #{e.message}"
end
true
true
true
true
Ractor::IsolationError: cannot assign unshareable object to OBJ
The directive applies from where it appears to the end of the current scope, and it can be repeated, so a file can
switch modes. Here it is scoped to one module:
# frozen_string_literal: true
module Shelf
module Strict
# shareable_constant_value: experimental_everything
DEFAULTS = { loan_days: 14, tags: ["new"] }
p DEFAULTS.frozen?, DEFAULTS[:tags].frozen?
end
module Loose
LIST = [1]
p LIST.frozen?
end
end
true
true
false
An invalid mode is ignored with warning: invalid value for shareable_constant_value: bogus. The directive does not
affect Module#const_set, and it is unrelated to Object#freeze on ordinary objects, which is covered in
Object Lifecycle, Freezing and Copying.
Ractors themselves are in Ractors, and the reasoning about constants in
Variables, Constants and Scope.
warn_indent
warn_indent controls the warning mismatched indentations at 'end' with 'def' at 3, which is emitted when an end
is not aligned with the keyword that it closes. The warning is on under ruby -w, and an explicit
# warn_indent: false suppresses it for the rest of the file:
# frozen_string_literal: true
def first
1
end
# warn_indent: false
def second
2
end
p first + second
warn_indent.rb:5: warning: mismatched indentations at 'end' with 'def' at 3
3
Only the first end is reported, because the second def follows # warn_indent: false. In my runs with Prism on 4.0.7, # warn_indent: true on its own did not make the
warning appear without -w, so rely on -w (or RuboCop’s Layout/EndAlignment) to catch it and use the directive only
to silence the warning.
Source: warn_indent directive
Directive order in a real file
A typical library file therefore starts like this, in this order: shebang (scripts only), # encoding: (only when not
UTF-8), # frozen_string_literal: true, and then everything else. RuboCop’s Style/MagicCommentFormat enforces the
spelling and Layout/EmptyLineAfterMagicComment the blank line after the last directive.
Line layout and continuation
Ruby has no statement terminator. A newline ends an expression, and ; can be used in its place.
# frozen_string_literal: true
titles = %w[dune emma ulysses]
# 1. One statement per line; ';' joins statements on one line.
count = titles.size; first = titles.first
puts "#{count} titles, first is #{first}"
# 2. A trailing operator or comma keeps the expression open.
total = count +
10 +
# comments are allowed between continued lines
1
puts total
# 3. A leading dot continues the chain; comments may sit between the links.
shout = titles
.map(&:capitalize)
# sort after capitalizing
.sort
.join(", ")
puts shout
# 4. Ruby 4.0: a leading &&, ||, and or or continues the expression.
long_enough = titles.first.size > 3
&& titles.last.size > 3
&& !titles.empty?
puts long_enough
# 5. A backslash joins lines at parse time; adjacent string literals concatenate.
message = "Loan period: " \
"14 days"
puts message
# 6. 'then' separates a condition from its body on one line.
size = case count when 3 then "three" else "other" end
puts size
3 titles, first is dune
14
Dune, Emma, Ulysses
true
Loan period: 14 days
three
Source: Code layout
When a line break does not end the expression
Ruby continues an expression onto the next line when the current line is unambiguously incomplete. The documented cases:
| Situation | Example | Why it works |
|---|---|---|
Trailing binary operator |
|
|
Trailing comma or open bracket |
|
The argument list is still open. |
Trailing |
|
An assignment needs a value. |
Leading |
|
A statement cannot begin with |
Leading |
|
New in 4.0 (Feature #20925). Comment lines between operands are allowed. |
Trailing backslash |
a string, a backslash as the last character of the line, then another string |
An explicit continuation. |
Source: NEWS for Ruby 4.0.0 (language changes) and Feature #20925
The leading-&& rule is new in Ruby 4.0. In Ruby 3.4 and earlier the same code is a syntax error (verified on
3.4.11: unexpected '&&', ignoring it), so people wrote the operator at the end of the line instead. Code written for
4.0 with leading operators will not parse on older Rubies; a gem that still supports 3.x should keep the
trailing-operator style until it drops them.
Pitfalls of line breaks
A line that is already a complete expression ends there, whatever follows:
# frozen_string_literal: true
total = 1
+ 2
puts total
Source: Code layout
Ruby warns (possibly useless use of +@ in void context), prints 1, and + 2 has been evaluated and thrown away.
(This example is deliberately wrong; RuboCop reports it as Lint/Void.) The fix is the trailing operator: total = 1 +
then 2.
A leading comma has no such rescue:
$ ruby -wc split_args.rb
split_args.rb:4: warning: possibly useless use of a literal in void context
ruby: split_args.rb:4: syntax error found (SyntaxError)
2 |
3 | puts "Dune"
> 4 | , "Emma"
| ^ unexpected ',', ignoring it
Source: Code layout
A blank line breaks a leading-dot chain; Ruby reports unexpected '.' at the first dot after the blank line. And a
backslash continuation must be the very last character on the line (a trailing space after it makes it a syntax error),
and it prevents comments between the joined lines:
# frozen_string_literal: true
TEXT = "line 1" \
# a comment here
"line 2"
puts TEXT
Source: Code layout
Output: line 1. The backslash joins "line 1" to the comment line, the comment swallows the rest, and the line with
"line 2" becomes a separate, useless expression (warning: possibly useless use of a literal in void context).
Finally, and/or have lower precedence than assignment, which turns a continued line into a trap:
# frozen_string_literal: true
title = "Dune"
ok = title.empty?
or title.size == 4
p ok
false
This is (ok = title.empty?) or (title.size == 4): ok is assigned false and the second operand is evaluated and
discarded. Use || for boolean logic and keep and/or for control flow
(save or raise "cannot save"); see Operators and Precedence.
Source: Operator precedence
Separators in compound statements
In some control structures a keyword can replace a newline, which is what makes one-line if and while possible.
| Construct | Optional separator |
|---|---|
|
|
|
|
|
|
anywhere |
|
Whether the then and do forms are good style is a matter for Control Expressions.
Indentation
Ruby does not require indentation, and the community standard is two spaces with no tabs. The only language-level support
is the mismatched indentations warning shown above. Layout is enforced by RuboCop’s Layout/* cops
(see Style, Linting and Static Analysis).
Whitespace is sometimes significant
Spaces change meaning in a few places. With def foo(x = 5) = x, puts -1 prints -1 but warns
ambiguous first argument; put parentheses or a space even after '-' operator; foo [1] passes an array to foo
(result [1]), while foo[1] indexes the result of foo (5[1] is 0); and p (1 + 2) * 3 is parsed as
p((1 + 2) * 3) and prints 9. A space before the parenthesis of a method call is why RuboCop’s
Lint/ParenthesesAsGroupedExpression and Lint/AmbiguousOperator exist. The grammar behind this lives with the operators: see
Calling Methods and Argument Forwarding.
Identifiers and naming
An identifier is a name made of letters, digits and underscores that does not start with a digit. Ruby allows any
Unicode letter, so título, π and Éxito are valid names in a UTF-8 file.
# frozen_string_literal: true
título = "Dune"
p título
Éxito = 1
p Éxito
p defined?(título)
p defined?(Éxito)
"Dune"
1
"local-variable"
"constant"
Source: defined?
Non-ASCII identifiers are legal, and they make a file harder to type, search and review in a mixed team, so the community style guide asks for ASCII identifiers (and English names) in code. A name that starts with a digit is a syntax error, and a hyphen is the minus operator.
What the first character means
The first character (the sigil) tells Ruby what kind of name it is, before any declaration:
| Form | Kind | Notes |
|---|---|---|
|
local variable or method name |
starts with a lower-case letter or |
|
constant (also a class or module name) |
starts with an upper-case letter; a second assignment warns
|
|
instance variable |
per object; |
|
class variable |
shared along an inheritance tree; see Variables, Constants and Scope |
|
global variable |
also the special variables |
|
symbol |
see Symbols |
|
conventional "unused" name |
|
Source: Pre-defined global variables
The constant rule is why class loan is a syntax error: a class name must be a constant. Real messages from 4.0.7:
$ ruby -wc bad_class.rb # class loan
bad_class.rb:3: syntax errors found (SyntaxError)
> 3 | class loan
| ^~~~ unexpected constant path after `class`; class/module name must be CONSTANT
$ ruby -wc bad_start.rb # 1book = "Dune"
bad_start.rb:3: syntax error found (SyntaxError)
> 3 | 1book = "Dune"
| ^~~~ unexpected local variable or method, expecting end-of-input
$ ruby -wc hyphen.rb # my-title = "Dune"
hyphen.rb:3: warning: possibly useless use of - in void context
Syntax OK
Source: Modules and Classes and Assignment: Local Variable Names
The last case is dangerous: my-title = "Dune" parses as my - (title = "Dune"), passes -c, and fails only at run
time with undefined local variable or method 'my'.
Reassigning a constant is allowed but warned about, and a lower-case name is not a valid constant name:
# frozen_string_literal: true
MAX_LOANS = 5
MAX_LOANS = 6
p MAX_LOANS
begin
Object.const_get(:max_loans)
rescue NameError => e
puts e.message
end
const.rb:4: warning: already initialized constant MAX_LOANS
const.rb:3: warning: previous definition of MAX_LOANS was here
6
wrong constant name max_loans
Source: Module#const_get
(This example is deliberately wrong; RuboCop reports Lint/ConstantReassignment.)
Suffixes: ?, ! and =
A method name may end in one of three characters. They are part of the name, not operators, and Ruby assigns no meaning to them; the meaning is a convention that the whole standard library follows.
| Suffix | Convention | Examples |
|---|---|---|
|
A predicate: returns |
|
|
The "dangerous" variant of a method of the same name: it mutates the receiver, raises instead of returning |
|
|
A setter. |
|
Source: Method names
A setter call is syntax sugar, which makes one pitfall worth knowing: inside a method, clerk_name = "Ana" creates a
local variable; only self.clerk_name = "Ana" calls the setter. (A private setter can be called with self. as an
exception, see Visibility and Encapsulation.)
Operator method names
Operators are methods, and a class defines them with def and the operator as the name. The unary forms add @ to
distinguish them from the binary ones (-@ and +@; ! and ~ are already unary).
# frozen_string_literal: true
module Shelf
# The maximum number of loans a desk holds.
MAX_LOANS = 5
# CamelCase class, snake_case methods, SCREAMING_SNAKE_CASE constant.
class LoanDesk
attr_reader :open_loans
def initialize
@open_loans = []
@clerk_name = "anonymous"
end
def full?
open_loans.size >= MAX_LOANS
end
def add!(title)
raise ArgumentError, "desk is full" if full?
open_loans << title
self
end
def clerk_name=(value)
@clerk_name = value.strip
end
def +(other) = open_loans + other.open_loans
def [](index) = open_loans.fetch(index)
def -@ = open_loans.reverse
def !
open_loans.empty?
end
end
end
desk = Shelf::LoanDesk.new
desk.add!("Dune").add!("Emma")
p desk.full?
p desk[0]
p(-desk)
p desk + desk
p(!desk)
desk.clerk_name = " Ana "
p desk.instance_variable_get(:@clerk_name)
p desk.public_methods(false).sort
false
"Dune"
["Emma", "Dune"]
["Dune", "Emma", "Dune", "Emma"]
false
"Ana"
[:!, :+, :-@, :[], :add!, :clerk_name=, :full?, :open_loans]
The overridable operators are + - * / % ** == != < > <= >= <=> === =~ !~ << >> & | ^ ~ ! [] []= +@ -@, plus the
call operator .() (method call) and the backtick method. &&, ||, and, or, defined? and assignment itself
cannot be redefined (a += 1 calls + and then assigns). The full list with precedence is in
Operators and Precedence.
Naming conventions
These are conventions, not rules; the compiler accepts other spellings, but the community style guide and RuboCop’s
Naming/* cops treat them as the standard, and every library follows them.
| What | Style | Example |
|---|---|---|
Local variable, method, parameter, symbol |
|
|
Class, module |
|
|
Constant (not a class or module) |
|
|
Predicate |
|
|
Dangerous variant |
|
|
Conversion |
|
|
Setter / getter |
|
|
Unused variable or parameter |
|
|
Private helper |
plain name; privacy comes from |
|
File |
|
|
Naming guidance for design (a class name is a noun, a method name a verb phrase, a predicate reads as a question) is
developed in Idioms, Style and Clean Code; how attr_reader,
attr_writer and attr_accessor generate getters and setters is in
Classes and Objects.
Keywords
Ruby has 41 reserved words. A reserved word cannot be used as the name of a local variable or constant, and it cannot start a method definition without a receiver, because the parser treats it as syntax.
| Group | Keywords | Details |
|---|---|---|
Definitions |
|
Methods and Parameters,
Classes and Objects,
Modules and Mixins; |
Conditionals |
|
Control Expressions; |
Loops and jumps |
|
Control Expressions; |
Exceptions |
|
|
Calls |
|
Methods and Parameters, Blocks, Procs and Lambdas, Method Lookup and super |
Logic |
|
low-precedence boolean operators (see above) |
Pseudo-values |
|
|
Source-location values |
|
|
Program-level |
|
Source: Keywords
Several things that look like keywords are ordinary methods, and so can be redefined, passed around and called with an
explicit receiver: puts, p, print, require, require_relative, raise, loop, lambda, proc, catch/throw,
private/public/protected, attr_accessor, include, extend, format, gets and binding. This matters when you
read private as a keyword: it is Module#private called with no arguments, which changes the default visibility of the
rest of the class body.
Reserved words as method names
A reserved word is fine as a method name when it follows a . or &., as a hash key with the key: syntax, and as a symbol.
You only cannot use it as a bare local variable, constant or def without a receiver.
# frozen_string_literal: true
module Shelf
# Keywords are legal method names when called with an explicit receiver.
class Loan
def initialize(return_date) = @return = return_date
def return = @return.to_s
def end = "closing"
def begin = "opening"
end
end
loan = Shelf::Loan.new("2026-11-01")
puts loan.return
puts loan.end
puts loan.begin
puts({ if: 1, class: "Book", end: 2 })
puts loan.method(:return).name.inspect
class_name = "ok"
p class_name
2026-11-01
closing
opening
{if: 1, class: "Book", end: 2}
:return
"ok"
Source: Method names
A bare keyword as a variable name is the classic beginner error. class = 2 does not say "unexpected keyword"; Prism
reports it as a malformed class definition (the same "class/module name must be CONSTANT" message as in the previous
section, repeated with several follow-up errors). Use klass, kind or class_name instead; klass is the
long-standing Ruby spelling for a variable that holds a class.
Contextual names: it, numbered parameters and the rest
it (Ruby 3.4) and _1 to _9 (Ruby 2.7, for example [[1, 2], [3, 4]].map { _1 + _2 } gives [3, 7]) name block
parameters without declaring them. Neither is a reserved word:
it is a contextual name, which refers to the implicit parameter only when the block has no explicit parameters and
no local variable called it is in scope.
# frozen_string_literal: true
titles = %w[dune emma]
names = titles.map { "#{it}!" }
p names
it = 5
p it
begin
titles.map { it.upcase }
rescue NoMethodError => e
puts e.message
end
["dune!", "emma!"]
5
undefined method 'upcase' for an instance of Integer
(The last line is a deliberate failure: once it = 5 has been assigned, it is that local variable, not the block
parameter.) The details, the Ruby 4.0 Binding#implicit_parameters and the interaction with nested blocks are in
Blocks, Procs and Lambdas.
Program-level forms
__FILE__, __LINE__ and friends
Four names describe where the current code is. The first three are keywords, evaluated by the parser; __method__,
__callee__ and __dir__ are Kernel methods.
| Name | Value |
|---|---|
|
The path of the current file, as it was given to |
|
The line number of the token. |
|
The script’s |
|
The name (a |
|
The name the method was called by; differs from |
|
The absolute path of the directory of the current file (the same as |
Source: Kernel#__method__, Kernel#__callee__ and Kernel#__dir__
The idiom if __FILE__ == $PROGRAM_NAME (or $0) is true only when the file is the program that was started, and false
when the same file is loaded by require; it lets a file be both a library and a script, and is the standard way to put a
quick demonstration at the bottom of a library file. require_relative "shelf/version" is equivalent to
require File.expand_path("shelf/version", __dir__).
BEGIN and END
BEGIN { ... } runs before any other code in the file; END { ... } runs when the program is about to exit. They are
rare outside one-liners (ruby -ne 'BEGIN { count = 0 }; END { puts count }; count += gets.to_i'), and RuboCop’s
Style/BeginBlock and Style/EndBlock flag them.
# frozen_string_literal: true
# BEGIN must appear at top level, but it runs first.
puts "main body"
BEGIN { puts "BEGIN block runs first" }
END { puts "END block runs last" }
at_exit { puts "at_exit handler" }
def describe
puts __method__.inspect
puts __callee__.inspect
end
alias show describe
describe
show
puts __FILE__
puts __LINE__
puts File.basename(__dir__)
puts __FILE__ == $PROGRAM_NAME
puts DATA.read
__END__
line one
line two
BEGIN block runs first
main body
:describe
:describe
:describe
:show
program_level.rb
19
examples
true
line one
line two
at_exit handler
END block runs last
Source: BEGIN and END
The rules:
-
BEGINmust be at the top level of a file and must use braces (notdo ... end). In a method it is a syntax error (BEGIN is permitted only at toplevel). -
END { }registers its block the first time that line is executed, and only once, even if executed again. It runs in reverse order of registration together withat_exithandlers, so the example above prints theat_exithandler first (registered last). -
ENDinside a method warnsEND in method; use at_exit, and the block is still registered once however many times the method is called. UseKernel#at_exitinstead, which registers a new handler on every call. -
Local variables assigned in a
BEGINblock are visible to the rest of the file (ruby -e 'BEGIN { c = 0 }; c += 1; p c'prints1), which is the one-liner pattern above.
Source: Kernel#at_exit
__END__ and DATA
A line that contains only __END__ ends the program text. Ruby stops parsing there, and the rest of the file is data. If
the file is the program’s main script (the file given to ruby), the constant DATA is defined as a File object
positioned just after the __END__ line.
# frozen_string_literal: true
puts DATA.class
puts DATA.each_line.map(&:chomp).inspect
__END__
Dune
Emma
File
["Dune", "Emma"]
Source: Object::DATA
Facts that matter in practice:
-
DATAexists only for the main script. A required library that has an__END__section does not get its ownDATA; the constant, if any, is the main script’s. -
__END__must be alone on its line, in column 0, with no trailing text. -
Because it is a
File,DATA.pos,DATA.rewind(which goes to the start of the file, source included),DATA.readandDATA.each_lineall work; the data is read from disk, so the file must still exist when you read it. -
__END__ends the program text for tools too; it is also the usual marker after whichruby -xstops. The simpler alternative for larger fixtures is a heredoc, or a separate file undertest/fixtures.
alias, undef and defined?
alias
alias new_name old_name makes a second name for an existing method. It is a keyword, so the names are written without a
comma and without a colon (symbols also work), and it works in any scope. Module#alias_method is the method form: it takes
strings or symbols with commas, can be used with send or in a loop, and returns the new name as a symbol since Ruby 3.0.
# frozen_string_literal: true
module Shelf
class Catalog
def initialize = @books = []
def add(title)
@books << title
self
end
def titles = @books.dup
alias entries titles
alias_method :list, :titles
alias all titles
def legacy_dump = titles.join("; ")
undef legacy_dump
end
end
catalog = Shelf::Catalog.new.add("Dune")
p catalog.entries, catalog.list, catalog.all
p Shelf::Catalog.instance_method(:entries).name
p Shelf::Catalog.instance_method(:entries).original_name
p catalog.respond_to?(:legacy_dump)
begin
catalog.legacy_dump
rescue NoMethodError => e
puts e.message
end
$old_name = 7
alias $new_name $old_name
$new_name = 8
p $old_name
["Dune"]
["Dune"]
["Dune"]
:entries
:titles
false
undefined method 'legacy_dump' for an instance of Shelf::Catalog
8
Source: alias and Module#alias_method
-
An alias copies the method body at the time of aliasing: redefining
titleslater does not changeentries. This is what makesaliasuseful for wrapping a method (alias old_titles titles, then define a newtitlesthat callsold_titles), a pattern thatModule#prependnow replaces in most code (see Refinements and Monkey Patching). -
aliasalso works on global variables (alias $new_name $old_name), where both names refer to the same variable. -
Method#nameis the alias name andMethod#original_namethe name it was defined with;__method__returns the original name and__callee__the alias, as the earlier example shows. -
Aliasing a method that does not exist raises
NameError. -
RuboCop’s
Style/Aliasprefersaliasin a class body andalias_methodelsewhere (inside a method or a block).
undef and remove_method
undef name (or undef :name, or several names separated by commas) prevents the class from responding to a method and
its ancestors' versions: a call raises NoMethodError even when a superclass defines the method.
Module#remove_method only removes the definition from the current class, so lookup continues to the ancestors.
# frozen_string_literal: true
module Demo
class Base
def hello = "base"
end
class Child < Base
undef hello
end
class Removed < Base
def hello = "child"
remove_method :hello
end
end
p Demo::Removed.new.hello
begin
Demo::Child.new.hello
rescue NoMethodError => e
puts e.message
end
"base"
undefined method 'hello' for an instance of Demo::Child
Source: undef, Module#undef_method and Module#remove_method
Module#undef_method is the method form of undef. Prefer making a method private, or not inheriting it, over undef
in application code; the keyword mostly appears in BasicObject subclasses and in library code. See
Method Lookup and super for the lookup order that makes the
distinction matter.
defined?
defined?(expression) does not evaluate the expression (in the sense of running its side effects); it returns a string
that describes it, or nil if the expression would not be valid. It is a keyword, and it is handled by the parser, which is
why defined?(puts "x") prints nothing.
# frozen_string_literal: true
@count = 1
p defined?(@count)
p defined?(@missing)
p defined?(puts)
p defined?(String)
p defined?(yield)
p defined?(super)
p defined?(@loans = [])
p defined?(nil)
p defined?(self)
p defined?(true)
p defined?(1 + 1)
p defined?(@@cv)
p defined?($stdout)
p defined?(Shelf::Book)
p defined?(String.new)
p defined?(Object.nothing)
p defined?(Process::Status)
p defined?(a && b)
p defined? @count && @count.zero?
p(defined?(@count) && @count.zero?)
p defined?(@missing) && @missing.zero?
"instance-variable"
nil
"method"
"constant"
nil
nil
"assignment"
"nil"
"self"
"true"
"method"
nil
"global-variable"
nil
"method"
nil
"constant"
"expression"
"expression"
false
nil
Source: defined?
| Result | Expression |
|---|---|
|
a local variable that is defined |
|
a method call that would succeed, including operators ( |
|
any other valid expression, |
|
an assignment |
|
a defined name of that kind ( |
|
inside a method that has a block, or a super method |
|
the pseudo-variables |
|
the thing is not defined |
The low precedence is the pitfall in the example above: without parentheses, defined? @count && @count.zero? is
defined?(@count && @count.zero?), and it returns "expression" whether or not @count is set. Write
defined?(@count) && @count.zero?. Even then, prefer a reflection method that says what you mean:
instance_variable_defined?(:@count), Module#const_defined?, respond_to? or Binding#local_variable_defined?, which
are less error-prone and work with names built at run time.
The regexp special variables are handled specially: defined?($1) is nil unless the last match had a first group, and
defined?($&) is nil when there is no active match.
Seeing what Ruby sees
Because the lexer and parser are available as libraries, you can inspect exactly how Ruby tokenizes a line. Ripper (the
standard-library lexer, based on the parse.y grammar) and Prism (the default parser since 3.4) both expose it.
# frozen_string_literal: true
require "prism"
require "ripper"
source = "price = copies * 2 # per week\n"
Ripper.lex(source).each { |(_pos, type, token, _state)| puts "#{type} #{token.inspect}" }
result = Prism.lex("loan = 1 + 2")
result.value.each { |(token, _lex_state)| puts "#{token.type} #{token.value.inspect}" }
parsed = Prism.parse("loan = 1 + 2")
puts parsed.value.statements.body.first.class
p Prism.parse("def").errors.map(&:message)
on_ident "price"
on_sp " "
on_op "="
on_sp " "
on_ident "copies"
on_sp " "
on_op "*"
on_sp " "
on_int "2"
on_sp " "
on_comment "# per week\n"
IDENTIFIER "loan"
EQUAL "="
INTEGER "1"
PLUS "+"
INTEGER "2"
EOF ""
Prism::LocalVariableWriteNode
["unexpected end-of-input; expected a method name", "expected a delimiter to close the parameters", "unexpected end-of-input, assuming it is closing the parent top level context", "expected an `end` to close the `def` statement"]
Source: Ripper.lex and Prism.parse
Ripper keeps comments and whitespace as tokens, so it is what documentation tools and syntax highlighters build on. Prism
keeps the full tree, recovers from errors (it reports all four problems for def above, not just the first), and is the
parser used by Ruby 3.4 and later and by many tools.
Warnings and errors from this page’s topics
| Message (Ruby 4.0) | Cause and fix |
|---|---|
|
Source bytes not valid in the script encoding. Re-save as UTF-8 or add |
|
Magic comment after code. Move it to the top. |
|
Value is not |
|
Mutation of a chilled string (3.4+, deprecation warnings only). Add the magic comment and use |
|
Mutating a frozen literal. Build a new string, or use |
|
|
|
A line break ended the expression early. Check the line before. |
|
A constant was assigned twice. Use a variable or freeze once. |
|
|
|
|
|
A class or module name that is not capitalized, or a keyword used as a variable. |
|
|
Where older books and tutorials are outdated
Material written before Ruby 3.4 (including popular books) is accurate about the language’s shape and wrong about several details of the surface covered here.
| Older statement | Ruby 4.0 |
|---|---|
"String literals are frozen by default since Ruby 3.4." |
No. They are chilled: not frozen, and a mutation warns only under |
|
|
"Ruby reads and executes the file line by line." |
The whole file is parsed first (with Prism since 3.4), then compiled and run; syntax errors stop the run before it starts. |
A line starting with |
|
` is a syntax error; put the operator at the end of the previous line. |
Valid since Ruby 4.0 (Feature #20925). The trailing form still works on every version. |
|
|
|
|
|
UTF-8 is the default since Ruby 2.0; the comment is redundant, and RuboCop flags it. |
|
|
The catalogue of every outdated statement is on What’s New and Migrating Older Code, and the common mistakes beginners make with layout and naming are collected in Common Mistakes and Gotchas.
Version notes
| Version | Change |
|---|---|
2.0 |
UTF-8 became the default source encoding. |
2.7 |
Comments allowed between the lines of a leading-dot chain; numbered block parameters ( |
3.0 |
|
3.4 |
Chilled string literals; Prism the default parser; |
4.0 |
Leading |
|