Beginner

Variables and Types in Odin

Learn about variables, data types, distinct types, and type conversions in Odin with practical Docker-ready examples

Odin is statically and strongly typed — every variable has a fixed type determined at compile time. Odin’s declaration syntax reads left-to-right: name : type = value for variables, name :: value for constants. This contrasts with C’s right-to-left type name ordering and makes declarations read naturally as “name is a type, initialized to value.”

What sets Odin apart from most systems languages is its distinct type system. You can create new types that share the same underlying representation as an existing type but are completely incompatible at compile time. This catches unit-mismatch bugs — like accidentally dividing meters by kilograms — before your code ever runs. Combined with Odin’s strict rule against implicit type conversions, the compiler becomes a powerful ally for correctness.

In this tutorial you will learn how to declare variables and constants, work with Odin’s primitive types, convert between types explicitly, and use distinct types and enumerations to make your code safer.

Variable Declarations

Odin offers two declaration styles. Explicit declarations spell out the type: name : type = value. Short declarations use := to let the compiler infer the type from the value. Constants use :: and are evaluated at compile time. Every variable in Odin has a zero value — the type-specific default when no initializer is provided.

Create a file named variables.odin:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
package main

import "core:fmt"

main :: proc() {
    // Explicit typed declarations: name : type = value
    age : int = 30
    name : string = "Odin Developer"
    pi : f64 = 3.14159
    active : bool = true

    fmt.println("=== Explicit Declarations ===")
    fmt.println("age:", age)
    fmt.println("name:", name)
    fmt.println("pi:", pi)
    fmt.println("active:", active)

    // Short declarations with type inference: name := value
    city := "Stockholm"
    year := 2016
    ratio := 0.618

    fmt.println()
    fmt.println("=== Short Declarations ===")
    fmt.println("city:", city)
    fmt.println("year:", year)
    fmt.println("ratio:", ratio)

    // Zero values — every type has a well-defined default
    zero_int : int
    zero_float : f64
    zero_bool : bool

    fmt.println()
    fmt.println("=== Zero Values ===")
    fmt.println("int:", zero_int)
    fmt.println("f64:", zero_float)
    fmt.println("bool:", zero_bool)

    // Constants use :: and are evaluated at compile time
    PI :: 3.14159265358979
    APP_NAME :: "CodeArchaeology"
    MAX_RETRIES :: 5

    // Typed constant: name : type : value
    TIMEOUT : int : 30

    fmt.println()
    fmt.println("=== Constants ===")
    fmt.println("PI:", PI)
    fmt.println("App:", APP_NAME)
    fmt.println("Max retries:", MAX_RETRIES)
    fmt.println("Timeout:", TIMEOUT)
}

The declaration patterns break down like this: a single : introduces the type, and = assigns a value. Two colons :: means the binding is constant. So x : int = 5 is a mutable variable, x := 5 is a mutable variable with inferred type, X :: 5 is a constant with inferred type, and X : int : 5 is a constant with an explicit type.

Zero values guarantee that variables are always initialized. Numeric types default to 0, booleans to false, and strings to the empty string. This eliminates the undefined-variable bugs common in C.

Types, Conversions, and Distinct Types

Odin provides sized integer types with guaranteed widths (i8, i16, i32, i64 and their unsigned counterparts u8, u16, u32, u64), two float types (f32, f64), and built-in string, bool, and rune types. The plain int type is pointer-sized (64 bits on most modern platforms).

Odin requires explicit casts for all type conversions — there are no implicit promotions or coercions. You convert with target_type(value), for example f64(my_int). This strictness extends to Odin’s distinct types, which let you create new types that share an underlying representation but cannot be mixed accidentally.

Create a file named variables_types.odin:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
package main

import "core:fmt"

// Distinct types: same underlying representation, incompatible at compile time
Meters :: distinct f64
Seconds :: distinct f64

// Enumeration type
Direction :: enum {
    North,
    South,
    East,
    West,
}

