Skip to content

A symbol is a name, not a string

Level: 101 · a Python programmer who has just met :foo and {key: value}

One line: A Ruby symbol is an immutable name of which exactly one object exists per spelling — :a.equal?(:a) is true while two "a" literals are two objects — and it is what Ruby uses for method names, hash keys and &:upcase; Python has one string type and covers the same ground with interning, getattr and enum.Enum.

A symbol is the thing you type after a colon: :name, :upcase, :+. It is not a string. There is exactly one :a in the whole program — every occurrence of the literal, and every "a".to_sym, evaluates to the very same object — and it is frozen, so it can never change. That is what makes it the right thing to be a name: Ruby names its methods with symbols — methods returns them, send takes one, def returns one — and a hash keyed by symbols is the ordinary way to pass options. A string is a sequence of bytes that you compare by content and may change in place; a symbol is an identity that you compare by identity. The program prints six numbered groups, and the Python twin prints the closest thing Python has for each.

Verified output of symbols_are_names_rb.rb — regenerated by tools/run_examples.py, never hand-typed.

1. one object per name
   :a.equal?(:a)                                true
   "a".equal?("a")                              false
   "a".to_sym.equal?(:a)                        true
   ("dyn" + "amic").to_sym.equal?(:dynamic)     true
2. a symbol is not a string
   :a == "a"                                    false
   :a.class                                     Symbol
   :a.to_s                                      "a"
   "a".to_sym                                   :a
   [:a, "a"].uniq                               [:a, "a"]
3. symbols are frozen
   :a.frozen?                                   true
   :a.name.frozen?  (one shared String)         true
   :a.to_s.frozen?  (a fresh String)            false
   :a.name << "b"                               FrozenError
4. literal forms and hash keys
   %i[a b]                                      [:a, :b]
   :"hello world"                               :"hello world"
   {a: 1}                                       {a: 1}
   {a: 1}.keys.first.class                      Symbol
   {"a" => 1}.keys.first.class                  String
   {a: 1}["a"]                                  nil
   h[:k] = 1; h["k"] = 2; h                     {k: 1, "k" => 2}
   h.size                                       2
5. method names are symbols
   "x".send(:upcase)                            "X"
   def greet; end                               :greet
   "x".method(:upcase).name                     :upcase
   "x".methods.include?(:upcase)                true
6. to_proc makes &:sym work
   :upcase.to_proc.call("x")                    "X"
   %w[a b].map(&:upcase)                        ["A", "B"]
   [1, 2].inject(:+)                            3

Reading the output

  • Row 1. :a.equal?(:a) is true: two spellings of the same symbol are one object, whatever built them — a literal, to_sym on a literal, or to_sym on a string assembled at run time. Two "a" literals are two different String objects.
  • Row 2. :a == "a" is false. They are different classes, Symbol and String, and uniq keeps both. to_s and to_sym cross over.
  • Row 3. Every symbol is frozen. Symbol#name returns one shared frozen String, so appending to it is a FrozenError; Symbol#to_s hands back a fresh, unfrozen copy each time.
  • Row 4. %i[a b] builds an array of symbols; a symbol with a space needs quotes, :"hello world". {a: 1} is the symbol-keyed hash — inspect prints it back as {a: 1} and a string-keyed one as {"a" => 1} — and because :k and "k" are different objects, one hash holds both as two separate keys, and {a: 1}["a"] is nil. That trap has its own page: string and symbol keys.
  • Row 5. A method's name is a symbol: send(:upcase) calls it, def greet; end evaluates to :greet, method(:upcase).name is :upcase, and methods returns an array of them.
  • Row 6. Symbol#to_proc turns :upcase into a block that calls that method on its argument, which is why map(&:upcase) and inject(:+) read the way they do. The symbol to proc page follows that &.

Compared with Python

Python has no symbol type, so str does double duty as the text type and the name type: getattr(obj, "upper") takes a string, dir() returns strings, a function's __name__ is a string, and a dict's keys are usually strings (rows 4 and 5). Two mechanisms cover the rest of a symbol's job. Interning gives "one object per spelling": sys.intern returns the canonical object for a string, and CPython interns identifier-like literals on its own, which is why a = "abc"; b = "abc"; a is b is True while a string built with join is a separate object until it is interned (row 1). Enumerations give a fixed set of named, immutable, identity-compared values: Color.RED is Color.RED, Color["RED"] looks a member up by name, Color.RED == "RED" is False, and a member cannot be reassigned (rows 2 and 3). Every str is immutable, so the frozenness Ruby reserves for symbols is simply how strings are (row 3). dict(a=1) is the nearest thing to the {a: 1} spelling, and its keys are still strings; row 4's ending runs the other way round from Ruby — d["k"] = 1; d["k"] = 2 leaves one key, because there is only one kind of "k". For &:sym, operator.methodcaller("upper") and map(str.upper, …) are the spellings (row 6).

