Data Types & Values

Everything a program touches is data, and every piece of data has a type. This lesson is the toolbox tour: which types Odin gives you out of the box, what a fresh variable holds before you touch it, how the compiler guesses types for you, and why it refuses to guess when the answer would be ambiguous.

Do not try to memorise the tables on this page. Read the explanations, run the examples, and let the tables become reference material you come back to. Types are one of those topics you learn by using, and the reasoning behind Odin's rules matters far more than the list.

Why Types Matter

A type answers two questions at once: how many bytes do I occupy? and which operations make sense on me? Answering those questions before the program runs is what lets a compiler catch mistakes that would otherwise become crashes at three in the morning.

A Type Is a Promise

When you write count: int, you are making a promise to the compiler: "this name always holds a whole number". The compiler holds you to it. Try to store the text "three" in that name and you get an error before anything runs — not a mysterious failure later.

That is the whole value of static typing: mistakes surface while you are still looking at the code.

Strong, but Not Noisy

Odin is strongly typed, which has a specific meaning here: values of different types are not mixed automatically. You will see this most clearly with numbers, where Odin is stricter than the languages most people start with.

a := 10        // an int
b := 2.5       // an f64 — a floating-point number

// sum := a + b   // ERROR: an int and an f64 are different types.
sum := f64(a) + b // OK: we said out loud what we meant

Two lines of typing, and in return a whole category of subtle numeric bug disappears. Odin's position is that if you want to mix two number types, saying so is a feature, not a chore.

The Basic Types

Odin's basic types fit comfortably on one screen. There are numbers in two flavours, a boolean, and text. Everything else in the language is built on top of these.

Whole Numbers

The everyday integer type is simply int. It is platform-sized: 64 bits on a 64-bit machine and 32 bits on a 32-bit machine, so it always matches what the CPU does best. It is also the type the compiler picks for a plain integer literal.

When the exact size matters — reading a binary file, matching a hardware register, sending data over a network — ask for a fixed width instead. Each of these types says exactly how many bits it occupies, so there is no room for doubt.

TypeWhat it holdsReach for it when
intA signed whole number, platform-sized (64 bits on a 64-bit machine)Almost always — it is the default
uintThe same size, but never negativeCounting, bit patterns, sizes
i8 i16 i32 i64Signed integers of 1, 2, 4, and 8 bytesAn exact storage size is part of the design
u8 u16 u32 u64The unsigned versions of the same widthsRaw byte data, colours, hashes
i128 u128128-bit integersVery large numbers where 64 bits are not enough
u32le u64beFixed width with a byte order (little- or big-endian)Reading file formats and network protocols
lives := 3                  // an int — the default for integer literals
shield: u8 = 250            // an unsigned byte: valid range is 0 .. 255
big :: u64(1) << 40         // a 64-bit value, shifted into position

// Odin can tell you the limits of a type, which is handy when validating input:
fmt.println(max(u8))        // 255
fmt.println(min(i8))        // -128
A friendly warning. Signed integers wrap around when you cross their limits, and Odin's default build mode checks for that at runtime. If you ever need the wrap-around on purpose, you will see how to ask for it explicitly — for now, let the compiler catch it for you.

Floating-Point Numbers

Floats hold numbers with a fractional part. Odin offers two that you will use constantly: f32 (4 bytes) and f64 (8 bytes, more precise).

A decimal literal such as 1.5 is f64 by default. When you want an f32 — half the memory, and the usual choice for graphics — say so with a type or a conversion.

ratio := 1.5                // f64 — the default for literals with a decimal point
precise: f64 = 0.1
approximate: f32 = 0.1      // half the size, half the precision
also_f32 := f32(0.1)        // the same thing, written as a conversion

That word precision deserves a demonstration, because it surprises everyone once. An f32 cannot store 7.2 exactly — it stores the closest value it can represent:

small: f32 = 7.2

fmt.println(small)            // 7.1999998 — the limited precision becomes visible
fmt.printfln("%.1f", small)   // 7.2 — formatting hides it again

Nothing is broken; this is how floating-point arithmetic works in every language. Odin simply lets you see it, and the choice between f32 and f64 is a real design decision rather than a hidden default.

Booleans

A bool has exactly two possible values: true and false. You will mostly meet booleans as the result of a comparison rather than as something you type out by hand.

temperature := 25.0
ready := true

// A comparison produces a bool, which you can store and reuse.
warm := temperature > 20.0

fmt.println(warm)    // true
fmt.println(!ready)  // false — ! inverts a boolean

Conditions in if and for must be booleans. Odin has no notion of "truthy" values, so if 1 is a type error — and that is a good thing: you will never wonder whether zero means false in someone else's code.

The families of Odin basic types: signed and unsigned integers of several widths, floating-point numbers, a boolean, and text types, with the everyday defaults highlighted
The basic types grouped by family. The highlighted entries are the ones you will type every day; the rest exist for the moments when an exact size or byte order matters.

Text

A string holds UTF-8 text. Internally it is a sequence of bytes together with a length, which has one consequence worth internalising early: len counts bytes, not characters.

