Modern OCaml 5 Edition (2026)

Learn Modern OCaml for Python and Javascript Developers

In 2026, JavaScript/TypeScript is everywhere, and Python drives AI. But as systems grow, developers face a wall: unhandled state mutations, sluggish execution, and concurrency headaches. Enter OCaml.

OCaml provides the holy grail: native C-like speed, sub-second compile times, and a type system that eliminates entire classes of bugs. It’s a pragmatic functional language—defaulting to immutability but allowing mutation when performance demands it. With the arrival of OCaml 5, you get unparalleled Multicore parallelism and Effect Handlers. It's time to build robust systems.

2. The Rosetta Stone: Types Grid

OCaml is statically and strongly typed, but its advanced Hindley-Milner type inference means you almost never write types by hand. The compiler simply figures it out. Nulls are handled safely by the option type.

Concept OCaml (Statically Typed) Python 3.12+ (Type Hints) JS (ES2026)
Integer let x = 42 x: int = 42 const x = 42;
Float let pi = 3.14 pi: float = 3.14 const pi = 3.14;
String let name = "Hi" name: str = "Hi" const name = "Hi";
List (Immutable) let nums = [1; 2; 3] nums: list[int] = [1, 2, 3] const nums = [1, 2, 3];
Array (Mutable) let arr = [|1; 2; 3|] N/A (Lists are mutable) const arr = [1, 2, 3];
Null/None let val = None (* Option *) val: int | None = None const val = null;
Exceptions let res = Error "Oops" (* Result *) Exception (Runtime) Error (Runtime)

3. Guess the Number Game

OCaml Features Introduced: Pipeline operator (|>), pattern matching, option types, and tail recursion.

OCaml (main.ml)
let () = Random.self_init ()

(* We use recursion instead of a `while` loop. The `rec` keyword allows a function to call itself. *)
let rec game_loop secret =
  print_string "> "; 
  flush stdout; (* Ensure prompt is drawn before waiting for input *)
  
  let input = read_line () in
  
  (* int_of_string_opt returns an option (Some int or None). 
     The pipeline operator `|>` passes `input` as the last argument to the function. *)
  match input |> int_of_string_opt with
  | None ->
      print_endline "Please type a valid number!";
      game_loop secret
  | Some guess ->
      if guess < secret then begin
        print_endline "Higher!";
        game_loop secret
      end else if guess > secret then begin
        print_endline "Lower!";
        game_loop secret
      end else
        print_endline "You win!"

let () =
  let secret_number = Random.int 100 + 1 in
  print_endline "Guess the number between 1 and 100!";
  game_loop secret_number
Python 3.12+
import random

def main():
    secret_number = random.randint(1, 100)
    print("Guess the number between 1 and 100!")
    
    while True:
        try:
            guess = int(input("> ").strip())
        except ValueError:
            print("Please type a valid number!")
            continue
            
        if guess < secret_number: 
            print("Higher!")
        elif guess > secret_number: 
            print("Lower!")
        else:
            print("You win!")
            break

if __name__ == "__main__": 
    main()
Node.js (ES2026)
import * as readline from 'node:readline/promises';
import { stdin as input, stdout as output } from 'node:process';

async function main() {
    const rl = readline.createInterface({ input, output });
    const secret = Math.floor(Math.random() * 100) + 1;
    console.log("Guess the number between 1 and 100!");

    while (true) {
        const guess = parseInt((await rl.question('> ')).trim(), 10);
        if (isNaN(guess)) {
            console.log("Please type a valid number!");
            continue;
        }

        if (guess < secret) console.log("Higher!");
        else if (guess > secret) console.log("Lower!");
        else {
            console.log("You win!");
            break;
        }
    }
    rl.close();
}
main();

4. Arithmetic Command Line Game

OCaml Features Introduced: let ... in bindings, Printf module, and advanced pattern matching guards.

OCaml
let rec play_loop () =
  let a = Random.int 10 + 1 in
  let b = Random.int 10 + 1 in
  
  (* %! forces stdout to flush immediately *)
  Printf.printf "What is %d + %d? %!" a b;
  
  let input = read_line () in
  if input = "quit" then
    print_endline "Thanks for playing!"
  else
    let correct_answer = a + b in
    
    (* 'when' adds a guard condition to a match case *)
    (match int_of_string_opt input with
     | Some ans when ans = correct_answer -> 
         print_endline "Correct!"
     | Some _ -> 
         Printf.printf "Wrong! It was %d\n" correct_answer
     | None -> 
         print_endline "Please enter a number or 'quit'.");
         
    play_loop ()

let () =
  Random.self_init ();
  print_endline "Solve the addition problems! Type 'quit' to exit.";
  play_loop ()
Python 3.12+
import random

def main():
    print("Solve the addition problems! Type 'quit' to exit.")
    
    while True:
        a, b = random.randint(1, 10), random.randint(1, 10)
        user_input = input(f"What is {a} + {b}? ").strip()
        
        if user_input == "quit":
            print("Thanks for playing!")
            break
            
        try:
            if int(user_input) == a + b: 
                print("Correct!")
            else: 
                print(f"Wrong! It was {a + b}.")
        except ValueError:
            print("Please enter a number or 'quit'.")

