Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

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.

  • Beginner
  • 15 minutes
  • Examples run with julia 1.13.1 and Python 3.14.8
  • By MySmartCoPilot
Free lesson, Pro answers: 15 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

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.

aliasing.jl: two names, a copy, and a copy of a copy Julia · aliasing.jl
# 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)
aliasing.jl, run with Julia 1.13.1 Python · aliasing_session.py
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:

Interactive · step by step Two names, one vector, then a copy Step through aliasing.jl up to its vector of vectors. Arrows are bindings; watch which vector each name reaches when a line writes into one.
Two names, one vector, then a copy, step 6 of 6. Showing offsets, saved, Vector A ([0.0, -0.25, 0.75]), Vector B ([9.0, -0.25, 0.75]). In focus: Vector B.bound tobound tooffsetsa namesaveda nameVector A[0.0, -0.25, 0.75]Vector B[9.0, -0.25, 0.75]

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. 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.
  2. 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.
  3. 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].
  4. One object. saved === offsets printed true. === asks whether two values are one and the same object, and both names reach Vector A.
  5. 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.
  6. 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

  1. Question 1 of 2 After a = [1, 2, 3], b = a and b[2] = 20, what does println(a) print?

    Choose one answer.

    A hint

    Ask how many vectors exist after b = a.

    Show the answer: Question 1 of 2

    Answer: [1, 20, 3]

    Assignment binds a name; it never copies the value. After b = a both names are bound to one vector, so b[2] = 20 writes into the vector that a prints: [1, 20, 3].

  2. Question 2 of 2 grid = [[0, 0], [0, 0]], then g2 = copy(grid) and g2[1][1] = 5. What is grid now?

    Choose one answer.

    A hint

    copy makes a new outer vector. What does it hold?

    Show the answer: Question 2 of 2

    Answer: [[5, 0], [0, 0]]

    copy copies the outer structure only. Its documentation calls it a shallow copy, and the run showed shallow[1][1] = -99.0 changing runs. Use deepcopy when the inner vectors must be independent too.

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:

calibrate.jl: calibrate, calibrate! and a rebinding Julia · calibrate.jl
"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)
calibrate.jl, run with Julia 1.13.1 Python · calibrate_session.py
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:

Interactive · message by message What a call does with the caller's vector Step through two calls on readings. The argument raw is bound to the caller's vector; follow what each function does with that binding.
What a call does with the caller's vector, step 6 of 6. Messages so far: The script to The function: calibrate!(readings, 0.25); The function to The vector: raw[i] -= 0.25, three times; The function to The script: returns raw; The script to The function: reset_readings(readings); The function to The script: returns the zeros.The scriptnames it readingsThe functionraw: a new vector of zerosThe vector[20.25, 20.75, 19.5]calibrate!(readings, 0.25)raw[i] -= 0.25, three timesreturns rawreset_readings(readings)returns the zeros

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. The call. calibrate!(readings, 0.25) binds the argument raw to the vector that the script calls readings. Nothing is copied.
  2. Writing through raw. raw[i] -= offset writes into that vector, once for each of its three readings.
  3. 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.
  4. Another call. reset_readings(readings) binds its own raw to the same vector, as every call does.
  5. 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.
  6. 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

  1. Question 1 of 2 v = [3, 1, 2]. After which of these lines does v still print [3, 1, 2]?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: w = sort(v)

    A function's argument is bound to the caller's object. Functions that write into it end in ! by convention, so only sort(v), which builds a new vector, leaves v as it was.

  2. Question 2 of 2 What does data hold after this code runs?

    Read the code, then choose one answer.

    function clear(v)
        v = Float64[]
        return v
    end
    data = [1.5, 2.5]
    clear(data)
    Show the answer: Question 2 of 2

    Answer: [1.5, 2.5], unchanged

    Inside clear, v starts bound to the caller's vector, and v = Float64[] moves that local binding to a new vector. Only a write into the object, such as v[1] = 0 or empty!(v), changes what the caller sees.

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:

types.jl: typeof, supertypes and isa Julia · types.jl
# 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))
types.jl, run with Julia 1.13.1 Python · types_session.py
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:

