Enter a list of integers in IEx and you may get something unexpected:
[99, 97, 116]
#=> ~c"cat"
You entered three numbers, but IEx displayed text. The ~c makes it look as if Elixir changed the value into some special character type.
It didn’t.
The first clue is this:
?a == 97
#=> true
In Elixir, ?a evaluates to the integer 97. Once you understand what those integers represent, the strange output starts to make sense.
A Character Is an Integer
Unicode assigns a number, called a code point, to each character. The lowercase a has code point 97, while c is 99 and t is 116.
Elixir’s question mark syntax gives you that number directly:
i ?a
# Type
# Integer
There is no separate character type hiding behind ?a. It is convenient syntax for an integer.
Bruce puts it directly in class:
“When you see an a in Elixir and you’re in Unicode, you’re going to be looking at a 97, just the integer value. Character data is nothing but integers.”
— Bruce Tate
That explains the opening result. [99, 97, 116] can be read as three ordinary integers, but those integers are also the code points for c, a, and t. Elixir cannot distinguish the two meanings by type because both use the same list structure.
When every value can be displayed as a printable character, IEx chooses the character form:
~c"cat" == [99, 97, 116]
#=> true
is_list(~c"cat")
#=> true
The display changed. The value underneath is still a list.
🎯 Join Groxio's Newsletter
Weekly lessons on Elixir, system design, and AI-assisted development -- plus stories from our training and mentoring sessions.
We respect your privacy. No spam, unsubscribe anytime.
A Chain Versus a Packed Row
A charlist uses Elixir’s ordinary linked-list structure. Each cell holds a value and a pointer to the rest of the list:
[99 | pointer] -> [97 | pointer] -> [116 | end]
Those pointers take space. They are useful when you want the normal behavior of a list, such as efficiently adding an element to the front, but they are expensive if your real goal is to store a block of text.
Strings use a different shape. In Elixir, a string is a binary containing UTF-8 encoded bytes packed next to one another:
| 99 | 97 | 116 |
For ASCII characters such as c, a, and t, the Unicode code points and UTF-8 byte values happen to match. That gives us this convenient comparison:
<<99, 97, 116>>
#=> "cat"
<<99, 97, 116>> == "cat"
#=> true
The distinction matters once you leave ASCII. A charlist contains code points, while a UTF-8 string contains the bytes used to encode those code points. One visible character may require more than one byte.
That is why these functions measure storage rather than what a reader sees:
byte_size("cat")
#=> 3
bit_size("cat")
#=> 24
Elixir also gives you direct access to Erlang’s binary tools:
:binary.bin_to_list("cat")
#=> [99, 97, 116]
The leading colon identifies an Erlang module. Elixir runs on the BEAM, so those functions are available without another library or wrapper.
Make IEx Show the List
The printable display is pleasant until you are debugging code and need to see the exact integers.
Suppose IEx shows this:
value = [99, 97, 116]
#=> ~c"cat"
Instead of mentally translating the characters, tell IO.inspect/2 how to display charlists:
IO.inspect(value, charlists: :as_lists)
#=> [99, 97, 116]
This changes only the presentation. It does not convert the value or create a different data structure.
The rule is easy to keep: when a list unexpectedly looks like text, inspect it as a list.
Choosing the Representation
Strings are the usual choice for text you render, store, or send. Their packed binary representation fits that work, and most Elixir text APIs expect it.
Charlists remain useful when an Erlang API expects one or when a problem genuinely benefits from list operations over code points. They are not inferior strings. They are lists doing a different job.
Once you can see the representation beneath the printed value, IEx stops looking clever and starts giving you useful clues about the data in front of you.
In Blog #22, that binary text becomes visible when we render Tetris shapes as SVG. Blog #23 follows the bytes into string construction, where the cost of concatenation explains why IO lists exist.
📚 Learn Elixir Text by Mental Model
This comes from our Learning Elixir course, where Bruce teaches the mental models behind Elixir's core data structures through real-world scenarios. Learn how integers, charlists, and UTF-8 binaries shape the text your programs render, store, and send. Available via monthly subscription -- try it for one month.
— Paulo & Bruce