Julia Module 2 – Variables, numbers and arithmetic
Variables, bindings and Unicode names
How = binds a name to a value without copying it, typeof and isa, typing α and x̄ with Tab, and when const and typed globals pay off, on Julia 1.13 and 1.10.
What you will learn
- Predict when two names share one array, and make independent copies with copy or deepcopy
- Predict whether a function call changes the caller's array, using the ! naming convention
- Read a value's type with typeof and test it with isa
- Type Unicode names such as α, x̄ and Δx with a backslash name and Tab
- Choose between const, a typed global and an ordinary global for a value a script uses
Before you start
What you’ll learn · Before you start
What you will learn
- Predict when two names share one array, and make independent copies with copy or deepcopy
- Predict whether a function call changes the caller's array, using the ! naming convention
- Read a value's type with typeof and test it with isa
- Type Unicode names such as α, x̄ and Δx with a backslash name and Tab
- Choose between const, a typed global and an ordinary global for a value a script uses
Before you start
On this page
A calibration script keeps a backup of three sensor offsets with saved = offsets, zeroes the first entry of the
backup, and the original changes with it: offsets now starts with 0.0. Nothing was copied. In Julia, = gives a
value one more name, and every name reaches the same vector.
Each behaviour below is a run recorded on Julia 1.13.1: what = and a function call do with your data, how to read a
value’s type with typeof and test it with isa, how to type names such as α, x̄ and Δx so that a formula reads as it
is printed, and when a constant or a typed global is worth declaring. One of these behaviours is different on the 1.10
LTS, so its run there is shown too.
Assignment gives a value another name, and never copies it
Here is the backup that went wrong, then one that works, and a case that catches careful readers too: a vector whose elements are vectors.
# Calibration offsets of three temperature sensors, in °C.
offsets = [0.5, -0.25, 0.75]
saved = offsets # a second name for the same vector
saved[1] = 0.0 # writes into that vector
println("offsets = ", offsets, ", saved === offsets: ", saved === offsets)
saved = copy(offsets) # a new vector with the same numbers
saved[1] = 9.0
println("offsets = ", offsets, ", saved = ", saved, ", saved === offsets: ", saved === offsets)
# copy makes a new outer vector, but the inner vectors are shared.
runs = [[20.25, 20.5], [19.75, 20.0]]
shallow = copy(runs)
deep = deepcopy(runs)
shallow[1][1] = -99.0
deep[2][1] = 0.0
println("runs = ", runs)
println("deep = ", deep) from session import files, run
# Two names for one vector, a copy, a rebinding, then copy against deepcopy on a vector of vectors.
files("aliasing.jl")
run("""
julia aliasing.jl
""") Output
$ julia aliasing.jl offsets = [0.0, -0.25, 0.75], saved === offsets: true offsets = [0.0, -0.25, 0.75], saved = [9.0, -0.25, 0.75], saved === offsets: false runs = [[-99.0, 20.5], [19.75, 20.0]] deep = [[20.25, 20.5], [0.0, 20.0]]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 aliasing_session.py
The first line of output shows the problem: offsets starts with 0.0 although the program wrote only to saved, and
saved === offsets is true. === asks whether two values are identical in the sense that no program could tell them
apart, and for a vector that means one and the same object. The manual describes b = a as binding the name b to
the same array, and that is all a binding: A name attached to a value. Assignment with = creates or moves a binding and never copies the value, so several names can be bound to one object. is: a name attached to a value. Step through the
program up to its vector of vectors, and watch the arrows rather than the numbers:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- One vector, one name. offsets = [0.5, -0.25, 0.75] creates a vector and binds the name offsets to it. The name is a label; the vector is the object that holds the three numbers.
- A second name. saved = offsets copies nothing. It binds a second name to the vector that offsets already names, so there is still only one vector.
- Writing through one name. saved[1] = 0.0 writes into the vector itself. offsets is bound to that same vector, so the run printed offsets = [0.0, -0.25, 0.75].
- One object. saved === offsets printed true. === asks whether two values are one and the same object, and both names reach Vector A.
- A copy, and a name moves. saved = copy(offsets) builds Vector B with the same numbers and binds saved to it. Only the arrow of saved moves; Vector A is not touched.
- Writing into the copy. saved[1] = 9.0 now writes into Vector B. offsets still printed [0.0, -0.25, 0.75], and saved === offsets printed false.
Writing into a vector, as saved[1] = 0.0 does, is mutation: Changing an object itself, as v[1] = 0 writes into a vector. Every name bound to that object sees the change; binding a name to another value is not mutation.: it changes the object, so
every name bound to it sees the change. Assigning to a name, saved = copy(offsets), only moves that name, and no
object changes. copy built a second vector, so the later saved[1] = 9.0 left offsets alone and === turned
false.
A backup made with =
Wrong: backup = data before an experiment that changes data. Both names reach one vector, so the backup changes
with it. Right: backup = copy(data) for a vector of numbers, and backup = deepcopy(data) when its elements are
vectors or other objects that can change.
copy makes a shallow copy: A copy of a container that holds the same element objects as the original. copy of a vector of vectors makes a new outer vector that shares the inner ones.: its documentation says the outer structure is copied but
not all internal values. The last two lines of the run show what that means for runs, a vector holding two vectors.
shallow = copy(runs) is a new outer vector that holds the same two inner vectors, so shallow[1][1] = -99.0 changed
runs too. deepcopy(runs) is a deep copy: A copy made recursively, all the way down, so that it shares no object with the original. Julia's deepcopy makes one., copied all the way down, so writing into it
left runs as it was.
Key idea
= binds a name and never copies: a write into an object is seen through every name bound to it, so make the copy
you need with copy, or with deepcopy when the elements can change too.
Checkpoint
Check: Names, objects and copies
A function gets the caller’s object, not a copy
Calling a function binds its arguments the same way: the name inside the function is bound to the object the caller
passed. A function that writes into its argument therefore changes the caller’s data, and Julia’s style guide asks
for a warning in the name: a function that changes at least one of its arguments ends in !, as sort! does beside
sort. This file defines both kinds for sensor readings, and a third function that only looks as if it clears its
argument:
"Subtract `offset` from every reading in `raw`, in place, and return `raw`."
function calibrate!(raw, offset)
for i in eachindex(raw)
raw[i] -= offset
end
return raw
end
"Return a calibrated copy of `raw`, and leave `raw` as it was."
calibrate(raw, offset) = calibrate!(copy(raw), offset)
"Looks as if it clears `raw`, but it only rebinds its own name."
function reset_readings(raw)
raw = zeros(length(raw))
return raw
end
readings = [20.5, 21.0, 19.75]
fixed = calibrate(readings, 0.25)
println("calibrate: ", readings, " → ", fixed)
calibrate!(readings, 0.25)
println("calibrate!: ", readings)
reset_readings(readings)
println("reset_readings: ", readings) from session import files, run
# A function receives the caller's vector itself: calibrate! changes it, calibrate works on a copy, and assigning
# to the argument's name inside a function changes nothing outside it.
files("calibrate.jl")
run("""
julia calibrate.jl
""") Output
$ julia calibrate.jl calibrate: [20.5, 21.0, 19.75] → [20.25, 20.75, 19.5] calibrate!: [20.25, 20.75, 19.5] reset_readings: [20.25, 20.75, 19.5]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 calibrate_session.py
calibrate returned new numbers and left readings as it was, because it runs calibrate! on copy(raw).
calibrate! changed readings itself, which is what its name promised. reset_readings changed nothing, although
it assigns zeros to raw. Step through the two calls that reach the vector:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- The call. calibrate!(readings, 0.25) binds the argument raw to the vector that the script calls readings. Nothing is copied.
- Writing through raw. raw[i] -= offset writes into that vector, once for each of its three readings.
- Back in the script. calibrate! returns raw, the same vector, and readings now prints [20.25, 20.75, 19.5]. The ! in the name said the argument would change.
- Another call. reset_readings(readings) binds its own raw to the same vector, as every call does.
- Rebinding a local name. raw = zeros(length(raw)) binds raw to a new vector of zeros. Only the function's name moved; the script's vector was not written.
- Nothing changed. The function returns the zeros, which the script ignores, and readings still prints [20.25, 20.75, 19.5]. Assigning to an argument never reaches the caller; writing into the object does.
Building calibrate as calibrate! on a copy is a common Julia pattern: the version that works in place does the
arithmetic and saves the memory of a new vector, and the copying version is one line on top of it. The ! is a
convention, not a rule the language checks, so it protects callers only if every function that changes an argument
follows it. The other naming conventions of the manual and the style guide, with examples from Base and this lesson:
| Kind of name | Convention | Examples |
|---|---|---|
| Variables | lower case; an underscore only where the name would be hard to read without one | offsets, readings |
| Functions | lower case, without underscores | isapprox, eachindex, haskey |
| Functions that change an argument | end in ! |
sort!, calibrate! |
| Types and modules | capitalised camel case | UnitRange, SparseArrays |
| Constants | upper case with underscores | LOAD_PATH, VERSION |
Checkpoint
Check: Arguments and the ! convention
Values carry types, and names do not
Every value knows its type, and typeof asks for it. Here are ten values, the chain of types above Int64, and five
tests with isa:
# Every value knows its type; typeof asks for it.
for x in (1, 1.0, 1//2, 2.0 + 3im, 0xff, big(10)^20, true, 'α', "α", nothing)
println(rpad(repr(x), 24), typeof(x))
end
# isa asks whether a value belongs to a type, or to any type above its own.
using InteractiveUtils # supertypes, like subtypes, is a REPL helper that a script loads
println(supertypes(Int64))
println(1 isa Int64, " ", 1 isa Integer, " ", 1 isa Real, " ", 1.0 isa Integer, " ", true isa Integer)
# A name has no type of its own: it can be bound to an Int64, then to a String.
x = 1
x = "one"
println(typeof(x)) from session import files, run
# typeof on ten values, the supertypes of Int64, five isa tests, and one name rebound to another type.
files("types.jl")
run("""
julia types.jl
""") Output
$ julia types.jl
1 Int64
1.0 Float64
1//2 Rational{Int64}
2.0 + 3.0im ComplexF64
0xff UInt8
100000000000000000000 BigInt
true Bool
'α' Char
"α" String
nothing Nothing
(Int64, Signed, Integer, Real, Number, Any)
true true true false true
String
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 types_session.py
1 is an Int64 and 1.0 a Float64: a decimal point changes the type. 1//2 is an exact fraction,
Rational{Int64}, and 2.0 + 3im is a ComplexF64; the last lesson of this module covers both. The last line
shows the other half of the rule: x was bound to an Int64 and then to a String, and Julia did not object,
because an ordinary name has no type of its own.
typeof gives the value’s concrete type, the one the value itself has. The types above it, such as Integer and
Real, are A type that no value has directly, such as Integer, Real or Number. It groups concrete types like Int64 and Bool, so that isa tests and methods can treat them alike.,
and isa answers a wider question: is the value of this type, or of any type above its own? supertypes(Int64)
printed the chain from Int64 up to Any, and every isa test is a step along such a chain:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- The value's own type. typeof(1) is Int64, the concrete type of the value. 1 isa Int64 is true.
- One step up. The supertype of Int64 is Signed, the signed integer types. 1 isa Signed is true too.
- Integer. 1 isa Integer is true, and so is true isa Integer, because Bool is an integer type as well: true + true is 2.
- Real. 1 isa Real is true. Float64 joins the chain only here, so 1.0 isa Real is true while 1.0 isa Integer is false.
- Number. Number also holds the complex numbers, so 2.0 + 3im isa Number is true, though it is not Real.
- Any. Every value isa Any, the top of every chain. isa accepts a value's own type and every type above it; typeof names only the first.
true isa Integer was true because Bool sits under Integer, so true + true is 2. 1.0 isa Integer was false:
Float64 joins the chain only at Real, even when its value is a whole number.
Testing a type with ==
Wrong: typeof(x) == Integer. It is false for every value, because typeof gives a concrete type such as Int64,
and Int64 is not the type Integer itself. Right: x isa Integer, which is true for an Int64, a UInt8 or a
Bool.
Checkpoint
Check: Types belong to values
Unicode names are typed with a backslash and Tab
Physics and mathematics write α, x̄ and Δx, and Julia accepts such names: the manual allows Unicode names in UTF-8. To
type one at the REPL, write a backslash and a LaTeX-like name, then press Tab, and the name turns into its character.
? followed by a character prints how to type it:
from session import repl
# Unicode names at the REPL: a backslash, a LaTeX-like name, then Tab, which replaces the name with its character.
# "?" followed by a character prints how to type it.
repl([
"\\alpha\t = 0.5",
"x\\bar\t = 2.0",
"\\Delta\tx = 0.25",
"\\sqrt\t16",
"?x̄",
]) Output
$ julia --startup-file=no -q julia> α = 0.5 0.5 julia> x̄ = 2.0 2.0 julia> Δx = 0.25 0.25 julia> √16 4.0 help?> x̄ "x̄" can be typed by x\bar<tab> search: x̄ No documentation found for private binding Main.x̄. x̄ is of type Float64. Summary ≡≡≡≡≡≡≡ primitive type Float64 Supertype Hierarchy ≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡ Float64 <: AbstractFloat <: Real <: Number <: Any
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 unicode_session.py
\alpha became α and \Delta became Δ. The bar of x̄ is a separate combining character that follows its letter, so
you type x\bar and then Tab, which is what help printed; it also showed that x̄ holds a Float64. The manual notes
that several Julia editing environments complete the same names. The ones numerical code meets most:
| Characters | Typed as | What they are |
|---|---|---|
| α, β, γ, Δ, ∇ | \alpha, \beta, \gamma, \Delta, \nabla |
letters for names |
| x̄, Eₖ | x\bar, E\_k |
a combining bar, a subscript |
| π, ℯ | \pi, \euler |
Julia’s constants π and ℯ |
| √, ÷ | \sqrt, \div |
the functions sqrt and div |
| ≈, ≠, ≤, ∈ | \approx, \ne, \le, \in |
comparisons |
Find the name of a character
At your own julia> prompt, type \hbar and press Tab to get ħ, then type ?ħ and press Enter: help prints the
name that types it, which is how to look up any symbol you meet in someone else’s code.
In a script the same names make a formula read the way it is printed. This one works out the kinetic energy, the momentum and the mean speed of a 160 g ball:
# A 160 g ball bowled at 38 m/s, with names written as a physics text writes them.
m = 0.160 # mass, kg
v = 38.0 # speed, m/s
Eₖ = m * v^2 / 2 # kinetic energy, J (typed E\_k Tab)
p = m * v # momentum, kg·m/s
speeds = [36.5, 38.0, 39.5] # three deliveries, m/s
v̄ = sum(speeds) / length(speeds) # mean speed (typed v\bar Tab)
Δv = maximum(speeds) - minimum(speeds)
println("Eₖ = ", Eₖ, " J, p = ", p, " kg·m/s")
println("v̄ = ", v̄, " m/s, Δv = ", Δv, " m/s")
# Operators written as symbols are ordinary functions with ordinary names.
println(√(2Eₖ / m) ≈ v, " ", 38.0 ∈ speeds, " ", 7 ÷ 2, " ", ≈(0.1 + 0.2, 0.3)) The symbols in its last line are functions with Unicode names: √ is sqrt, ∈ is in, ÷ is integer division
div, and ≈ is isapprox, which the last call uses with two arguments like any other function. The run printed
Eₖ = 115.52 J and confirmed that √(2Eₖ/m) gives back the 38 m/s:
from session import files, run
# Unicode names in a script, then numeric literal coefficients and the one form that is not a product.
files("greek_physics.jl", "coefficients.jl")
run("""
julia greek_physics.jl
!julia coefficients.jl
""") Output
$ julia greek_physics.jl Eₖ = 115.52 J, p = 6.08 kg·m/s v̄ = 38.0 m/s, Δv = 3.0 m/s true true 3 true $ julia coefficients.jl 2x = 6, 2(x + 1) = 8, 2x^2 = 18, 2^2x = 64, 1/2x = 0.16666666666666666 ERROR: LoadError: MethodError: objects of type Int64 are not callable. In case you did not try calling it explicitly, check if a Int64 has been passed as an argument to a method that expects a callable instead. The object of type `Int64` exists, but no method is defined for this combination of argument types when trying to treat it as a callable object. Maybe you forgot to use an operator such as *, ^, %, / etc. ? Stacktrace: [1] top-level scope @ ~/demo/coefficients.jl:3 [2] include(mod::Module, _path::String) @ Base ./Base.jl:309 [3] exec_options(opts::Base.JLOptions) @ Base ./client.jl:344 [4] _start() @ Base ./client.jl:577 in expression starting at ~/demo/coefficients.jl:3 [exit status 1]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 greek_session.py
A number before a name multiplies it
The second file of that run shows a numeric literal coefficient: A number written directly before a name or a parenthesis, as in 2x or 3(x + 1), which multiplies it. It binds more tightly than * and /, so 1/2x means 1/(2x).: with
x = 3, 2x is 6 and 2(x + 1) is 8, as on paper. The manual gives it a higher precedence than * and /, so 1/2x
is 1/(2x), 0.1666…, not 1.5. With ^ it acts like a minus sign in front: 2x^2 is 2(x²) = 18, while in an exponent
2^2x is 2^(2x) = 64. One form is not a product: a name followed by parentheses, x(x + 1), is a call of x, and the
run stopped with a MethodError saying that an Int64 is not callable.
Characters that look alike
Two characters can look the same and still be different code points; Unicode’s security standard, UTS #39, calls them confusables. The manual lists a few pairs that Julia treats as one character, among them the micro sign µ and the Greek letter μ. Most look-alikes stay apart, and this file, whose comments say which letter is which, shows both cases:
# Two pairs of letters that look alike. The first pair is one name to Julia, the second is two.
µ = 2.5e-6 # typed with the micro sign, U+00B5
println("μ = ", μ) # read back with the Greek letter mu, U+03BC
а = 1.0 # this а is Cyrillic, U+0430
println(codepoint('а'), " is not ", codepoint('a'))
println(a) # this a is Latin, U+0061, and was never assigned from session import files, run
# Julia reads the micro sign as the Greek letter mu, so both spell one name; a Cyrillic letter stays a name of its own.
files("lookalikes.jl")
run("""
!julia lookalikes.jl
""") Output
$ julia lookalikes.jl μ = 2.5e-6 1072 is not 97 ERROR: LoadError: UndefVarError: `a` not defined in `Main` Suggestion: check for spelling errors or missing imports. Stacktrace: [1] top-level scope @ ~/demo/lookalikes.jl:7 [2] include(mod::Module, _path::String) @ Base ./Base.jl:309 [3] exec_options(opts::Base.JLOptions) @ Base ./client.jl:344 [4] _start() @ Base ./client.jl:577 in expression starting at ~/demo/lookalikes.jl:7 [exit status 1]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 lookalikes_session.py
μ printed the value assigned through µ: one name. But а = 1.0, with a Cyrillic а, created a name that the Latin
a of the last line does not reach, so the run stopped with UndefVarError although every line looks right. In code
that others read, keep to letters they can tell apart, and when a name seems to vanish, look at its code points.
Checkpoint
Check: Unicode names and coefficients
const fixes a name’s value, and a typed global fixes its type
An ordinary global can be bound to any value of any type at any moment. Two declarations promise less. A
const global: A global name declared with const. It may not be assigned again with =, which lets the compiler rely on its value and type; declaring it again is well defined from Julia 1.12. may not be assigned again with =, and a
typed global: A global declared with a type, as in g::Float64 = 9.81, available since Julia 1.8. Every value assigned to it is converted to that type, or the assignment fails. keeps one type. Constants come with a version story. This file computes
how long a fall of 20 m takes with gravity rounded to 9.81, then declares the constant again with its standard value,
9.80665, and calls the same function; a second file tries plain assignment instead:
# Standard gravity, first rounded, then corrected by declaring the constant again.
const GRAVITY = 9.81 # m/s²
fall_time(h) = sqrt(2h / GRAVITY) # seconds to fall h metres, from rest
before = fall_time(20.0) # compiled and run with the first value
const GRAVITY = 9.80665 # the exact standard value
after = fall_time(20.0) # the same function, called again
println("before: ", before)
println("after: ", after)
println("fresh: ", sqrt(2 * 20.0 / GRAVITY)) # Plain assignment to a constant, without const, is refused.
const GRAVITY = 9.81
GRAVITY = 10 from session import files, run, use
# The same files on the current release and on the 1.10 LTS: redefining a constant with const is well defined from
# 1.12 on, and plain assignment to a constant is refused on both.
files("const_versions.jl", "const_assign.jl")
run("""
julia const_versions.jl
!julia const_assign.jl
""")
use("1.10.12")
run("""
julia const_versions.jl
!julia const_assign.jl
""") Output
$ julia const_versions.jl before: 2.019275109384609 after: 2.0196199771025523 fresh: 2.0196199771025523 $ julia const_assign.jl ERROR: LoadError: invalid assignment to constant Main.GRAVITY. This redefinition may be permitted using the `const` keyword. Stacktrace: [1] top-level scope @ ~/demo/const_assign.jl:3 [2] include(mod::Module, _path::String) @ Base ./Base.jl:309 [3] exec_options(opts::Base.JLOptions) @ Base ./client.jl:344 [4] _start() @ Base ./client.jl:577 in expression starting at ~/demo/const_assign.jl:3 [exit status 1] $ julia const_versions.jl WARNING: redefinition of constant Main.GRAVITY. This may fail, cause incorrect answers, or produce other errors. before: 2.019275109384609 after: 2.019275109384609 fresh: 2.0196199771025523 $ julia const_assign.jl ERROR: LoadError: invalid redefinition of constant Main.GRAVITY Stacktrace: [1] top-level scope @ ~/demo/const_assign.jl:3 in expression starting at ~/demo/const_assign.jl:3 [exit status 1]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 const_session.py
On 1.13.1 the second call used the new value: after equals a fresh computation, 2.0196199771025523 s. On 1.10.12
Julia warned that the redefinition may cause incorrect answers, and then gave one: fall_time had been compiled with
9.81 built in, so after repeated the old 2.019275109384609 while a fresh computation printed the new number. From
1.12 on, the release notes say, redefining a constant is well defined and follows
world age: A counter that grows with every change to Julia's methods and bindings. Running code sees the definitions of the world it started in, so a new method reaches it only after it returns or through invokelatest. More in “Notebooks, editors and the Revise workflow”, as new methods do. Plain assignment without const, the GRAVITY = 10 of
const_assign.jl, stopped with an error on both versions. Walk through the four releases that matter here:
- 1.8
Typed globals arrive new syntax
A global can be declared with a type, as in g::Float64 = 9.81. Every later assignment is converted to that type or fails, and the type never changes.
- 1.10 LTS
Redefining a constant warns
Declaring const GRAVITY a second time prints a warning that it may cause incorrect answers. Code compiled before it can keep the old value, as fall_time did in the 1.10.12 run.
- 1.12
Constants follow world age changed behaviour
Redefining a constant is well defined and follows world age, the release notes say: code that runs after the new definition sees the new value. Struct definitions may be replaced the same way.
- 1.13
The current release
It keeps the 1.12 rules. Every example in this lesson ran on 1.13.1, where redefining GRAVITY was silent and the next call used the new value.
The timeline has 4 events, from 1.8 to 1.13.
Constants on the 1.10 LTS
On Julia 1.10, redefining a constant prints a warning, and code compiled before it may keep the old value, as the 1.10.12 run showed. Restart Julia after changing a constant there, or keep a value that changes in a typed global or pass it as an argument.
A typed global, which Julia has had since 1.8, is declared with its type. Every later assignment is converted to that type, or fails:
# A typed global keeps one type: every value assigned to it is converted to that type.
g::Float64 = 9.81
g = 10 # an Int64, converted to 10.0
println(g, " ", typeof(g))
g = "ten" # no conversion from a String to a Float64 exists from session import files, run
# A global declared as Float64 accepts an integer (converted) and refuses a string.
files("typed_global.jl")
run("""
!julia typed_global.jl
""") Output
$ julia typed_global.jl
10.0 Float64
ERROR: LoadError: MethodError: Cannot `convert` an object of type String to an object of type Float64
The function `convert` exists, but no method is defined for this combination of argument types.
Closest candidates are:
convert(::Type{T}, !Matched::T) where T<:Number
@ Base number.jl:6
convert(::Type{T}, !Matched::Number) where T<:Number
@ Base number.jl:7
convert(::Type{T}, !Matched::T) where T
@ Base Base_compiler.jl:136
...
Stacktrace:
[1] top-level scope
@ ~/demo/typed_global.jl:5
[2] include(mod::Module, _path::String)
@ Base ./Base.jl:309
[3] exec_options(opts::Base.JLOptions)
@ Base ./client.jl:344
[4] _start()
@ Base ./client.jl:577
in expression starting at ~/demo/typed_global.jl:5
[exit status 1]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 typed_global_session.py
Why declare either one? The performance tips explain that an untyped global may change its value, and with it its type, at any point, so the compiler cannot rely on either. This program sums the same million readings from an ordinary global, a constant, a typed global and an argument:
# The same sum over a million readings, read from three kinds of global and from an argument.
readings = fill(0.5, 10^6) # an ordinary global: any value may replace it
const READINGS = readings # a constant binding to the same vector
typed_readings::Vector{Float64} = readings # a typed global
function sum_global()
s = 0.0
for r in readings
s += r
end
return s
end
function sum_const()
s = 0.0
for r in READINGS
s += r
end
return s
end
function sum_typed()
s = 0.0
for r in typed_readings
s += r
end
return s
end
function sum_argument(xs)
s = 0.0
for r in xs
s += r
end
return s
end
"Bytes allocated by one call f(args...), and the fastest of five calls in milliseconds."
function measure(f, args...)
f(args...) # the first call compiles
bytes = @allocated f(args...)
# Base.donotdelete stops the compiler from skipping a sum whose result is never used.
best = minimum(@elapsed(Base.donotdelete(f(args...))) for _ in 1:5)
return bytes, round(best * 1000; digits = 2)
end
for (label, f, args) in (("ordinary global", sum_global, ()), ("const global", sum_const, ()),
("typed global", sum_typed, ()), ("argument", sum_argument, (readings,)))
bytes, ms = measure(f, args...)
println(rpad(label, 17), f(args...), " ", lpad(bytes, 8), " bytes ", ms, " ms")
end from session import files, run
# Bytes allocated and the fastest of five runs for each kind of global. The times depend on the computer and on what
# else it is doing; the bytes do not.
files("global_speed.jl")
run("""
julia global_speed.jl
""") Output
$ julia global_speed.jl ordinary global 500000.0 79991840 bytes 113.46 ms const global 500000.0 0 bytes 0.9 ms typed global 500000.0 0 bytes 0.9 ms argument 500000.0 0 bytes 0.9 ms
This output changes from run to run: Times differ from run to run and from computer to computer; the sums and the bytes allocated do not.
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 global_speed_session.py
Reading the ordinary global allocated 79,991,840 bytes, about 80 bytes for each reading, and took 113.46 ms in this run. The constant, the typed global and the argument each allocated nothing and took 0.9 ms, about 126 times less. The tips name the fix that needs no global at all: pass the data to the function as an argument.
Key idea
Make a global const when its value never changes, give it a type when its value changes but its type does not, and pass data that changes as an argument; on 1.10, never rely on redefining a constant.
Checkpoint
Check: Constants and typed globals
Practise: names, types and globals
These questions replay the runs of this lesson. The idea you know least is asked first, and each answer says why it holds or what it misses:
Practice that adapts to you
Practise this lesson
15 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Warm-up (fresher to 2–5 years): what does b = a do when a is a vector, and how do you get a copy you can change
on its own?
b = a copies nothing: it binds the name b to the vector that a already names, so both names reach one object and
b === a is true. Writing into it through either name, as in b[1] = 0, changes what the other name sees, while
assigning to a name, b = [9, 9], only moves that name. For an independent copy of a vector of numbers, use
b = copy(a). copy is shallow, though: for a vector of vectors it makes a new outer vector that still shares the
inner ones, so a write into b[1][1] reaches a. deepcopy(a) copies everything recursively and shares nothing.
Function arguments follow the same rule, which is why a function that writes into its argument should end in !.
Two harder questions follow, about a function that damaged its input and about a script slowed by its globals. Note your answer first, then open each first step:
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Fresher to senior
A colleague's normalize function rescales a vector of raw sensor readings for a plot. After the plot, statistics computed from the same raw vector come out wrong. What happened, and how would you change the function and its name?
-
Mid to senior
A script keeps physical constants and its data vector in ordinary globals, and its hot loop allocates about 80 bytes per reading. Why, which three fixes would you weigh, and what would you check before redefining one of those constants on Julia 1.10?
Key takeaways
=binds a name to a value and never copies it, so a write into a vector is seen through every name bound to it;copymakes a shallow copy anddeepcopya fully independent one.- A function’s argument is bound to the caller’s object, so a function that writes into it changes the caller’s data,
and by convention its name ends in
!. - Values carry types and ordinary names do not:
typeofgives a value’s concrete type, andisaalso accepts every abstract type above it, such asIntegerorReal. - Unicode names are typed as a backslash, a LaTeX-like name and Tab; a number written before a name multiplies it, and letters that look alike can be different names.
- A constant fixes a global’s value and a typed global its type, which removes the allocations of an ordinary global; redefining a constant is well defined from Julia 1.12 and unreliable on 1.10.
Terms in this lesson
- binding
- A name attached to a value. Assignment with = creates or moves a binding and never copies the value, so several names can be bound to one object.
- const global
- A global name declared with const. It may not be assigned again with =, which lets the compiler rely on its value and type; declaring it again is well defined from Julia 1.12.
- deep copy
- A copy made recursively, all the way down, so that it shares no object with the original. Julia's deepcopy makes one.
- mutation
- Changing an object itself, as v[1] = 0 writes into a vector. Every name bound to that object sees the change; binding a name to another value is not mutation.
- numeric literal coefficient
- A number written directly before a name or a parenthesis, as in 2x or 3(x + 1), which multiplies it. It binds more tightly than * and /, so 1/2x means 1/(2x).
- shallow copy
- A copy of a container that holds the same element objects as the original. copy of a vector of vectors makes a new outer vector that shares the inner ones.
- typed global
- A global declared with a type, as in g::Float64 = 9.81, available since Julia 1.8. Every value assigned to it is converted to that type, or the assignment fails.
References
- Variables (The Julia Project)
- Scope of variables: constants and typed globals (The Julia Project)
- Essentials: copy, deepcopy, ===, isa and typeof (The Julia Project)
- Style guide: naming conventions and functions that modify their arguments (The Julia Project)
- Types (The Julia Project)
- Unicode input (The Julia Project)
- Integers and floating-point numbers: numeric literal coefficients (The Julia Project)
- Mathematical operations: operator precedence and juxtaposition (The Julia Project)
- Performance tips: avoid untyped global variables (The Julia Project)
- The world age mechanism (The Julia Project)
- Julia 1.12 release notes (The Julia Project)
- Julia 1.12 highlights: redefinition of constants (The Julia Project)
- Julia 1.13 release notes (The Julia Project)
- UTS #39: Unicode Security Mechanisms (confusables) (Unicode Consortium)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress