•
5 min read
Character Data: The Numbers Underneath Your Text
How Elixir Represents Characters, Charlists, and UTF-8 Strings

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

Bruce Tate's avatar
Bruce Tate
System architecture expert and author of 10+ Elixir books.
Paulo Valim's avatar
Paulo Valim
Full-stack Elixir developer and educator teaching modern Elixir and AI-assisted development.