main :: proc() {
    // Sized integer types
    small : i8 = 127
    medium : i32 = 2_147_483_647
    large : i64 = 9_223_372_036_854_775_807
    unsigned : u8 = 255

    fmt.println("=== Integer Types ===")
    fmt.println("i8:", small)
    fmt.println("i32:", medium)
    fmt.println("i64:", large)
    fmt.println("u8:", unsigned)

    // Float types
    single : f32 = 3.14
    double : f64 = 3.141592653589793

    fmt.println()
    fmt.println("=== Float Types ===")
    fmt.println("f32:", single)
    fmt.println("f64:", double)

    // Strings and runes
    greeting := "Hello, World"

    fmt.println()
    fmt.println("=== Strings and Runes ===")
    fmt.println("greeting:", greeting)
    fmt.println("length:", len(greeting))
    fmt.printf("first byte as char: %c\n", greeting[0])

    // Explicit type conversions — no implicit conversions in Odin
    int_val : i32 = 42
    float_val := f64(int_val)
    back_to_int := i32(float_val + 0.9)  // Truncates toward zero

    fmt.println()
    fmt.println("=== Type Conversions ===")
    fmt.println("i32 to f64:", float_val)
    fmt.println("f64 to i32 (42.9):", back_to_int)

    // Distinct types in action
    distance : Meters = 100.0
    time_val : Seconds = 9.58

    // distance / time_val would NOT compile — the types are incompatible
    // Must convert to the shared base type (f64) for arithmetic
    speed := f64(distance) / f64(time_val)

    fmt.println()
    fmt.println("=== Distinct Types ===")
    fmt.printf("distance: %v meters\n", f64(distance))
    fmt.printf("time: %v seconds\n", f64(time_val))
    fmt.printf("speed: %.2f m/s\n", speed)

    // Enumeration values
    dir := Direction.North

    fmt.println()
    fmt.println("=== Enums ===")
    fmt.println("direction:", dir)
    fmt.println("as integer:", int(dir))
}

Distinct types are one of Odin’s most powerful features. Meters :: distinct f64 creates a type that is stored as an f64 but is type-incompatible with plain f64 and with other distinct types like Seconds. The compiler catches any attempt to mix them without an explicit cast. This prevents entire categories of unit-mismatch bugs at compile time — something that languages like C and Go cannot enforce without wrapper structs.

Enumerations in Odin are first-class types with named values that default to sequential integers starting from zero. They provide type safety — you cannot accidentally assign an integer to an enum variable without an explicit cast.

Running with Docker

1
2
3
4
5
6
7
8
# Pull the Odin image
docker pull primeimages/odin:latest

# Run the variable declarations example
docker run --rm -v $(pwd):/app -w /app primeimages/odin:latest sh -c "cp variables.odin /tmp/variables.odin && cd /tmp && odin run ."

# Run the types and conversions example
docker run --rm -v $(pwd):/app -w /app primeimages/odin:latest sh -c "cp variables_types.odin /tmp/variables_types.odin && cd /tmp && odin run ."

Each file is copied to /tmp before compilation because odin run . compiles all .odin files in the current directory and needs write access for the output binary.

Expected Output

Output from variables.odin:

=== Explicit Declarations ===
age: 30
name: Odin Developer
pi: 3.14159
active: true

=== Short Declarations ===
city: Stockholm
year: 2016
ratio: 0.618

=== Zero Values ===
int: 0
f64: 0
bool: false

=== Constants ===
PI: 3.14159265358979
App: CodeArchaeology
Max retries: 5
Timeout: 30

Output from variables_types.odin:

=== Integer Types ===
i8: 127
i32: 2147483647
i64: 9223372036854775807
u8: 255

=== Float Types ===
f32: 3.14
f64: 3.141592653589793

=== Strings and Runes ===
greeting: Hello, World
length: 12
first byte as char: H

=== Type Conversions ===
i32 to f64: 42
f64 to i32 (42.9): 42

=== Distinct Types ===
distance: 100 meters
time: 9.58 seconds
speed: 10.44 m/s

=== Enums ===
direction: .North
as integer: 0

Key Concepts

  • Left-to-right declarationsname : type = value for variables, name :: value for constants, name := value for inferred variables
  • No implicit type conversions — Odin requires explicit casts like f64(my_int), preventing silent data loss and type confusion
  • Zero values guarantee every variable is initialized — 0 for numbers, false for bools, "" for strings
  • Distinct types create compile-time-incompatible types from the same underlying representation, catching unit-mismatch bugs before the code runs
  • Sized integer types (i8, i16, i32, i64 and unsigned variants) have guaranteed widths, unlike C’s platform-dependent sizes
  • Enumerations are first-class types with named values and integer backing, providing type-safe alternatives to raw constants
  • Typed constants (NAME : type : value) let you constrain a constant to a specific type, while untyped constants (NAME :: value) adapt to their usage context
  • Underscores in numeric literals (2_147_483_647) improve readability without affecting the value

Running Today

All examples can be run using Docker:

docker pull primeimages/odin:latest
Last updated:

Comments

Loading comments...

Leave a Comment

2000 characters remaining