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 |
Immutable |
A symbol is frozen: |
No constructor |
|
Identifier-like |
The class is designed for names. Its methods are a read-only subset of the |
Comparable |
|
Shareable |
A symbol can be used from any |
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 |
|
|
Identity |
A new object per evaluation, unless frozen and deduplicated |
One object per name |
Mutable |
Yes (except a frozen string or literal in a |
No, always frozen |
Methods |
Many, including the mutating ones ( |
A short list, none of them mutating |
Typical use |
Text, user input, output, data |
Names, keys, states, options, method names |
Conversion to the other |
|
|
Encoding |
Any |
US-ASCII for ASCII names, otherwise the encoding of the source string |
Equality with the other |
|
|
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,:@@cvarand:$gvarfor 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 (%Iinterpolates). RuboCop prefers%i[]for arrays of plain symbols (Style/SymbolArray). -
key: valueand"key": valueinside 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>
Source: Literals: Symbol Literals
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 |
|---|---|
|
A |
|
The frozen, shared |
|
The symbol itself (the aliases exist for symmetry with |
|
The symbol with that name, created if it does not exist yet. |
|
The literal form including the colon ( |
|
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
Source: NEWS for Ruby 3.4.0
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.upcaseis not valid; use a block{ it.title.upcase }or twomapcalls). -
It uses a normal method call, so it respects visibility:
&:secretfor a private method raisesNoMethodError(private method 'secret' called for an instance of Strongbox). Use a block withsendif you really need private access, and ask whether you should. -
Refinements are honoured: a refined method can be called with
&:namewhere the refinement is active (since Ruby 2.4&:nameandsendsee refinements). -
injectandreduceaccept 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 noto_procconversion 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
Source: Symbol.all_symbols
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_symis 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_symbolslists 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"}
Source: Hash#transform_keys
What to take from the example:
-
Hash#inspectin Ruby 3.4 and later prints a key asname: valuewhen the symbol is a simple name that can be used as a label, and as"on loan": 1when 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 frominspect, but the=>form is still valid code, and it is required for non-symbol keys ({ "title" => "Dune" }). -
transform_keys(&:to_s)andtransform_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 withJSON.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**optionsalso accepts a hash with string keys (since Ruby 2.7, Feature #14183), whichdescribe(**{ "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
:overduis 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 acase/inwith noelse, which raisesNoMatchingPatternErrorfor an unknown value. -
Text from outside becomes a symbol through the allow-list:
STATES.find { it.name == text }returns the symbol ornil. It never callsto_symon the input, so no new symbol is created. If the input must reachto_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
Modulewith constants or aDataobject 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_variablesandclass_variablesreturn arrays of symbols; instance variables are listed with their@(:@name). -
send,public_send,respond_to?,method,define_method,attr_accessor,instance_variable_get,const_getandModule#alias_methodaccept a symbol or a string and treat them the same way. -
public_send(:name)with a constant symbol is the same asobj.name, which RuboCop’sStyle/SendWithLiteralMethodNamepoints out. Usesendandpublic_sendwhen the name is a variable, and preferpublic_sendto 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 |
|
Comparing |
|
Converting |
|
# 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]
Source: Symbol comparison methods
Notes on the results:
-
:a <=> "a"isnil, not0, because the other operand is not a symbol, and==with a string isfalse. -
The case methods (
upcase,downcase,capitalize,swapcase) andsuccreturn symbols, so the chain stays symbolic::az.succis:ba. -
casecmpandcasecmp?compare symbol to symbol only; with a string they returnnil. TheComparableoperators on a symbol and a string raiseArgumentError(comparison of Symbol with String failed). -
There is no
+or*, no<<and nosub; useto_sand rebuild, or a quoted symbol with interpolation. -
matchand=~set the match globals ($~,$1) as the string versions do;match?does not. They take aRegexp(a string argument raisesTypeErrorinmatch?). -
start_with?andend_with?were added toSymbolin 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 |
|
|
|
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 |
Most dynamic symbols are garbage collected since Ruby 2.2; still use an allow-list for untrusted input |
|
Returns a fresh mutable string |
Same today, but chilled since Ruby 3.4 (a warning when mutated under |
|
Missing |
Added in Ruby 3.0 and returns the shared frozen string |
Error messages |
|
|
Keyword arguments |
|
Non-symbol keys are accepted by |
Common exceptions and warnings
-
NoMethodErrorforundefined method 'new' for class Symbol: symbols have no constructor; use a literal orString#to_sym. -
NoMethodErrorfor a method that does not exist onSymbol(undefined method '+' for an instance of Symbol): symbols are not strings; callto_sornamefirst. -
FrozenError(can't modify frozen Symbol: :on loan) when something tries to set an instance variable on a symbol. -
KeyErrorfromHash#fetchwithkey 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(orWarning[:deprecated] = true), when a string fromSymbol#to_sis mutated.
Pitfalls
-
String key against symbol key.
params["title"]on a hash with symbol keys isnil, silently. Pick one kind per hash, convert at the boundary withtransform_keys, and usefetchwhere a missing key is a bug. -
Calling
to_symon 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; usename, or copy withString.newon 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_sis 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_procto chain.&:namesends one message; a nested call needs a block.
Version notes
| Version | Change |
|---|---|
1.8.7 / 1.9 |
|
2.2 |
Dynamic symbols are garbage collected; |
2.7 |
|
3.0 |
|
3.4 |
|
4.0 |
Frozen strings and the symbol table use a lock-free hash set (Feature #21268). |
4.1 (preview) |
The string from |
Summary
-
A symbol is an interned, frozen name: one object per name, compared by identity, created by a literal or
String#to_sym, never bynew. -
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_sortransform_keys. -
Use
namefor read-only text andto_sonly when you will not mutate it; the string fromto_sis chilled since 3.4. -
&:nameis 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: valuesince 3.4, quoting those that need it.
References
-
Symbol (Ruby 4.0 core documentation)
-
Literals (Ruby 4.0 syntax guide)
-
Comparable, Kernel and Ractor (Ruby 4.0 core documentation)
-
GC (garbage collection)
-
NEWS for Ruby 2.2.0 (garbage-collected symbols)
-
NEWS for Ruby 3.0.0 (
Symbol#to_proclambda,Symbol#name) -
NEWS for Ruby 3.4.0 (chilled
Symbol#to_s,Hash#inspect) -
NEWS for Ruby 4.0.0 (symbol table lock-free hash set)