Skip to content

`@import`

One of the first Zig builtins you will learn is @import.

@import

One of the first Zig builtins you will learn is @import.

You have already seen it many times:

const std = @import("std");

This imports Zig’s standard library and gives it the local name std.

Without @import, your program would not know where std.debug.print comes from.

@import is how Zig loads code from other files and modules.

It is one of the most important builtins in the language.

What @import Does

@import loads another Zig source file or builtin module.

Example:

const math = @import("math.zig");

This loads the file math.zig.

You can then access declarations inside that file:

const result = math.add(10, 20);

Think of @import as: “load this file and give me access to its contents.”

A Simple Example

Suppose you have this project:

project/
├── main.zig
└── math.zig

Contents of math.zig:

pub fn add(a: i32, b: i32) i32 {
    return a + b;
}

Contents of main.zig:

const std = @import("std");
const math = @import("math.zig");

pub fn main() void {
    const result = math.add(5, 7);

    std.debug.print("{}\n", .{result});
}

Output:

12

The file main.zig imports math.zig, then calls math.add.

pub Makes Declarations Visible

Notice this line:

pub fn add(a: i32, b: i32) i32 {

The word pub means public.

Only public declarations are accessible from imported files.

Example:

fn hidden() void {}

This function is private to its file.

If another file imports this module, it cannot access hidden.

But this works:

pub fn visible() void {}

This is an important Zig design rule: visibility is explicit.

Importing the Standard Library

The most common import is:

const std = @import("std");

"std" is a special builtin module provided by the compiler.

It gives access to Zig’s standard library.

For example:

std.debug.print("hello\n", .{});

or:

const ArrayList = std.ArrayList;

The standard library contains collections, allocators, file APIs, networking, JSON support, testing tools, threading primitives, and much more.

You will use std constantly.

Import Paths

@import takes a string path.

Example:

@import("utils.zig")

Relative paths are based on the importing file.

Suppose:

src/
├── main.zig
└── utils/
    └── math.zig

From main.zig:

const math = @import("utils/math.zig");

The path uses forward slashes.

Imported Files Are Namespaces

When you import a file, the file becomes a namespace.

Example:

const math = @import("math.zig");

Then:

math.add(1, 2)
math.sub(10, 3)
math.mul(4, 5)

Everything stays grouped under math.

This avoids name conflicts.

For example, imagine two files both define open:

const file_api = @import("file.zig");
const network_api = @import("network.zig");

Then:

file_api.open(...)
network_api.open(...)

The namespaces keep things clear.

Importing Specific Declarations

Sometimes you want a shorter name.

Example:

const std = @import("std");
const print = std.debug.print;

Now:

print("hello\n", .{});

This is common in Zig code.

You still imported std, but you created a local shortcut.

@import Happens at Compile Time

Imports are resolved during compilation.

The compiler reads the imported files, checks them, and builds them into the final program.

There is no runtime importing system like Python’s import or JavaScript’s require.

This means:

  • imports are static
  • the compiler knows the full program structure
  • unused code can often be removed
  • errors appear during compilation

Zig Does Not Use Header Files

In C, you often have:

math.h
math.c

The .h file contains declarations.

The .c file contains implementations.

Zig does not use this model.

A Zig file contains both declarations and implementations together.

Example:

pub fn add(a: i32, b: i32) i32 {
    return a + b;
}

No separate header file is needed.

This simplifies many projects.

Circular Imports

Be careful with modules importing each other.

Example:

a.zig imports b.zig
b.zig imports a.zig

This can create dependency cycles.

Zig tries to keep compilation simple and predictable, so circular dependencies are usually a design problem.

A better design often extracts shared code into a third module.

Example:

common.zig

Both modules import common.zig instead of importing each other.

Importing Builtin Modules

Some names are special.

Example:

const builtin = @import("builtin");

This gives information about the current target and compiler settings.

Example:

builtin.os.tag
builtin.cpu.arch

You can use this for platform-specific behavior.

Example:

if (builtin.os.tag == .windows) {
    // Windows code
} else {
    // Other systems
}

This is common in systems programming.

Zig Encourages Small Modules

A Zig project is usually split into many small files.

For example:

src/
├── main.zig
├── lexer.zig
├── parser.zig
├── token.zig
├── ast.zig
└── codegen.zig

Each file handles one area of the program.

Then main.zig imports what it needs.

This style keeps projects easier to understand.

Importing Does Not Automatically Run Code

In some languages, importing a module can execute top-level code automatically.

Zig avoids most hidden behavior.

Top-level declarations are evaluated during compilation when needed, but Zig does not use module initialization systems like many scripting languages.

Usually, behavior starts from main.

This makes program startup easier to reason about.

A More Realistic Example

math.zig:

pub fn square(x: i32) i32 {
    return x * x;
}

pub fn cube(x: i32) i32 {
    return x * x * x;
}

main.zig:

const std = @import("std");
const math = @import("math.zig");

pub fn main() void {
    const a = math.square(4);
    const b = math.cube(3);

    std.debug.print("square: {}\n", .{a});
    std.debug.print("cube: {}\n", .{b});
}

Output:

square: 16
cube: 27

This pattern appears constantly in Zig projects.

Common Beginner Mistakes

Forgetting pub

Example:

fn add(a: i32, b: i32) i32 {
    return a + b;
}

Then another file tries:

math.add(1, 2)

This fails because add is private.

You need:

pub fn add(a: i32, b: i32) i32 {
Wrong Relative Path

Example:

@import("math.zig")

If the file is actually inside utils/, the path is wrong.

Correct:

@import("utils/math.zig")
Forgetting the Namespace

Example:

const math = @import("math.zig");

add(1, 2)

This fails because add belongs to the math namespace.

Correct:

math.add(1, 2)

Key Idea

@import is Zig’s mechanism for loading modules and source files.

Imported files become namespaces.

Public declarations are exposed with pub.

Imports are resolved at compile time, not runtime.

When you see:

const something = @import("file.zig");

read it as:

“Load this Zig module and make its public declarations available through the name something.”