Symbols

Published by Programming Line in Ruby. Last updated .

A symbol in Ruby is a lightweight, immutable identifier that looks like :name — a colon followed by a name. Symbols exist to label things: hash keys, method names, states, options — anywhere you need a fixed, meaningful name rather than user-facing text. Unlike a string, which is a fresh, mutable object every time you write it, a symbol with a given name is always the exact same object in memory, which makes symbols fast to compare and cheap to use as identifiers throughout your code.

Overview: What Is a Symbol?

Everything in Ruby is an object, including symbols. A symbol is an instance of the Symbol class, and it represents a name or a label. You’ve almost certainly used one already: every hash you write with the key: value shorthand, like { name: "Ada" }, is actually using the symbol :name as its key, not the string "name".

What makes symbols different from strings comes down to two things: interning and immutability. Ruby keeps a single internal table of symbols. The first time the interpreter sees :name, it creates one Symbol object and stores it in that table. Every subsequent appearance of :name anywhere in your program — even in a completely different file or method — refers to that same object. Strings work the opposite way: every string literal you write creates a brand-new String object, even if two literals contain identical text. That’s why "name".object_id == "name".object_id is false, but :name.object_id == :name.object_id is always true.

Symbols are also always frozen — you cannot append to one, change its case in place, or otherwise mutate it. There is no Symbol#upcase! or Symbol#<<. If you need to transform the text a symbol represents, you convert it to a string first with to_s, do your work, and convert back with to_sym (or its alias intern) if you need a symbol again.

This combination — one shared object per name, and that object never changing — is exactly why symbols are the idiomatic choice for hash keys and internal identifiers. Comparing two symbols for equality is just an object-identity check (fast, constant time), while comparing two strings has to walk through and compare every character. It’s also why method names, keyword argument names, and instance variable names are represented internally as symbols: Ruby’s method dispatch (the mechanism that also powers send, define_method, and method_missing) works by looking up a symbol in a table on the object’s class, not by comparing strings.

One historical wrinkle worth knowing: in old Ruby versions (before 2.2), symbols were never garbage collected, so repeatedly calling to_sym on unpredictable, attacker-controlled strings (like raw user input) could leak memory forever — a real denial-of-service vector. Since Ruby 2.2, dynamically created symbols are eligible for garbage collection, which removed most of that danger. Even so, it remains good practice to reserve symbols for identifiers you control (option names, state names, hash keys) and use strings for arbitrary text that comes from users or the outside world.

Syntax

:identifier
:"identifier with spaces or symbols"
:"#{interpolated}"
%i[red green blue]
"text".to_sym
some_symbol.to_s
Form What it does
:name The basic symbol literal — a colon plus a valid identifier.
:"multi word" Quoted form, required when the name has spaces, dashes, or other characters not valid in a bare identifier.
:"#{expr}" A dynamically built symbol; the interpolated part is evaluated first, then converted to a symbol.
%i[a b c] Shorthand array literal for an array of symbols — equivalent to [:a, :b, :c].
"text".to_sym / .intern Converts a string to a symbol.
:sym.to_s Converts a symbol to a string (without the leading colon).

Examples

Example 1: Symbols Are Interned, Strings Are Not

s1 = :name
s2 = :name
str1 = "name"
str2 = "name"

puts s1 == s2
puts s1.object_id == s2.object_id
puts str1 == str2
puts str1.object_id == str2.object_id
puts s1.class
puts s1.frozen?

Output:

true
true
true
false
Symbol
true

Both == comparisons return true because both pairs are equal in content. But the object_id comparisons reveal the real difference: s1 and s2 are literally the same object in memory (Ruby interned :name once), while str1 and str2 are two distinct String objects that just happen to hold the same characters. The last line confirms every symbol is frozen — you get immutability for free.

Example 2: Symbols as Hash Keys

person = { name: "Ada", age: 36, language: :ruby }

puts person[:name]
puts person[:age]
puts person[:language]

person.each do |key, value|
  puts "#{key.inspect} => #{value.inspect}"
end

puts person.keys.map(&:class).uniq

Output:

Ada
36
ruby
:name => "Ada"
:age => 36
:language => :ruby
Symbol

The { name: "Ada" } shorthand is sugar for { :name => "Ada" } — the keys are symbols. Iterating with each preserves insertion order (a guarantee Ruby hashes have made since 1.9), and inspect shows a symbol with its leading colon, while a string shows with quotes. The final line confirms every key in this hash is a Symbol, not a String.

Example 3: Symbols for State and Control Flow

def traffic_light_message(state)
  case state
  when :red
    "Stop"
  when :yellow
    "Caution"
  when :green
    "Go"
  else
    "Unknown signal"
  end
end

[:red, :yellow, :green, :blue].each do |light|
  puts "#{light}: #{traffic_light_message(light)}"
end

Output:

red: Stop
yellow: Caution
green: Go
blue: Unknown signal