if __name__ == "__main__": main()
Node.js
import * as readline from 'node:readline/promises';
import { stdin as input, stdout as output } from 'node:process';

async function main() {
    const rl = readline.createInterface({ input, output });
    console.log("Solve addition! Type 'quit' to exit.");

    while (true) {
        const a = Math.floor(Math.random() * 10) + 1;
        const b = Math.floor(Math.random() * 10) + 1;
        
        const userInput = (await rl.question(`What is ${a} + ${b}? `)).trim();
        if (userInput === 'quit') break;
        
        const answer = parseInt(userInput, 10);
        if (isNaN(answer)) console.log("Enter a number or 'quit'.");
        else if (answer === a + b) console.log("Correct!");
        else console.log(`Wrong! It was ${a + b}.`);
    }
    rl.close();
}
main();

5. State Machine & Settings

OCaml Features Introduced: Variants (Algebraic Data Types), Record syntax, and immutable state passing.

OCaml (The power of Variants)
(* Variants form the backbone of safe State Machines *)
type operation = Add | Multiply
type app_state = Menu | Playing | Quit

(* Record syntax allows us to name fields in a complex structure *)
type settings = {
  min_val : int;
  max_val : int;
  op : operation;
}

let default_settings = { min_val = 1; max_val = 10; op = Add }

(* We pass the State and Settings explicitly. No global variables! *)
let rec run_machine state settings =
  match state with
  | Quit -> ()
  | Menu ->
      Printf.printf "1. Play  2. Set Multiply  3. Quit\n> %!";
      (match read_line () with
       | "1" -> run_machine Playing settings
       (* We use 'with' to create a new copy of settings, updating only 'op' *)
       | "2" -> 
           print_endline "Operation set to Multiplication.";
           run_machine Menu { settings with op = Multiply }
       | "3" -> run_machine Quit settings
       | _   -> 
           print_endline "Invalid option.";
           run_machine Menu settings)

  | Playing ->
      let a = Random.int (settings.max_val - settings.min_val + 1) + settings.min_val in
      let b = Random.int (settings.max_val - settings.min_val + 1) + settings.min_val in
      
      let symbol, correct = 
        match settings.op with
        | Add      -> ("+", a + b)
        | Multiply -> ("*", a * b)
      in

      Printf.printf "What is %d %s %d? ('menu' to go back) %!" a symbol b;
      let input = read_line () in
      
      if input = "menu" then
        run_machine Menu settings
      else begin
        (match int_of_string_opt input with
         | Some ans when ans = correct -> print_endline "Correct!"
         | Some _ -> Printf.printf "Wrong, it was %d\n" correct
         | None -> print_endline "Not a number.");
        run_machine Playing settings
      end

let () =
  Random.self_init ();
  run_machine Menu default_settings

6. Flashcards Quizzer: Higher Order Functions

OCaml Features Introduced: List.iter and iterating over data functionally.

type flashcard = { question : string; answer : string }

let ask_card card =
  Printf.printf "Q: %s (Press Enter)%!" card.question;
  let _ = read_line () in
  Printf.printf "A: %s\n\n" card.answer

let () =
  let deck = [
    { question = "Capital of France?"; answer = "Paris" };
    { question = "2 ** 8?"; answer = "256" };
    { question = "OCaml build tool?"; answer = "Dune" }
  ] in

  (* Instead of `for` loops, we use `List.iter` to run an action over a list *)
  List.iter ask_card deck;
  
  print_endline "Deck completed!"

7. Fullstack Web: js_of_ocaml & Bonsai

OCaml Features Introduced: Compiling to Javascript (js_of_ocaml) and Jane Street's Incremental UI framework (Bonsai).

OCaml doesn't just run on the backend. The compiler can emit highly optimized Javascript using js_of_ocaml. If you build web apps, Jane Street open-sourced Bonsai, a library that calculates DOM updates incrementally (instead of virtual DOM diffing).

open! Core
open! Bonsai_web

(* A Bonsai component is purely functional and incremental *)
let counter_component =
  (* Create a piece of state. It returns a 'value' and an 'inject' function to update it *)
  let%sub state, set_state = Bonsai.state (module Int) ~default_model:0 in
  
  (* We map over the state to return a Virtual DOM node *)
  return
    (let%map state = state and set_state = set_state in
     Vdom.Node.div
       [ Vdom.Node.button
           ~attr:(Vdom.Attr.on_click (fun _ -> set_state (state + 1)))
           [ Vdom.Node.text "Increment" ]
       ; Vdom.Node.text (sprintf " Count: %d" state)
       ])

let () =
  (* Mount the app to the DOM *)
  Bonsai_web.Start.start counter_component

8. The Game Changer: OCaml 5 Multicore

Python is struggling to remove the GIL. Javascript runs on a single thread. OCaml 5 introduces true shared-memory parallel domains and algebraic effect handlers.

