Literals

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). A literal is source text that the parser turns into an object without any method call on your part: 42, "text", :name, [1, 2], { a: 1 }, 1..5, /re/, ->(x) { x }. Ruby has more literal forms than most languages, and each one has rules about escapes, interpolation, mutability and how many objects it creates. This page walks through every form listed in the literals syntax guide, then covers the questions that cross all of them: which literals create a fresh object on every evaluation, which are frozen, and how the frozen-string-literal magic comment, the chilled strings introduced in Ruby 3.4 and the command-line switches fit together.

It builds on Getting Started and Program Structure and Lexical Elements (tokens, comments, magic comments, __END__). The classes behind each literal have their own pages: Numbers, Strings, Encodings and Unicode, Symbols, Arrays, Hashes, Ranges, Regular Expressions and Blocks, Procs and Lambdas. Mutability and copying are treated in depth in Object Lifecycle, Freezing and Copying.

Every example was syntax-checked with ruby -wc, run with ruby -w on Ruby 4.0.7 (the chilled-string example is run without -w on purpose, and prints its warnings through code) and checked with RuboCop 1.92 using the configuration of the running example (double-quoted strings, new cops enabled). Output is copied from real runs. Because this page is about literal forms that style tools often rewrite, a few examples start with a # rubocop:disable line naming the cop that would object (for example Style/CharacterLiteral) and end with the matching # rubocop:enable: the line tells you which cop disagrees with the form being demonstrated. Sources are the Ruby 4.0 documentation. Examples that use the shelf gem are run with ruby -I lib; see the shelf gem source for its files and Getting Started for the gem layout.

The literal map

Literal Class New object per evaluation? Frozen?

nil, true, false

NilClass, TrueClass, FalseClass

No, there is exactly one of each

Yes

42, 0xFF, 1_000

Integer

No, small values are immediate; big values are new objects

Yes

1.5, 2e10

Float

No for most values (immediate "flonum"), possibly yes for extreme magnitudes

Yes

3r, 2i, 1.5ri

Rational, Complex

Not guaranteed

Yes

"text", 'text', %q(text), heredocs

String

Yes by default; with the magic comment, plain literals are shared and frozen

Chilled by default (3.4+), frozen with the magic comment

"a#{x}" (interpolated)

String

Yes, always

Never (since 3.0)

:name, :"quoted", %s(x)

Symbol

No, symbols are unique by name

Yes

[1, 2], %w[a b], %i[a b]

Array

Yes, always

No

{ a: 1 }

Hash

Yes, always

No

1..5, 1.., ..5

Range

Literal with literal ends is created once at parse time

Yes (since 3.0)

/re/, %r{re}

Regexp

Literal without interpolation is created once

Yes (since 3.0)

->(x) { x }

Proc (a lambda)

Yes, one per evaluation

No

`cmd`, %x(cmd)

String (output of a shell command)

Runs the command on every evaluation

No

The last two columns are verified by the identity example below.

nil, true and false

nil, true and false are keywords that evaluate to the only instances of NilClass, TrueClass and FalseClass. They cannot be instantiated (NilClass.respond_to?(:new) is false), compare with equal?, and are frozen. nil and false are the only falsy values: every other object, including 0, "", [] and {}, is truthy in a condition. nil means "no value", and its conversions return empty values: nil.to_a is [], nil.to_s is "", nil.to_h is {}. The logical operators &, | and ^ on these three objects are ordinary methods and always return true or false (never the other operand), unlike && and ||, which are keywords that return one of their operands; both families are compared in Operators and Precedence.

# frozen_string_literal: true

p nil.class, true.class, false.class
p nil.to_a, nil.to_s, nil.to_h, nil.inspect
p nil & true, nil | 1, true ^ false, false ^ true
labels = [nil, false, 0, "", [], {}].map { |value| value ? "truthy" : "falsy" }
p labels
p nil.equal?(nil), NilClass.respond_to?(:new)

title = nil
p title.to_s.empty?, title&.upcase
begin
  title.upcase
rescue NoMethodError => e
  puts e.message
end

Output:

NilClass
TrueClass
FalseClass
[]
""
{}
"nil"
false
true
true
true
["falsy", "falsy", "truthy", "truthy", "truthy", "truthy"]
true
false
true
nil
undefined method 'upcase' for nil

