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.

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, END in a method) are reported here.

Compile

The tree becomes YARV instructions. BEGIN blocks are hoisted to the front. String literals are allocated as frozen or chilled according to frozen_string_literal.

Run

Top-level statements execute in order. END blocks and at_exit handlers run when the program ends.

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.

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>

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

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

-E ext[:int], --encoding

Sets Encoding.default_external and optionally Encoding.default_internal (for example -E iso-8859-1:utf-8).

-U

Sets Encoding.default_internal to UTF-8, so text read from IO is transcoded to UTF-8.

-Ku, -Ke, -Ks, -Kn

Legacy: sets the script and external encoding to UTF-8, EUC-JP, Windows-31J or ASCII-8BIT. Prefer -E and a magic comment.

-x[dir]

Skips everything before a #!ruby line, so Ruby code embedded in a mail, a shell script or a text file can run. The text after a __END__ line ends the program as usual.

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.

  • =begin and =end delimit a block comment. Both must start in the first column, and each must be at the start of its own line. An indented =begin is a syntax error.

  • Anything after =begin or =end on 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 a def or class is 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/=end is rarely used in modern Ruby and RuboCop’s Style/BlockComments flags 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

frozen_string_literal

true, false

Makes plain string literals frozen (true) or mutable (false) in this file.

In the first comment section, before any code.

encoding (or coding)

any Encoding name

Sets the script encoding, see above.

Line 1, or line 2 after a shebang.

shareable_constant_value

none, literal, experimental_everything, experimental_copy

Makes the values assigned to constants Ractor-shareable.

Before the constants it should cover; applies to the rest of the scope.

warn_indent

true, false

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: true and # frozen-string-literal: true both work, and a space after # is optional. Use the canonical lower-case form.

  • The "first comment section" for frozen_string_literal is 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, and ruby -w says 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"

