Data Types, Variables and Operators#
Let’s kick things off by learning Julia’s data types, variables, and operators. These are the bricks. Everything else in the book is built on top of them.
Data#
In plain words, data is any information that intelligence consumes. Computers store data as numbers, text, images, audio, and video, and a program reads, transforms, and produces it. Data lives in variables, data structures, or files, and it’s the raw material every program works with.
Here’s some everyday data, the kind you’d actually find in a real application:
x = 1
name = "Amit Shukla"
song = "what_am_i_made_of.mp3"
movie = "Barbie.mp4"
addresses = ["Malibu", "Brooklyn"]
zipcodes = ["90265", "11203"]
Nothing exotic here. A number, some text, and a couple of lists. We’ll spend the rest of this chapter understanding what Julia actually does with each of these behind the scenes, because that is where Julia earns its speed.
Variables#
A variable is a name bound to a value. Julia has no var, let, or const requirement for ordinary code — you just assign:
account = "GL-4001"
balance = 12_500.75 # underscores are legal digit separators, and very readable
open_items = 3
Two immediate observations.
First, you didn’t declare any types, and yet Julia knows exactly what each of these is:
julia> typeof(account)
String
julia> typeof(balance)
Float64
julia> typeof(open_items)
Int64
This is the pattern you’ll see over and over: types are always there, always concrete, and usually inferred. You are not writing an untyped language that Julia types at runtime. You’re writing a typed language that spares you from saying the obvious out loud.
Second, Julia source is UTF-8, so variable names aren’t limited to ASCII. This is completely legal:
α = 0.05 # type \alpha then press Tab in the REPL
Δbalance = 42.0
π # already defined for you: 3.1415926535897...
I’d be careful with this in code other people have to maintain, but for numerical work it’s a genuine quality-of-life feature. Writing σ² = variance(data) instead of sigma_squared keeps your code looking like the paper it came from. In the REPL and in VS Code, type the LaTeX name and hit Tab.
Constants#
If a value genuinely will not change, mark it with const:
const TAX_RATE = 0.0825
const FISCAL_YEAR_START = "07-01"
This isn’t just documentation. A const at the top level of a module lets the compiler bake the value’s type into the code it generates. Non-constant global variables are one of the classic Julia performance traps, precisely because the compiler can’t assume their type will stay put. We’ll come back to this in the Performance chapter — for now, if it’s a fixed setting, make it const.
Data types#
A data type is a classification of data that tells the compiler how to store and operate on a value. It defines what a variable can hold and which operations are legal on it.
Adding two whole numbers behaves like adding two decimals, more or less. But “adding” two pieces of text, or two lists, means something completely different from adding numbers. The type is how the compiler knows which behavior you meant.
Why does this matter for speed? Because when the compiler knows the exact type ahead of time, it doesn’t have to check at runtime. It can lay out the data efficiently and generate tight machine code.
Julia’s answer to the static-versus-dynamic argument is unusual, and it’s worth stating clearly: Julia is dynamically typed, but it infers concrete types aggressively and compiles specialized code for them. You get the ergonomics of a dynamic language and, when your types are predictable, the machine code of a static one. The corollary — and it’s the whole ballgame — is that when your types aren’t predictable, you get neither.
Type annotations#
You can annotate a type with ::, read as “is an instance of”:
temperature::Float64 = 99 # the 99 is converted to 99.0
println(temperature) # 99.0
You’ll mostly use :: in three places: function arguments (to control dispatch), struct fields (to control memory layout), and occasionally to assert something to the compiler. You will not use it everywhere the way you might in Java or C++. Sprinkling type annotations on every local variable does not make Julia faster — the compiler already inferred them.
A quick detour: computer memory#
Before we look at the type hierarchy, let’s spend two minutes on how memory is structured. It feels like a tangent, but this small mental model pays off enormously once you start writing performance-sensitive code.
At the top, a CPU core talks to a tiny, blazing-fast L1 cache. L1 is fed by a larger, slightly slower L2, which is fed by L3, which is fed by the comparatively glacial main memory (RAM). The first rule of fast code falls right out of this picture: data that’s already sitting in a nearby cache is cheap to reach; data that has to be hauled up from main memory is expensive. That expensive trip is called a cache miss.
Within a running program, memory is organized into a stack and a heap. The stack is ordered and statically sized, so access is essentially instantaneous — but the compiler has to know each value’s size ahead of time. The heap exists for everything else: values whose size isn’t known until runtime, reached through a pointer.
Here’s why I’m telling you this in a chapter about types. In Julia, an array of Float64 is a contiguous block of 64-bit floats — exactly the memory layout a C program would use, exactly what the CPU’s cache prefetcher loves. An array of Any, on the other hand, is an array of pointers to values scattered across the heap. Same syntax, same operations, wildly different performance. The type you choose is the memory layout you get. That’s the connection, and it’s why Julia programmers care about types more than the syntax would suggest.
The number types#
Julia’s numeric types are laid out in a clean hierarchy, and unusually, they’re defined in Julia itself, not baked secretly into the compiler. Int64 is a struct in the standard library. So is Complex. This is the “no privileged built-ins” principle: a type you define can be exactly as fast and exactly as well-integrated as the ones that shipped with the language.
Integers#
count = 8 # Int64 on a 64-bit machine
typeof(count) # Int64
Int is an alias for whatever your machine’s native word size is — Int64 on any modern computer. When you need a specific width, the whole family is available: Int8, Int16, Int32, Int64, Int128, and the unsigned UInt8 … UInt128.
Reach for plain Int as your default and only specify a width when you have a reason to (binary file formats, memory-constrained arrays, interfacing with C).
Warning
Integer overflow is silent This surprises newcomers, so let’s get it out of the way:
julia> typemax(Int64)
9223372036854775807
julia> typemax(Int64) + 1
-9223372036854775808
It wrapped around, quietly. Julia does this because that’s what the hardware does, and adding an overflow check to every integer addition would cost you the performance you came here for. If you need arbitrary precision, BigInt is available and unlimited — just slower. For financial totals in a real ledger, this is worth thinking about.
Floats#
pi_ish = 3.14159 # Float64, the default
small = Float32(3.14159) # fewer bits, less precise, half the memory
Float64 is the everyday choice. Float32 and Float16 trade precision for size and speed, which matters enormously in machine learning where you’re moving billions of numbers.
Floating-point numbers can’t represent every decimal exactly, so 0.1 + 0.2 won’t be exactly 0.3. That’s the nature of binary floating point, not a Julia quirk. Use isapprox (or its operator form ≈, typed \approx+Tab) when comparing floats:
julia> 0.1 + 0.2 == 0.3
false
julia> 0.1 + 0.2 ≈ 0.3
true
The rest of the numeric family#
This is where Julia’s numerical heritage shows. All of these are first-class, all work with the ordinary operators, and all compose with each other:
z = 3 + 4im # Complex{Int64}
r = 3//4 # Rational{Int64} — exact, no floating point error
b = BigFloat(1) / 3 # arbitrary precision
r + 1//4 # 1//1 exactly one, no rounding
Rational deserves a moment. 3//4 is stored as a numerator and a denominator, so arithmetic on it is exact. If you’ve ever been burned by accumulated floating-point error in a financial calculation, that’s a tool worth knowing about.
Bool and characters#
ready = true # Bool
grade = 'A' # Char — note the SINGLE quotes
Watch that last one. In Julia, 'A' (single quotes) is a single character and "A" (double quotes) is a string. They’re different types and not interchangeable. This trips up Python programmers constantly.
Strings#
greeting = "Hello, Julia"
println(length(greeting)) # 12
A few things that will save you time:
Concatenation uses *, not +.
first = "Amit"
last = "Shukla"
full = first * " " * last # "Amit Shukla"
This looks bizarre at first. The reasoning is mathematical: + conventionally denotes a commutative operation, and "ab" * "cd" is emphatically not the same as "cd" * "ab". Once you’ve seen it a few times it stops bothering you. There’s also string(first, " ", last), which handles non-string arguments too.
Interpolation uses $.
account = "GL-4001"
balance = 12_500.75
println("Account $account has a balance of $balance")
println("Doubled: $(balance * 2)") # any expression inside $( )
This is what you’ll actually use most of the time.
Indexing works on characters, but Julia is honest about Unicode.
julia> s = "Mojo🔥"
julia> s[1]
'M': ASCII/Unicode U+004D
julia> s[1:4]
"Mojo"
Indices are byte positions, not character positions, and Julia will throw an error rather than silently hand you half a character. For text that might contain emoji or non-Latin scripts, iterate with for c in s or use eachindex(s) instead of assuming index arithmetic works. It’s a small amount of extra care that saves you from a whole category of bug.
Strings are immutable. s[1] = 'X' is an error. Build new strings instead of mutating old ones.
Arrays: a first look#
Arrays are important enough to get their own chapter, but you can’t write two lines of Julia without them, so here’s the introduction.
zipcodes = ["90265", "11203"] # Vector{String}
balances = [1200, 800, 1500, 300] # Vector{Int64}
Notice what Julia inferred: Vector{String}, not “an array of stuff.” The {String} part says every element is a String, which means the array is a tight, contiguous block of memory rather than a scattered collection of pointers.
Three facts to internalize now, because they differ from most languages you might know:
Julia arrays are 1-indexed.
balances[1]is the first element. This is deliberate — Julia’s heritage is mathematical and scientific computing, where you index from one. You’ll get used to it faster than you expect.endis a keyword inside indexing.balances[end]is the last element,balances[end-1]the second-to-last.Arrays are mutable and passed by reference. If you hand an array to a function, that function can change it. This is a big enough deal that it gets its own chapter later.
julia> balances[1]
1200
julia> balances[end]
300
julia> push!(balances, 950) # the ! means "this mutates its argument"
5-element Vector{Int64}:
1200
800
1500
300
950
That trailing ! in push! is a naming convention, not syntax. Julia programmers write ! on the end of any function that modifies its arguments. It’s just a convention, but it’s followed rigorously enough that you can read a line of unfamiliar code and know whether it’s destructive.
Operators#
With types in hand, operators are the easy part:
a = 10
b = 3
a + b # 13 addition
a - b # 7 subtraction
a * b # 30 multiplication
a / b # 3.333 true division — always returns a float
a ÷ b # 3 integer division (also written div(a, b))
a % b # 1 remainder
a ^ b # 1000 exponentiation (note: ^ not **)
Comparisons return a Bool, and — a lovely touch — they chain the way they do in mathematics:
0 < a < 100 # true, and it means what you'd hope
a > b # true
a != b # true
Two Julia-specific conveniences worth learning immediately.
Numeric literal coefficients. You can write multiplication by juxtaposition when the left side is a number:
x = 4
2x + 1 # 9 — no * needed
3x^2 - 2x + 1 # reads exactly like the polynomial it is
Updating operators. a += 1, a *= 2, and friends all work as you’d expect.
Here’s something elegant about all of this: those operators aren’t special compiler syntax. a + b is genuinely a call to the function +, and you can prove it:
julia> +(10, 3)
13
Which means you can define + for your own types and have it just work. That’s not an advanced trick reserved for library authors; it’s the ordinary way things are done in Julia, and we’ll do it ourselves in the Multiple Dispatch chapter.
Try it yourself#
Create a variable holding your name and another holding your age, then print a single sentence using both, via interpolation.
Compute what
1/3 + 1/3 + 1/3gives you as aFloat64, and then what1//3 + 1//3 + 1//3gives you as aRational. Explain the difference to yourself.Given
readings = [20.5, 22.3, 19.8, 25.1], print the first, the last, and the number of elements.
Solutions#
# 1.
name = "Amit"
age = 42
println("$name is $age years old, and $(age * 12) months old.")
# 2.
1/3 + 1/3 + 1/3 # 1.0 — but this is luck; floats often don't land exactly
1//3 + 1//3 + 1//3 # 1//1 — exact, by construction
# 3.
readings = [20.5, 22.3, 19.8, 25.1]
println(readings[1]) # 20.5
println(readings[end]) # 25.1
println(length(readings)) # 4
That’s the foundation. You now know how Julia stores data, how variables and types work, why the memory layout of a typed array matters, and how operators are really just function calls in disguise. Next we’ll put these to work with loops and control flow.