The last lines show the classic failure: calling a method on nil raises NoMethodError. Since Ruby 3.3 the message names the receiver by class and, since 3.4, quotes the method name with plain single quotes: undefined method 'upcase' for nil. For nil, true and false the message says for nil, for true and for false; for other objects it says for an instance of Shelf::Book. Older material shows the Ruby 3.2 and earlier form, undefined method `upcase' for nil:NilClass. The safe navigation operator &. returns nil instead of raising, and to_s / Array() / Hash() conversions turn nil into an empty value; see Conversions and Coercion.

Integers

An integer literal is a sequence of digits. An underscore may separate digits for readability, and a prefix selects the base. All of the literals below are Integer objects, and Ruby has no separate fixed-width and arbitrary-precision classes: Integer grows as needed (the old Fixnum and Bignum classes were unified in Ruby 2.4 and their constants removed in 3.2).

Form Base Notes

1234, 1_234

decimal

underscores can appear between digits, never doubled, leading, or trailing

0d170, 0D170

decimal

explicit decimal prefix; rarely useful

0xAA, 0Xaa

hexadecimal

digits 0-9a-fA-F

0o252, 0O252, 0252

octal

a bare leading zero means octal, a classic trap; prefer 0o

0b1010, 0B1010

binary

digits 0 and 1

# frozen_string_literal: true

p 1_234, 0xAA, 0o252, 0b1010_1010
p 0d170, 0252 # rubocop:disable Style/NumericLiteralPrefix
p 170.to_s(2), 170.to_s(16), 170.to_s(8)
p 2**100, (2**100).class
p 7 / 2, -7 / 2, 7 % 3, -7 % 3, 7.fdiv(2), 7.divmod(2)
p 1234.digits, 255.bit_length
p Integer("0x1f", 16), Integer("010"), Integer("0b11", 2)
p "010".to_i, "010".oct, "0x1f".hex
begin
  Integer("08")
rescue ArgumentError => e
  puts e.message
end
p(-2.abs, -2**2, (-2)**2)
p 1.frozen?, 1.equal?(1)

Output:

1234
170
170
170
170
170
"10101010"
"aa"
"252"
1267650600228229401496703205376
Integer
3
-4
1
2
3.5
[3, 1]
[4, 3, 2, 1]
8
31
8
3
10
8
31
invalid value for Integer(): "08"
2
-4
4
true
true

Things worth knowing:

  • Integer#/ is floor division and % takes the sign of the divisor (-7 / 2 is -4, -7 % 3 is 2). Use fdiv for a float quotient and divmod for both parts. See Numbers.

  • A leading minus on a numeric literal is part of the literal: -2.abs is 2 (the literal -2), but -2**2 is -4 because ** binds tighter than a minus sign applied to an expression. Write (-2)**2 for 4.

  • Integer("010") follows Ruby’s literal rules and returns 8, but "010".to_i ignores prefixes and returns 10, and Integer("08") raises ArgumentError (invalid value for Integer(): "08") because 8 is not an octal digit. Pass an explicit base (Integer("010", 10)) when parsing user input.

  • Malformed literals are syntax errors, reported by the Prism parser (the default parser since Ruby 3.4): 1__0 gives invalid underscore placement in number, 1_ gives trailing '_' in number, 0o9 gives invalid octal number; numeric literal without digits and 0b2 the binary equivalent. 1.e3 is not a float: it parses as a method call and raises NoMethodError (undefined method 'e3' for an instance of Integer).

  • A numeric literal followed by a method call needs no parentheses (3.times, 255.to_s(2)), and integers are immutable objects.

Floats

A float literal needs digits on both sides of the decimal point, or an exponent. 12.34, 1234e-2, 1.234E1 and 1_000.5 are all valid; .5 and 1. are not (1. followed by a name is a method call). Floats are IEEE 754 double-precision numbers, with the usual consequences: not every decimal fraction is representable, comparisons need a tolerance, and Float::INFINITY and Float::NAN exist as constants (and from division by 0.0; integer division by zero raises ZeroDivisionError).

# frozen_string_literal: true

p 12.34, 1.234E1, 1_000.5, 1e3, 5e-324
p 1234e-2 # rubocop:disable Style/ExponentialNotation
p 0.1 + 0.2, (0.1 + 0.2 - 0.3).abs < Float::EPSILON
nan = 0.0 / 0.0
p Float::INFINITY, -Float::INFINITY, nan, nan == Float::NAN, nan.nan?
p Float::MAX, Float::MIN, Float::DIG, Float::EPSILON
p 1 / 0.0, 0.0 / 0.0, -1 / 0.0
p 3.7.floor, 3.7.ceil, 3.7.round, 3.75.round(1), 2.5.round, 3.5.round, 2.5.round(half: :even)
p 10.0.to_s, 1e14.to_s, 1e15.to_s, 0.0001.to_s, 0.00009.to_s
p 1.0.eql?(1), 1.0.equal?(1.0), 1.0 == 1 # rubocop:disable Lint/FloatComparison
begin
  Integer(1 / 0.0)
rescue FloatDomainError => e
  puts e.message
end

Output:

12.34
12.34
1000.5
1000.0
5.0e-324
12.34
0.30000000000000004
true
Infinity
-Infinity
NaN
false
true
1.7976931348623157e+308
2.2250738585072014e-308
15
2.220446049250313e-16
Infinity
NaN
-Infinity
3
4
4
3.8
3
4
2
"10.0"
"100000000000000.0"
"1.0e+15"
"0.0001"
"9.0e-05"
false
true
true
Infinity

Points to remember:

  • Float#to_s prints the shortest representation that round-trips. It uses plain notation for magnitudes from 0.0001 up to just below 1e15 and scientific notation outside that window, as the example shows (1e14 is "100000000000000.0", 1e15 is "1.0e+15", 0.0001 is "0.0001", 0.00009 is "9.0e-05").

  • Float::NAN is not equal to anything, itself included. Test with nan?. Float::EPSILON is the gap above 1.0, the usual tolerance for comparisons: (a - b).abs < Float::EPSILON for values near one, or a relative tolerance in general. The Lint/FloatComparison cop flags exact comparisons for this reason.

  • Float#round rounds halves away from zero by default (2.5.round is 3); half: :even and half: :down change that. Integer(Float::INFINITY) raises FloatDomainError with the message Infinity.

  • A literal outside the representable range evaluates to Infinity and ruby -w warns Float 1e400 out of range.

  • For money and exact decimal arithmetic use Rational, Integer cents, or the bigdecimal gem, which has been a bundled gem rather than part of the default libraries since Ruby 3.4; under Bundler it needs a Gemfile entry (see RubyGems and Bundler).

Rational and imaginary literals

Two numeric suffixes build exact and complex numbers directly in source (both since Ruby 2.1):

  • r makes a Rational: 3r is (3/1), 2/3r is (2/3), 1.2r is (6/5) (the decimal text is converted exactly, not through a float), 1_1/2_1r is (11/21), and fractions are reduced automatically.

  • i makes a Complex with a zero real part: 2i is (0+2i), and 3 + 4i is (3+4i). The two suffixes combine as ri (12.3ri is (0+(123/10)*i)); ir is a syntax error.

The slash in 2/3r is the ordinary division operator applied to the integer 2 and the rational 3r, so the precedence rules of Operators and Precedence apply. This is why 1.2/3r is a Float divided by a Rational and produces a float (0.39999999999999997), while 1.2r / 3 is exactly (2/5).

# frozen_string_literal: true

p 1r, 2/3r, -2/3r, 2/-3r, 1.2r, 2/4r, 1_1/2_1r
p 1.2/3r, 1.2r / 3, 3r.class
p 0.1r + 0.2r == 0.3r
p 0.1 + 0.2 == 0.3 # rubocop:disable Lint/FloatComparison
p Rational("1/3"), Rational(0.75), 0.75.to_r, 0.1.rationalize(Rational(1, 100))
p 1i, 1i * 1i, 12.3ri, 3 + 4i, (3 + 4i).abs, (3 + 4i).conjugate, 2i.class
p Complex("1+2i"), Complex(1, 2) * Complex(3, 4)
p((1/3r) + (1/6r))

Output:

(1/1)
(2/3)
(-2/3)
(-2/3)
(6/5)
(1/2)
(11/21)
0.39999999999999997
(2/5)
Rational
true
false
(1/3)
(3/4)
(3/4)
(1/10)
(0+1i)
(-1+0i)
(0+(123/10)*i)
(3+4i)
5.0
(3-4i)
Complex
(1+2i)
(-5+10i)
(1/2)

The class methods Kernel#Rational and Kernel#Complex parse strings (Rational("1/3")) and convert floats (Rational(0.75) is exact because 0.75 is a dyadic fraction; 3.14.to_r shows the huge exact ratio of the binary value, so prefer rationalize or a r literal for decimal data). RuboCop’s Layout/SpaceAroundOperators dislikes spaces around the slash of a rational literal, so the documented style is 2/3r without spaces. See Numbers for the full numeric tower.

Strings

The most common forms are double-quoted and single-quoted strings. Both produce String objects; the difference is what the parser does with the text.

  • Double quotes process escape sequences and interpolation. Any expression may appear inside #{...}; its value is converted with to_s (so nil becomes the empty string, a symbol becomes its name, an array becomes its inspect form). The shorthand #@ivar, #@@cvar and #$gvar interpolates a variable without braces.

  • Single quotes disable interpolation and every escape except \\ (a backslash) and \' (a single quote). A backslash before any other character is kept literally.

  • A string literal may span lines, and a backslash at the end of a line inside a double-quoted string joins the next line (a "continuation", an empty string).

# frozen_string_literal: true

author = "Ursula Le Guin"
count = 3
puts "Double: 'single' and \"escaped\""
puts "#{author} wrote #{count} books (#{count * 2} copies)"
puts "Tab:\tNewline:\\n Unicode: é \u{1F4DA} Hex: \x41 Octal: \101"
p "\a", "\e", "\s", "\cA", "\c?", "\M-a", "\u0000", "\0"
puts "Unknown escape: \q \z"
puts "line one \
continued"
missing = nil
symbol = :sym
list = [1, "two", nil]
ratio = 1.0
p "nil: #{missing}|", "sym: #{symbol}", "arr: #{list}", "float: #{ratio}"
p "café".length, "café".bytesize, "café".encoding, __ENCODING__

Output:

Double: 'single' and "escaped"
Ursula Le Guin wrote 3 books (6 copies)
Tab:	Newline:\n Unicode: é 📚 Hex: A Octal: A
"\a"
"\e"
" "
"\u0001"
"\u007F"
"\xE1"
"\u0000"
"\u0000"
Unknown escape: q z
line one continued
"nil: |"
"sym: sym"
"arr: [1, \"two\", nil]"
"float: 1.0"
4
5
#<Encoding:UTF-8>
#<Encoding:UTF-8>

Escape sequences

Escape sequences are available in double-quoted strings, character literals, non-quoted and double-quoted heredocs, backtick strings, double-quoted symbols and %Q, %W, %I and % literals. An unknown escape such as \q yields the character itself ("q"), as the example output shows.

Escape Meaning

\a, \b, \t, \n

bell (07), backspace (08), horizontal tab (09), newline (0A)

\v, \f, \r, \e

vertical tab (0B), form feed (0C), carriage return (0D), escape (1B)

\s

space (20), useful at the end of a line in a squiggly heredoc or a %w list

\\

a backslash

\nnn

octal byte value of one to three digits ("\101" is "A")

\xnn

hexadecimal byte value of one or two digits

\unnnn

Unicode code point with exactly four hex digits

\u{n...}

one or more code points, each of one to six hex digits, separated by spaces ("\u{1F4DA 41}")

\cx, \C-x

control character ("\cA" is "\u0001"); \c? and \C-? are DEL (7F)

\M-x, \M-\C-x

meta character (sets the high bit), meta-control

a backslash then a newline

line continuation: an empty string, used to fold a long literal

Two cautions follow from the table. First, \x and octal escapes insert bytes, so "\xff" is an invalid UTF-8 string; \u inserts a character and is the right tool for text. Second, the encoding of a literal comes from the source file (UTF-8 by default since Ruby 2.0, __ENCODING__), and a \u escape forces UTF-8. Details, including force_encoding, valid_encoding? and transcoding, are in Encodings and Unicode.

Single quotes, adjacent literals and character literals

# frozen_string_literal: true

# rubocop:disable Style/StringLiterals, Lint/InterpolationCheck, Lint/ImplicitStringConcatenation, Style/CharacterLiteral, Style/RedundantPercentQ

p 'No #{interpolation} and no \n escape'
p 'Only \\ and \' are escapes, so \q stays as is'
p 'a\nb'.length, "a\nb".length
p "con" "cat" "en" "ation"
p %q(percent) 'single' "double"
p "This string contains " \
  "no newlines."
p ?a, ?\n, ?\s, ?\u{41}, ?あ, ?\C-a

# rubocop:enable Style/StringLiterals, Lint/InterpolationCheck, Lint/ImplicitStringConcatenation, Style/CharacterLiteral, Style/RedundantPercentQ

Output:

"No \#{interpolation} and no \\n escape"
"Only \\ and ' are escapes, so \\q stays as is"
4
3
"concatenation"
"percentsingledouble"
"This string contains no newlines."
"a"
"\n"
" "
"A"
"あ"
"\u0001"
  • Adjacent string literals are concatenated at parse time, whatever their quoting: "con" "cat". A percent literal may be concatenated only if it is not the last piece. In "a" 'b' %q(c) the parser reads the % as the modulo operator and warns '%' after local variable or literal is interpreted as binary operator even though it seems like string literal; the program then fails with a NameError for c. Putting the percent literal first works (%q(percent) 'single' "double").

  • ?a is the character literal: a question mark and one character or escape produces a one-character string (?\n, ?\u{41}, ?\C-a). More than one character (?abc) is a syntax error. RuboCop’s Style/CharacterLiteral prefers "a", and the form is rarely used outside golfing and old code.

  • Interpolation inside a single-quoted string is not performed, which RuboCop’s Lint/InterpolationCheck reports because '#{x}' is usually a mistake.

Percent literals

A percent sign followed by a letter and a delimiter writes a literal without needing to escape quotes. The delimiter may be any non-alphanumeric ASCII character; the four bracket pairs (), [], {} and <> nest, so they may appear unescaped inside as long as they are balanced. Any delimiter can be escaped with a backslash.

Form Produces Interpolation and escapes

%q(text)

a String, like single quotes

none; only \\ and the delimiter are escapes

%Q(text), %(text)

a String, like double quotes

both

%w[a b]

an Array of strings split on whitespace

none; a backslash escapes a space

%W[a#{x} b]

an Array of strings

both, and escapes like \t work

%i[a b]

an Array of symbols

none

%I[a#{x} b]

an Array of symbols

both

%s(text)

one Symbol

none

%r{text}flags

a Regexp

as a regexp literal, so / needs no escape

%x(cmd)

the output of a shell command

as backticks

# frozen_string_literal: true

# rubocop:disable Style/PercentLiteralDelimiters, Style/RedundantPercentQ, Style/BarePercentLiterals, Style/WordArray, Style/SymbolArray

p %q(It's "quoted" #{not_interpolated})
p %Q(Total: #{1 + 1} \t tab)
p %(Parens (nested) are fine: #{2 * 3})
p %q[brackets [nested] ok], %q{braces {nested} ok}, %q<angle <nested> ok>
p %q|pipes|, %q:colons:, %q!bang!
p %q(escaped \) paren and backslash \\ and \n kept)
p %w[Dune Emma Ulysses], %W[#{1 + 1}a b\ c]
p %i[available lent overdue], %I[state_#{1 + 1} other]
p %s(a symbol with spaces)
p %r{/books/(\d+)}i

# rubocop:enable Style/PercentLiteralDelimiters, Style/RedundantPercentQ, Style/BarePercentLiterals, Style/WordArray, Style/SymbolArray

Output:

"It's \"quoted\" \#{not_interpolated}"
"Total: 2 \t tab"
"Parens (nested) are fine: 6"
"brackets [nested] ok"
"braces {nested} ok"
"angle <nested> ok"
"pipes"
"colons"
"bang"
"escaped ) paren and backslash \\ and \\n kept"
["Dune", "Emma", "Ulysses"]
["2a", "b c"]
[:available, :lent, :overdue]
[:state_2, :other]
:"a symbol with spaces"
/\/books\/(\d+)/i

Remarks:

  • Whitespace separates the words of %w and %i: space, tab, newline, carriage return, form feed and vertical tab. Nested delimiters do not create nested arrays; the result is always flat (%w[foo[bar baz]qux] is ["foo[bar", "baz]qux"]).

  • The %W and %I forms run interpolation per word, and an interpolation that yields a string with spaces stays one element.

  • RuboCop’s defaults prefer %w/%i for arrays of three or more simple words, square brackets for %w/%i, parentheses for %q/%Q/%s/%x and braces for %r (Style/PercentLiteralDelimiters), and discourage %q and %Q when a plain quote works (Style/RedundantPercentQ). Those are style opinions, not language rules.

Heredocs

A here document writes a multi-line string without escaping quotes. The opener <<IDENTIFIER is an expression that can be followed by more code on the same line (a method call, an argument separator, a closing parenthesis); the body starts on the next line and runs until a line containing only the identifier.

  • <<ID keeps all whitespace, and the terminator must start in column 0.

  • <<-ID allows the terminator to be indented, but the body keeps its indentation.

  • <<~ID (the squiggly heredoc, Ruby 2.3) also removes the indentation of the least-indented non-blank line from every line. Blank lines and lines of only whitespace are ignored when measuring. For the measurement a tab counts as advancing to the next multiple of eight columns. Interpolated text is inserted after the dedent and is never re-indented, as the Items: case in the example shows.

  • The identifier may be quoted. <<~'ID' disables interpolation and escapes (a "raw" heredoc), <<~"ID" is the default behaviour and allows any character except the quote and a newline in the identifier, and an identifier written in backticks (<<~ followed by ID in backticks) runs the body as a shell command, like a backtick literal.

  • Several heredocs can be opened on the same line; their bodies follow in order. This is legal but hard to read.

  • A method call goes after the opener (<<~TEXT.lines.map(&:strip)), not after the terminator.

# frozen_string_literal: true

# rubocop:disable Layout/HeredocIndentation, Layout/ClosingHeredocIndentation

title = "Dune"
plain = <<TEXT
Plain heredoc for #{title}.
  Indentation is kept.
TEXT
dash = <<-TEXT
  Dash form keeps leading spaces for #{title}.
  TEXT
squiggly = <<~TEXT
  Squiggly form removes the common indent.
    Relative indent is kept for #{title}.

  Blank lines do not count.
TEXT
raw = <<~'TEXT'
  Raw: #{title} and \n are literal.
TEXT
shout = <<~TEXT.upcase.lines.map(&:strip)
  first line
  second line
TEXT
p plain, dash, squiggly, raw, shout

def render(header, body) = "#{header.strip}|#{body.strip}"
puts render(<<~HEAD, <<~BODY)
  Catalogue
HEAD
  Entry: #{title}
BODY

tabbed = <<~TEXT
  	tab then text
  two spaces
TEXT
p tabbed
titles = %w[Dune Emma]
interp = <<~TEXT
  Items:
  #{titles.join("\n")}
TEXT
p interp
quoted = <<~"QUOTED TEXT"
  Any quoted identifier works: #{title}
QUOTED TEXT
p quoted

# rubocop:enable Layout/HeredocIndentation, Layout/ClosingHeredocIndentation

Output:

"Plain heredoc for Dune.\n  Indentation is kept.\n"
"  Dash form keeps leading spaces for Dune.\n"
"Squiggly form removes the common indent.\n  Relative indent is kept for Dune.\n\nBlank lines do not count.\n"
"Raw: \#{title} and \\n are literal.\n"
["FIRST LINE", "SECOND LINE"]
Catalogue|Entry: Dune
"\ttab then text\ntwo spaces\n"
"Items:\nDune\nEmma\n"
"Any quoted identifier works: Dune\n"

Style guidance reflected by RuboCop: prefer <<~ (Layout/HeredocIndentation flags << with an indented body), align the closing identifier with the opener (Layout/ClosingHeredocIndentation), and use a meaningful delimiter such as TEXT, SQL or HEADER rather than EOS or END (Naming/HeredocDelimiterNaming). With the magic comment frozen_string_literal: true, a heredoc without interpolation is frozen and one with interpolation is not, like any other string literal (see Frozen and chilled string literals).

Symbols

A symbol literal starts with a colon. Symbols are immutable, interned names: every use of :available is the same object for the life of the process, so they make cheap hash keys, method names and state labels (the shelf Loan class keeps its states as symbols). The forms are:

  • plain names: :title, :Constant, :empty?, :save!, :name=, :@ivar, :@@cvar, :$global;

  • operator names: :+, :[]=, :<=>, :!, :=~;

  • quoted names for anything else: :"quoted symbol", :"9lives", and a double-quoted form with interpolation and escapes, :"with#{1 + 1}"; a single-quoted form :'raw #{x}' suppresses both;

  • %s(name) and the arrays %i[a b] / %I[a#{x} b];

  • in a hash literal, title: and "quoted key": build symbol keys (see Hashes).

# frozen_string_literal: true

# rubocop:disable Lint/SymbolConversion

p :available, :"quoted symbol", :"with#{1 + 1}", :'raw #{x}', %s(spaced out)
p :+, :[]=, :empty?, :save!, :name=, :Constant, :@ivar, :$global, :<=>, :"9lives"
p :"foo\sbar", :a <=> :b, %i[a b]
p :a.equal?(:a), "a".to_sym.equal?(:a), :a.frozen?, "a".equal?("a")
p :title.to_s, :title.size, :upcase.to_proc.call("Dune")
p :"".inspect, :"a-b", :"a b".to_proc.class

# rubocop:enable Lint/SymbolConversion

Output:

:available
:"quoted symbol"
:with2
:"raw \#{x}"
:"spaced out"
:+
:[]=
:empty?
:save!
:name=
:Constant
:@ivar
:$global
:<=>
:"9lives"
:"foo bar"
-1
[:a, :b]
true
true
true
true
"title"
5
"DUNE"
":\"\""
:"a-b"
Proc

Symbol#inspect shows the shortest valid literal, adding quotes only when needed (:"quoted symbol", :"9lives", :"a-b"). Dynamic symbols created with String#to_sym or :"..#{}.." are garbage collected since Ruby 2.2, so the old advice about symbol leaks from user input no longer applies in the same form, but they still cost lookups and have no use as free-form text. Symbol#to_proc (map(&:upcase)) has existed since Ruby 1.8.7. The string a symbol returns from to_s is chilled since Ruby 3.4 (mutating it warns under -W:deprecated), while Symbol#name returns a frozen string. The full symbol API is in Symbols.

Arrays

An array literal is a comma-separated list between brackets. Elements are arbitrary expressions evaluated left to right, nesting is free, and a trailing comma is allowed (RuboCop’s Style/TrailingCommaInArrayLiteral forbids it on a single line). The splat operator * expands an enumerable in place ([*isbns, "extra"], [*1..3, *"a".."c"]), and [*nil] is []. %w and %i are shorthands for arrays of strings and symbols.

# frozen_string_literal: true

isbns = %w[9780134685991 9780596516178]
p [1, 1 + 1, [1 + 2, [4]]], [], %w[a b], %i[x y]
p [*isbns, "extra"], [*1..3, *"a".."c"]
p [*nil], [*[1, 2]], [*"str"] # rubocop:disable Lint/RedundantSplatExpansion
p Array.new(2) { [] }, Array.new(2, 0)
shared = Array.new(2, [])
shared[0] << :oops
p shared
fresh = Array.new(2) { [] }
fresh[0] << :ok
p fresh
def literal = []
p literal.equal?(literal), literal.frozen?
p [[1, 2], [3, 4]].to_h, Array(nil), Array(1..3), Array("a\nb")
first, (second, third), *rest = 1, [2, 3], 4, 5 # rubocop:disable Style/ParallelAssignment
p [first, second, third, rest]

Output:

[1, 2, [3, [4]]]
[]
["a", "b"]
[:x, :y]
["9780134685991", "9780596516178", "extra"]
[1, 2, 3, "a", "b", "c"]
[]
[1, 2]
["str"]
[[], []]
[0, 0]
[[:oops], [:oops]]
[[:ok], []]
false
false
{1 => 2, 3 => 4}
[]
[1, 2, 3]
["a\nb"]
[1, 2, 3, [4, 5]]

Two traps appear in the example:

  • Array.new(2, []) stores the same inner array twice, so appending through one index changes both. The block form Array.new(2) { [] } evaluates the block per slot and creates distinct arrays.

  • An array literal inside a method creates a new array on every call (literal.equal?(literal) is false), which is why Style/MutableConstant asks you to freeze arrays assigned to constants.

first, (second, third), *rest = 1, [2, 3], 4, 5 is a multiple assignment, which also uses array syntax on its right side; see Assignment and Multiple Assignment.

Hashes

A hash literal is a comma-separated list of pairs between braces. Two syntaxes exist, and they can be mixed in one literal:

  • key: value creates a symbol key and is the idiomatic form for identifiers and for keyword arguments (the same syntax is used when calling a method with keywords).

  • expression => value allows any object as the key: strings, numbers, arrays, nil.

Further rules:

  • A quoted key, "a b": 1, is a symbol key (:"a b"), not a string. Interpolation works ("c#{1 + 1}": 2 is c2:), and if: or class: are fine because the colon makes it a label.

  • Value omission (Ruby 3.1): { title:, year: } means { title: title, year: year }, taking the value from the local variable or method of the same name. The same works in keyword arguments (Shelf::Isbn.new(digits:)).

  • The double splat **other merges another hash in place, and later entries win, so { **defaults, loan_days: 21 } overrides while { loan_days: 21, **defaults } is overridden.

  • Since Ruby 3.4, Hash#inspect prints {name: "Ico", "k" => 1, 1 => 2}: symbol keys that can be written as labels use key: value, other keys have spaces around =>. Output from earlier versions ({:name=>"Ico"}) is obsolete.

  • A string used as a key is duplicated and frozen by the hash, so later mutation of the original does not corrupt the table (h.keys.first.equal?(key) is false).

  • Insertion order is preserved: reassigning an existing key keeps its position, deleting and re-adding moves it to the end.

  • The default value of Hash.new([]) is one shared array: counts[:a] << 1 mutates the default and stores nothing (the example prints {} and [1]). Use Hash.new { |hash, key| hash[key] = [] }. RuboCop reports this as Lint/SharedMutableDefault.

# frozen_string_literal: true

title = "Dune"
year = 1965
p({ title:, year: })
p({ "title" => title, 1 => :one, nil => 0, [1, 2] => :pair, title: })
p({ "a b": 1, "c#{1 + 1}": 2, "a-b": 3, "9x": 4, if: 5, "x=": 6 })
defaults = { loan_days: 14, renewals: 1 }
p({ **defaults, loan_days: 21 }, { loan_days: 21, **defaults })
p({ a: 1, "a" => 2 }.keys, { 1 => :int, 1.0 => :float }.size)
key = +"mutable"
h = { key => 1 }
key << "!"
p h, h.keys.first.frozen?, h.keys.first.equal?(key)
p({}.class, {}.default, Hash.new(0)[:missing])
counts = Hash.new([]) # rubocop:disable Lint/SharedMutableDefault
counts[:a] << 1
p counts, counts[:b], counts.default
p({ a: 1 }.inspect, { "a" => 1 }.to_s, { 1 => { b: [nil] } }.inspect)
h2 = { b: 1, a: 2 }
h2[:c] = 3
h2.delete(:b)
h2[:b] = 4
p h2

Output:

{title: "Dune", year: 1965}
{"title" => "Dune", 1 => :one, nil => 0, [1, 2] => :pair, title: "Dune"}
{"a b": 1, c2: 2, "a-b": 3, "9x": 4, if: 5, "x=": 6}
{loan_days: 21, renewals: 1}
{loan_days: 14, renewals: 1}
[:a, "a"]
2
{"mutable" => 1}
true
false
Hash
nil
0
{}
[1]
[1]
"{a: 1}"
"{\"a\" => 1}"
"{1 => {b: [nil]}}"
{a: 2, c: 3, b: 4}

Syntax pitfalls:

  • A pair of braces at the start of a statement or as the sole argument of a command call is parsed as a block: puts {} calls puts with an empty block and prints a blank line. Parenthesize (puts({})).

  • m(a: 1) and m({ a: 1 }) both pass a hash to a method taking one positional parameter; with keyword parameters the two are different since Ruby 3.0 (see Calling Methods and Argument Forwarding).

  • A literal with the same key twice keeps the last value and ruby -w warns, for example key :a is duplicated and overwritten on line 1. The warning notes a: and "a": as the same key.

The complete method list is in Hashes.

Ranges

A range literal is two expressions joined by .. (inclusive) or ... (exclusive of the end). Either end may be omitted: (1..) is endless and (..5) is beginless. (nil..nil) has neither bound. A range is not limited to numbers: it works for any object that implements <=> and, for iteration, succ (strings, dates, your own classes).

# frozen_string_literal: true

p [1..5, 1...5, (1..), (..5), (1..nil), (nil..nil), ("a".."e")]
p (1..5).to_a, (1...5).size, ("a".."e").to_a, (1.0..2.0).include?(1.5)
p (1..).first(3), (..5).include?(3), (1..10) % 3, (1..10).step(3).to_a
p (1..5).frozen?, Range.new(1, 5, true), (1..5).eql?(1..5), (1..5).equal?(1..5)
begin
  (..5).each { |n| puts n }
rescue TypeError => e
  puts e.message
end
begin
  (1.0..2.0).each { |n| puts n }
rescue TypeError => e
  puts e.message
end
n = 3
p (1..n + 1).to_a, 1..n # rubocop:disable Lint/AmbiguousRange
p (1..10).sum, (1...1).to_a, (5..1).to_a, 5.downto(1).to_a
p (1..10) === 5, Integer === 5, # rubocop:disable Style/CaseEquality
  (1..3).cover?(1..2), ("a".."z").cover?("mm"), ("a".."z").include?("mm")
p (..-1), (3..).size

Output:

[1..5, 1...5, 1.., ..5, 1.., nil..nil, "a".."e"]
[1, 2, 3, 4, 5]
4
["a", "b", "c", "d", "e"]
true
[1, 2, 3]
true
((1..10).%(3))
[1, 4, 7, 10]
true
1...5
true
false
can't iterate from NilClass
can't iterate from Float
[1, 2, 3, 4]
1..3
55
[]
[]
[5, 4, 3, 2, 1]
true
true
true
true
false
..-1
Infinity

What to note:

  • Endless ranges (Ruby 2.6) and beginless ranges (Ruby 2.7) need parentheses when used as an argument or receiver, because 1.. followed by a newline or comma is ambiguous. (1..).first(3) is [1, 2, 3], and (3..).size is Infinity.

  • (..5).each raises TypeError (can't iterate from NilClass), and so does iterating a float range (can't iterate from Float); include? and cover? still work on them.

  • The step operator (1..10) % 3 (Ruby 2.6) returns an arithmetic sequence like step(3).

  • Precedence: .. binds more loosely than arithmetic, so 1..n + 1 means 1..(n + 1); RuboCop’s Lint/AmbiguousRange asks you to wrap complex bounds.

  • A range whose ends are literals is created once; all range objects are frozen since Ruby 3.0, including Range.new(1, 5, true).

  • cover? compares with <=>, while include? iterates for non-numeric ranges, so ("a".."z").cover?("mm") is true and ("a".."z").include?("mm") is false. A range in a case/when uses ===, which is cover?.

  • Ranges in conditions behave as flip-flops in some positions; see Operators and Precedence.

More in Ranges.

Regexp literals

/pattern/ creates a Regexp; %r{pattern} does the same with another delimiter, which avoids escaping slashes. Trailing letters set modes: i ignores case, m makes . match newlines (Ruby’s name for dot-all; ^ and $ always match at line boundaries), x allows whitespace and # comments in the pattern, o interpolates once, and u, n, e, s choose the encoding. Interpolation (/#{digits}/) is allowed; use Regexp.escape for text that must be matched literally. A regexp literal is frozen since Ruby 3.0, and a literal without interpolation is created once, so it is cheap in loops (though two textually identical literals are two objects).

If a regexp literal with named groups appears on the left of =~, the named captures become local variables, as the example shows with prefix and rest. An invalid pattern in a literal is a syntax error; with Regexp.new it is a RegexpError at run time (end pattern with unmatched parenthesis: /(/).

# frozen_string_literal: true

p(/foo/, %r{foo/bar}, /foo/i, /a b # note/x, /a.b/m)
p(/\d+/.class, /x/.frozen?, /x/.equal?(/x/), /x/.source, /x/i.options)
isbn = "978-0134685991"
p isbn.match?(/\A\d{3}-\d{10}\z/), isbn =~ /\d{10}/, Regexp.last_match(0)
if /(?<prefix>\d{3})-(?<rest>\d+)/ =~ isbn
  p prefix, rest
end
digits = "13"
p(/#{digits}/, /#{Regexp.escape('a.b')}/, /#{digits}/o.source)
p Regexp.union("a.b", /c|d/), Regexp.escape("1+1=2?")
begin
  Regexp.new("(")
rescue RegexpError => e
  puts e.message
end
p "x" !~ /y/

Output:

/foo/
/foo\/bar/
/foo/i
/a b # note/x
/a.b/m
Regexp
true
false
"x"
1
true
4
"0134685991"
"978"
"0134685991"
/13/
/a\.b/
"13"
/a\.b|(?-mix:c|d)/
"1\\+1=2\\?"
end pattern with unmatched parenthesis: /(/
true

Behind the syntax lie flavour details (Onigmo, lookbehind, \h, \R, absolute anchors \A and \z, Regexp.timeout as a defence against catastrophic backtracking) covered in Regular Expressions. Note the use of \A and \z rather than ^ and $ to validate whole strings, which is the safe habit for input checking.

Lambda literals

->(params) { body } creates a lambda, a Proc that checks its arity and returns from itself. The parentheses around the parameters are optional (-> x { x }), a lambda without parameters is -> { ... }, parameters may have defaults, a rest parameter, keywords and a block parameter, and do ... end can replace the braces (->(x) do ... end). A lambda is called with call, .(), [] or yield; lambda? is true, and calling with the wrong number of arguments raises ArgumentError (wrong number of arguments (given 0, expected 1)). The numbered parameters _1, _2 and the it parameter (Ruby 3.4) can replace explicit parameters in a one-expression body.

# frozen_string_literal: true

add_one = ->(value) { value + 1 }
greet = -> { "hello" }
loose = lambda do |name, punctuation = "!"|
  "#{name}#{punctuation}"
end
multi = ->(first, second = 2, *rest, key:, opt: 1, **others, &blk) { [first, second, rest, key, opt, others, blk] }
p add_one.call(1), add_one.(1), add_one[1], add_one.yield(1) # rubocop:disable Style/LambdaCall
p greet.call, loose.call("Dune"), loose.call("Dune", "?")
p add_one.lambda?, add_one.arity, multi.arity, multi.parameters
p proc { |a, b| [a, b] }.call(1), proc { |a, b| [a, b] }.call([1, 2])
begin
  add_one.call
rescue ArgumentError => e
  puts e.message
end
p [1, 2, 3].map { it * 2 }, [1, 2, 3].map(&add_one)
p add_one.class, add_one.curry.class
p ->(x) { x }.frozen?

Output:

2
2
2
2
"hello"
"Dune!"
"Dune?"
true
1
-3
[[:req, :first], [:opt, :second], [:rest, :rest], [:keyreq, :key], [:key, :opt], [:keyrest, :others], [:block, :blk]]
[1, nil]
[1, 2]
wrong number of arguments (given 0, expected 1)
[2, 4, 6]
[2, 3, 4]
Proc
Proc
false

Differences from a proc { }: proc { |a, b| } accepts too few or too many arguments and destructures a single array argument ([1, nil] and [1, 2] in the example), while a lambda is strict. Kernel#lambda with a non-literal block raises ArgumentError since Ruby 3.3. The literal is different from a block: a block is not an object until captured with &. See Blocks, Procs and Lambdas.

Backticks and %x

Text in backticks, or in a %x literal, is interpolated like a double-quoted string and then run as a shell command; the literal evaluates to the command’s standard output, and Process.last_status (the $? variable) holds the Process::Status. A command that cannot be found raises Errno::ENOENT; a command that exits non-zero does not raise. A heredoc opened with backticks does the same for its whole body.

# frozen_string_literal: true

version = `echo shelf`
p version, Process.last_status.success?, Process.last_status.exitstatus, Process.last_status.class
p %x(echo "#{1 + 1}" done) # rubocop:disable Style/CommandLiteral
p `exit 3`, Process.last_status.exitstatus
begin
  `no-such-command-xyz`
rescue Errno::ENOENT => e
  puts e.message
end
puts <<~`SHELL`
  echo heredoc runs a shell
SHELL

Output:

"shelf\n"
true
0
Process::Status
"2 done\n"
""
3
No such file or directory - no-such-command-xyz
heredoc runs a shell

A command built from untrusted input is a command-injection hole: never interpolate user data into a backtick literal. Prefer Open3.capture2e("cmd", arg) or system("cmd", arg) with separate arguments, which bypass the shell. See Processes, Signals and Shell and Security.

Pseudo-variables and other literal-like keywords

A few keywords evaluate like literals but depend on where they appear:

  • __FILE__ is the path of the current source file, __LINE__ its line number, __dir__ (a method) the directory of the file, __method__ the name of the current method (as a symbol, or nil at top level) and __ENCODING__ the encoding of the source file.

  • self is the current object (main at top level).

  • __END__ on a line by itself ends the program text; everything after it is readable through the DATA constant, an open File positioned after the marker, but only in the main script.

# frozen_string_literal: true

def where = [__method__, __LINE__]

p __FILE__, __dir__.class, __LINE__, __ENCODING__, where, self, self.class
p defined?(DATA), DATA.read
__END__
Dune,9780441172719

Output:

"pseudo.rb"
String
5
#<Encoding:UTF-8>
[:where, 3]
main
Object
"constant"
"Dune,9780441172719\n"

if __FILE__ == $PROGRAM_NAME is the usual guard that runs code only when a file is executed directly rather than required; it is covered in Command-Line Programs.

Which literals create a new object

The question matters for performance, for aliasing bugs and for thread and Ractor sharing. The rules, checked below:

  • Evaluating an array or hash literal creates a new, mutable object every time. A string literal creates a new mutable one every time unless the magic comment is on, in which case every evaluation of the same literal returns the same frozen object (and identical literals in the same file are deduplicated).

  • Integers (of immediate size), symbols, nil, true, false and most floats are immediate values: no allocation, always identical.

  • Range and regexp literals without interpolation are created once at parse time and frozen.

# frozen_string_literal: true

def objects
  [[1, 2], { a: 1 }, +"text", "text", 1..2, /re/, :sym, 42, 4.2, nil]
end

first = objects
second = objects
first.zip(second).each do |a, b|
  puts format("%-10<kind>s same object: %-5<same>s frozen: %<frozen>s",
              kind: a.class, same: a.equal?(b), frozen: a.frozen?)
end

Output:

Array      same object: false frozen: false
Hash       same object: false frozen: false
String     same object: false frozen: false
String     same object: true  frozen: true
Range      same object: true  frozen: true
Regexp     same object: true  frozen: true
Symbol     same object: true  frozen: true
Integer    same object: true  frozen: true
Float      same object: true  frozen: true
NilClass   same object: true  frozen: true

The +"" and -"" unary operators give explicit control: +str returns the string itself if it is mutable and a mutable copy if it is frozen or chilled; -str returns a frozen, deduplicated copy. dup and String.new always produce a mutable string.

Frozen and chilled string literals

Strings are the one literal whose mutability depends on configuration. Three mechanisms interact.

The magic comment

A comment at the top of a file (in the first comment section, before any code) controls the literals of that file only:

# frozen_string_literal: true

With it, a plain string literal is allocated once at parse time and frozen. Interpolated strings, String.new, dup, ""` and string concatenation results are not frozen (interpolated literals stopped being frozen in Ruby 3.0). A string literal with `# frozen_string_literal: false+ is mutable and unchanged. The default is no comment, which is now "chilled" (next section). Emacs-style -*- frozen_string_literal: true -*- also works. Every library file of the shelf gem starts with the comment, and RuboCop’s Style/FrozenStringLiteralComment enforces it.

# frozen_string_literal: true

title = "Dune"
p title.frozen?, "Dune".equal?(title), "Dun" "e".frozen? # rubocop:disable Lint/ImplicitStringConcatenation
count = 1
p "a#{count}b".frozen?, <<~TEXT.frozen?, <<~TEXT.frozen?
  plain heredoc
TEXT
  interpolated #{title}
TEXT
p %w[a b].first.frozen?, :sym.to_s.frozen?, 1.to_s.frozen?, nil.to_s.frozen?
begin
  title << " Messiah"
rescue FrozenError => e
  puts e.message
  p e.receiver.equal?(title)
end
copy = title.dup
copy << " Messiah"
unfrozen = +title
unfrozen << "!"
p copy, unfrozen, (+title).equal?(title), (+copy).equal?(copy), (-copy).frozen?
p String.new("Dune").frozen?, String.new.encoding, "".encoding, String.new(capacity: 64).encoding
buffer = +""
buffer << "built " << "in place"
p buffer, buffer.frozen?

Output:

true
true
true
false
true
false
true
false
false
true
can't modify frozen String: "Dune"
true
"Dune Messiah"
"Dune!"
false
true
true
false
#<Encoding:BINARY (ASCII-8BIT)>
#<Encoding:UTF-8>
#<Encoding:BINARY (ASCII-8BIT)>
"built in place"
false

Details shown by the example:

  • The literal and a second occurrence of the same literal are the same object ("Dune".equal?(title)).

  • FrozenError has a receiver and the message can't modify frozen String: "Dune".

  • String.new with no argument returns an empty string with the BINARY (ASCII-8BIT) encoding, while ""` is UTF-8 because it copies the literal. Use `"" or String.new(capacity: 64, encoding: "UTF-8") for buffers of text.

  • +title returned a copy because the literal is frozen; +copy returned copy itself because it was already mutable.

Chilled strings (Ruby 3.4 and later)

When a file has no magic comment, string literals (and the string returned by Symbol#to_s) are chilled: they behave as mutable strings and frozen? returns false, but Ruby remembers that the literal is "supposed" to be frozen in the future. The first mutation of a chilled string emits a warning of the deprecated category and then turns the string into an ordinary mutable one. By default the deprecated category is off, so nothing is printed; it is enabled with ruby -w, ruby -W:deprecated, RUBYOPT=-W:deprecated or Warning[:deprecated] = true.

# rubocop:disable Style/FrozenStringLiteralComment

# No magic comment: string literals are "chilled".
title = "Dune"
p title.frozen?
title << " Messiah"
p title

Warning[:deprecated] = true
other = "Emma"
other << " Woodhouse"
other << " again"
p other
literal = :sym.to_s
literal << "bol"
p literal
p "x".equal?("x")

# rubocop:enable Style/FrozenStringLiteralComment

Output:

false
"Dune Messiah"
"Emma Woodhouse again"
"symbol"
false

Standard error (the deprecation warnings):

chilled.rb:11: warning: literal string will be frozen in the future (run with --debug-frozen-string-literal for more information)
chilled.rb:15: warning: string returned by :sym.to_s will be frozen in the future

The warning appears once per string. It names the line and offers --debug-frozen-string-literal, which adds a second line saying where the string was created:

chilled.rb:6: warning: literal string will be frozen in the future
chilled.rb:4: info: the string was created here

(That is the real output of the first mutation in the example when it is run with -W:deprecated --debug-frozen-string-literal, chilled.rb being the file name used here.) The category is documented in Warning.categories, which lists :deprecated, :experimental, :performance and :strict_unused_block. The String#+@ operator was changed in 3.4 to return a copy of a chilled string, so +"text" is the portable way to ask for a mutable string.

Command-line switches and environment

Switch Effect

--enable-frozen-string-literal (or --enable=frozen-string-literal)

Freezes every string literal in files without an explicit comment, as if each had frozen_string_literal: true. Mutation raises FrozenError. Useful in CI to find code that mutates literals; the 4.0 option documentation marks the feature as disabled by default.

--disable-frozen-string-literal

Turns chilled behaviour off: literals are plain mutable strings and no deprecation warning is produced. This is the documented way to silence the 3.4 warning.

--debug-frozen-string-literal

Records where each chilled or frozen literal was created; the warning or the FrozenError then reports the allocation line.

-W:deprecated, -W:no-deprecated

Enables or disables the deprecated warning category, which carries the chilled-string warning. -w and -W2 enable it too.

RUBYOPT=--enable-frozen-string-literal

The same switches through the environment, for gems and test runs without editing a command line; a source file’s own magic comment wins.

Running the earlier chilled example with --enable-frozen-string-literal stops at the first mutation with chilled.rb:6:in '<main>': can't modify frozen String: "Dune" (FrozenError).

Where this is heading

Strings are still not frozen by default in Ruby 4.0; the discussion of making them frozen is the open bug tracker tracker item Feature #20205 ("Enable frozen_string_literal by default"), still open, and chilled strings are the migration path: Ruby warns now so code can be fixed before any change. The practical advice is:

  1. Put # frozen_string_literal: true in every file you write and let RuboCop enforce it.

  2. Build strings you will mutate with ""`, `String.new` (mind the encoding), `dup` or `<<` on a `"" buffer; prefer "#{a} #{b}" or Array#join to repeated <<.

  3. Run the test suite once with -W:deprecated (or RUBYOPT=-W:deprecated) and fix the warnings; run it once with --enable-frozen-string-literal to find the rest.

  4. Do not add .freeze to a string literal when the magic comment is present: it is redundant (Style/RedundantFreeze), and Style/MutableConstant still asks you to freeze arrays, hashes and interpolated strings assigned to constants.

Freezing is not deep (a frozen array can hold mutable strings) and the shareable_constant_value magic comment, the Ractor.make_shareable method and Object#freeze are discussed in Object Lifecycle, Freezing and Copying and Ractors.

Literals in the shelf scenario

The running example uses literals everywhere: a heredoc of CSV-like rows, %w-style words, hash value omission through keyword arguments, and a frozen-constant list of symbols. This script builds a Shelf::Catalog from literal data.

# frozen_string_literal: true

require "shelf"

rows = <<~CSV
  9780134685991,Effective Java,Joshua Bloch
  9780596516178,The Ruby Programming Language,David Flanagan
CSV

catalog = Shelf::Catalog.new
rows.each_line(chomp: true) do |line|
  digits, title, name = line.split(",")
  catalog.add(Shelf::Book.new(isbn: Shelf::Isbn.new(digits:), title:, author: Shelf::Author.new(name:)))
end

catalog.each { |book| puts book }
p catalog.map(&:isbn).map(&:to_s)
p Shelf::Loan::STATES
begin
  Shelf::Isbn.new(digits: "978-0-13-468599-2")
rescue ArgumentError => e
  puts e.message
end

Output:

Effective Java by Joshua Bloch
The Ruby Programming Language by David Flanagan
["9780134685991", "9780596516178"]
[:available, :lent, :overdue, :returned]
invalid ISBN: 978-0-13-468599-2

Source: Literals

Shelf::Isbn.new(digits:) uses hash/keyword value omission. Shelf::Loan::STATES is %i[available lent overdue returned].freeze: a symbol array literal frozen so callers cannot change it. The last two lines show the validation of the Isbn value object: a literal ISBN with a bad checksum raises ArgumentError at construction.

Warnings and errors that literals produce

Source What Ruby says (ruby -w, Ruby 4.0.7)

1__0

syntax error: invalid underscore placement in number

1_

syntax error: trailing '_' in number

0o9, 0b2

syntax error: invalid octal number; numeric literal without digits (binary likewise)

1.e3

NoMethodError: undefined method 'e3' for an instance of Integer

1e400

warning: Float 1e400 out of range, value Infinity

{ a: 1, "a": 2 }

warning: key :a is duplicated and overwritten on line 1

puts -1

warning: ambiguous first argument; put parentheses or a space even after `- operator`

x = 5; p x /2

warning: '/' after local variable or literal is interpreted as binary operator even though it seems like regexp literal

"a" 'b' %q(c)

warning: '%' after local variable or literal is interpreted as binary operator even though it seems like string literal, then NameError

puts {}

no error: the braces are an empty block, so a blank line is printed

mutation of a chilled string with -W:deprecated

warning: literal string will be frozen in the future

mutation of a frozen literal

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

Regexp.new("(")

RegexpError: end pattern with unmatched parenthesis: /(/

Where older material is outdated

Books and articles written before 2024 give advice that no longer matches Ruby 4.0. For the concepts on this page:

Older claim Current behaviour

"String literals are frozen by default since Ruby 3.4."

They are chilled: mutable, frozen? is false, and a warning appears only under -W:deprecated. The magic comment or --enable-frozen-string-literal is still needed for real freezing in 4.0, and Feature #20205 remains open.

{:name=>"Ico"} as the inspect output of a hash.

{name: "Ico"} since Ruby 3.4, with spaces around => for non-symbol keys.

undefined method `upcase' for nil:NilClass

