Basic Types
Elixir's basic types are small in number but have a few quirks that trip up newcomers. The trickiest aren't the integers or floats — those behave more or less the way you'd expect. The traps are atoms, the difference between strings and charlists, and the fact that booleans are atoms in disguise. Get those right early and the rest of the language feels coherent.
Integers
Integers in Elixir are arbitrary precision. There's no int32 versus int64, no overflow, no wrap-around. You can multiply two enormous numbers and the runtime grows the representation as needed.
iex> 2 ** 100
1267650600228229401496703205376
iex> factorial = fn n -> Enum.reduce(1..n, 1, &*/2) end
iex> factorial.(50)
30414093201713378043612608166064768844377641568960512000000000000
You can write integers in different bases for clarity:
0b1010 # binary, equals 10
0o17 # octal, equals 15
0xFF # hexadecimal, equals 255
1_000_000 # underscores for readability, equals 1000000
Integer division uses div/2 and remainder uses rem/2. The / operator always returns a float, even on integers, which is one of the few syntax footguns:
iex> 10 / 3
3.3333333333333335
iex> div(10, 3)
3
iex> rem(10, 3)
1
This catches Python and JavaScript developers who expect / to integer-divide. It doesn't.
Floats
Floats are 64-bit IEEE 754 doubles. Same as Python, JavaScript, Ruby. Same precision issues apply.
iex> 0.1 + 0.2
0.30000000000000004
For money or anything where precision matters, don't use floats. Use the Decimal library from Hex, or store cents as integers. Stripe's API returns amounts in cents for exactly this reason.
# don't store $19.99 as 19.99
# store 1999 as an integer, format on display
amount_cents = 1999
Useful float functions:
iex> Float.round(3.14159, 2)
3.14
iex> Float.ceil(3.1)
4.0
iex> trunc(3.9)
3
trunc/1 returns an integer. Float.ceil/1 returns a float. Mixing them up causes type errors downstream.
Atoms
Atoms are constants whose value is their own name. They're written with a leading colon: :ok, :error, :user_not_found. If you've used Ruby symbols, atoms are similar — the same atom in two places is the same value, compared in constant time.
iex> :ok == :ok
true
iex> :ok === :ok
true
Atoms are everywhere in Elixir. They're how you tag tuples ({:ok, value}, {:error, reason}), how you name modules (under the hood, String is the atom :"Elixir.String"), and how you label keys in keyword lists.
The catch: atoms are not garbage collected. Every distinct atom you create lives forever in an internal table, with a default cap of about 1,048,576 entries. Crash the table by exceeding the limit and your VM is gone.
This means: never call String.to_atom/1 on user input. Ever.
# DON'T do this with untrusted input
def parse_role(input), do: String.to_atom(input)
# DO use the existing-atom variant, which raises if the atom isn't already created
def parse_role(input), do: String.to_existing_atom(input)
to_existing_atom/1 only succeeds if the atom has been created elsewhere (in code, in a config, etc). An attacker sending random strings can't fill your atom table.
Booleans Are Atoms
true and false are atoms. Specifically, they're :true and :false, but the colon is optional for these (and for nil).
iex> true == :true
true
iex> is_atom(false)
true
iex> is_boolean(:hello)
false
This explains a few things. Why is_boolean/1 exists separately from is_atom/1. Why functions returning success/failure often use :ok and :error rather than true and false — they're all the same family.
nil is also an atom. The Elixir convention is that nil and false are "falsy" in if and &&, and everything else is "truthy."
iex> if nil, do: "yes", else: "no"
"no"
iex> if 0, do: "yes", else: "no"
"yes"
iex> if "", do: "yes", else: "no"
"yes"
Notice that 0 and "" are truthy. This differs from Python, JavaScript, and Ruby. Don't rely on falsy zeros.
Strings Are UTF-8 Binaries
A string in Elixir is a UTF-8 encoded binary. Binaries are sequences of bytes. Double-quoted literals create binaries.
iex> "hello"
"hello"
iex> is_binary("hello")
true
iex> byte_size("hello")
5
iex> String.length("hello")
5
byte_size/1 and String.length/1 differ when the string contains multi-byte characters:
iex> byte_size("café")
5
iex> String.length("café")
4
The é takes two bytes in UTF-8. This bites you when you slice strings by byte and end up with invalid UTF-8. Use String.slice/3 and friends, which are codepoint-aware:
iex> String.slice("café", 0, 3)
"caf"
iex> String.at("café", 3)
"é"
String concatenation uses <>:
iex> "hello" <> " " <> "world"
"hello world"
Interpolation uses #{}:
iex> name = "Ada"
iex> "Hello, #{name}"
"Hello, Ada"
Heredocs work like Ruby:
text = """
This is a
multi-line string.
"""
The opening triple-quote and closing triple-quote determine indentation. The leading whitespace on the closing line is stripped from each line of the body.
Charlists Are Different
A charlist is a list of integer codepoints, written with single quotes:
iex> 'hello'
~c"hello"
iex> [104, 101, 108, 108, 111]
~c"hello"
(Recent Elixir versions display charlists with the ~c sigil to make them distinguishable from strings at the REPL.)
Charlists exist because Erlang strings are charlists. Any time you call into Erlang stdlib (which Elixir does heavily under the hood), you're handling charlists. The :inet module returns IP addresses as charlists. :os.cmd/1 returns command output as a charlist.
iex> :os.cmd(~c"echo hello")
~c"hello\n"
iex> List.to_string(:os.cmd(~c"echo hello"))
"hello\n"
Don't use charlists for application code. Convert at the boundary. Strings (binaries) are what every Elixir library expects.
# converting between
iex> List.to_string(~c"hello")
"hello"
iex> String.to_charlist("hello")
~c"hello"
Type Checking With is_* Functions
Elixir is dynamically typed but provides a battery of type-check functions. They're allowed in guards (more on that later) and they're how pattern matching distinguishes shapes at runtime.
is_integer(42) # true
is_float(3.14) # true
is_number(42) # true (covers both)
is_binary("hi") # true
is_atom(:ok) # true
is_boolean(true) # true
is_list([1, 2]) # true
is_tuple({1, 2}) # true
is_map(%{}) # true
is_function(&IO.puts/1) # true
is_pid(self()) # true
is_nil(nil) # true
Use these for guards and quick assertions, not as a substitute for pattern matching:
# fine
def double(n) when is_integer(n), do: n * 2
# better — pattern matches and is_* in one place
def process(%{name: name}) when is_binary(name), do: ...
There's also i/1 in IEx, which gives a richer breakdown:
iex> i :ok
Term
:ok
Data type
Atom
Reference modules
Atom
Implemented protocols
IEx.Info, Inspect, List.Chars, String.Chars
Useful when you're debugging a value of unclear type.
Common Pitfalls
Calling String.to_atom/1 on user input. This is the most cited security issue in Elixir code review. Atoms aren't garbage collected. Use String.to_existing_atom/1 if you must convert at all.
Confusing strings and charlists. Single quotes mean charlist, double quotes mean string. They look similar in error messages until you read carefully. If you ever see [104, 101, 108, ...] in a value, you're holding a charlist.
Using floats for money. Always use integer cents or the Decimal library. Float arithmetic accumulates errors that show up later as off-by-a-penny bugs.
Assuming 0 is falsy. Only nil and false are. if list_count == 0, do: ... is fine; if list_count, do: ... will run when count is zero.
Forgetting / returns a float. Use div/2 for integer division. The default / always coerces. This bites people benchmarking against Python or Ruby.
Comparing across types. Elixir lets you order any two terms ({} < [] < atom < ...) but doing so is almost always a bug. Compare like with like.
Key Takeaways
- Integers are arbitrary precision. Floats are IEEE 754. Don't use floats for money.
- Atoms are constants identical to themselves. They're not garbage collected; never create them from untrusted input.
- Booleans
trueandfalseare atoms. Onlynilandfalseare falsy. - Strings are UTF-8 binaries. Charlists are lists of integer codepoints. Convert at the Erlang boundary.
byte_size/1andString.length/1differ for multi-byte characters.- Type-check with
is_*guards or pattern matching, not with explicit type tags.