Data Types & Values
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.
| Type | What it holds | Reach for it when |
|---|---|---|
int | A signed whole number, platform-sized (64 bits on a 64-bit machine) | Almost always — it is the default |
uint | The same size, but never negative | Counting, bit patterns, sizes |
i8 i16 i32 i64 | Signed integers of 1, 2, 4, and 8 bytes | An exact storage size is part of the design |
u8 u16 u32 u64 | The unsigned versions of the same widths | Raw byte data, colours, hashes |
i128 u128 | 128-bit integers | Very large numbers where 64 bits are not enough |
u32le u64be | Fixed 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
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.
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)
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.
| Type | Category | Reach for it when |
|---|---|---|
bool | Logic | Conditions and flags — only true or false |
int | Integer | The default whole number: counts, indices, sizes you compute |
uint | Integer | A count that can never be negative |
i8 … i64, u8 … u64 | Integer | An exact storage size is part of the problem |
i128, u128 | Integer | Numbers larger than 64 bits can hold |
u32le, u64be | Integer | Binary formats where the byte order is specified |
f32 | Float | Graphics, audio, and large arrays of numbers |
f64 | Float | General mathematics — the default for decimal literals |
string | Text | UTF-8 text you read and print |
cstring | Text | A NUL-terminated string for talking to C libraries |
rune | Text | One 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.
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
intis platform-sized and is the default; ask foru8,i32,u64, or an endian-tagged type when an exact size or byte order is part of the problem.- Decimal literals are
f64; sayf32when 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, andauto_castare the specialist tools.distinctmakes a same-bits, different-meaning type — free at runtime, priceless for keeping units and identifiers apart.typeid_of,size_of,align_of, and#assertlet you inspect and document your assumptions.
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 →