Elixir vs Python vs Rust
Three Languages, Three Goals
The comparison only makes sense once you see what each language was built for. None of them is "better"; each optimizes a different axis and pays a different price.
- Python (1991) optimizes human time. Readable syntax, a REPL, and the largest library ecosystem in computing make it the default for scripting, data science, and machine learning. The price is execution speed and the global interpreter lock, which historically forced concurrency through multiprocessing or async libraries.
- Rust (2015) optimizes machine guarantees. Ownership and borrowing prove memory safety at compile time with no garbage collector, which makes it a credible C/C++ replacement for kernels, engines, and embedded systems. The price is a steep learning curve and slower iteration while you fight the borrow checker.
- Elixir (2012) optimizes system resilience. It inherits the Erlang/OTP platform built for telephone switches that ran for nine nines of availability, and it makes concurrent, distributed, fault-tolerant services the default rather than an advanced topic. The price is raw numeric throughput and a smaller general-purpose ecosystem.
The landscape figure places them by what they optimize. Notice that the axes are properties of the runtime and type system, not fashion.
Head-to-Head
The table compares the properties engineers actually choose on. "Guarantee" means what the language or runtime promises without extra libraries.
| Property | Elixir 1.20 (BEAM) | Python 3.13 | Rust 2024 |
|---|---|---|---|
| Paradigm | functional, actor-model processes | imperative/OO, multi-paradigm | generic, ownership-based systems |
| Typing | dynamic + gradual set-theoretic types (1.18+) | dynamic + optional type hints | static, inferred, memory-safe |
| Memory | GC, per-process heap, isolated | GC, shared heap | no GC, ownership + borrowing |
| Concurrency | preemptive, millions of processes, no locks | GIL; threads, asyncio, multiprocessing | OS threads, async runtime, explicit and safe |
| Fault tolerance | built in — links, monitors, supervision trees | none at runtime level | Result types force handling; no supervisor model |
| Distribution | language-level nodes and messaging | libraries (RPC, queues) | libraries (gRPC, tokio) |
| Numeric loops | slow — delegate to NIFs/Nx | slow in pure Python — NumPy delegates to C | native speed, zero-cost abstractions |
| Startup & footprint | fast spawn (~µs), KB per process | fast start, heavy per-thread cost | smallest footprint, no runtime |
| Ecosystem center | web, real-time, messaging, embedded (Nerves) | data science, ML, scripting, glue | systems, CLI, engines, WASM, infra |
| Hot code reload | yes (BEAM), zero-downtime deploys | no | no |
| Learning curve | moderate; pattern matching is the first wall | gentle | steep; ownership is the first wall |
Elixir vs Python
Both are dynamically typed and pleasant to iterate in, so the real question is what happens when the
prototype becomes infrastructure. Python executes on a single-threaded interpreter with a global lock;
a CPU-bound request blocks the event loop unless you reach for multiprocessing or worker
pools. On the BEAM, every connection or job is a process with its own heap, scheduled preemptively —
concurrency is not a library you add, it is the execution model.
Typing tells a story of convergence: Python's type hints and Elixir's gradual type system both let teams add static checks incrementally. The difference is structural — Elixir's immutable data makes a function's behaviour depend only on its inputs, which makes tests parallel by default and eliminates whole classes of aliasing bugs Python must handle by convention.
# word_count.py — count word frequencies in a phrase
import collections
def word_count(text: str) -> dict[str, int]:
# normalize case, strip punctuation, then count
words = text.lower().replace(",", "").split()
return dict(collections.Counter(words))
print(word_count("Fun to learn, fun to ship"))
# {'fun': 2, 'to': 2, 'learn': 1, 'ship': 1}
Elixir vs Rust
Rust proves properties about your program before it runs: no data races, no use-after-free, no null dereferences. That is exactly what you want for an encoder, an OS driver, or a game engine. Elixir instead proves properties while it runs: a process that hits an unexpected state crashes alone and is restarted clean, while the rest of the system keeps serving. Rust prevents the bug; Elixir survives it. The table row that matters most is fault tolerance — Rust has no equivalent of a supervision tree.
// word_count.rs — the same counting task in Rust
use std::collections::HashMap;
fn word_count(text: &str) -> HashMap {
let mut counts = HashMap::new();
// split_whitespace handles every run of whitespace
for word in text.replace(',', "").split_whitespace() {
// entry() finds or inserts the counter, then bumps it
*counts.entry(word.to_lowercase()).or_insert(0) += 1;
}
counts
}
fn main() {
// Debug pretty-prints the map: {"fun": 2, "to": 2, ...}
println!("{:?}", word_count("Fun to learn, fun to ship"));
}
The Same Program in Elixir
Now the Elixir version. Read it as a pipeline: the text flows left to right through transformations,
and the |> pipe operator feeds each result into the next call as its first argument.
There is no loop variable to mutate and no accumulator to initialize — Enum.frequencies/1
already does the counting.
# word_count.exs — run with: elixir word_count.exs
"Fun to learn, fun to ship"
|> String.downcase() # "fun to learn, fun to ship"
|> String.replace(",", "") # strip punctuation
|> String.split() # ["fun", "to", "learn", "fun", "to", "ship"]
|> Enum.frequencies() # %{"fun" => 2, "to" => 2, ...}
|> IO.inspect() # print the final map
Three things to notice. First, immutability: each step produces a new value, so you can insert an
IO.inspect/1 between any two steps to debug without side effects. Second, the pipe style
scales: a ten-stage data transformation stays as readable as a two-stage one. Third, the standard
library is small and orthogonal — most of this program is data flowing through generic functions.
Decision Checklist
Use the checklist below when you inherit or start a system.
| If your dominant concern is… | Reach for | Why |
|---|---|---|
| Analysis, models, notebooks, glue scripts | Python | Ecosystem depth (NumPy, PyTorch, pandas) is unmatched |
| Kernel modules, codecs, embedded firmware, CLIs, WASM | Rust | No GC, native speed, compile-time memory safety |
| Long-lived services, real-time features, message pipelines, multi-node state | Elixir | Processes, supervision, and distribution are runtime guarantees |
| A web product with live updates | Elixir (Phoenix + LiveView) | Server-driven UI over one persistent socket, no client framework required |
| A hot numeric kernel inside any of the above | Rust inside Elixir (Rustler NIF) | Keep orchestration simple, make the kernel fast |
Practice
- Run the Elixir version above, then extend the pipeline to exclude the word
"to"from the counts. - Rewrite the Python version with a type error deliberately introduced (pass an
int). Compare what happens with the Elixir version given the same mistake. - Write three sentences: which of the three languages would you pick for (a) a price-ticker dashboard, (b) a CSV-to-report script, (c) a video codec — and why.
Next: Install & Toolchain