skip to content

Symbols & Interning

A symbol is an immutable, interned name: every :name is the same object, unlike equal strings. Interviewers ask how symbols differ from strings and whether they can leak memory.

on this pageshow

explore

questions

3

In Ruby, how does a Symbol such as :delivered differ from the String "delivered" in identity, mutability and typical use?

level: juniorimportance: must knowfreq 80%

answer

  1. one object per name
  2. same object_id on every use
  3. frozen, no mutating methods
  4. identifiers versus text
  5. to_sym and to_s convert

basics

~20 s

A Symbol is an immutable, interned name: every :delivered is the same object with one object_id, while each evaluation of the literal "delivered" builds a new String. Use symbols for identifiers and fixed flags, strings for text.

solid answer

~40 s

A `Symbol` represents a name inside the interpreter. Ruby keeps one object per name, so `:delivered.object_id` is identical everywhere in the process, and `Symbol#==` is documented as an identity check - cheap however long the name is. Symbols are frozen and have no mutating methods: `:delivered << "!"` raises `NoMethodError`, and `upcase` returns a new symbol. A `String` is text: each evaluation of `"delivered"` (without the frozen-string-literal magic comment) creates a new object, and strings compare by content. Ruby itself uses symbols for method, constant and variable names, and they suit fixed flags such as a parcel's `:pending`, `:in_transit` and `:delivered` status. A symbol never equals a string: `:delivered == "delivered"` is `false`, so convert explicitly with `to_s` or `to_sym`.

code

ruby · 11 lines
ruby
status = :in_transit

:delivered.object_id == :delivered.object_id    # => true
"delivered".object_id == "delivered".object_id  # => false

:delivered == "delivered"     # => false
:delivered.to_s == "delivered" # => true
:delivered.frozen?            # => true
:delivered.upcase             # => :DELIVERED

:delivered << "!"             # NoMethodError: Symbol has no <<

go deeper

for a junior

Know that a symbol is an immutable name with one object per name, that it never equals a string, and when to pick each type.

for a middle

Explain interning and why Symbol#== is an identity check, and show that Ruby reports method and variable names as symbols.

for a senior

Review code for symbol and string mix-ups at boundaries, such as statuses arriving as strings from JSON or a database and compared to symbols.

for a principal

Set conventions for where names become symbols, validated at the boundary, and where data stays a string, so APIs are consistent across a codebase.

## What a Symbol is The Ruby documentation defines a **`Symbol`** as an object that "represents a named identifier inside the Ruby interpreter", and contrasts it with a **`String`**, which "represents text or data". That distinction drives everything else: - A symbol is a **name**. It has no mutable contents and exists to be compared and looked up. - A string is a **value**. It can be built, edited, sliced and encoded, and two strings with the same characters are still two objects. ## One object per name Ruby **interns** symbols: for a given name there is exactly one `Symbol` object for the whole run of the program, whatever the name means in each context. The documentation's own example shows `:Fred` having the same `object_id` whether `Fred` is a class in one module, a constant in another and a method at the top level. ```ruby :delivered.object_id == :delivered.object_id # => true "delivered".object_id == "delivered".object_id # => false ``` The second line is `false` because each evaluation of a string literal creates a new `String` (unless the file uses the `frozen_string_literal` magic comment, a separate topic). Because a symbol is unique per name, **`Symbol#==` is an identity check**, documented as returning `true` only if the other object "is the same object". Comparing two symbols costs the same whether the name has 3 characters or 300. ## Immutability Symbols are **frozen** and the class defines no mutating methods: 1. `:delivered.frozen?` is `true`. 2. `:delivered << "!"` raises `NoMethodError` - `Symbol` has no `<<` at all. 3. Case and slicing methods exist but return **new values**: `:delivered.upcase` is `:DELIVERED`, `:delivered[0, 3]` is the String `"del"`. 4. `:delivered.dup` returns the very same symbol; there is nothing to copy. `Symbol` also includes `Comparable`, so `:a < :b` works and arrays of symbols sort by name. ## Where Ruby itself uses symbols Every name the interpreter manages comes back as a symbol: - `Module#constants`, `instance_methods`, `instance_variables`, `class_variables`, `global_variables` and `local_variables` return arrays of symbols such as `[:@status]` or `[:$stdout]`. - Method names given to `define_method`, `attr_reader`, `private` and similar calls are symbols. - Keyword argument names are symbols. That is the natural home for symbols in application code too: **names chosen by the programmer**, not text chosen by users. ## Choosing between them on a delivery record | Value on a parcel record | Type | Why | |---|---|---| | status: pending, in transit, delivered, returned | `Symbol` | a small, fixed set of names defined in code | | recipient name, delivery note, address | `String` | free text supplied by people | | carrier tracking code from an API | `String` | external data, unbounded set of values | | an attribute name used for sorting or reporting | `Symbol` | an identifier referring to code | A status stored in a database or received as JSON arrives as a **String**. Converting it with `to_sym` at the boundary is common, but only for values you have validated against the known set - creating symbols from arbitrary input has its own memory caveats. ## Common confusions - **A symbol is not a string with a colon.** `:delivered == "delivered"` is `false`; compare `status.to_s == "delivered"` or `status == "delivered".to_sym`. - **Interned does not mean faster everywhere.** Symbols make comparison and lookup cheap; they do not make text processing faster, and converting back and forth allocates. - **Symbols are not only for performance.** Their main job is expressing intent: a symbol says "this is one of a known set of names". - **Symbols are not a cache for strings.** Turning every string into a symbol to "save memory" moves data into the symbol table instead.

  • In Ruby, why is comparing two symbols cheap regardless of name length?
    Because Ruby keeps exactly one `Symbol` object per name, `Symbol#==` only needs to check whether both sides are the same object; it never looks at characters. Comparing two strings has to compare their contents, so the cost grows with length. This is one reason Ruby uses symbols internally for method and variable names that are looked up constantly.
  • In Ruby, what does Object#instance_variables return for a parcel object with @status and @weight set?
    It returns an array of symbols, `[:@status, :@weight]`, in the order the variables were assigned. The interpreter reports every name it manages - constants, methods, instance, class, global and local variables - as symbols, which is the clearest illustration that a symbol is a name rather than text.

A symbol is like a numbered locker tag handed out once per name: everyone who asks for tag delivered gets the same tag. A string is a sticky note: writing delivered twice gives two separate notes that merely say the same thing.

saying these in an interview costs you the question

  • Symbols are just strings with a colon in front.
  • :status == "status" is true because Ruby converts one side.
  • Every :pending literal allocates a new Symbol object.
  • Symbols can be mutated in place with << like strings.
  • Symbols are the right type for user-supplied text such as delivery notes.
open as a page

In Ruby 4.0, how do to_sym, Symbol#to_s, Symbol#name and %i[] literals convert between strings and symbols?

level: middleimportance: should knowfreq 45%

basics

~20 s

String#to_sym (alias intern) finds or creates the symbol; Symbol#to_s (alias id2name) returns a new String that warns on mutation since Ruby 3.4; Symbol#name returns a cached frozen String; %i[a b] builds [:a, :b], and %I interpolates.

open as a page

In Ruby 4.0, can calling to_sym on untrusted input such as a delivery status parameter leak memory, and how would you guard against it?

level: seniorimportance: should knowfreq 40%

basics

~20 s

Mostly not since Ruby 2.2: symbols created at runtime by to_sym are garbage-collected once unreferenced. Symbols from source code, and dynamic ones pinned as method names, live for the whole process, so map untrusted input through an allowlist instead.

open as a page