Odin · packages & imports

Packages, folders, and imports — four levels deep

A package is a directory of .odin files, each starting with the same package <name> line. Every file in that folder shares one flat scope — a top-level name in one file is just there for the next file, no import between them. Click a name to follow where it's used.

Both files below build together as one package. Output is real: claim shared-scope.
📁 greeter/
main.odin
package main // reads BANNER + calls greet — // both live in the OTHER file, // no import between them. main :: proc() { fmt.println(BANNER) fmt.println(greet("packages")) }
greeting.odin
package main // same folder, same package line // -> same scope as main.odin. BANNER :: "one folder, one package, one scope" greet :: proc(name: string) -> string { return fmt.aprintf("hello, %s", name) }
two files · one package main · one shared top-level scope
$ odin run .   # build the FOLDER as a package
one folder, one package, one scope
hello, packages

The whole rule

directory → one package
package main × 2 files
shared names: BANNER, greet
imports between them: 0

What you're seeing

no per-file privacy by default Every top-level name is visible to the whole package. To narrow that you opt in to @(private="file") (hidden from sibling files too) or @(private) (hidden from other packages). Visibility is something you add, not the default.

Sharing scope ends at the folder edge. To reach another package you import it — that binds a local name you call through (fmt.println). The string after a colon prefix says where to look; an optional name before the path renames it locally.

Three places the compiler looks

core:
The standard library that ships with the compiler.
import "core:fmt"
vendor:
Bundled bindings to common libraries (sdl3, raylib, glfw…).
import "vendor:sdl3"
"./path"
A relative path to your own folder — no prefix, just the path.
import "./mymath"

The alias renames the package — locally

A name before the path is a local alias. Inside this file the package answers only to that name. Toggle it:


    

the alias replaces the name — it doesn't add one Once you write import strs "core:strings", the bare name strings is undeclared in that file. Reaching for it is a compile error: Undeclared name: strings (claim bare-name-undeclared). Aliasing is per-file; another file in the same package can bind a different name or use the default.

Level 2 sold you the import. Level 3 is the rulebook the compiler enforces around it — and each rule shows up as a refusal at build time, never a runtime surprise. Click one to see the exact error.

Four ways the package system can bite, four real compiler messages. Every string below is what the compiler actually printed.

The through-line: the directory boundary is load-bearing, and so is the package name on every file in it. Disagree on the name, point at a folder that isn't there, reach past @(private), or bind two packages to the same identifier — the compiler stops the build and tells you which one. None of these can survive to run.

@(private) is name-resolution only — no runtime cost @(private) is a decision the compiler makes once, during name resolution, about whether a name is visible outside its package. There's no check, no flag, no overhead at run time — the symbol is simply absent from what other packages can see, exactly like the four refusals above happen before a program exists.

All of it converges on one command. odin run . builds the current folder as a package — every .odin file in it, plus every package those files import. The single-file shortcut (-file) can't do this; a relative import needs the whole folder in view.

Here is the full lesson program — a package main that aliases core:strings, imports the sibling mymath folder (itself two files sharing scope), and calls across all of it. One odin run ., every name resolved:

$ odin run .
lesson 01b - packages and imports
contains 'hello' in 'hello world': true
length of [3, 4] = 5.0000
circumference of r=1 circle = 6.2832
mymath version: 0.1.0
shouted: HELLOPE
PI from constants.odin, used by vectors.odin: 3.14159265358979

The emergent payoff: that last line is the whole idea in one print. PI is defined in mymath/constants.odin; it's used by mymath/vectors.odin with no import between them — because they're one package. main reaches the result through a single import "./mymath" and the mymath. qualifier. Folders gave you free intra-package sharing; imports gave you deliberate, named cross-package access. Two keywords, and the scope graph of an entire program falls out of the directory tree.

And then they're gone: packages are a compile-time concept only. There's no runtime package object, no reflection over packages, and an import runs no code — it just tells the compiler to also consider those names while resolving this file. By the time the program runs, every name is an address and the boundaries have been erased.

L1 a folder of files sharing one flat scope, no imports between them → L2 an import binds a name from another package, an alias renaming it locally → L3 the compiler enforces the boundary with four exact refusals → L4 odin run . builds the folder, wires every import, then erases the packages at runtime.

Every output line and error string on this page is real compiler output, captured by claims under
claims/lessons/01b-packages-and-imports — shared-scope · alias-in-use · bare-name-undeclared · solution · break-package-mismatch · break-missing-import · break-cross-private · break-name-collision.