Elixir for Python programmers

This page attempts to illustrate some of the differences between Elixir and Python.

Python Elixir Notes
Overview
Websites
First appeared 20 February 1991 May 2012, running on a platform that appeared in 1986.
VM Typically runs on its VM, whether CPython or PyPy. Runs on the Erlang virtual machine (BEAM). IronPython and Jython run in foreign VMs.
Multi-core concurrency Multi-core execution in the same Python process is possible with threads, but because of the global interpreter lock, only one thread can be executing Python bytecode. Other threads can wait on IO or call into C code, but they cannot run more Python code (exception: pypy-stm). Multi-core execution is possible using Erlang processes, which are preempted by the Erlang VM. Erlang processes receive and send messsages without sharing data structures. They can be linked into a hierarchy, monitored with tools like etop, and support per-process memory limits.
Garbage collection The Erlang VM gives each Erlang process its own heap. For garbage collection, the Erlang VM uses a "a per-process generational semi-space copying collector using Cheney's copy collection algorithm together with a global large object space".
Pattern matching No general pattern matching except through third-party libraries. Pattern matching is built into the VM and can be used with =, case, function arguments, lists, binaries, and more. A pattern matching tutorial in Erlang.
File extensions
  • .py for source files
  • .pyc, .pyo for compiled bytecode
  • .ex for source files that are to be compiled
  • .exs for source files that are to be interpreted
  • .beam for compiled BEAM bytecode
When to use .ex and .exs files
REPL
Start the REPL
  • python
  • python3
  • ipython for some use cases
  • iex
  • iex -S mix to start with modules loaded from your mix project
Exit the REPL
  • Ctrl-d on Linux
  • Ctrl-z, Return on Windows
  • Ctrl-c, Ctrl-c
  • Ctrl-c, a, Enter
  • Ctrl-g, q, Enter
Get last evaluation in REPL
_

(underscore)

v()
Get help for a function
help(func)
h func
Package and project managers
pip and virtualenv mix, which puts dependencies in your project's deps/ folder
Auditing deps pip install executes code in downloaded dependencies without giving you a chance to audit them. mix deps.get downloads but doesn't execute code in packages from Hex, giving you a chance to audit them before compiling (and thus executing) them.
Types and data structures
Atoms No direct equivalent :some_name is an atom, a constant symbol that does not need pre-declaration. Atoms are not garbage-collected and there is a default maximum of 1,048,576 atoms, so you must be careful not to create a large quantity of atoms dynamically.
Booleans and null
True
False
None
true
false
nil
Elixir true, false, and nil are equivalent to the atoms :true, :false, and :nil.
Lists [1, 2, 3] is a mutable dynamic array [1, 2, 3] is a linked list
Tuples (1, "2", 3) {1, "2", 3}
Maps {"x": 3, "y": 3.5} is a mutable hash table %{"x" => 3, "y" => 3.5} is an immutable map Large Erlang/Elixir maps use a HAMT (hash-array mapped trie)
Strings
  • "string" is a bytestring in Python 2.x, and unicode in Python 3.x
  • u"string" is unicode
  • b"string" is a bytestring
  • "string" is an Erlang binary with UTF-8 encoding
  • 'string' is an Erlang list with 6 codepoints,
    [115, 116, 114, 105, 110, 103]
Syntax and common operations
Calling Function calls require () Function calls usually don't require (), so IO.puts "hi" and IO.puts("hi") are equivalent.
Implicit return Functions return None if there is no return statement. Functions return the last expression. Elixir has no return statement; instead, use if, case, or cond.
Named functions Named functions can be defined at the top-level module scope, in a class, or in another function.

Named functions can not be defined at the top-level scope (including the REPL) or in another function; they can only be in a defmodule declaration:

defmodule MyModule do
  def add(x, y) do
    x + y
  end
end

MyModule.add(2, 3)
# => 5
"It's because there is a bug in Erlang."
Multiple declarations Defining another function with the same name will just overwrite the previously defined function.

Elixir supports defining multiple functions of the same name with different patterns or arities:

defmodule MyModule do
  def op("double", n) do
    n + n
  end

  def op("square", n) do
    n * n
  end

  def op(n) do
    n * n * n
  end
end

MyModule.op("double", 10)
# => 20
MyModule.op("square", 10)
# => 100
MyModule.op(10)
# => 1000
Optional arguments

Optional arguments must be listed last:

def add(x, y=3):
    return x + y

add(2)
# => 5

Even though Python's collections and objects are generally mutable, default parameters e.g. y=dict() are evaluated only when the function is defined, not on every function call.

Optional arguments can be listed at any position, as long as they do not conflict with an existing declaration:

defmodule MyModule do
  def add(x, y \\ 3) do
    x + y
  end
end

MyModule.add(2)
# => 5

or