This is the classic use case for symbols: representing a small, fixed set of named states. A case/when matches each symbol against state using ===, which for symbols behaves like ==. Notice "#{light}" prints red rather than :red — string interpolation calls to_s, which strips the leading colon.

How It Works Step by Step

When the Ruby parser encounters a symbol literal like :name, here’s what happens:

  1. Ruby checks its global symbol table for an entry named name.
  2. If no such symbol exists yet, it creates one Symbol object and stores it in the table.
  3. If the symbol already exists (because it appeared earlier — anywhere in the program), Ruby returns a reference to that exact same object instead of creating a new one.
  4. The expression :name now evaluates to that shared, frozen object.

This is why equality checks between symbols are effectively instant: comparing :name == :name only has to check “are these the same object?” (a pointer comparison), never “do these characters match, one by one?” the way two strings must. It’s also the mechanism behind method dispatch: when you call obj.some_method, Ruby converts some_method to a symbol and looks it up in a method table on obj‘s singleton class, then its class, then any included modules, then up the superclass chain — the same symbol-table lookup strategy, just applied to methods instead of hash keys.

Common Mistakes

Mistake 1: Assuming String Keys Work on a Symbol-Keyed Hash

Symbols and strings are never == to each other, so a hash built with symbol keys will not respond to string lookups (or vice versa).

config = { host: "localhost", port: 3000 }
puts config["host"]

Output:

(blank line)

This prints a blank line, because config["host"] doesn’t find a matching key and returns nil, and puts nil just prints an empty line. The hash’s keys are the symbols :host and :port, not the strings "host" and "port". Fix it by using the same type you stored:

config = { host: "localhost", port: 3000 }
puts config[:host]

Output:

localhost

Mistake 2: Comparing a Symbol to a String and Expecting a Match

status = :active
if status == "active"
  puts "User is active"
else
  puts "User is not active"
end

Output:

User is not active

Even though :active and "active" “mean” the same thing to a human, they are different classes and never equal in Ruby. Either compare symbol to symbol, or explicitly convert one side:

status = :active
if status == :active
  puts "User is active"
else
  puts "User is not active"
end

Output:

User is active

Mistake 3: Trying to Mutate a Symbol

Symbols are always frozen, so bang methods that mutate strings in place, like upcase!, don’t exist on Symbol.

name = :hello
puts "Before: #{name}"
name.upcase!
puts "After: #{name}"

Output (before it crashes):

Before: hello

The script prints Before: hello and then raises NoMethodError: undefined method 'upcase!' for :hello:Symbol, so the second puts never runs. If you need an uppercased version, work with a string and convert back if you need a symbol again:

name = :hello
upcased = name.to_s.upcase
puts upcased
puts upcased.to_sym.inspect

Output:

HELLO
:HELLO

Best Practices

  • Use symbols for internal identifiers you control: hash keys, option names, state names, and method names. Use strings for text that’s user-facing, comes from external input, or needs to be manipulated character by character.
  • Prefer keyword arguments (which are symbol-keyed under the hood) for methods that take several parameters — it makes call sites self-documenting: create_user(name: "Ada", age: 36) reads better than three positional arguments.
  • Use %i[a b c] instead of [:a, :b, :c] when writing an array of plain symbol literals — it’s shorter and just as clear.
  • Use &:method_name to turn a symbol into a block shorthand, e.g. names.map(&:upcase) instead of names.map { |n| n.upcase }, when the block just calls one method.
  • Don’t try to mutate a symbol — convert to a string with to_s, transform it, and convert back with to_sym if needed.
  • Avoid calling to_sym in a loop over large amounts of unbounded, unpredictable text (like raw user input); reach for symbols when the set of possible names is small and known ahead of time.

Practice Exercises

  • Write a method http_status_name(code) that takes a symbol like :ok, :not_found, or :server_error and returns a human-readable string (“OK”, “Not Found”, “Server Error”), with a default of “Unknown Status” for anything else. Use case/when.
  • Given colors = ["red", "green", "blue"], write one line of code that converts it into an array of symbols using map and to_sym. Expected result: [:red, :green, :blue].
  • Given a hash settings = { theme: "dark", font_size: 14 }, write code that prints each key and value on its own line in the format theme is dark and font_size is 14, using each and string interpolation.

Summary

  • A symbol (:name) is an immutable, interned label — the same symbol name always refers to the exact same object in memory.
  • Symbols differ from strings: two equal string literals are different objects, but two equal symbols are the same object, which makes symbol equality checks fast.
  • Every symbol is frozen; there are no bang (mutating) methods on Symbol — convert to a string with to_s if you need to transform the text.
  • The key: value hash shorthand and keyword arguments both use symbols under the hood, which is why symbols are the idiomatic choice for hash keys and option names.
  • Symbols and strings are never == to each other — mixing them up in comparisons or hash lookups is a common source of silent bugs.
  • Since Ruby 2.2, dynamically created symbols are garbage collected, but it’s still best practice to reserve symbols for known, bounded sets of identifiers rather than arbitrary user text.