Every time Ruby developers start talking about # frozen_string_literal: true, someone inevitably brings up symbols. It happens so often it’s almost a ritual. Many people assume symbols are just frozen strings wearing a different outfit, but that’s not actually true. Let’s unpack why this confusion exists and what really sets them apart.
What Symbols Really Are
A Symbol in Ruby is an immutable identifier.
It’s stored once in Ruby’s symbol table, and every reference to it points to the same object:
:status.object_id == :status.object_id # => true
Ruby uses symbols internally for method names, variable names, and hash keys.
They’re meant to represent identity, not text.
If you think of symbols as labels that are fast to compare and cheap to store, you’ll use them correctly.
What Frozen Strings Really Are
A frozen string is still a String object, it just can’t be modified:
s = "ready".freeze
s << "set" # => FrozenError: can't modify frozen String
Freezing prevents accidental mutation and can improve performance by reusing literal objects.
However, whether two identical string literals point to the same object depends on whether frozen string literals are enabled.
Without the # frozen_string_literal: true magic comment, each literal creates a new string:
"ready".object_id == "ready".object_id # => false
With frozen string literals enabled, Ruby automatically interns identical literals:
# frozen_string_literal: true
"ready".object_id == "ready".object_id # => true
This interning only applies to string literals known at parse time. Strings created dynamically and then frozen are still separate objects and are not globally interned like symbols.
That means Ruby reuses the same string object only for literal values, not for dynamically generated ones.
Why People Confuse Them
Both symbols and frozen strings are immutable and can be reused in memory.
That’s where the confusion starts, but it’s only surface-level.
There are two key differences.
1. The semantic difference
Symbols are meant to represent names or identifiers in your program such as method names, variable names, and event types.
Strings, even when frozen, represent text content that is meant to be read, displayed, or stored.
2. The implementation difference
Both symbols and frozen strings can be interned, meaning Ruby keeps a single copy in memory for identical values.
However, all symbols are always interned, while only some strings are (those made literal under # frozen_string_literal: true or explicitly interned).
This subtle difference means even if two strings have the same content, one could be interned and another not:
a = "foo".freeze
b = String.new("foo")
a == b # => true (same content)
a.object_id == b.object_id # => false (different objects)
Symbols don’t have that split personality because there’s always exactly one :foo object in memory.
A Note on Hashing and Performance
Because a string can exist both as an interned and a non-interned version, Ruby’s String#hash must compute the hash based on its content, which takes O(n) time.
A symbol, however, has a unique internal ID, so Symbol#hash can be computed in constant time, O(1).
This ensures that two strings with the same content, even if one is interned and the other is not, always produce the same hash value.
That’s part of why symbols make efficient hash keys for things like configuration maps or method lookups.
Why It Matters
They may look the same, but they’re not interchangeable:
:status == "status" # => false
Freezing a string doesn’t make it a symbol, it just prevents mutation.
If you need a key, event name, or label, use a symbol.
If you’re handling user input or text data, use a string.
Symbols are safe for internal identifiers.
Strings are safe for external, unpredictable data.
Memory Notes
Before Ruby 2.2, symbols were never garbage-collected, which made dynamic symbol creation dangerous.
From Ruby 2.2 onward, runtime symbols are GC-safe, but the convention to avoid symbolizing user input still stands for clarity, safety, and backward compatibility.
Frozen strings, on the other hand, are always garbage-collected normally.
The Takeaway
Symbols represent identity.
Frozen strings represent content.
Both are immutable and both can be reused in memory, but their intent and behavior are different.
Ruby’s frozen string literal feature was about safer, faster strings, not about turning them into symbols.
So next time someone says “A frozen string is basically a symbol,” remember:
Not every frozen thing is a symbol.
Top comments (1)
Big thanks to Jean Boussier for reviewing this and pointing out key details about symbol interning and hashing. His feedback helped make this post more accurate and much clearer.