undefined method 'upcase' for nil (receiver by class from 3.3, quote style from 3.4).

Fixnum and Bignum as separate classes.

One Integer class (2.4); the constants are gone (3.2).

_1 is the way to write an implicit block parameter.

it (Ruby 3.4) is the preferred single implicit parameter; _1 remains for numbered parameters.

Symbols are never garbage collected.

Dynamic symbols are collected since Ruby 2.2; static symbols in source are not.

Interpolated strings are frozen by the magic comment.

Not since Ruby 3.0.

Range and regexp literals are mutable objects.

Frozen since Ruby 3.0.

Hash value omission, endless ranges, %i, rational and imaginary suffixes are not mentioned.

Value omission { x:, y: } is Ruby 3.1; endless ranges 2.6; beginless 2.7; %i 2.0; 3r and 2i 2.1; squiggly heredocs 2.3.

Benchmark or ostruct as parts of the standard library used with literals.

Both are bundled gems in Ruby 4.0 and need a Gemfile entry under Bundler.

Summary

  • Know the quoting rules (single, double, percent, heredoc, character) and the one escape you must remember in single quotes (\\ and \').

  • Use 0o for octal, _ for readability, r and i for exact and imaginary numbers, and never compare floats with ==.

  • Prefer key: value, value omission and %w/%i; remember that Hash.new([]) and Array.new(n, []) share one object.

  • Parenthesize endless and beginless ranges; use \A/\z with regexp literals for validation.

  • Put # frozen_string_literal: true in every file, mutate through +"", and test with -W:deprecated and --enable-frozen-string-literal.

References