Modules & Packages
*. This lesson covers the export rule, the four import forms, initialization order, and how nimble turns a folder of modules into a package.
Modules and Import
One file is one module. To build real programs you split responsibilities across files and let the compiler join them; the export marker is the only thing standing between your internals and the rest of the program.
Defining and Using a Module
Two files, one library and one program. The library exports three symbols and keeps one helper private.
# mathx.nim — a library module
const Pi* = 3.14159 # '*' exports the constant
proc square*(x: float): float = # '*' exports the procedure
x * x
proc secret(): int = # no '*': invisible to importers
42
proc describe*(): string =
"secret is " & $secret() # private helpers stay usable inside the module
# main.nim — the program that consumes the module
import mathx # module name = file name, same folder
echo Pi # 3.14159
echo square(3.0) # 9.0
# echo secret() # compile error: 'secret' is not exported
echo describe() # secret is 42
The compiler reports an unexported symbol exactly like a misspelling: the name simply does not exist outside the module. That is the point — the module boundary is enforced at compile time, not by convention.
Import Forms
import std/strutils # everything the module exports
from std/strutils import parseInt # only the names you list
import std/[algorithm, sequtils] # the bracket form: several modules at once
import std/tables as tbl # keep a handle for qualified access
var data = @[3, 1, 2]
data.sort # from algorithm, imported unqualified
echo data.sorted # 'sorted' returns a NEW sequence
echo tbl.newTable[string, int]() # qualified through the alias, collision-free
Prefer from ... import when a symbol name is short and generic; it documents at the top of the file where that name comes from. Prefer an alias or a qualified import when two modules export the same name.
Visibility Beyond Procedures
type
Counter* = object # exported type …
value: int # … with a PRIVATE field: no direct poking
proc newCounter*(): Counter = Counter(value: 0)
proc increment*(c: var Counter) = inc c.value # controlled mutation
proc value*(c: Counter): int = c.value # read-only accessor
var count = newCounter()
count.increment()
count.increment()
echo count.value # 2
# count.value = 99 # compile error: the field is private
Exporting the type but not the field is the standard Nim encapsulation idiom: callers can store and pass a Counter, but only the module can change its representation. You may later change value to a different layout without breaking a single caller.
Module Initialization
Importing a module also runs its top-level code — once per program, before your main body continues. Understanding that order prevents the classic "why did this print before anything else?" confusion.
Top-Level Statements
# lib.nim
echo "initializing lib" # runs the first time 'lib' is imported
let limit* = 10
# main.nim
import lib # prints: initializing lib
import lib # second import is a no-op: init runs once
echo limit # 10
Keep top-level code in a library minimal: constants, type definitions, and cheap setup. Anything expensive belongs in an explicit init procedure so the caller decides when to pay for it.
when isMainModule
The same file can be a library and a program. isMainModule is true only when the file is the one passed to the compiler, so the demo code never runs for importers.
# cli.nim — usable as a library and runnable as a program
import std/os
proc greet*(name: string): string = "hello " & name
when isMainModule: # true only for 'nim r cli.nim'
let name = if paramCount() > 0: paramStr(1) else: "world"
echo greet(name)
include versus import
# include pastes the file's text into THIS module: same scope, no new namespace,
# no export markers, no boundary. Use it to split one module across files.
include "shared_defs.nim"
# import creates a real module boundary: private by default, explicit exports.
import shared_helpers
echo shared_helpers.publicName()
Rule of thumb: include for generated or platform-specific fragments of one module, import for everything that deserves a boundary.
Packages and Nimble
A package is a folder with a .nimble manifest at its root. The manifest declares metadata, dependencies and build targets; the nimble tool reads it to build, test, install and publish.
The Nimble Manifest
# myapp.nimble — package manifest read by the nimble tool
version = "0.1.0"
author = "Elucian Moise"
description = "Small Nim CLI example"
license = "MIT"
srcDir = "src" # modules live under src/
bin = @["myapp"] # compile src/myapp.nim into an executable
requires "nim >= 2.0.0" # dependencies, resolved from the nimble store
nimble build # compile every 'bin' target
nimble run # build, then run the first binary
nimble test # run the modules under tests/
nimble install # install this package (or a dependency) locally
Project Layout
myapp/
myapp.nimble # manifest: metadata, deps, targets
src/
myapp.nim # entry point named after the package (convention)
mylib.nim # library modules
tests/
test_mylib.nim # files starting with 'test' are picked up by nimble test
docs/
Name the entry module after the package: for a package mylib, the importable root module is src/mylib.nim. Consumers then write import mylib and never see the folder layout.
Compiler Configuration
# nim.cfg — plain configuration; the same folder as the .nimble file
--path:"src" # additional module search path
--define:release # compile with optimizations
--warningAsError:"UnusedImport"
# config.nims — the same settings as a script, for logic the plain file cannot express
switch("path", "src")
if hostOS == "windows":
switch("define", "windowsBuild")
Reach for config.nims only when a setting must be computed; nim.cfg is easier to read and review.
Review Checklist
- Export the smallest surface that callers need; keep fields private and expose accessors.
- Prefer
from x import yso the origin of every name is obvious. - Guard demo code with
when isMainModuleinstead of deleting it. - Keep top-level code cheap; move real setup into an explicit
init. - One responsibility per module, named after the responsibility — not after the type count.