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)istrue: two spellings of the same symbol are one object, whatever built them — a literal,to_symon a literal, orto_symon a string assembled at run time. Two"a"literals are two differentStringobjects. - Row 2.
:a == "a"isfalse. They are different classes,SymbolandString, anduniqkeeps both.to_sandto_symcross over. - Row 3. Every symbol is frozen.
Symbol#namereturns one shared frozenString, so appending to it is aFrozenError;Symbol#to_shands 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 —inspectprints it back as{a: 1}and a string-keyed one as{"a" => 1}— and because:kand"k"are different objects, one hash holds both as two separate keys, and{a: 1}["a"]isnil. 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; endevaluates to:greet,method(:upcase).nameis:upcase, andmethodsreturns an array of them. - Row 6.
Symbol#to_procturns:upcaseinto a block that calls that method on its argument, which is whymap(&:upcase)andinject(:+)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¶
- 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 withtransform_keys(&:to_s)and back withtransform_keys(&:to_sym). Is the string-keyed hash==to the symbol-keyed one? Is the round-tripped one? - Build the Ruby counterpart of a Python
Enum: a moduleColorwithRED = :red,GREEN = :green,BLUE = :blueand a frozenALLarray, plusColor.valid?(x)usingALL.include?. Show thatColor.valid?(:red)istrue,Color.valid?("red")isfalse, and what"red".to_symdoes 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.
See also¶
- Ruby has four kinds of equality —
equal?, the identity test row 1 uses, beside the other three freezeis shallow and raisesFrozenError— what frozen means for the objects that are not symbols- String and symbol keys — row 4's trap in full, including what
JSON.parsegives you - Hashes and default values — the
{a: 1}and{"a" => 1}spellings and what lookup does with each - Symbol to proc — the
&:upcaseof row 6, and what else&accepts - Symbol ↗ — the Ruby 4.0 docs; sys.intern ↗ and enum ↗ — Python's two halves of the idea
- Twenty-three Ruby traps for a Python programmer — The twenty-three places where a Python reflex gives the wrong answer in Ruby
- There is more than one way, and one obvious way — Ruby's idioms