name := "Odin"
greeting := "Hellope, " + name   // strings join with the + operator

fmt.println(greeting)         // Hellope, Odin
fmt.println(len(greeting))    // 13 — the number of BYTES in the text

// Back ticks make a raw string: no escape sequences, exactly the bytes you typed.
path := `C:\Users\public\notes.txt`

For plain English the two counts agree, so this distinction feels theoretical. It stops being theoretical the moment your text contains an accent, a Greek letter, or an emoji — where one visible character can occupy several bytes.

Runes

A rune is a single Unicode code point: one logical character, whatever number of bytes it takes to encode. Single quotes produce a rune; double quotes produce a string.

letter := 'A'        // a rune
newline := '\n'      // a rune holding the newline character
heart := '\u2764'    // a rune written with a Unicode escape

// Rule of thumb:
//   'x'  → one code point (rune)
//   "x"  → text (string)
Why both exist. Text is bytes because that is what files and networks carry; characters are code points because that is what humans read. Odin keeps the two types separate so you always know which one you are holding. We return to text handling in earnest once you have arrays and slices.

The Basic Types at a Glance

One table to keep nearby for the first weeks of Odin. If you remember only the right-hand column, you will pick well almost every time.

TypeCategoryReach for it when
boolLogicConditions and flags — only true or false
intIntegerThe default whole number: counts, indices, sizes you compute
uintIntegerA count that can never be negative
i8 … i64, u8 … u64IntegerAn exact storage size is part of the problem
i128, u128IntegerNumbers larger than 64 bits can hold
u32le, u64beIntegerBinary formats where the byte order is specified
f32FloatGraphics, audio, and large arrays of numbers
f64FloatGeneral mathematics — the default for decimal literals
stringTextUTF-8 text you read and print
cstringTextA NUL-terminated string for talking to C libraries
runeTextOne Unicode code point

Odin also has imaginary-number literals (write them with an i suffix) and matching complex types, plus matrix and vector types for mathematics. You will not need them for a while, but it is good to know the toolbox is deeper than the shelf you are looking at.

Every Value Starts at Zero

Java, C#, and Rust programmers are used to this; C and C++ programmers are in for a pleasant surprise. In Odin you cannot accidentally read rubbish from memory, because there is no such thing as uninitialised memory.

The Zero Value

Declare a name without giving it a value and Odin fills in the natural zero for that type. You never have to write the zero yourself.

count: int       // 0
ratio: f64       // 0.0
ready: bool      // false
name: string     // "" — an empty string
letter: rune     // 0 — the NUL code point

fmt.println(count, ratio, ready)   // 0 0 false

There Is No "Uninitialised"

The guarantee goes further than local variables. Any memory you obtain is zeroed as well, and — this is the part that surprises people — Odin has no constructors and no custom default values. A record of twenty fields starts life as twenty zeros, every time, in every context.

That sounds restrictive, and it is a deliberate trade. In exchange you get a property you can build whole debugging sessions on: if a value is nonsense, you put the nonsense there. There is no third possibility, no leftover data from something else that used the memory first.

Thinking in Odin: zero is a language guarantee rather than an implementation detail. It is also why Odin's standard types stay constructor-free — a type that cannot hide state behind a constructor is a type you can trust when you inspect it.

Type Inference and the Constant Rules

You have been writing := since the first lesson, letting the compiler choose the type. Here is exactly what it chooses, and why the rule is a little cleverer than "guess the type".

What := Chooses

The compiler looks at the literal on the right and picks the everyday type for that kind of value:

n  := 42        // int      — the default integer type
f  := 7.2       // f64      — the default floating-point type
s  := "text"    // string
c  := 'A'       // rune
ok := n > 0     // bool     — the result of a comparison

// You can always ask Odin what it decided:
fmt.println(typeid_of(type_of(f)))   // f64
fmt.println(typeid_of(type_of(n)))   // int

Two of those defaults are worth fixing in memory, because they explain most early surprises: a plain integer literal becomes an int, and a literal with a decimal point becomes an f64 — never an f32.

Constants Are Untyped Until They Must Be Typed

Now the clever part. A numeric literal such as 7.2 does not really have a type of its own. It is what Odin calls an untyped constant: it borrows the type it is being assigned to, as long as the value fits without losing precision.

small: f32 = 7.2      // OK — the constant 7.2 adopts the type f32
whole: f64 = 3        // OK — the integer constant 3 becomes 3.0
tiny:  u8  = 250      // OK — 250 fits in a byte

// too_big: u8 = 300  // ERROR — 300 cannot be represented in a u8.

This is why writing f32 values never needs a cast, while assigning one variable to another might. A constant can adapt; a variable already has a type and will not change it.

Converting Between Types

Sooner or later you must move a value from one type to another. Odin has zero patience for doing this by accident, and a simple, readable way to do it on purpose.

Explicit Conversion with T(value)

The everyday converter is the type name followed by the value in parentheses. Read it as "make me one of these from that":

a := 3            // int
b := 2.5          // f64

