Symbols

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 Symbol is an immutable, interned name: the same name is always the same object, comparing two symbols is an identity check, and a symbol cannot be changed or created with new. Symbols are what Ruby itself uses for identifiers (method, constant and variable names are reported as symbols), and what programs use for hash keys, keyword arguments, option names and small sets of states. This page covers what a symbol is and how it differs from a string, every way to write one, the conversions to and from strings and procs, the lifetime of symbols (including garbage collection of dynamic ones), the use of symbols as hash keys and state values, the methods that Symbol offers, and the pitfalls.

It builds on Literals (symbols are introduced there with the other literal forms) and on Blocks, Procs and Lambdas (&:name is a conversion to a proc). The pages that use symbols most are Hashes (symbol keys and the 3.4 inspect format), Strings (the text type that symbols are usually confused with), Encodings and Unicode and Dynamic Dispatch and Method Definition (send, public_send, define_method). A Ruby symbol is closest to an interned string constant, or to an enum member that carries no type of its own.

Every example was syntax-checked with ruby -wc, run with ruby -w on Ruby 4.0.7 and linted with RuboCop 1.92 using the configuration of the running shelf gem (double-quoted strings, NewCops: enable, Style/Documentation off). The output shown is the real output; the counts in the symbol-table example differ from run to run. The examples that need the gem are run with ruby -Ilib, so require "shelf" finds it (see the shelf gem source).

What a symbol is

The Ruby reference defines a symbol as "a named identifier inside the Ruby interpreter" and adds that the same Symbol object is created for a given name for the duration of the program, whatever the name is used for. In practice:

Property Meaning

Interned

There is one Symbol object per name. :lent, :"lent", "lent".to_sym and %s(lent) are the same object, so equal? is true and == is a pointer comparison.

Immutable

A symbol is frozen: :lent.frozen? is true, and it cannot be given instance variables or singleton methods. There is no << and no upcase!.

No constructor

