Zig Variables and Constants

Zig draws a sharp line between values that change and values that stay fixed. This distinction is not just a style choice — it is enforced by the compiler. Understanding var and const is the foundation for writing correct Zig programs.

The Container Analogy

  const score = 100;
  +---------------+
  |  score = 100  |  ← Sealed box. Cannot change.
  +---------------+

  var lives = 3;
  +---------------+
  |  lives = 3    |  ← Open box. Contents can change.
  +---------------+
  
  lives = lives - 1;
  +---------------+
  |  lives = 2    |
  +---------------+

A const is a sealed container. You set the value once and it stays that way for the entire program. A var is an open container — you can replace what is inside at any time.

Declaring Constants

const max_speed = 300;
const gravity = 9.8;
const app_name = "Zig Learner";

Use const for values you know will not change — configuration values, mathematical constants, fixed strings, and anything that represents a truth about your program rather than a changing state. The compiler enforces this: trying to reassign a const causes a compile error immediately.

Why Prefer const by Default

Zig encourages using const whenever possible. A value that cannot change is a value that cannot cause unexpected bugs later. When you read code written with const, you know exactly what that value will be at any point in the program. Code becomes easier to reason about.

Declaring Variables

var score: i32 = 0;
score = score + 10;
score = score + 5;

Use var when the value must change during the program's execution — counters, user input, running totals, game state, and similar data. The i32 is the type: a 32-bit signed integer. Zig requires you to specify a type for var declarations when the compiler cannot infer it.

Type Inference

Zig can figure out the type from the value you assign, so you do not always need to write the type explicitly:

const pi = 3.14159;      // Zig infers: f64 (floating point)
const count = 42;        // Zig infers: comptime_int
const message = "Hello"; // Zig infers: *const [5:0]u8 (string)

When Type Inference Works and When It Does Not

  const x = 10;       ← Works. Zig sees 10 and infers comptime_int.
  var y = 10;         ← Works. Zig infers comptime_int.
  var z: i32 = 10;    ← Works. You told Zig the exact type.
  var w;              ← ERROR. No value and no type. Zig has nothing to infer.

When you write var without an initial value, you must provide a type explicitly. The compiler needs to know how much memory to reserve for the variable.

Undefined — Allocating Without Initializing

Sometimes you want to declare a variable and fill it later. Zig uses undefined as a placeholder:

var buffer: [10]u8 = undefined;
// ... later in the code ...
buffer[0] = 65;  // Now we set a value

undefined tells Zig to allocate the memory but not write any specific value into it. In debug builds, Zig fills the memory with a recognizable pattern (0xAA) to help catch bugs where you accidentally read from uninitialized memory. In release builds, the memory contains whatever was there before — reading it before writing is a bug.

Variable Types at a Glance

  Declaration          | Can Reassign? | Needs Type? | Needs Value?
  ---------------------|---------------|-------------|-------------
  const x = 5;         |     No        |  Optional   |    Yes
  const x: i32 = 5;    |     No        |   Yes       |    Yes
  var x = 5;           |     Yes       |  Optional   |    Yes
  var x: i32 = 5;      |     Yes       |   Yes       |    Yes
  var x: i32 = undef.; |     Yes       |   Yes       |  No (undef)

Shadowing — Reusing Names in Inner Blocks

const x = 10;
{
    const x = 20;  // This x lives only inside this block
    // Inside here, x is 20
}
// Out here, x is still 10

Zig allows you to declare a new variable with the same name inside an inner block. The inner variable "shadows" the outer one. Once the block ends, the inner variable disappears and the outer one is visible again. Shadowing is useful but can cause confusion if overused — use it deliberately.

Constants in Functions

Function parameters in Zig behave like const by default. You cannot modify a parameter inside a function unless you copy it first:

fn double(n: i32) i32 {
    // n = n * 2;  ← ERROR: n is const
    const result = n * 2;
    return result;
}

This design prevents a common mistake where a function accidentally modifies a caller's data through a parameter.

Comptime Constants

const BUFFER_SIZE = 1024;

Constants known at compile time — meaning their value is fixed in your source code, not computed from user input at runtime — are called comptime-known values. Zig uses these for array sizes, type parameters, and other situations where the compiler needs to know a value before the program runs. This is fundamentally different from a constant computed at runtime, like a value read from a configuration file.

Practical Example: A Simple Score Tracker

const std = @import("std");

pub fn main() void {
    const player_name = "Arjun";
    var score: i32 = 0;
    const bonus = 50;

    score += 100;  // First round
    score += 75;   // Second round
    score += bonus;

    std.debug.print("{s} scored {d} points.\n", .{player_name, score});
}

Output:

Arjun scored 225 points.

player_name and bonus never change — they are const. score grows as the game progresses — it is var. This pattern — const for fixed truths, var for changing state — is the right mental model for writing clean Zig code.

Leave a Comment

Your email address will not be published. Required fields are marked *