Coming from Python

You know Python: dynamic types, exceptions, for loops, classes. March keeps the readability but moves the safety checks to compile time instead of runtime.


Functions

-- Python: def add(x, y): return x + y
fn add(x, y) do x + y end

No return keyword: the last expression is the result. No colon. No indentation rules: do...end delimits the body.

Type annotations are optional (the compiler infers them) but good for documentation:

fn add(x : Int, y : Int) : Int do x + y end

Private functions use pfn, enforced by the compiler, not just a _ naming convention:

pfn helper(s : String) : String do String.to_uppercase(s) end

No None: use Option

There’s no None that silently infects your program. A value that might be absent has type Option(a):

fn find_user(id : Int) : Option(User) do
  -- returns Some(user) or None
end

match find_user(42) do
  None       -> println("not found")
  Some(user) -> println(user.name)
end

The compiler forces you to handle both cases. No AttributeError: 'NoneType' object has no attribute 'name' surprises.

Option.unwrap_or is the safe equivalent of x or default:

Option.unwrap_or(find_user(42), default_user)

No exceptions: use Result

Instead of try/except, functions that can fail return Result(ok, err):

fn parse_int(s : String) : Result(Int, String) do
  String.to_int(s)
end

Chain fallible operations with let?: it propagates the error up and returns early, like a try block that’s in the type:

fn run(input : String) : Result(Int, String) do
  let? n    = parse_int(input)
  let? user = fetch_user(n)
  Ok(user.score)
end

If parse_int returns Err("not a valid integer"), run immediately returns that error; fetch_user never runs. No exception handler needed.


No classes: modules + record types

# Python
class Counter:
    def __init__(self): self.count = 0
    def increment(self): self.count += 1
    def get(self): return self.count

March keeps data and functions separate. Records hold data; functions in the same module operate on them:

type Counter = { count : Int }

fn new() : Counter do { count: 0 } end
fn increment(c : Counter) : Counter do { c with count: c.count + 1 } end
fn get(c : Counter) : Int do c.count end

{ c with count: ... } creates a new record with one field changed; values are immutable by default. For mutable shared state, use actor or Vault.


No for loops: higher-order functions

# Python
doubled = [x * 2 for x in nums if x > 0]
total   = sum(nums)
first   = next((x for x in nums if x > 10), None)
let doubled = nums |> List.filter(fn x -> x > 0) |> List.map(fn x -> x * 2)
let total   = List.fold_left(nums, 0, fn (acc, x) -> acc + x)
let first   = List.find(nums, fn x -> x > 10)

|> is the pipe operator: it passes the left side as the first argument to the right side.


Pattern matching replaces isinstance and if/elif

# Python
if isinstance(shape, Circle):
    return 3.14 * shape.r ** 2
elif isinstance(shape, Rect):
    return shape.w * shape.h
else:
    return 0
match shape do
  Circle(r)  -> 3.14159 *. r *. r
  Rect(w, h) -> w *. h
  Point      -> 0.0
end

The compiler checks exhaustiveness: if you add a new constructor to Shape later, every match that doesn’t cover it becomes a compile error.

match do without a subject replaces if/elif/else chains:

match do
  score >= 90 -> "A"
  score >= 80 -> "B"
  _           -> "C"
end

Static typing without the ceremony

You’re used to Python being dynamic. March is fully static but the type checker is mostly invisible:

fn double(n) do n * 2 end       -- inferred: Int -> Int
let xs = [1, 2, 3]              -- inferred: List(Int)
let pair = ("hello", 42)        -- inferred: (String, Int)

Annotate at module boundaries for documentation and better error messages:

fn fetch(id : Int) : Result(User, String) do ... end

Concurrency & shared state

Python reaches for a class with mutable self state, plus threading or asyncio for concurrency. March has no mutable objects: state that changes over time lives in an actor, a process you talk to by message.

# Python
class Counter:
    def __init__(self):
        self.count = 0
    def inc(self, n):
        self.count += n
-- March: state lives in an actor; messages drive the transitions
actor Counter do
  state { value : Int }
  init  { value: 0 }

  on Increment(n : Int) do
    { state with value: state.value + n }
  end
end

You spawn the actor and send it messages; it owns its state and no other code can touch it.

Python March
class with mutable self actor with state { … }
threading.Thread / asyncio task Task.async(fn () -> …) + Task.await
asyncio.gather(...) Task.await_many([...])
module-level / global mutable state a Vault (in-memory key-value store)

See Actors and Choosing a concurrency primitive.


Numeric arrays (NumPy users)

Python’s list comprehensions are fine for everyday code, but for large numeric workloads you reach for NumPy. March’s equivalent is NativeArray: a flat, contiguous array (not List’s linked cells or Array’s trie) that march --compile can auto-vectorize into real SIMD instructions:

# Python / NumPy
arr = np.arange(1_000_000, dtype=np.float64)
doubled = arr * 2.0
total = doubled.sum()
let arr = NativeArray.make_float(1_000_000, 0.0)
let doubled = NativeArray.map_float(arr, fn x -> x *. 2.0)
let total = NativeArray.sum_float(doubled)

The accurate comparison, not an overclaim: on a same-machine benchmark at 5M elements, March ties or beats NumPy on sum and is competitive (within 3x, and beating hand-written OCaml/Rust) on map; narrowing to NativeArray’s f32 element width (50% of the bytes, double the SIMD lanes) closes most of the remaining gap, beating NumPy on sum and map and reaching parity on map2. See SIMD Benchmarks for the full numbers and methodology, and SIMD & Native Arrays for what does and doesn’t vectorize. There’s also an explicit Simd module (128-bit vector types) for when you need guaranteed vector codegen instead of an optimizer decision, covered in the same guide.


What’s the same

  • println(...) for output
  • List literals: [1, 2, 3]
  • Tuple literals: (1, "hello")
  • Boolean: true, false, &&, ||, !
  • String concatenation: "hello " ++ name (instead of f-strings)
  • Integer arithmetic: +, -, *, /; float arithmetic adds .: +., -., *., /.
march — interactive
Click run on any snippet to try it here.
march>