sum := f64(a) + b        // 5.5 — widen the int, then add
byte_value := u8(250)    // a byte, because 250 fits in one

// Narrowing on purpose: keep the low byte of a larger number.
low_byte := u8(300 % 256)   // 44

Notice what the conversions are not: they are not tricks to sneak past the type system. Each one is a small, explicit statement about your intent, and a reader can see it.

Why Odin Refuses Implicit Conversion

Most languages quietly convert between numeric types for you. Odin refuses, on principle, because those quiet conversions are a well-documented source of expensive bugs: a value that silently loses precision, a signed number that becomes unsigned, a big integer truncated into a small one.

count: int = 10
ratio: f32 = 1.5

// mixed := count * ratio        // ERROR: int and f32 are different types
mixed := f32(count) * ratio      // OK — and now the conversion is on record

Compare the two lines honestly. The second is one word longer and tells the reader that a conversion happens at exactly that point. When a numeric bug does appear — and eventually one will, in any language — this is the line where you look.

cast, transmute, auto_cast — Handle With Care

There are three sharper tools in the same family. cast(T)value converts with fewer of the value-preservation rules; transmute(T)value reinterprets the underlying bits as another type; auto_cast lets the type expected by the surrounding code decide.

They exist for real reasons — memory layout, binary formats, interop — and they will appear later in this track next to the problems they solve. For now, treat them as the sharp knives of the drawer: T(value) is the one you use every day, and you reach for the others only when you can explain precisely why.

Distinct Types

Here is a small idea with an outsized payoff. Odin lets you declare a type that has exactly the same representation as another one but is a different type as far as the compiler is concerned.

Same Bits, Different Meaning

A classic example is units. Metres and seconds are both numbers, and mixing them is a bug that has embarrassed more than one spacecraft:

Meters  :: distinct f64
Seconds :: distinct f64

distance: Meters  = 42.5
elapsed:  Seconds = 9.5

// speed := distance / elapsed            // ERROR: Meters and Seconds are different types
speed := f64(distance) / f64(elapsed)      // 4.47... — we chose to drop the units

fmt.printfln("%.2f", speed)                // 4.47

Nothing changes in memory: a Meters is an f64, eight bytes, same instructions. What changes is that the compiler will not let you treat a distance as a duration by accident. The moment you convert to plain f64, you are saying out loud that units no longer matter here.

When to Reach for distinct

You will recognise the moment in four familiar situations:

  • Physical units — metres, feet, seconds, degrees, radians.
  • Identifiers — a user id and an order id should never be interchangeable, even though both are integers.
  • Indices and counts — an index into one array is not an index into another.
  • Money and measurements — cents versus currency, bytes versus elements.

The standard library uses the same trick for its vector types — a three-component vector is declared as a distinct array type — which is how Odin gets vector behaviour without a special syntax for vectors.

Asking a Value What It Is

Two small reflection tools will save you time while you are learning, and two sizing tools will save you performance later.

typeid_of and type_of

When you are not sure what the compiler inferred, just ask it. And when you want to state an assumption about sizes, use a compile-time assertion — it costs nothing at runtime and it documents the assumption exactly where it belongs:

n := 42
fmt.println(typeid_of(type_of(n)))   // int

// #assert is checked while compiling, so a wrong assumption stops the build.
#assert(size_of(u8) == 1)            // a byte is always one byte
#assert(size_of(f32) == 4)           // an f32 is always four bytes

size_of and align_of

size_of tells you how many bytes a type occupies; align_of tells you the alignment the hardware prefers. Both take a value or a type name.

fmt.println(size_of(int))    // 8 on a 64-bit machine, 4 on a 32-bit one
fmt.println(size_of(f64))    // 8 — on every platform
fmt.println(align_of(f64))   // 8 — the boundary the CPU likes to read from

Those last two lines matter more than they look. Once you start designing data structures, the difference between a well-aligned record and a badly-aligned one can be the difference between a fast program and a slow one — which is exactly what the data-oriented design lessons are about.

Where This Goes Next

Types describe what a value is. The next lesson is about what you can do with values: the operators, and the small rules that decide which one runs first.

The Page in One Breath

  • int is platform-sized and is the default; ask for u8, i32, u64, or an endian-tagged type when an exact size or byte order is part of the problem.
  • Decimal literals are f64; say f32 when you want half the memory.
  • Everything starts at zero — Odin has no constructors and no uninitialised memory.
  • An untyped constant adapts to the type it is assigned to, as long as the value fits; a typed variable never converts silently.
  • T(value) is the everyday conversion; cast, transmute, and auto_cast are the specialist tools.
  • distinct makes a same-bits, different-meaning type — free at runtime, priceless for keeping units and identifiers apart.
  • typeid_of, size_of, align_of, and #assert let you inspect and document your assumptions.
Nice work. For a five-minute exercise, declare a Meters and a Seconds, try to add them, read the error message carefully, and then convert properly. Reading one Odin error message on purpose teaches you more than ten minutes of reading about them.

Continue with Operators & Expressions →