---
title: 'Symbols and Strings'
source: 'https://academia.sh/en/courses/ruby-fundamentals/symbols-and-strings'
course: 'Ruby Fundamentals'
language: en
updated: '2026-10-21T05:03:43+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Shows with equal? that a string turns into a symbol with to_sym and why the identity of the two representations differs'
    - 'Measures with frozen? and FrozenError that the magic comment freezes string literals'
    - 'Shows that the magic comment does not change the responder, and that the comment takes effect per file'
---

# Symbols and Strings

A string turning into a symbol and the identity difference between the two representations; string literals frozen by a magic comment, and the comment taking effect per file; the magic comment not changing the responder.

The previous lesson measured the responder of a receiverless call, and it left a question: when `"name".to_sym` is written, a string turns into a symbol, and a symbol is also an object. Once a string turns into a symbol, who answers the message? And does a magic comment written at the top of a file change that answer? In the JavaScript Fundamentals course (M06/K01), the primitive types and the symbol were measured; in Ruby, a symbol is not a separate primitive but an instance of the `Symbol` class. A string, on the other hand, is a mutable object, and this lesson measures the difference between those two objects.

In M11/K03, the question "who sees this change" was measured. In this lesson, a mutable string is only a subject of observation; a single example is seen in which two names share the same string, but the axis of the lesson is the responder, not sharing.

## From String to Symbol

A symbol is an immutable value that has a name: `:name`. It is obtained from a string with `to_sym`. The file below measures the conversion, the identity of the two representations, and the methods of the symbol itself. `equal?` is used for identity: do two expressions point at the same object?

```ruby
# symbol_conversion.rb — a string turns into a symbol
puts "name".method(:to_sym).owner
p "name".to_sym
puts :name.equal?(:name)
puts "name".equal?("name")
p :name.to_s
p :name.upcase
puts :name.method(:to_s).owner
puts :name.method(:upcase).owner
```

```
String
:name
true
false
"name"
:NAME
Symbol
Symbol
```

`to_sym` is defined in the string's own class, and the return value is printed as `:name`. The real difference is in identity: `:name.equal?(:name)` returned `true`, so a symbol with the same name is always the same object. `"name".equal?("name")` is `false`: two string literals are two separate objects even when their contents are the same. `:name.to_s` produces a string again, and `:name.upcase` produces a new symbol; the `Symbol` class answers both.

**LB15.** In this lesson, a "string literal" is a string written in quotes in the source code. What is measured is whether it produces a new string object on every evaluation. No address or `object_id` is printed; identity is compared only with `equal?`.

## String Operations

A string carries many methods. The file below tries length, upper-casing, slicing, splitting, joining, repeating, and formatting on a string. The identifiers and the printed strings were kept ASCII.

```ruby
# string_operations.rb — string methods
text = "welcome"

puts text.length
puts text.upcase
puts text[0, 3]
p "a,b,c".split(",")
puts text + " world"
puts "ha" * 3
puts "%s-%03d" % ["code", 7]
puts "name".to_sym.class
puts :name == "name"
```

```
7
WELCOME
wel
["a", "b", "c"]
welcome world
hahaha
code-007
Symbol
false
```

`length` gives seven, `upcase` gives the upper-case copy, and `[0, 3]` gives the first three characters. `split` returns an array, `+` joins two strings, `*` repeats, and `%` formats. These operations were called not to change `text` but to produce a new value; `length` returned a number, `split` an array, and the others a new string. The last two lines show the boundary between the two representations: `"name".to_sym` gives a `Symbol`, and `:name == "name"` returns `false`; a symbol and a string that carries the same name are not counted as equal.

## Character and Byte

A string is a sequence of characters, but it is kept in memory as bytes. Turkish letters are a single character each, yet they take more than one byte. Turkish letters are used on purpose in this measurement, because this is exactly its subject.

```ruby
# char_byte.rb — the character count and the byte count are separate things
puts "ig".length
puts "ig".bytesize
puts "ığ".length
puts "ığ".bytesize
```