Verified output of symbols_are_names_py.py — regenerated by tools/run_examples.py, never hand-typed.

1. one object per name (interning)
   sys.intern("a") is sys.intern("a")           True
   a = "abc"; b = "abc"; a is b                 True
   c = "".join(["ab", "c"]); a is c             False
   sys.intern(c) is a                           True
2. a str is the only string; Enum is the nearest name type
   "a" == "a"  (no symbol to differ from)       True
   type("a").__name__                           str
   Color.RED.name                               'RED'
   Color["RED"] is Color.RED                    True
   Color.RED == "RED"                           False
3. every str is immutable; an Enum member is fixed
   s = "a"; s[0] = "b"                          TypeError
   hash("a") == hash("a")  (usable as a key)    True
   Color.RED is Color.RED                       True
   Color.RED = 1                                AttributeError
4. literal forms and dict keys
   ["a", "b"]  (no %i)                          ['a', 'b']
   "hello world"  (just a str)                  'hello world'
   dict(a=1)  (the {a: 1} spelling)             {'a': 1}
   type(next(iter(dict(a=1))))                  str
   type(next(iter({"a": 1})))                   str
   {"a": 1}["a"]                                1
   d["k"] = 1; d["k"] = 2; d                    {'k': 2}
   len(d)                                       1
5. method names are strings
   getattr("x", "upper")()                      'X'
   def greet(): ...  (a statement)              greet.__name__ == 'greet'
   str.upper.__name__                           'upper'
   "upper" in dir("x")                          True
6. methodcaller and map do the job of &:sym
   operator.methodcaller("upper")("x")          'X'
   list(map(str.upper, ["a", "b"]))             ['A', 'B']
   functools.reduce(operator.add, [1, 2])       3

The Python library's string literals ↗ page lists the ways Python spells a string — and there is no symbol among them.

Try it

  1. Tally the words in %w[apple Banana cherry apple] under downcased symbol keys, print the hash and the classes of its keys, then convert it to string keys with transform_keys(&:to_s) and back with transform_keys(&:to_sym). Is the string-keyed hash == to the symbol-keyed one? Is the round-tripped one?
  2. Build the Ruby counterpart of a Python Enum: a module Color with RED = :red, GREEN = :green, BLUE = :blue and a frozen ALL array, plus Color.valid?(x) using ALL.include?. Show that Color.valid?(:red) is true, Color.valid?("red") is false, and what "red".to_sym does to the second answer.
Solution to 1

symbols_are_names_kata_rb.rb in full — pasted here by tools/run_examples.py from the file CI runs.

# Kata: tally words under symbol keys, then move the same hash between symbol
# keys and string keys and watch what == thinks of the two.

words = %w[apple Banana cherry apple]
tally = words.map { |w| w.downcase.to_sym }.tally
puts "   symbol keys   #{tally.inspect}"
puts "   key classes   #{tally.keys.map(&:class).uniq.inspect}"

strings = tally.transform_keys(&:to_s)
puts "   string keys   #{strings.inspect}"
puts "   key classes   #{strings.keys.map(&:class).uniq.inspect}"

back = strings.transform_keys(&:to_sym)
puts "   strings == tally   #{strings == tally}"
puts "   back == tally      #{back == tally}"
puts "   tally[:apple]      #{tally[:apple].inspect}"
puts "   tally[\"apple\"]     #{tally["apple"].inspect}"

Verified output of symbols_are_names_kata_rb.rb — regenerated by tools/run_examples.py, never hand-typed.

   symbol keys   {apple: 2, banana: 1, cherry: 1}
   key classes   [Symbol]
   string keys   {"apple" => 2, "banana" => 1, "cherry" => 1}
   key classes   [String]
   strings == tally   false
   back == tally      true
   tally[:apple]      2
   tally["apple"]     nil

See also