defmodule MyModule do
  def add(x \\ 10, y) do
    x + y
  end
end

MyModule.add(2)
# => 12

With both examples above, Elixir defines two separate functions, MyModule.add/1 and MyModule.add/2.

Anonymous functions
square = lambda n: n ** 2
square(10)
# => 100
square = fn n -> n * n end
square.(10)
# => 100
Why are there two kinds of functions in Elixir?
print with newline
print(obj)
IO.puts(obj)
print w/o newline
print(obj, end='')
IO.write(obj)
repr repr(obj) inspect(obj)
print(repr(obj)) IO.inspect(obj), which also returns obj after printing.
Pipe operator

No pipe operator, unless you want to use macropy. In many cases, of course, the returned object has a relevant method for further processing:

" hello world ".strip().upper()
# => 'HELLO WORLD'

The |> pipe operator works like Clojure's -> thread-first macro:

" hello world " |> String.trim |> String.upcase
# => "HELLO WORLD"

which is equivalent to:

String.upcase(String.trim(" hello world "))    
# => "HELLO WORLD"
Exponent ** exponentiation operator works on both ints/longs and floats No exponentiation operator; :math.pow exists but always returns a float. An integer exponentiation function for Elixir.
Mapping
list(n * n for n in [1, 2, 3, 4])
# => [1, 4, 9, 16]

list(map(lambda n: n * n, [1, 2, 3, 4]))
# => [1, 4, 9, 16]
for n <- [1, 2, 3, 4], do: n * n
# => [1, 4, 9, 16]

Enum.map([1, 2, 3, 4], fn n -> n * n end)
# => [1, 4, 9, 16]
Elixir comprehensions.
Mapping with a named function
list(map(str.upper, ["hello", "world"]))
# => ['HELLO', 'WORLD']

The function must be captured with the & operator:

["hello", "world"] |> Enum.map(&String.upcase/1)
# => ["HELLO", "WORLD"]
assert
assert x == 3, "not 3"

Elixir doesn't have an assert in Kernel, but you can use the one in ExUnit.Assertions:

import ExUnit.Assertions, only: [assert: 1, assert: 2]

assert x == 3, "not 3"

You can also "assert" with pattern matching or by pinning a variable with ^. The downside is that may get a less-informative error on failures:

x = 2

^x = 3
# => ** (MatchError) no match of
# =>    right hand side value: 3

{:ok, val} = {:fail, nil}
# => ** (MatchError) no match of
# =>    right hand side value: {:fail, nil}
String operations
strip
' text \n'.strip()
# => 'text'
" text \n" |> String.trim
# => "text"
Elixir trim* functions operate on graphemes and strip all Unicode whitespace.
rstrip
' text \n'.rstrip()
# => ' text'
" text \n" |> String.trim_trailing
# => " text"
lstrip
' text \n'.lstrip()
# => 'text \n'
" text \n" |> String.trim_leading
# => "text \n"
split
' hello world '.split(' ')
# => ['', 'hello', 'world', '']
" hello world " |> String.split(" ")
# => ["", "hello", "world", ""]
split max=N
'hello world again'.split(' ', 1) # max splits
# => ['hello', 'world again']
"hello world there" |> String.split(" ", parts: 2)
# => ["hello", "world there"]
upper
'hello world'.upper()
# => 'HELLO WORLD'
"hello world" |> String.upcase
# => "HELLO WORLD"
lower
'HELLO WORLD'.lower()
# => 'hello world'
"HELLO WORLD" |> String.downcase
# => "hello world"
Replace all occurences
'hello world'.replace("l", "x")
# => 'hexxo worxd'
"hello world" |> String.replace("l", "x")
# => "hexxo worxd"
Replace first match
'hello world'.replace("l", "x", 1)
# => 'hexlo world'
"hello world" |> String.replace("l", "x", global: false)
# => "hexlo world"
Repeat / duplicate
'*' * 20
# => '********************'
"*" |> String.duplicate(20)
# => "********************"
Count graphemes Unsupported
"\u0067\u0308" |> String.length
# => 1
# Note: g̈
O(N) in Elixir
Count codepoints
len(u"\u0067\u0308")
# => 2
"\u0067\u0308" |> String.to_charlist |> length
# => 2
O(1) in Python,
O(N) in Elixir
Count bytes
len(u"\u0067\u0308".encode('utf-8'))
# => 3
"\u0067\u0308" |> byte_size
# => 3
O(N) in Python with encoding step,
O(1) in Elixir
needle in haystack
"ello" in "hello world"
# => True
"hello world" |> String.contains?("ello")
# => true
startswith
'hello world'.startswith('hello')
# => True
"hello world" |> String.starts_with?("hello")
# => true
endswith
'hello world'.endswith('hello')
# => False
"hello world" |> String.ends_with?("hello")
# => false