```
2
2
2
4
```

`"ig"`, made of ASCII letters, is two characters and two bytes. `"ığ"` stays two characters but grows to four bytes, because `ı` and `ğ` take two bytes each. So `length` counts characters and `bytesize` counts bytes; the two diverge in non-ASCII text. The encoding of the symbol was not measured here; this lesson measures the identity of a symbol, not its content.

## Symbol Spellings

There is more than one way to write a symbol. A name that contains a space is written in quotes, the `%i[...]` shortcut is used to write several symbols together, and string interpolation works in a symbol just as in a string.

```ruby
# symbol_spellings.rb — ways to write a symbol
p :"full name"
p %i[alpha beta gamma]
label = "rose"
p :"prefix_#{label}"
p :ant <=> :bee
p %i[gamma alpha beta].sort
```

```
:"full name"
[:alpha, :beta, :gamma]
:prefix_rose
-1
[:alpha, :beta, :gamma]
```

`:"full name"` is the quoted spelling, and it keeps its quotes when printed, because the name contains a space. `%i[...]` produces an array of three symbols. `:"prefix_#{label}"` builds the symbol at run time with interpolation. Symbols can be ordered: `<=>` gives `-1` according to the character order of the names, and `sort` arranges the array accordingly. A symbol is still an object; these spellings only open a different door to it.

## The Magic Comment

The line `# frozen_string_literal: true`, written at the very top of a file, is called a magic comment. It freezes the string literals in the file. In the file below the comment is on the first line, and the line carrying the file name is the second.

```ruby
# frozen_string_literal: true
# commented.rb — the magic comment freezes string literals
puts "name".frozen?
puts "name".equal?("name")

text = "name"
begin
  text << "x"
rescue FrozenError => e
  puts e.class
end

puts "name".dup.frozen?
mutable = +"name"
puts mutable.frozen?
```

```
true
true
FrozenError
false
false
```

While the comment was in effect, `"name".frozen?` was `true`, and two separate `"name"` literals turned out to be the same object (`equal?` was `true`): because the literals are frozen, Ruby can share the same content in a single object. Appending to the string with `<<` raised `FrozenError`; only the class of the caught exception was printed. `dup` does not give a frozen copy, and `+"name"` returns an unfrozen string; both gave `false`. A frozen string stays frozen; if a mutable string is needed, a copy of it is taken.

## Measuring the Difference Between a Symbol and a String

The earlier sections measured the spellings and the mutability of symbols and strings. This section puts their conversion into each other and their identity behavior side by side. The axis is the owner: who answers the `to_s` and `<<` messages?

```ruby
# round_trip.rb — a round trip between a symbol and a string
s = :name
puts s.to_s
puts s.to_s.to_sym == s
puts s.equal?(:name)
puts "name".equal?("name")
puts "name".to_sym.equal?(:name)
label = "Anna"
puts "Hello #{label}"
puts 'Hello #{label}'
puts "Hello #{label.size + 1}"
puts s.method(:to_s).owner
```

```
name
true
true
false
true
Hello Anna
Hello #{label}
Hello 5
Symbol
```

`:name.to_s` gave `"name"`, and the way back with `to_sym` produced the same symbol: `s.to_s.to_sym == s` is `true`. In this example the conversion lost no information on the round trip. Identity is a separate question. `s.equal?(:name)` gave `true`: the same symbol spelling is the same object everywhere. `"name".equal?("name")` gave `false`: even though the two string literals carry the same text, they are two separate objects. `"name".to_sym.equal?(:name)` was `true` again: once a string moves to a symbol, the identity is the identity of the symbol. These three measurements show the division of labor between a symbol and a string: a symbol is used for identity, and a string is used for text. If we are going to hold a name as a label, it helps that a symbol is the same object everywhere; if the text is going to be changed or formatted, a string is needed.

