Loops & Control Flow#
So far our programs have run straight from top to bottom. Real programs need to make decisions (“if this customer is overdue, flag the invoice”) and repeat work (“for every row in the dataset, clean it up”). That’s what control flow is for.
Making decisions with if#
balance = 1200
if balance > 1000
println("Premium account")
end
Add elseif to test more conditions, and else for the fallback:
function classify(balance)
if balance > 10_000
println("Status: VIP")
elseif balance > 1000
println("Status: Premium")
else
println("Status: Standard")
end
end
classify(25_000) # Status: VIP
classify(1200) # Status: Premium
classify(50) # Status: Standard
Three beginner notes that trip people up:
The condition does not need parentheses, and there’s no colon at the end of the line.
Indentation is for humans, not the compiler. Julia uses the
endkeyword to close blocks. Coming from Python this feels like extra typing; coming from anywhere else it feels normal. Either way, indent consistently — four spaces is the community convention.It’s
elseif, one word. Notelif, notelse if.
Warning
Conditions must actually be Bool
Julia will not accept a number where a boolean belongs:
julia> if 1
println("nope")
end
ERROR: TypeError: non-boolean (Int64) used in boolean context
No truthiness. 0 isn’t false, an empty array isn’t false. Write the comparison you actually mean: if count > 0, if !isempty(items). It’s stricter than Python, and it catches real bugs.
The ternary operator#
For picking one of two values, there’s a compact form:
a, b = 7, 12
smaller = a < b ? a : b # 7
Read it as: “is a < b? then a, otherwise b.” The spaces around ? and : are required.
Short-circuit operators as statements#
This is idiomatic Julia and you’ll see it constantly in real code:
x > 0 || error("x must be positive")
isvalid(record) && process(record)
|| only evaluates its right side if the left is false; && only if the left is true. So the first line reads “either x is positive, or we throw an error” — a guard clause in one line. Perfectly readable once you’ve seen it a few times.
Repeating with for#
A for loop walks through the items of a collection, one at a time. This is the loop you’ll use most.
temps = [20.5, 22.3, 19.8, 25.1]
for temp in temps
println("$temp °C")
end
Each time around, temp holds the next value. You can write ∈ instead of in if you’re feeling mathematical (\in + Tab); = also works and you’ll see it in older code.
Counting with ranges#
for i in 1:4
println(i) # prints 1, 2, 3, 4
end
1:4 is a range, and it’s worth knowing that it’s not a list — it’s a tiny object that stores its start, step, and stop, and generates values as needed. 1:1_000_000_000 costs almost no memory.
Ranges take a step in the middle:
for x in 9:-3:3 # start:step:stop
println(x) # 9, 6, 3
end
Note the ordering: start:step:stop, with the step in the middle. Python programmers reach for range(9, 0, -3) and get it wrong the first time. I certainly did.
Looping over data with its index#
Three patterns, each right in a different situation:
temps = [20.5, 22.3, 19.8, 25.1]
# 1. You only need the value
for t in temps
println(t)
end
# 2. You need position and value — this is usually what you want
for (day, t) in enumerate(temps)
println("Day $day → $t °C")
end
# 3. You need the raw indices (e.g. to write back into the array)
for i in eachindex(temps)
temps[i] = temps[i] * 1.8 + 32
end
Prefer eachindex(temps) over 1:length(temps). They’re identical for ordinary arrays, but eachindex also does the right thing for arrays with unusual index ranges, and it lets the compiler skip some bounds checks. It’s a free habit — pick it up now.
Nested loops, the Julia way#
Julia lets you flatten nested loops into a single for:
for i in 1:2, j in 1:3
println("($i, $j)")
end
That’s exactly equivalent to two nested loops, just less indented. Note that break inside it exits both levels, since it’s really one loop.
Repeating with while#
Use while when you don’t know in advance how many times you’ll loop.
countdown = 5
while countdown > 0
println(countdown)
countdown -= 1
end
println("Liftoff!")
The danger with while is the infinite loop: if the condition never becomes false, your program runs forever. Here, countdown -= 1 is what guarantees we eventually stop. Always make sure something inside the loop moves you toward the exit. (And if you do hang the REPL, Ctrl-C is your friend.)
Steering loops: break and continue#
break exits the loop immediately:
invoices = [200, 450, -1, 700]
for amount in invoices
if amount < 0
println("Bad record found, stopping.")
break
end
println("Processing invoice: $amount")
end
continue skips the rest of the current pass and jumps to the next item:
invoices = [200, 450, -1, 700]
total = 0
for amount in invoices
amount < 0 && continue # ignore bad records, keep going
total += amount
end
println("Clean total: $total") # 1350
break says “I’m done with this loop entirely.” continue says “skip this one, but keep looping.”
When things go wrong: try / catch#
Sometimes a line of code fails and you’d rather handle it than crash.
raw = ["1200", "800", "n/a", "1500"]
total = 0
for s in raw
try
total += parse(Int, s)
catch e
println("Skipping unparseable value: $s")
end
end
println("Total: $total") # 3500
Two words of caution, because this is easy to overuse.
First, try/catch in Julia has real runtime cost, and — more importantly — it blocks some compiler optimizations in the surrounding code. Don’t put it inside a hot loop that runs a million times. For the parsing case above, tryparse returns nothing on failure instead of throwing, and is much cheaper:
for s in raw
v = tryparse(Int, s)
v === nothing && continue
global total += v
end
Second, catching everything hides bugs. If you only expect a parse failure, catch that specifically. finally is available too, for cleanup that must happen either way:
f = open("ledger.csv")
try
process(f)
finally
close(f)
end
Though for files specifically, the block form handles this for you:
open("ledger.csv") do f
process(f)
end # closed automatically, even if process() throws
A small, real example#
Let’s tie it together. Imagine we’re scanning a week of account balances and want to count how many days fell below a threshold — the kind of quick check you’d do before feeding data to a model.
function count_low_days(balances, threshold)
low_days = 0
for (day, balance) in enumerate(balances)
if balance < threshold
low_days += 1
println("Day $day is low: $balance")
end
end
return low_days
end
balances = [1200, 800, 1500, 300, 950, 2000, 600]
n = count_low_days(balances, 1000)
println("Total low-balance days: $n")
Run it and you’ll see exactly which days fell short, plus a final count. That single pattern — loop over data, test a condition, accumulate a result — is the backbone of an enormous amount of data work.
Note
Why did I wrap that in a function? I could have written the loop at the top level of a script and it would have produced the same output. But code inside a function gets compiled and type-specialized; code at the top level of a script does not, because its variables are globals whose types could change at any moment.
This is the most consequential piece of Julia advice in the whole book, so I’m planting it here in chapter four rather than saving it for the performance chapter: put your real work inside functions. The same loop can be an order of magnitude faster purely by living inside a function. We’ll dig into exactly why in the Performance chapter.
Try it yourself#
Print the numbers 1 through 10, but skip every multiple of 3.
Given
prices = [9.99, 19.50, 4.25, 30.00], compute and print the total using a loop.Starting from 100.0, repeatedly halve a number with a
whileloop, printing each step, until it drops below 1.Given
readings = [20.5, -99.0, 22.3, -99.0, 19.8]where-99.0means “sensor failed”, compute the average of only the valid readings.
Solutions#
# 1.
for n in 1:10
n % 3 == 0 && continue
println(n)
end
# 2.
function total_price(prices)
total = 0.0
for p in prices
total += p
end
return total
end
println(total_price([9.99, 19.50, 4.25, 30.00]))
# 3.
value = 100.0
while value >= 1.0
println(value)
global value /= 2.0 # `global` needed only at the top level of a script
end
# 4.
function valid_mean(readings, sentinel = -99.0)
total = 0.0
count = 0
for r in readings
r == sentinel && continue
total += r
count += 1
end
return count == 0 ? NaN : total / count
end
println(valid_mean([20.5, -99.0, 22.3, -99.0, 19.8])) # 20.866...
You can now branch and repeat, which means you can express real logic. Next we’ll package that logic into reusable functions — and in Julia, functions are considerably more interesting than they first appear.