Symbol.new does not exist; a symbol comes from a literal or from a conversion (String#to_sym).

Identifier-like

The class is designed for names. Its methods are a read-only subset of the String methods (length, [], upcase, start_with?, succ, <=>), and the results are symbols or plain values.

Comparable

Symbol includes Comparable and orders symbols by their names (String#<=>), so symbols sort and max works.

Shareable

A symbol can be used from any Ractor because it is frozen and immutable.

The practical consequence is that symbols are cheap to compare and cheap to keep: a hash with symbol keys looks up by identity, and an object that stores a state as :lent does not allocate a new string each time. The next example shows all spellings of one symbol, the contrast with strings, and the two things you cannot do.

# frozen_string_literal: true

# Every spelling of the same name is the same object.
plain = :"on loan"
text = "on loan"
from_string = text.to_sym
percent = %s(on loan)
p plain.equal?(from_string), plain.equal?(percent)
p [plain, from_string, percent].map(&:object_id).uniq.size

# Two strings with equal content are equal but not the same object.
first = String.new("available")
second = String.new("available")
p first == second, first.equal?(second)

# A symbol is immutable and has no constructor.
p plain.frozen?
begin
  plain.instance_variable_set(:@lent, true)
rescue FrozenError => e
  puts e.message
end
begin
  Symbol.new
rescue NoMethodError => e
  puts e.message
end
true
true
1
true
false
true
can't modify frozen Symbol: :"on loan"
undefined method 'new' for class Symbol

Source: Symbol

Two String objects with the same characters are == but not equal?, unless the literals are deduplicated (a # frozen_string_literal: true file makes identical literals the same object, which is why the example builds the strings with String.new). A symbol never has this distinction: equal names mean the same object.

Symbol versus string

A string represents text or data, and a symbol represents an identifier. A string is mutable (unless frozen), carries an encoding that can be changed and is usually a fresh object, so it suits text that is read, built or edited. A symbol is the name of something: a state, an option, a method, a key. The rule of thumb used throughout this section is that anything a program chooses from a fixed vocabulary (:available, :lent, :overdue) is a symbol, and anything that comes from outside or is displayed to a person is a string.

String Symbol

Literal

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

:lent, :"lent", %s(lent), %i[lent]

Identity

A new object per evaluation, unless frozen and deduplicated

One object per name

Mutable

Yes (except a frozen string or literal in a frozen_string_literal file)

No, always frozen

Methods

Many, including the mutating ones (<<, gsub!, replace)

A short list, none of them mutating

Typical use

Text, user input, output, data

Names, keys, states, options, method names

Conversion to the other

to_sym

to_s (a copy), name (the shared frozen string)

Encoding

Any

US-ASCII for ASCII names, otherwise the encoding of the source string

Equality with the other

"lent" == :lent is false

:lent == "lent" is false

The last row is the most common source of bugs: a symbol and a string with the same name are never equal, and never the same hash key. The section on hash keys shows the effect, and the conversion rules are in the next section.

Writing symbols

A symbol literal begins with a colon. The forms are:

  • :name, :Name, :name?, :name! and :name= for identifiers, constant names and the method names that end in ?, ! and =. The colon is part of the literal; :name= is the setter’s name, not an assignment.

  • :@ivar, :@@cvar and :$gvar for the names of instance, class and global variables. These are what reflection returns.

  • operator names: :+, :-, :[], :[]=, :!, :+@, :-@, :<=>, :=== and so on. They are valid method names.

  • :"any text", a quoted symbol, for names that are not identifiers (:"on loan", :"9lives", :"foo=bar", the empty symbol :""). The double-quoted form interpolates: :"report_#{year}". A single-quoted form (:'name') exists and does not interpolate.

  • %s(any text) is the percent form of one symbol (the delimiters follow the usual percent-literal rules).

  • %i[a b c] and %I[a#{1} b] build arrays of symbols (%I interpolates). RuboCop prefers %i[] for arrays of plain symbols (Style/SymbolArray).

  • key: value and "key": value inside a hash literal or a call define a symbol key (see the section on hash keys); the quoted label form with a double-quoted string has been available since Ruby 2.2.

The inspect form follows the same rules: a symbol is printed bare when it is a valid identifier or operator, and quoted otherwise, with the quotes showing what a literal would need. Symbol#inspect is what p prints.

# frozen_string_literal: true

# Symbols that need quotes are inspected with quotes.
p :available, :"on loan", :"9lives", :"", :a?, :a=, :[]=, :!, :+, :-@, :<=>
p :@title, :@@count, :$stdout, :Shelf, :"foo=bar"

# The quoted literal can interpolate; %s and %i are the percent forms.
year = 2026
p :"report_#{year}", %s(two words), %i[available lent]

# Encoding follows the content.
p :abc.encoding, :é.encoding
p "caf\xE9".dup.force_encoding(Encoding::ISO_8859_1).to_sym.encoding
:available
:"on loan"
:"9lives"
:""
:a?
:a=
:[]=
:!
:+
:-@
:<=>
:@title
:@@count
:$stdout
:Shelf
:"foo=bar"
:report_2026
:"two words"
[:available, :lent]
#<Encoding:US-ASCII>
#<Encoding:UTF-8>
#<Encoding:ISO-8859-1>

Ruby’s symbols carry an encoding like strings do. An ASCII-only name is US-ASCII, a name with non-ASCII characters keeps the encoding of the string it came from (UTF-8 for a UTF-8 source file, ISO-8859-1 for the last line), and two symbols with the same characters in different encodings are different symbols unless the characters are ASCII. Most programs only ever see US-ASCII and UTF-8 symbols.

Converting between symbols and strings

The conversions are small, and the differences between them matter:

Call Result

symbol.to_s, symbol.id2name

A String with the name (without the colon). A new object on each call, not frozen, but see the chilled string note below.

symbol.name

The frozen, shared String of the name (since Ruby 3.0). It is the same object on every call, so it allocates nothing.

symbol.to_sym, symbol.intern

The symbol itself (the aliases exist for symmetry with String).

string.to_sym, string.intern

The symbol with that name, created if it does not exist yet.

symbol.inspect

The literal form including the colon (":returned"), quoted when necessary.

symbol.to_proc

A lambda that sends the method named by the symbol to its first argument.

Prefer name when you only read the text (to compare, to build a message, to use as a hash key of another table), and to_s when you need a string you may change. Because name returns the same frozen object, equal? between two calls is true; to_s returns a different string every time.

# frozen_string_literal: true

state = :returned

p state.to_s, state.name, state.to_sym.equal?(state)
p state.id2name, state.intern.equal?(state)
p state.name.frozen?, state.to_s.frozen?
p state.inspect, state.to_proc.class

# The name is always the same String; to_s returns text you may want to change.
p state.name.equal?(state.name)
p state.to_s.equal?(state.to_s)

# Only the string-to-symbol direction can create a new symbol.
text = "returned"
p text.to_sym.equal?(state)
"returned"
"returned"
true
"returned"
true
true
false
":returned"
Proc
true
false
true

Source: Symbol#name

Source: Symbol#to_s

The chilled string from to_s

Symbol#to_s returns a mutable string today, but the language has announced that it will return a frozen one. Ruby 3.4 made the string returned by Symbol#to_s "chilled": it is still mutable and nothing happens by default, but when you mutate it and deprecation warnings are enabled (-W:deprecated, or Warning[:deprecated] = true) Ruby warns that the string will be frozen in a future version. The Ruby 3.4 NEWS records this as Feature #20350. The chilled string is the behaviour of Ruby 3.4 and 4.0; actually freezing it is a preparation for later, and the development NEWS lists the freezing for the upcoming Ruby 4.1 (4.1 was not released when this page was written, so confirm the final behaviour against its NEWS).

# frozen_string_literal: true

label = :lent.to_s
label << " out"
p label
"lent out"
chilled.rb:4: warning: string returned by :lent.to_s will be frozen in the future

The fix is to call name when a read-only string is enough, or to make an explicit copy with String.new(symbol.name) when a mutable one is needed. Code that calls to_s and then mutates the result is the pattern that will fail once the string is frozen, so it is worth rewriting now.

Symbol#to_proc and &:name

symbol.to_proc returns a proc that, called with an object, sends the symbol’s method to that object, passing the remaining arguments on. The & operator in an argument list converts its operand with to_proc, so list.map(&:title) is the idiom for list.map { it.title }: the symbol becomes a block. The result is a lambda since Ruby 3.0 (Feature #16260), which means it checks its arity: calling it with no argument raises ArgumentError with no receiver given, and its arity is -2 (one required receiver and any number of extra arguments).

# frozen_string_literal: true

require "shelf"

titles = %w[Dune Emma Ulysses].map { Shelf::Book.new(Shelf::Isbn.new("978-0-306-40615-7"), it, Shelf::Author.new("Anon")) }

# &:name is shorthand for a block that sends the message.
p titles.map(&:title)
p titles.map(&:title).map(&:upcase)

# The proc is a lambda: it checks arity and sends the remaining arguments.
to_s = :to_s.to_proc
p to_s.lambda?, to_s.arity
p to_s.call(255, 16)
begin
  to_s.call
rescue ArgumentError => e
  puts e.message
end

# A symbol can also name an operator for inject/reduce.
p [3, 4, 5].inject(:+), [3, 4, 5].reduce(1, :*)

# A symbol proc calls public methods only.
class Strongbox
  private

  def secret = 42
end
begin
  [Strongbox.new].map(&:secret)
rescue NoMethodError => e
  puts e.message
end
["Dune", "Emma", "Ulysses"]
["DUNE", "EMMA", "ULYSSES"]
true
-2
"ff"
no receiver given
12
60
private method 'secret' called for an instance of Strongbox

Source: Symbol#to_proc

Rules and limits of &:name:

  • It sends exactly one message and passes the block’s arguments through. It cannot chain (&:title.upcase is not valid; use a block { it.title.upcase } or two map calls).

  • It uses a normal method call, so it respects visibility: &:secret for a private method raises NoMethodError (private method 'secret' called for an instance of Strongbox). Use a block with send if you really need private access, and ask whether you should.

  • Refinements are honoured: a refined method can be called with &:name where the refinement is active (since Ruby 2.4 &:name and send see refinements).

  • inject and reduce accept a bare symbol for an operator or method name (inject(:+), reduce(1, :*)), which is the same idea without the &: the symbol is the method to send, and no to_proc conversion happens.

  • Method#to_proc, method(:name) and the composition operators >> and << give the same kind of callable for methods that take an object (see Blocks, Procs and Lambdas).

The idiom entered the core in Ruby 1.8.7 and 1.9, which is why older books describe it as new. It is now ordinary Ruby.

Dynamic symbols and garbage collection

Symbols that appear in source code (literals, method and variable names) exist for as long as the program does. Symbols created at run time by String#to_sym, String#intern or :"#{x}" are dynamic symbols. Before Ruby 2.2 every symbol was immortal, so a program that converted arbitrary input to symbols could exhaust memory, which is the classic symbol denial-of-service. Since Ruby 2.2 most dynamic symbols are garbage collected when nothing refers to them (the 2.2 NEWS: "Most symbols which are returned by String#to_sym and String#intern are GC-able").

# frozen_string_literal: true

before = Symbol.all_symbols.size
20_000.times do |i|
  name = "dynamic_#{i}_#{rand}"
  name.to_sym
end
during = Symbol.all_symbols.size
GC.start
after = Symbol.all_symbols.size

puts "before: #{before}"
puts "during: #{during}"
puts "after:  #{after}"
puts "all 20000 still present: #{during - before >= 20_000}"
before: 3768
during: 5077
after:  3768
all 20000 still present: false

The example creates twenty thousand dynamic symbols, prints the size of the symbol table before, during and after a garbage collection, and shows that most of the new symbols were reclaimed (the table grows by thousands, not by the full twenty thousand, even before the explicit GC.start). The numbers differ between runs and machines.

What this means in practice:

  • Converting external text with to_sym is no longer a guaranteed memory leak, but it is still a poor idea: the symbol table is shared, large and slow to scan, and symbols that are referenced elsewhere (stored in a hash, a constant or a long-lived structure) stay alive. The safe pattern is the allow-list shown later on this page.

  • Symbols that Ruby cannot collect stay for the life of the process: those written in source, and in general those used as the name of a method, constant or variable, which the interpreter keeps registered.

  • Symbol.all_symbols lists every symbol currently in the table. It is for diagnostics, and the size is large (several thousand) in a fresh process because the standard library and the core classes define their names.

  • Ruby 4.0 stores frozen strings and the symbol table in a lock-free hash set (Feature #21268), which makes interning symbols and strings from several threads or Ractors faster. This is an implementation detail with no change to the language.

Symbols as hash keys

Symbol keys are the default for hashes of fields and options. They are fast (lookup by identity), they are written without noise ({ title: "Dune" }), and since Ruby 3.4 the hash is printed in the same way. A symbol key and a string key are different keys even when the name is the same, and a Hash#fetch with the wrong kind of key raises KeyError with the key printed in its literal form (key not found: "title" or key not found: :title).

# frozen_string_literal: true

require "json"

record = { isbn: "978-0-306-40615-7", title: "Dune" }

# A symbol key and a string key are different keys.
p record[:title], record["title"]
begin
  record.fetch("title")
rescue KeyError => e
  puts e.message
end

# Hash#inspect in 3.4+ prints symbol keys as key: value and quotes the rest.
p record
p({ "on loan": 1, "a-b": 2, ok?: 3, if: 4, "c=": 5, Title: 6 })
p({ "title" => "Dune" })

# Converting between the two worlds.
p record.transform_keys(&:to_s)
p({ "isbn" => "978-0-306-40615-7" }.transform_keys(&:to_sym))

# JSON arrives with string keys unless asked.
text = '{"isbn": "978-0-306-40615-7", "title": "Dune"}'
p JSON.parse(text)
p JSON.parse(text, symbolize_names: true)

# Keyword arguments are symbol keys; a double splat accepts string keys too.
def describe(**options) = options
p describe(title: "Dune")
p describe(**{ "title" => "Dune" })
"Dune"
nil
key not found: "title"
{isbn: "978-0-306-40615-7", title: "Dune"}
{"on loan": 1, "a-b": 2, ok?: 3, if: 4, "c=": 5, Title: 6}
{"title" => "Dune"}
{"isbn" => "978-0-306-40615-7", "title" => "Dune"}
{isbn: "978-0-306-40615-7"}
{"isbn" => "978-0-306-40615-7", "title" => "Dune"}
{isbn: "978-0-306-40615-7", title: "Dune"}
{title: "Dune"}
{"title" => "Dune"}

What to take from the example:

  • Hash#inspect in Ruby 3.4 and later prints a key as name: value when the symbol is a simple name that can be used as a label, and as "on loan": 1 when it needs quotes. Operator-like or setter-like symbols (:"c=") are quoted; keywords (if:) and predicate names (ok?:) are not. The old {:name=>"Ico"} form of older books is gone from inspect, but the => form is still valid code, and it is required for non-symbol keys ({ "title" => "Dune" }).

  • transform_keys(&:to_s) and transform_keys(&:to_sym) convert a whole hash, which is the usual way to cross the boundary between program data and JSON, YAML or request parameters. The JSON library does the same with JSON.parse(text, symbolize_names: true).

  • The keys of a keyword argument list are symbols, and the double splat collects them into a hash: def describe(**options) receives {title: "Dune"}. A method that accepts **options also accepts a hash with string keys (since Ruby 2.7, Feature #14183), which describe(**{ "title" => "Dune" }) shows; a method with named keywords does not.

The hash-key rules are developed further on Hashes, including fetch, defaults and compare_by_identity.

Symbols for states and options

A symbol is the usual way to name one value out of a small fixed set. The shelf gem’s Loan keeps its states as symbols in a frozen array (STATES = %i[available lent overdue returned].freeze), validates them in the constructor and drives describe with case/in. This is the pattern: define the vocabulary once in a constant, reject anything else at the boundary, and use the symbols freely inside.

# frozen_string_literal: true

require "shelf"

dune = Shelf::Book.new(Shelf::Isbn.new("978-0-306-40615-7"), "Dune", Shelf::Author.new("Frank Herbert"))

p Shelf::Loan::STATES
loan = Shelf::Loan.new(dune, state: :lent)
puts loan.describe
p loan.state == :lent, loan.state.equal?(:lent)

begin
  Shelf::Loan.new(dune, state: :lost)
rescue ArgumentError => e
  puts e.message
end

# A misspelled symbol is a different, valid symbol; only validation catches it.
p Shelf::Loan::STATES.include?(:overdue), Shelf::Loan::STATES.include?(:overdu)

# Untrusted text becomes a state only through an allow-list.
def parse_state(text) = Shelf::Loan::STATES.find { it.name == text }
p parse_state("lent"), parse_state("whatever")
[:available, :lent, :overdue, :returned]
Dune is lent
true
true
unknown state: lost
true
false
:lent
nil

Source: Symbol

Points to keep in mind:

  • Ruby does not check a symbol against any set. A typo such as :overdu is a perfectly valid, different symbol, so a state machine needs the validation in the constructor (as above) or a comparison that fails loudly, such as a case/in with no else, which raises NoMatchingPatternError for an unknown value.

  • Text from outside becomes a symbol through the allow-list: STATES.find { it.name == text } returns the symbol or nil. It never calls to_sym on the input, so no new symbol is created. If the input must reach to_sym (for example when building keys), validate it first.

  • Symbols are a lightweight alternative to an enum type. When the states need behaviour (a label, a next state, an ordering) a small class, a Module with constants or a Data object per state is more explicit; see Struct, Data and Value Objects.

  • Constants can name the symbols if you want the editor and RuboCop to catch typos (LENT = :lent), at the cost of some noise. Most Ruby code uses the bare symbols and a test.

Symbols and reflection

The reflective methods of the language speak in symbols: a method name, an instance variable name and a constant name are symbols, and the methods that look something up by name accept symbols (and strings, which they convert). This is why symbols appear everywhere in metaprogramming.

# frozen_string_literal: true

require "shelf"

class Reader
  def initialize(name) = @name = name

  def greet = "Hello, #{@name}"
  attr_accessor :favourite
end

reader = Reader.new("Ada")
p reader.instance_variables
p Reader.instance_methods(false).sort
message = :greet
p reader.public_send(message)
p reader.respond_to?(:favourite=)
reader.send(:favourite=, "Dune")
p reader.instance_variable_get(:@favourite)

Reader.define_method(:shout) { greet.upcase }
puts reader.shout
p reader.method(:shout).name, Shelf.constants.sort.first(3)
p Shelf::Loan.const_get(:STATES).size
p local_variables, global_variables.include?(:$stdout)
[:@name]
[:favourite, :favourite=, :greet]
"Hello, Ada"
true
"Dune"
HELLO, ADA
:shout
[:Author, :Book, :Catalog]
4
[:reader, :message]
true

Source: Object#send

  • instance_variables, instance_methods, methods, constants, local_variables, global_variables and class_variables return arrays of symbols; instance variables are listed with their @ (:@name).

  • send, public_send, respond_to?, method, define_method, attr_accessor, instance_variable_get, const_get and Module#alias_method accept a symbol or a string and treat them the same way.

  • public_send(:name) with a constant symbol is the same as obj.name, which RuboCop’s Style/SendWithLiteralMethodName points out. Use send and public_send when the name is a variable, and prefer public_send to keep visibility.

  • A setter is the symbol :name=; calling it by name needs the equals sign (send(:favourite=, "Dune")).

The full tour of the reflection methods is on Reflection, Introspection and Tracing.

The methods of Symbol

Symbol is deliberately small. The core documentation groups its methods into querying, comparing and converting; all of them leave the receiver unchanged, and most simply delegate to the string of the name and return a symbol or a plain value:

Group Methods

Querying

length/size, empty?, []/slice, start_with?, end_with?, match, match?, =~, encoding; the class method Symbol.all_symbols

Comparing

<=>, ==, ===, casecmp, casecmp?; the operators from Comparable (<, >, between?, clamp)

Converting

to_s/id2name, name, to_sym/intern, to_proc, inspect, upcase, downcase, capitalize, swapcase, succ/next

# frozen_string_literal: true

lent = :lent
same = :lent
p :available <=> lent, lent <=> same, lent <=> "lent"
p lent == "lent", lent.eql?(same), lent.hash == same.hash
p %i[returned available lent].sort, %i[returned available lent].max
p :lent.length, :lent[0], :lent[1..], :lent.start_with?("le"), :lent.end_with?("nt")
p :Lent.downcase, :lent.upcase, :lent.capitalize, :lent.swapcase, :az.succ
p :lent.casecmp(:LENT), :lent.casecmp?(:LENT), :lent.empty?, :"".empty?
p :lent.match?(/en/), :lent =~ /n/, :lent.match(/e(n)/)[1]
begin
  :lent + :out
rescue NoMethodError => e
  puts e.message
end
p Symbol.ancestors.first(3)
-1
0
nil
false
true
true
[:available, :lent, :returned]
:returned
4
"l"
"ent"
true
true
:lent
:LENT
:Lent
:LENT
:ba
0
true
false
true
true
2
"n"
undefined method '+' for an instance of Symbol
[Symbol, Comparable, Object]

Notes on the results:

  • :a <=> "a" is nil, not 0, because the other operand is not a symbol, and == with a string is false.

  • The case methods (upcase, downcase, capitalize, swapcase) and succ return symbols, so the chain stays symbolic: :az.succ is :ba.

  • casecmp and casecmp? compare symbol to symbol only; with a string they return nil. The Comparable operators on a symbol and a string raise ArgumentError (comparison of Symbol with String failed).

  • There is no + or *, no << and no sub; use to_s and rebuild, or a quoted symbol with interpolation.

  • match and =~ set the match globals ($~, $1) as the string versions do; match? does not. They take a Regexp (a string argument raises TypeError in match?).

  • start_with? and end_with? were added to Symbol in Ruby 2.7 (Feature #16348).

Symbols and immutability

A symbol is frozen, dup and clone return the symbol itself, and Ractor.shareable? is true, whereas a mutable string is not shareable. A symbol is therefore safe to store in a constant, share between threads and Ractors, and use as a key without defensive copies, which is one more reason to use it for names.

# frozen_string_literal: true

# Symbols are frozen, so they can be shared between Ractors.
p Ractor.shareable?(:available), Ractor.shareable?("available".dup)

# dup and clone return the same object.
p :lent.dup.equal?(:lent), :lent.clone.equal?(:lent)

# Compare an unfrozen string and a symbol by converting once.
state = String.new("lent")
p state == :lent, state.to_sym == :lent, :lent.name == state
true
false
true
true
false
true
true

Source: Ractor

Where older books and tutorials are outdated

Topic Older material Ruby 4.0

Hash display

{:name=>"Ico"} and {:a=>1} in p output

{name: "Ico"} and {a: 1} since Ruby 3.4; the => form is still valid input, and "my key": 1 is printed for keys that need quotes

Symbol#to_proc

Introduced as "new in 1.9", with a mention of 1.8.7 back-ports; the result is a plain proc

Ordinary Ruby since 1.8.7, and the proc is a lambda since 3.0 (checks arity)

Symbols never freed

Every symbol lives forever, so to_sym on user input is a memory leak

Most dynamic symbols are garbage collected since Ruby 2.2; still use an allow-list for untrusted input

Symbol#to_s

Returns a fresh mutable string

Same today, but chilled since Ruby 3.4 (a warning when mutated under -W:deprecated), and announced to become frozen; use name for read-only text

Symbol#name

Missing

Added in Ruby 3.0 and returns the shared frozen string

Error messages

undefined method `x' for :a:Symbol

undefined method 'x' for an instance of Symbol (3.3 receiver wording, 3.4 apostrophes)

Keyword arguments

**opts only accepts symbol keys, and mixed hashes are split

Non-symbol keys are accepted by **opts since 2.7; separation of positional and keyword arguments since 3.0

Common exceptions and warnings

  • NoMethodError for undefined method 'new' for class Symbol: symbols have no constructor; use a literal or String#to_sym.

  • NoMethodError for a method that does not exist on Symbol (undefined method '+' for an instance of Symbol): symbols are not strings; call to_s or name first.

  • FrozenError (can't modify frozen Symbol: :on loan) when something tries to set an instance variable on a symbol.

  • KeyError from Hash#fetch with key not found: "title" when the key is a string but the hash has symbol keys (or the reverse).

  • ArgumentError (no receiver given) when a symbol proc is called with no argument.

  • NoMethodError (private method 'secret' called for an instance of Strongbox) from &:secret.

  • The deprecation warning string returned by :lent.to_s will be frozen in the future, shown only with -W:deprecated (or Warning[:deprecated] = true), when a string from Symbol#to_s is mutated.

Pitfalls

  • String key against symbol key. params["title"] on a hash with symbol keys is nil, silently. Pick one kind per hash, convert at the boundary with transform_keys, and use fetch where a missing key is a bug.

  • Calling to_sym on user input. It creates symbols named by the attacker. Garbage collection reduces the damage, it does not remove the need to validate; use an allow-list.

  • Mutating the result of to_s. It works today and warns under -W:deprecated; use name, or copy with String.new on purpose.

  • Symbols with spaces or punctuation. They need the quoted form, and a %i[] list cannot hold them (the elements are split on whitespace). Use %i[a b] only for simple names.

  • Using symbols as text. "Hello, #{name}" works with a symbol (to_s is called), but formatting symbols in user-visible output exposes internal names; translate them to strings at the edge.

  • Storing unbounded names as symbols. Even with garbage collection, a symbol referenced from a long-lived hash stays alive. Keys that come from data (identifiers, file names, user names) belong in strings.

  • Expecting :a.to_proc to chain. &:name sends one message; a nested call needs a block.

Version notes

Version Change

1.8.7 / 1.9

Symbol#to_proc in the core.

2.2

Dynamic symbols are garbage collected; "key": value quoted labels.

2.7

Symbol#start_with? and end_with?; non-symbol keys accepted by **opts.

3.0

Symbol#to_proc returns a lambda; Symbol#name added.

3.4

Hash#inspect prints key: value; the string from Symbol#to_s is chilled.

4.0

Frozen strings and the symbol table use a lock-free hash set (Feature #21268).

4.1 (preview)

The string from Symbol#to_s is planned to be frozen; not released when this page was written.

Summary

  • A symbol is an interned, frozen name: one object per name, compared by identity, created by a literal or String#to_sym, never by new.

  • Use symbols for vocabularies you control (states, options, keys, method names) and strings for text and data from outside.

  • A symbol and a string with the same characters are never equal and never the same hash key; convert at the boundary with to_sym, to_s or transform_keys.

  • Use name for read-only text and to_s only when you will not mutate it; the string from to_s is chilled since 3.4.

  • &:name is a lambda that sends one message; it respects visibility and refinements.

  • Dynamic symbols are garbage collected since 2.2, but still validate untrusted text with an allow-list before to_sym.

  • Hashes print symbol keys as key: value since 3.4, quoting those that need it.

References