1. Parallel Domains

Domains map 1:1 to OS threads. Because OCaml data is largely immutable, you can share data structures across domains effortlessly without locks.

let expensive_task n =
  (* Heavy CPU work *)
  fib n

let () =
  (* Spawn two tasks in parallel on separate CPU cores *)
  let d1 = Domain.spawn (fun () -> expensive_task 40) in
  let d2 = Domain.spawn (fun () -> expensive_task 41) in
  
  (* Wait for them to finish *)
  Printf.printf "Result 1: %d\n" (Domain.join d1);
  Printf.printf "Result 2: %d\n" (Domain.join d2)

2. Effect Handlers (Eio)

For I/O bound concurrency (like thousands of HTTP requests), OCaml 5 uses Effect Handlers via the Eio library. It looks like synchronous blocking code, but the runtime automatically yields the thread! No async/await coloring.

open Eio.Std

let fetch_url env url =
  (* Looks blocking, but yields underneath! *)
  traceln "Fetching %s..." url;
  Eio.Time.sleep env#clock 1.0;
  traceln "Done %s" url

let () =
  Eio_main.run @@ fun env ->
  (* Run both concurrently on a single thread *)
  Fiber.both
    (fun () -> fetch_url env "A")
    (fun () -> fetch_url env "B")

OCaml 5 Concurrency Model

How work is distributed

CPU
Domains (Parallelism)

Utilizes multiple cores. Best for heavy math, parsing, and number crunching.

+
I/O
Fibers / Effects (Concurrency)

Lightweight tasks (like Goroutines) running on top of domains. Best for databases/network.

Eradicate "Callback Hell" and colored async functions entirely.

9. Tips, Tricks & Beginner Gotchas

OCaml has a few quirks that trip up developers coming from Python or JS. Here are the immediate "gotchas" you should know.

+.

Gotcha: Float vs Integer Operators

In OCaml, + is strictly for integers! If you try to add floats like 3.14 + 2.0, the compiler will yell at you. For floats, you must use a dot suffix: +., -., *., /..
Correct: 3.14 +. 2.0

|>

The Pipeline Operator |>

Instead of wrapping functions inside functions process(sort(filter(list))), OCaml uses the pipeline operator to pass data forward, similar to a Unix pipe.

list |> filter |> sort |> process
This reads left-to-right and is incredibly idiomatic in OCaml.

=

Structural vs Physical Equality (= vs ==)

This is the exact opposite of Javascript!
In OCaml, = checks Structural Equality (do these two lists have the same values?). You should use = 99% of the time.
== checks Physical Equality (are these the exact same pointer in memory?). It is rarely used.

;;

The Double Semicolon ;;

When using the REPL (toplevel), you must end expressions with ;; to tell the compiler "evaluate this now". However, in compiled .ml files, you rarely ever write ;;.

10. Ecosystem Taxonomy

OCaml's ecosystem has a few standard libraries and popular tools. Here is what you need to know.

The built-in OCaml Stdlib is intentionally small and conservative. Many modern applications use Jane Street's Base or Core as a replacement standard library. It standardizes labeled arguments and modernizes the API. If a tutorial says open! Core, it's using the Jane Street standard library!

Yojson is the standard JSON library. ppx_deriving_yojson automatically generates the serialization code for your types!

(* The [@@deriving] attribute uses a PPX macro to auto-generate from_yojson and to_yojson *)
type user = {
  id : int;
  name : string;
  is_active : bool;
} [@@deriving yojson]

let () =
  let json_str = {| {"id": 1, "name": "Alice", "is_active": true} |} in
  let json = Yojson.Safe.from_string json_str in
  
  match user_of_yojson json with
  | Ok u -> Printf.printf "Parsed user: %s\n" u.name
  | Error err -> Printf.printf "Failed to parse: %s\n" err

Dream is the go-to web framework for OCaml today. It’s easy to use, highly performant, and has built-in support for sessions, CSRF, and websockets.

let () =
  Dream.run
  @@ Dream.logger
  @@ Dream.router [
       Dream.get "/" (fun _ ->
         Dream.html "Hello, modern web!");
       
       Dream.get "/echo/:word" (fun request ->
         let word = Dream.param request "word" in
         Dream.html (Printf.sprintf "You said: %s" word));
     ]

11. Tooling & Best Practices

The OCaml tooling ecosystem revolves around opam and dune. Don't fight them—embrace them!

opam

opam is the package manager for OCaml (like npm or pip). It also manages compiler versions (switches). You install libraries globally or locally per project via opam.

Dune

Dune is the standard build system. You define a dune file in your directory, and it figures out all dependencies, compilation order, and links everything incredibly fast. Run dune build and dune exec.

ocaml-lsp-server

The Language Server Protocol implementation for OCaml. Install the OCaml extension in VSCode, and it hooks into ocaml-lsp-server to give you instant type-hints, auto-complete, and inline errors.

ocamlformat

The standard code formatter (like Prettier or Black). Define an .ocamlformat file at your project root, integrate it with VSCode, and let it handle indentation automatically.