Tutorial: Learn Nim from Scratch (2026)
Nim is a statically typed systems language with Python-like indentation-based syntax. I have used it for CLI tools, game development, and web applications. The language compiles to C, C++, or JavaScript, giving you the performance of C with the expressiveness of a modern language.
Nim's metaprogramming capabilities are extraordinary. The AST is accessible at compile time, letting you write macros that transform code as naturally as processing data.
Metaprogramming
Nim macros operate on the AST at compile time. They receive NimNode objects and return transformed AST. This allows creating DSLs, code generation, and compile-time optimizations. Templates are simpler than macros: they do textual substitution with parameter injection. Static procedures run at compile time with static blocks.
import macros
macro assert(cond: untyped): untyped =
result = quote do:
if not `cond`:
raise newException(AssertionError,
"Assertion failed: " & `cond`.toStrLit.strVal)
assert(2 + 2 == 4)
template measure(body: untyped): untyped =
let start = cpuTime()
body
echo "Elapsed: ", cpuTime() - start
Types and Generics
Nim's type system includes ordinals, enums, sets, objects, tuples, and variants (with case). Generics use square brackets: proc foo[T](x: T). Concepts constrain generic types with requirements. Objects can be either ref (heap, garbage collected) or value types on the stack.
type
Color = enum cRed, cGreen, cBlue
Person = object
name: string
age: int
JsonNode = ref object
case kind: JsonKind
of jString: strVal: string
of jNumber: numVal: float
of jArray: children: seq[JsonNode]
proc first[T](s: seq[T]): T =
s[0]
let nums = @[1, 2, 3]
echo first(nums)
Performance
Nim compiles to C with no runtime overhead in critical paths. You can use {.pure.}, {.packed.}, and {.align.} pragmas for fine control. The GC is optional — you can use --gc:arc or --gc:orc for deterministic reference counting. The noGC option disables GC entirely for hard real-time systems.
type Vec3 = object
x, y, z: float32
{.push pure, align: 16.}
type Matrix = array[4, Vec3]
{.pop.}
proc dot(a, b: Vec3): float32 {.inline.} =
a.x * b.x + a.y * b.y + a.z * b.z
proc matVecMul(m: Matrix, v: Vec3): Vec3 =
result.x = dot(m[0], v)
result.y = dot(m[1], v)
result.z = dot(m[2], v)
Macros
Nim's macro system gives full access to the AST. dumpTree prints AST for debugging. newStmtList, newCall, newIdentNode build AST nodes programmatically. quote do: creates AST from Nim code. Slots (backtick) interpolate nodes. Macros can generate types, procedures, and even modify the compiler's behavior.
import macros
macro enumToString(T: typedesc): untyped =
result = newStmtList()
for sym in T.getType[1..^1]:
let name = sym.strVal
result.add quote do:
proc `name`(x: `T`): bool =
x == `sym`
macro dsl(body: untyped): untyped =
body.expectKind(nnkStmtList)
for child in body:
child.expectKind(nnkCommand)
echo child[0].repr, " -> ", child[1].repr
Concurrency
Nim uses threads with shared memory protected by channels and locks. The standard library provides threadpool for a spawn/sync model. For async I/O, Nim has asyncmacro and asyncdispatch similar to Python's asyncio. The spawn statement executes a procedure on a thread pool thread.
import threadpool
proc process(i: int): int =
result = i * i
let responses = collect:
for i in 0..<100:
spawn process(i)
sync()
for r in responses:
echo r
import asyncdispatch
proc fetch(url: string): Future[string] {.async.} =
let client = newAsyncHttpClient()
result = await client.getContent(url)
FFI
Nim calls C and C++ directly with no wrappers needed. The {.importc.} pragma binds to C functions. {.header.} includes C headers. {.nodecl.} suppresses declaration generation. For C++ use {.importcpp.}. Nim can also compile to JavaScript for frontend code, sharing types and procedures between client and server.
proc printf(frmt: cstring): cint {.importc, varargs, header: "".}
proc strlen(s: cstring): csize_t {.importc, header: "".}
printf("Hello from Nim! pi = %.2f\n", 3.14159)
# Export to C
proc myFunction(x: cint): cint {.exportc.} =
x * 2
Frequently Asked Questions
Nim vs Python?
Nim compiles to native code, has static types, and runs orders of magnitude faster. Python has a larger ecosystem. Syntax is similar.
Is Nim garbage collected?
Yes by default, but you can choose --gc:arc (reference counting), --gc:orc (cycle collector), or --gc:none (manual).
What is the difference between var and let?
var declares mutable variables. let declares immutable variables. const is compile-time constant.
How does Nim handle null?
Nim does not have null. Use Option[T] from the standard library for optional values. References are non-nil by default.
Originally published on Ayodhyyya. Last updated June 1, 2026.