Interpolation worked in double quotes: `"Hello #{label}"` printed `Hello Anna`, while the single-quoted `'Hello #{label}'` printed the text as it is, including `#{label}`. Inside `"#{label.size + 1}"` an expression ran and `5` was placed, because `Anna` has four letters. On the last line, the owner of the `to_s` message came out as `Symbol`: the conversion of a symbol to a string is answered by the class of the symbol itself. That interpolation does not work in single quotes prevents the two spellings from being used in place of each other: whoever wants to add the value of a variable into the text chooses double quotes, and whoever wants to keep the text as it is chooses single quotes. The interpolated expression can also be a number; the output `Hello 5` shows this.

```ruby
# in_place_new.rb — << is in place, + makes a new object; the Turkish letter measurement
a = +"name"
b = a
a << "x"
puts b
c = a + "y"
puts c
puts a.equal?(b)
puts a.equal?(c)
puts "ı".upcase
puts "İ".downcase.size
puts "ı".size
puts "ı".bytesize
puts a.method(:<<).owner
```

```
namex
namexy
true
false
I
2
1
2
String
```

`a << "x"` worked in place: `b`, which points at the same object as `a`, became `namex` too. `a + "y"`, on the other hand, produced a new string and did not change `a`; `c` became `namexy`. `a.equal?(b)` gave `true` and `a.equal?(c)` gave `false`: `<<` changes the same object, and `+` returns a new object. The owner of `<<` came out as `String` on the last line. The file started with `+"name"`, because this spelling gives a mutable string; there is no magic comment in this block.

The Turkish letters make a separate measurement. `"ı".upcase` gave `I`. `"İ".downcase.size` gave `2`: a single letter turned into two characters. If the expected result were a single-character `i`, the size would be `1`. `"ı".size` came out `1` and `"ı".bytesize` came out `2`: one character, two bytes. This is why the character count and the byte count cannot be used in place of each other. What these two measurements have in common is that they show the limit of thinking of a string as a flat sequence of characters. `size` counts characters and `bytesize` counts bytes; `upcase` and `downcase` do not have to turn one letter into one letter. Code that computes the length of a string, or whether it fits in a field, must know which unit it is counting.

**LB16.** That the result of `"İ".downcase` is two characters is the result of the default letter mapping of this version. That it is the same in another version or locale was not measured.

## Formatting and Comparison

A string is not only joined and changed; it is also formatted and sorted. This section writes a string with a format specification, aligns it, repeats it, moves it to the next string, compares it, and converts between a letter and a code point. The axis is the owner: of the three messages whose owner is printed, two are owned by `String` and one by `Kernel`.

```ruby
# formatting.rb — formatting and comparing a string
puts "%05.1f" % 3.14159
puts format("%-4s|", "ab")
puts "ab".center(6, "*")
puts "ab" * 3
puts "az".succ
puts "a" <=> "b"
puts "Ab".casecmp?("aB")
puts "a".ord
puts 98.chr
puts "ab".method(:center).owner
puts "ab".method(:%).owner
puts method(:format).owner
```

```
003.1
ab  |
**ab**
ababab
ba
-1
true
97
b
String
String
Kernel
```

The expression `"%05.1f" % 3.14159` gave `003.1`: the `%05.1f` format wrote the number with a total width of five, one decimal, and zero padding at the start. `format("%-4s|", "ab")` uses the same specification language; the `-` mark left-aligns the text and `ab  |` came out. Their owners differ: `%` is a method of `String`, and `format` is a method of `Kernel`. The same format language can be called through two different doors. `"ab".center(6, "*")` added stars on both sides and gave `**ab**`, and `"ab" * 3` wrote the string three times in a row: `ababab`.

`"az".succ` gave `ba`: the next string is the value that follows in order, starting from the last letter, and after `z` comes `a` with a carry into a left digit. `"a" <=> "b"` gave `-1`; the ordering message reports the result as a number. `"Ab".casecmp?("aB")` returned `true`, ignoring case. `"a".ord` gave `97`, and `98.chr` returned `b`; the two directions are inverses of each other. The owner lines showed `String` for `center` and `%`; in these two jobs, where the receiver is a string, the responder is the string's own class. No owner was printed for `succ`, `<=>`, `casecmp?`, and `ord`. Formatting a string needs no separate helper class: a string carries alignment, repetition, and format specification as its own methods.