Interactive · stage by stage Climbing from Int64 to Any with isa Step up the chain that supertypes(Int64) printed. At each type, isa answers whether 1, and some other values, belong to it.
Climbing from Int64 to Any with isa, step 6 of 6. Showing Int64, Signed, Integer, Real, Number, Any. In focus: Any.Int64typeof(1): a concrete typeSignedsigned integer typesIntegeralso Bool and UnsignedRealalso floats and rationalsNumberalso complex numbersAnyevery value

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. The value's own type. typeof(1) is Int64, the concrete type of the value. 1 isa Int64 is true.
  2. One step up. The supertype of Int64 is Signed, the signed integer types. 1 isa Signed is true too.
  3. Integer. 1 isa Integer is true, and so is true isa Integer, because Bool is an integer type as well: true + true is 2.
  4. 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.
  5. Number. Number also holds the complex numbers, so 2.0 + 3im isa Number is true, though it is not Real.
  6. 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

  1. Question 1 of 2 What does typeof(3//4) return?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Rational{Int64}

    3//4 is a Rational{Int64}, an exact fraction with an Int64 numerator and denominator. typeof always returns a concrete type; types such as Real only appear when you test with isa.

  2. Question 2 of 2 Which of these is true?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: true isa Integer

    isa accepts the value's own type and every type above it. Bool is below Integer, so true isa Integer holds, while a Float64 is Real but not Integer, and comparing typeof with an abstract type is always false.

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:

Typing Unicode names in the Julia 1.13.1 REPL Python · unicode_session.py
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:

greek_physics.jl: formulas with Unicode names Julia · greek_physics.jl
# 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:

greek_physics.jl and coefficients.jl, run with Julia 1.13.1 Python · greek_session.py
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:

lookalikes.jl: two pairs of look-alike letters Julia · lookalikes.jl
# 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
lookalikes.jl, run with Julia 1.13.1 Python · lookalikes_session.py
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.

Unicode Character Inspector Paste a name that Julia says is not defined and see each character's code point, script and look-alikes.

Checkpoint

Check: Unicode names and coefficients

  1. Question 1 of 2 How do you type the name x̄ in the Julia REPL?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Type x, then \bar, then press Tab

    x̄ is two characters, x and a combining bar. Type x, then \bar, then Tab. When you meet a character you cannot type, ? followed by it prints how to type it.

  2. Question 2 of 2 With x = 4, what is 1/2x?

    Type a number.

    A hint

    A number written before a name binds more tightly than /.

    Show the answer: Question 2 of 2

    Answer: 0.125

    A numeric literal coefficient binds more tightly than division, so 1/2x is 1/(2x) = 1/8 = 0.125, not (1/2)x = 2. The run printed 0.16666666666666666 for the same expression with x = 3.

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:

const_versions.jl: declaring a constant again Julia · const_versions.jl
# 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))
const_assign.jl: assigning to a constant without const Julia · const_assign.jl
# Plain assignment to a constant, without const, is refused.
const GRAVITY = 9.81
GRAVITY = 10
Both files on Julia 1.13.1, then on the 1.10 LTS Python · const_session.py
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:

Timeline · 4 events in order Constants and typed globals across Julia releases Walk through four releases and see when typed globals arrived and how redefining a constant changed.
  1. 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.

  2. 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.

  3. 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.

  4. 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:

typed_global.jl: a global that keeps its type Julia · typed_global.jl
# 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
typed_global.jl, run with Julia 1.13.1 Python · typed_global_session.py
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:

global_speed.jl: one sum, four ways to reach the data Julia · global_speed.jl
# 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
global_speed.jl, run with Julia 1.13.1 Python · global_speed_session.py
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

  1. Question 1 of 2 On Julia 1.10.12, const_versions.jl declared GRAVITY again and then called fall_time(20.0) a second time. What did that call return?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: The old answer, 2.019275109384609, as if GRAVITY were still 9.81

    On 1.10.12 the redefinition printed WARNING: redefinition of constant Main.GRAVITY, and the next call of fall_time still used 9.81. A fresh computation in the same run gave the new value, so the stale one was simply wrong.

  2. Question 2 of 2 g::Float64 = 9.81 is followed by g = 3. What is g afterwards?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: 3.0, a Float64

    A typed global keeps one type and converts every value assigned to it. g = 3 stores 3.0, and g = "ten" fails with a MethodError, because no conversion from a String to a Float64 exists.

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.

  1. Question 1 of 15 Which line gives b a vector of numbers that you can change without changing a?

    Choose one answer.

    Show the answer: Question 1 of 15

    Answer: b = copy(a)

    Assignment never copies, so a vector you may change on its own needs copy(a) (or deepcopy(a) when its elements are mutable objects themselves).

  2. Question 2 of 15 After a = [1, 2] and b = copy(a), what are a == b and a === b?

    Choose one answer.

    Show the answer: Question 2 of 15

    Answer: true and false

    == asks whether two vectors hold equal values; === asks whether they are the same object. A fresh copy is equal in value and a different object.

  3. Question 3 of 15 A simulation keeps state = [[0.0, 1.0], [2.0, 0.5]], one vector per particle, and saves start = copy(state) before it runs. Afterwards start holds the final positions. Why, and what fixes it?

    Choose one answer.

    Show the answer: Question 3 of 15

    Answer: copy shares the inner vectors the simulation writes into; use deepcopy(state)

    A shallow copy of a vector of vectors is a new outer vector that holds the same inner vectors. The simulation wrote into those, so start changed too. deepcopy copies everything recursively.

  4. Question 4 of 15 What does the ! at the end of a function name such as sort! tell you?

    Choose one answer.

    Show the answer: Question 4 of 15

    Answer: By convention, the function changes at least one of its arguments

    The ! warns the caller that the call changes an argument. Julia does not enforce it, so code is only as safe as its names are honest.

  5. Question 5 of 15 Using calibrate and calibrate! from the lesson, what does this print?

    Read the code, then choose one answer.

    r = [1.0, 2.0]
    c = calibrate(r, 0.5)
    calibrate!(r, 0.5)
    println(r, " ", c)
    Show the answer: Question 5 of 15

    Answer: [0.5, 1.5] [0.5, 1.5]

    calibrate runs calibrate! on copy(r), so c is a new vector and r is untouched until calibrate!(r, 0.5) writes into it. Each vector was calibrated exactly once.

  6. Question 6 of 15 You are writing normalize!(v), which scales v in place, and normalize(v), which must leave v alone. Which definition of normalize keeps one copy of the arithmetic and is correct?

    Choose one answer.

    Show the answer: Question 6 of 15

    Answer: normalize(v) = normalize!(copy(v))

    Write the arithmetic once, in the version that changes its argument, and build the other as a call on a copy. The copy is what keeps the caller's data safe.

  7. Question 7 of 15 What is typeof(2.0 + 3im)?

    Choose one answer.

    Show the answer: Question 7 of 15

    Answer: ComplexF64

    A complex number takes the type of its parts, promoted to one type: 2.0 + 3im has Float64 parts, and Complex{Float64} is written ComplexF64.

  8. Question 8 of 15 supertypes(Int64) printed (Int64, Signed, Integer, Real, Number, Any). With x = 5, which test is false?

    Choose one answer.

    Show the answer: Question 8 of 15

    Answer: x isa AbstractFloat

    isa is true for every type in the value's chain, here Int64 up to Any, and false for types on other branches, such as AbstractFloat, where Float64 lives.

  9. Question 9 of 15 x = 1 is followed by x = "one" in a script. What happens?

    Choose one answer.

    Show the answer: Question 9 of 15

    Answer: x is now bound to a String

    Values carry types and ordinary names do not, so a name can be bound to an Int64 and then to a String. Only a typed global, x::Int = 1, would refuse the string.

  10. Question 10 of 15 What is √ in Julia?

    Choose one answer.

    Show the answer: Question 10 of 15

    Answer: The function sqrt under another name, typed as \sqrt and Tab

    Julia defines √ as sqrt, ∈ as in, ÷ as div and ≈ as isapprox. They are names like any others, typed with their backslash names and Tab.

  11. Question 11 of 15 With x = 3, what is 2^2x?

    Type a number.

    A hint

    In an exponent, the coefficient binds to the name first.

    Show the answer: Question 11 of 15

    Answer: 64

    In an exponent a numeric literal coefficient binds first, so 2^2x is 2^(2x) = 2^6 = 64, as the run printed. By contrast 2x^2 is 2(x^2) = 18.

  12. Question 12 of 15 A script stops with UndefVarError: a not defined, although the line above it reads a = 1.0. The code was copied from a PDF. What is the likeliest cause?

    Choose one answer.

    Show the answer: Question 12 of 15

    Answer: The a in the assignment is a look-alike character with another code point

    Look-alike characters, which Unicode calls confusables, are different code points and therefore different names. Julia merges only a few pairs, such as the micro sign and μ. An inspector that shows code points finds the impostor.

  13. Question 13 of 15 What does GRAVITY = 10 do after const GRAVITY = 9.81 in Julia 1.13?

    Choose one answer.

    Show the answer: Question 13 of 15

    Answer: It fails with an error about an invalid assignment to a constant

    const forbids assignment with =. From Julia 1.12 a new const GRAVITY = … declaration is well defined, but GRAVITY = 10 stays an error.

  14. Question 14 of 15 global_speed.jl summed the same million readings four ways. Which way allocated 79,991,840 bytes?

    Choose one answer.

    Show the answer: Question 14 of 15

    Answer: Reading the ordinary global

    Only the ordinary global allocated, 79,991,840 bytes for a million readings. A constant, a typed global and an argument all gave the compiler a type it could rely on.

  15. Question 15 of 15 A tolerance is read in a hot loop. The user changes it between runs of the loop, but it is always a Float64. How should the script hold it?

    Choose one answer.

    Show the answer: Question 15 of 15

    Answer: As a typed global, tol::Float64 = 1e-6, or better as an argument of the function

    Match the declaration to what changes: const for a value that never changes, a typed global for a value that changes but keeps its type, and an argument whenever the loop is inside a function you call.

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.

  1. 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?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. 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?

    Model answer, Pro. First step and follow-ups

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; copy makes a shallow copy and deepcopy a 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: typeof gives a value’s concrete type, and isa also accepts every abstract type above it, such as Integer or Real.
  • 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

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.