The rules:

  • Literals with interpolation ("Dune #{x}") are never frozen and never shared (since Ruby 3.0). String.new, dup, +"..." (unary plus, String#+@) and String#* results are always new, mutable strings.

  • The same literal text in the same file refers to the same object ("Dune".equal?("Dune") is true), 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 of RuntimeError).

  • The elements of %w[...] and heredocs without interpolation are literals, so they are frozen too (verified: %w[x y].first.frozen? and a plain <<~TXT are true; a heredoc with #{...} is false). Strings that are not literals, such as :sym.to_s or "a" * 2, are unaffected, while Symbol#name always 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)
Setting Result for a literal in the file

# frozen_string_literal: true

Frozen. Mutation raises FrozenError.

# frozen_string_literal: false

Mutable, no warning (an explicit opt-out; the warning is not shown).

no comment (Ruby 3.4 and 4.0)

Chilled: frozen? is false; mutation works but warns when deprecation warnings are on.

no comment and --enable=frozen-string-literal (also RUBYOPT)

Frozen everywhere, in every file that has no comment of its own; useful to find mutations in a test run.

no comment and --disable=frozen-string-literal

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

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

none (default)

No change, no check.

literal

A literal (or a literal nested in a literal) assigned to a constant is deeply frozen. Anything else must already be shareable or Ractor::IsolationError is raised.

experimental_everything

Every assigned value is made shareable with Ractor.make_shareable, in place. This can deep-freeze an object that belongs to someone else (X = SomeGem::DEFAULTS).

experimental_copy

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.

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

total = a + then b

+ needs a right operand.

Trailing comma or open bracket

Book.new(isbn, then title)

The argument list is still open.

Trailing =

x = then 1

An assignment needs a value.

Leading . or &. (fluent chain)

list then .map { ... }

A statement cannot begin with ., so Ruby joins it to the previous line. Comment lines between links are allowed (since Ruby 2.7).

Leading &&, ||, and, or (Ruby 4.0)

ready then && valid

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.

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.

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

if, elsif, unless, case ... when, case ... in

then between the condition and the body

while, until, for

do between the condition and the body

begin ... rescue

then after the exception list (rescue ArgumentError then 0)

anywhere

; between statements

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

name, _name

local variable or method name

starts with a lower-case letter or _

Name

constant (also a class or module name)

starts with an upper-case letter; a second assignment warns already initialized constant

@name

instance variable

per object; nil and a warning-free read if never set

@@name

class variable

shared along an inheritance tree; see Variables, Constants and Scope

$name

global variable

also the special variables $stdout, $0, $PROGRAM_NAME, $;

:name

symbol

see Symbols

_

conventional "unused" name

_ and names that start with _ suppress the unused-variable warning

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

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

(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 true or false (or at least something with a clear truthiness).

Array#empty?, String#start_with?, Hash#key?, Object#respond_to?, Comparable#between?

!

The "dangerous" variant of a method of the same name: it mutates the receiver, raises instead of returning nil, or otherwise surprises. It does not mean "mutating" by itself, and a bang method needs a safe sibling with the same name without the bang.

sort and sort!, map and map!, save and save!; also exit! (skips the at_exit handlers) and Array#flatten! (returns nil when nothing changed)

=

A setter. object.name = value calls name=(value) and evaluates to value, whatever the method returns.

attr_writer :title, Shelf.config.loan_days = 21

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

snake_case

loan_days, find_by_isbn, :due_date

Class, module

CapitalCase (acronyms stay upper-case)

Shelf::Catalog, HTTPClient, XMLParser

Constant (not a class or module)

SCREAMING_SNAKE_CASE

MAX_LOANS, DEFAULT_LOCALE

Predicate

? suffix, no is_/has_ prefix

available?, not is_available?

Dangerous variant

! suffix, only with a safe sibling

strip and strip!

Conversion

to_x (explicit), to_xxx for a full form, as_x for a cheap view

to_s, to_str, to_a, to_h

Setter / getter

name= / name (no get_/set_)

attr_accessor :title

Unused variable or parameter

_ or a name with a leading underscore

catalog.each_with_index { |book, _i| ... }

Private helper

plain name; privacy comes from private, not a leading underscore

valid? in Shelf::Isbn

File

snake_case.rb matching the constant

shelf/isbn.rb defines Shelf::Isbn

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

def, class, module, end, alias, undef, defined?

Methods and Parameters, Classes and Objects, Modules and Mixins; alias, undef and defined? below

Conditionals

if, elsif, else, unless, case, when, in, then

Control Expressions; in also introduces patterns (Pattern Matching)

Loops and jumps

while, until, for, do, break, next, redo, retry

Control Expressions; retry belongs to rescue

Exceptions

begin, rescue, ensure

Exceptions and Error Handling

Calls

return, yield, super

Methods and Parameters, Blocks, Procs and Lambdas, Method Lookup and super

Logic

and, or, not

low-precedence boolean operators (see above)

Pseudo-values

nil, true, false, self

Literals

Source-location values

__FILE__, __LINE__, __ENCODING__

Program-level forms

Program-level

BEGIN, END

BEGIN and END

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

__FILE__

The path of the current file, as it was given to ruby or require.

__LINE__

The line number of the token.

__ENCODING__

The script’s Encoding.

__method__

The name (a Symbol) of the enclosing method, or nil at the top level. For an aliased method it returns the original name.

__callee__

The name the method was called by; differs from __method__ for an alias.

__dir__

The absolute path of the directory of the current file (the same as File.dirname(File.realpath(__FILE__))).

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:

  • BEGIN must be at the top level of a file and must use braces (not do ... 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 with at_exit handlers, so the example above prints the at_exit handler first (registered last).

  • END inside a method warns END in method; use at_exit, and the block is still registered once however many times the method is called. Use Kernel#at_exit instead, which registers a new handler on every call.

  • Local variables assigned in a BEGIN block are visible to the rest of the file (ruby -e 'BEGIN { c = 0 }; c += 1; p c' prints 1), 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:

  • DATA exists only for the main script. A required library that has an __END__ section does not get its own DATA; 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.read and DATA.each_line all 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 which ruby -x stops. The simpler alternative for larger fixtures is a heredoc, or a separate file under test/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
  • An alias copies the method body at the time of aliasing: redefining titles later does not change entries. This is what makes alias useful for wrapping a method (alias old_titles titles, then define a new titles that calls old_titles), a pattern that Module#prepend now replaces in most code (see Refinements and Monkey Patching).

  • alias also works on global variables (alias $new_name $old_name), where both names refer to the same variable.

  • Method#name is the alias name and Method#original_name the 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/Alias prefers alias in a class body and alias_method elsewhere (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

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

"local-variable"

a local variable that is defined

"method"

a method call that would succeed, including operators (1 + 1)

"expression"

any other valid expression, a && b among them (it is not evaluated)

"assignment"

an assignment

"constant", "instance-variable", "class variable", "global-variable"

a defined name of that kind (Shelf::Book is nil when Shelf has not been loaded)

"yield", "super"

inside a method that has a block, or a super method

"self", "nil", "true"

the pseudo-variables

nil

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

invalid multibyte character 0xE9

Source bytes not valid in the script encoding. Re-save as UTF-8 or add # encoding:.

'frozen_string_literal' is ignored after any tokens

Magic comment after code. Move it to the top.

invalid value for frozen_string_literal: maybe

Value is not true or false.

literal string will be frozen in the future

Mutation of a chilled string (3.4+, deprecation warnings only). Add the magic comment and use +"..." or dup.

can't modify frozen String: "Dune" (FrozenError)

Mutating a frozen literal. Build a new string, or use +"".

mismatched indentations at 'end' with 'def' at 3

end not aligned with its keyword (-w).

possibly useless use of a literal in void context

A line break ended the expression early. Check the line before.

already initialized constant X

A constant was assigned twice. Use a variable or freeze once.

END in method; use at_exit

END inside a method. Use at_exit.

BEGIN is permitted only at toplevel

BEGIN inside a method or block.

class/module name must be CONSTANT (reported as an unexpected constant path after class)

A class or module name that is not capitalized, or a keyword used as a variable.

cannot assign unshareable object to X (Ractor::IsolationError)

shareable_constant_value: literal with a non-literal value.

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 -W:deprecated/-w. The magic comment or --enable=frozen-string-literal is still required to freeze them. Feature #20205 is open.

undefined method x' for nil:NilClass (backtick, `nil:NilClass)

undefined method 'x' for nil (3.3 dropped nil:NilClass and the class description; 3.4 replaced the backtick with a straight quote).

"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.

{:name=>"Ico"} as the printed form of a hash

{name: "Ico"} since 3.4; p and inspect use the new form. This matters when comparing output in tests.

_1/_2 are the way to write an implicit block parameter.

it (3.4) is the preferred single implicit parameter; _1 still works.

# encoding: utf-8 at the top of every file.

UTF-8 is the default since Ruby 2.0; the comment is redundant, and RuboCop flags it.

Fixnum and Bignum, taint and $SAFE

Fixnum and Bignum were unified into Integer in 2.4 and the constants removed in 3.2; $SAFE stopped having special meaning in 3.0 and the taint methods were removed in 3.2. Do not write them.

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 (_1).

3.0

frozen_string_literal no longer freezes interpolated literals; shareable_constant_value added (experimental); alias_method returns a symbol.

3.4

Chilled string literals; Prism the default parser; it; error messages use ' and omit nil:NilClass.

4.0

Leading &&, `

, `and, or continue the previous line (Feature #20925); Binding#implicit_parameters reads numbered and it parameters.