## Encoding and the Byte Reality

The character count and the byte count of a string can differ; this difference comes from the encoding of the string. This section asks about the encoding directly: the name of the encoding, the code point of a letter, validity, and the raw byte view. The axis is the owner; which class answers the byte query is printed at the end.

```ruby
# encoding.rb — encoding, code point, and byte
puts "a".encoding
puts "ı".bytes.inspect
puts "ı".ord
puts 305.chr(Encoding::UTF_8)
puts "ı".valid_encoding?
puts "\xff".valid_encoding?
puts "ı".unpack("U*").inspect
puts "ı".b.size
puts "a".method(:bytes).owner
```

```
UTF-8
[196, 177]
305
ı
true
false
[305]
2
String
```

The encoding of a string in the source file came out as `UTF-8`. `"ı".bytes` gave two bytes, `[196, 177]`; `"ı".ord` gave the code point of the letter, `305`. A byte and a code point are two separate numbers: the code point identifies the letter, and the bytes identify the encoded form of the letter. `305.chr(Encoding::UTF_8)` produced the letter back from the code point, and `ı` was printed. So `ord` and `chr` form a round trip.

The validity measurement gave two results. `"ı".valid_encoding?` was `true`, and `"\xff".valid_encoding?` was `false`: the byte `0xff` on its own does not form a valid letter in UTF-8 encoding. `"ı".unpack("U*")` returned `[305]`; that is, the same letter is decoded into a single code point from the combination of two bytes. `"ı".b.size` gave `2`: because `b` turns the string into its raw byte view, the size showed the byte count, not the character count. The `size` and `bytesize` difference of the earlier section reappears here at the level of encoding. The owner came out as `String`: the byte query is answered by the string's own class as well.

## The Bounding Measurement

The responder of this lesson is a class. Two slips are possible here. The first: one may think the magic comment changes the responder of a message. The second: one may think that once the comment is written, it affects the whole program. Both are wrong; the measurement puts two files side by side. First a file without the comment is written to disk, and then the main file with the comment pulls it in with `require_relative`.

```ruby
# uncommented.rb — no magic comment
def uncommented_frozen
  "name".frozen?
end

def uncommented_owner
  "name".method(:upcase).owner
end
```

```ruby
# frozen_string_literal: true
# main.rb — the comment is per file
require_relative "uncommented"

puts "name".method(:upcase).owner
puts uncommented_owner
puts "name".frozen?
puts uncommented_frozen
```

```
String
String
true
false
```

The owner of the message `"name".upcase` is the same in both files: `String`. The comment did not change the responder; what changed is the production of the literal. `"name".frozen?` was `true` in the file with the comment and stayed `false` in the file without it, although both ran in the same process. The comment takes effect per file: a comment in one file does not freeze the literals of another file that it pulls in.

## Summary

- `"name".to_sym` turns a string into a symbol and the `String` class answers the conversion; `:name.to_s` and `:name.upcase` belong to the `Symbol` class.
- A symbol with the same name is always the same object (`:name.equal?(:name)` is `true`); two string literals are different objects even if their contents are the same.
- The magic comment `# frozen_string_literal: true` freezes string literals: `frozen?` is `true`, and `<<` raises `FrozenError`.
- `dup` and `+"name"` give an unfrozen string; a frozen string itself stays frozen.
- The responder of this lesson is mostly a class (`String`, `Symbol`; the `Kernel` module for `format`): the magic comment does not change the responder of a message and affects only the literals in the file where it appears.
- `size` counts characters and `bytesize` counts bytes (`1` and `2` for `"ı"`); `<<` changes in place and `+` produces a new string; `ord` and `chr` form a round trip for the code point.

## Next Step

Strings and symbols were measured, but the question of who answers was always asked about single-valued objects. In a collection, for example in a hash, when a key that does not exist is asked about, who gives the answer? The next lesson, Arrays and Hashes, measures who supplies the value of a missing key.
