Thescope of a variable is the region of code within which a variable is accessible. Variable scoping helps avoid variable naming conflicts. The concept is intuitive: two functions can both have arguments calledx
without the twox
's referring to the same thing. Similarly, there are many other cases where different blocks of code can use the same name without referring to the same thing. The rules for when the same variable name does or doesn't refer to the same thing are called scope rules; this section spells them out in detail.
Certain constructs in the language introducescope blocks, which are regions of code that are eligible to be the scope of some set of variables. The scope of a variable cannot be an arbitrary set of source lines; instead, it will always line up with one of these blocks. There are two main types of scopes in Julia,global scope andlocal scope. The latter can be nested. There is also a distinction in Julia between constructs which introduce a "hard scope" and those which only introduce a "soft scope", which affects whethershadowing a global variable by the same name is allowed or not.
The constructs introducing scope blocks are:
Construct | Scope type | Allowed within |
---|---|---|
module ,baremodule | global | global |
struct | local (hard) | global |
for ,while ,try | local (soft) | global, local |
macro | local (hard) | global |
functions,do blocks,let blocks, comprehensions, generators | local (hard) | global, local |
Notably missing from this table arebegin blocks andif blocks which donot introduce new scopes. The three types of scopes follow somewhat different rules which will be explained below.
Julia useslexical scoping, meaning that a function's scope does not inherit from its caller's scope, but from the scope in which the function was defined. For example, in the following code thex
insidefoo
refers to thex
in the global scope of its moduleBar
:
julia> module Bar x = 1 foo() = x end;
and not ax
in the scope wherefoo
is used:
julia> import .Barjulia> x = -1;julia> Bar.foo()1
Thuslexical scope means that what a variable in a particular piece of code refers to can be deduced from the code in which it appears alone and does not depend on how the program executes. A scope nested inside another scope can "see" variables in all the outer scopes in which it is contained. Outer scopes, on the other hand, cannot see variables in inner scopes.
Each module introduces a new global scope, separate from the global scope of all other modules—there is no all-encompassing global scope. Modules can introduce variables of other modules into their scope through theusing or import statements or through qualified access using the dot-notation, i.e. each module is a so-callednamespace as well as a first-class data structure associating names with values.
If a top-level expression contains a variable declaration with keywordlocal
, then that variable is not accessible outside that expression. The variable inside the expression does not affect global variables of the same name. An example is to declarelocal x
in abegin
orif
block at the top-level:
julia> x = 1 begin local x = 0 @show x end @show x;x = 0x = 1
Note that the interactive prompt (aka REPL) is in the global scope of the moduleMain
.
A new local scope is introduced by most code blocks (see abovetable for a complete list). If such a block is syntactically nested inside of another local scope, the scope it creates is nested inside of all the local scopes that it appears within, which are all ultimately nested inside of the global scope of the module in which the code is evaluated. Variables in outer scopes are visible from any scope they contain — meaning that they can be read and written in inner scopes — unless there is a local variable with the same name that "shadows" the outer variable of the same name. This is true even if the outer local is declared after (in the sense of textually below) an inner block. When we say that a variable "exists" in a given scope, this means that a variable by that name exists in any of the scopes that the current scope is nested inside of, including the current one.
Some programming languages require explicitly declaring new variables before using them. Explicit declaration works in Julia too: in any local scope, writinglocal x
declares a new local variable in that scope, regardless of whether there is already a variable namedx
in an outer scope or not. Declaring each new variable like this is somewhat verbose and tedious, however, so Julia, like many other languages, considers assignment to a variable name that doesn't already exist to implicitly declare that variable. If the current scope is global, the new variable is global; if the current scope is local, the new variable is local to the innermost local scope and will be visible inside of that scope but not outside of it. If you assign to an existing local, italways updates that existing local: you can only shadow a local by explicitly declaring a new local in a nested scope with thelocal
keyword. In particular, this applies to variables assigned in inner functions, which may surprise users coming from Python where assignment in an inner function creates a new local unless the variable is explicitly declared to be non-local.
Mostly this is pretty intuitive, but as with many things that behave intuitively, the details are more subtle than one might naïvely imagine.
Whenx = <value>
occurs in a local scope, Julia applies the following rules to decide what the expression means based on where the assignment expression occurs and whatx
already refers to at that location:
x
isalready a local variable, then the existing localx
is assigned;x
isnot already a local variable and assignment occurs inside of any hard scope construct (i.e. within alet
block, function, struct or macro body, comprehension, or generator), a new local namedx
is created in the scope of the assignment;x
isnot already a local variable and all of the scope constructs containing the assignment are soft scopes (loops,try
/catch
blocks), the behavior depends on whether the global variablex
is defined:x
isundefined, a new local namedx
is created in the scope of the assignment;x
isdefined, the assignment is considered ambiguous:x
is assigned.You may note that in non-interactive contexts the hard and soft scope behaviors are identical except that a warning is printed when an implicitly local variable (i.e. not declared withlocal x
) shadows a global. In interactive contexts, the rules follow a more complex heuristic for the sake of convenience. This is covered in depth in examples that follow.
Now that you know the rules, let's look at some examples. Each example is assumed to be evaluated in a fresh REPL session so that the only globals in each snippet are the ones that are assigned in that block of code.
We'll begin with a nice and clear-cut situation—assignment inside of a hard scope, in this case a function body, when no local variable by that name already exists:
julia> function greet() x = "hello" # new local println(x) endgreet (generic function with 1 method)julia> greet()hellojulia> x # globalERROR: UndefVarError: `x` not defined in `Main`
Inside of thegreet
function, the assignmentx = "hello"
causesx
to be a new local variable in the function's scope. There are two relevant facts: the assignment occurs in local scope and there is no existing localx
variable. Sincex
is local, it doesn't matter if there is a global namedx
or not. Here for example we definex = 123
before defining and callinggreet
:
julia> x = 123 # global123julia> function greet() x = "hello" # new local println(x) endgreet (generic function with 1 method)julia> greet()hellojulia> x # global123
Since thex
ingreet
is local, the value (or lack thereof) of the globalx
is unaffected by callinggreet
. The hard scope rule doesn't care whether a global namedx
exists or not: assignment tox
in a hard scope is local (unlessx
is declared global).
The next clear cut situation we'll consider is when there is already a local variable namedx
, in which casex = <value>
always assigns to this existing localx
. This is true whether the assignment occurs in the same local scope, an inner local scope in the same function body, or in the body of a function nested inside of another function, also known as aclosure.
We'll use thesum_to
function, which computes the sum of integers from one up ton
, as an example:
function sum_to(n) s = 0 # new local for i = 1:n s = s + i # assign existing local end return s # same localend
As in the previous example, the first assignment tos
at the top ofsum_to
causess
to be a new local variable in the body of the function. Thefor
loop has its own inner local scope within the function scope. At the point wheres = s + i
occurs,s
is already a local variable, so the assignment updates the existings
instead of creating a new local. We can test this out by callingsum_to
in the REPL:
julia> function sum_to(n) s = 0 # new local for i = 1:n s = s + i # assign existing local end return s # same local endsum_to (generic function with 1 method)julia> sum_to(10)55julia> s # globalERROR: UndefVarError: `s` not defined in `Main`
Sinces
is local to the functionsum_to
, calling the function has no effect on the global variables
. We can also see that the updates = s + i
in thefor
loop must have updated the sames
created by the initializations = 0
since we get the correct sum of 55 for the integers 1 through 10.
Let's dig into the fact that thefor
loop body has its own scope for a second by writing a slightly more verbose variation which we'll callsum_to_def
, in which we save the sums + i
in a variablet
before updatings
:
julia> function sum_to_def(n) s = 0 # new local for i = 1:n t = s + i # new local `t` s = t # assign existing local `s` end return s, @isdefined(t) endsum_to_def (generic function with 1 method)julia> sum_to_def(10)(55, false)
This version returnss
as before but it also uses the@isdefined
macro to return a boolean indicating whether there is a local variable namedt
defined in the function's outermost local scope. As you can see, there is not
defined outside of thefor
loop body. This is because of the hard scope rule again: since the assignment tot
occurs inside of a function, which introduces a hard scope, the assignment causest
to become a new local variable in the local scope where it appears, i.e. inside of the loop body. Even if there were a global namedt
, it would make no difference—the hard scope rule isn't affected by anything in global scope.
Note that the local scope of a for loop body is no different from the local scope of an inner function. This means that we could rewrite this example so that the loop body is implemented as a call to an inner helper function and it behaves the same way:
julia> function sum_to_def_closure(n) function loop_body(i) t = s + i # new local `t` s = t # assign same local `s` as below end s = 0 # new local for i = 1:n loop_body(i) end return s, @isdefined(t) endsum_to_def_closure (generic function with 1 method)julia> sum_to_def_closure(10)(55, false)
This example illustrates a couple of key points:
Inner function scopes are just like any other nested local scope. In particular, if a variable is already a local outside of an inner function and you assign to it in the inner function, the outer local variable is updated.
It doesn't matter if the definition of an outer local happens below where it is updated, the rule remains the same. The entire enclosing local scope is parsed and its locals determined before inner local meanings are resolved.
This design means that you can generally move code in or out of an inner function without changing its meaning, which facilitates a number of common idioms in the language using closures (seedo blocks).
Let's move onto some more ambiguous cases covered by the soft scope rule. We'll explore this by extracting the bodies of thegreet
andsum_to_def
functions into soft scope contexts. First, let's put the body ofgreet
in afor
loop—which is soft, rather than hard—and evaluate it in the REPL:
julia> for i = 1:3 x = "hello" # new local println(x) endhellohellohellojulia> xERROR: UndefVarError: `x` not defined in `Main`
Since the globalx
is not defined when thefor
loop is evaluated, the first clause of the soft scope rule applies andx
is created as local to thefor
loop and therefore globalx
remains undefined after the loop executes. Next, let's consider the body ofsum_to_def
extracted into global scope, fixing its argument ton = 10
s = 0for i = 1:10 t = s + i s = tends@isdefined(t)
What does this code do? Hint: it's a trick question. The answer is "it depends." If this code is entered interactively, it behaves the same way it does in a function body. But if the code appears in a file, it prints an ambiguity warning and throws an undefined variable error. Let's see it working in the REPL first:
julia> s = 0 # global0julia> for i = 1:10 t = s + i # new local `t` s = t # assign global `s` endjulia> s # global55julia> @isdefined(t) # globalfalse
The REPL approximates being in the body of a function by deciding whether assignment inside the loop assigns to a global or creates new local based on whether a global variable by that name is defined or not. If a global by the name exists, then the assignment updates it. If no global exists, then the assignment creates a new local variable. In this example we see both cases in action:
t
, sot = s + i
creates a newt
that is local to thefor
loop;s
, sos = t
assigns to it.The second fact is why execution of the loop changes the global value ofs
and the first fact is whyt
is still undefined after the loop executes. Now, let's try evaluating this same code as though it were in a file instead:
julia> code = """ s = 0 # global for i = 1:10 t = s + i # new local `t` s = t # new local `s` with warning end s, # global @isdefined(t) # global """;julia> include_string(Main, code)┌ Warning: Assignment to `s` in soft scope is ambiguous because a global variable by the same name exists: `s` will be treated as a new local. Disambiguate by using `local s` to suppress this warning or `global s` to assign to the existing global variable.└ @ string:4ERROR: LoadError: UndefVarError: `s` not defined in local scope
Here we useinclude_string
, to evaluatecode
as though it were the contents of a file. We could also savecode
to a file and then callinclude
on that file—the result would be the same. As you can see, this behaves quite different from evaluating the same code in the REPL. Let's break down what's happening here:
s
is defined with the value0
before the loop is evaluateds = t
occurs in a soft scope—afor
loop outside of any function body or other hard scope constructs
local to thefor
loop bodys
is local to thefor
loop, it is undefined whent = s + i
is evaluated, causing an errors
and@isdefined(t)
, it would return0
andfalse
.This demonstrates some important aspects of scope: in a scope, each variable can only have one meaning, and that meaning is determined regardless of the order of expressions. The presence of the expressions = t
in the loop causess
to be local to the loop, which means that it is also local when it appears on the right hand side oft = s + i
, even though that expression appears first and is evaluated first. One might imagine that thes
on the first line of the loop could be global while thes
on the second line of the loop is local, but that's not possible since the two lines are in the same scope block and each variable can only mean one thing in a given scope.
We have now covered all the local scope rules, but before wrapping up this section, perhaps a few words should be said about why the ambiguous soft scope case is handled differently in interactive and non-interactive contexts. There are two obvious questions one could ask:
In Julia ≤ 0.6, all global scopes did work like the current REPL: whenx = <value>
occurred in a loop (ortry
/catch
, orstruct
body) but outside of a function body (orlet
block or comprehension), it was decided based on whether a global namedx
was defined or not whetherx
should be local to the loop. This behavior has the advantage of being intuitive and convenient since it approximates the behavior inside of a function body as closely as possible. In particular, it makes it easy to move code back and forth between a function body and the REPL when trying to debug the behavior of a function. However, it has some downsides. First, it's quite a complex behavior: many people over the years were confused about this behavior and complained that it was complicated and hard both to explain and understand. Fair point. Second, and arguably worse, is that it's bad for programming "at scale." When you see a small piece of code in one place like this, it's quite clear what's going on:
s = 0for i = 1:10 s += iend
Obviously the intention is to modify the existing global variables
. What else could it mean? However, not all real world code is so short or so clear. We found that code like the following often occurs in the wild:
x = 123# much later# maybe in a different filefor i = 1:10 x = "hello" println(x)end# much later# maybe in yet another file# or maybe back in the first one where `x = 123`y = x + 234
It's far less clear what should happen here. Sincex + "hello"
is a method error, it seems probable that the intention is forx
to be local to thefor
loop. But runtime values and what methods happen to exist cannot be used to determine the scopes of variables. With the Julia ≤ 0.6 behavior, it's especially concerning that someone might have written thefor
loop first, had it working just fine, but later when someone else adds a new global far away—possibly in a different file—the code suddenly changes meaning and either breaks noisily or, worse still, silently does the wrong thing. This kind of"spooky action at a distance" is something that good programming language designs should prevent.
So in Julia 1.0, we simplified the rules for scope: in any local scope, assignment to a name that wasn't already a local variable created a new local variable. This eliminated the notion of soft scope entirely as well as removing the potential for spooky action. We uncovered and fixed a significant number of bugs due to the removal of soft scope, vindicating the choice to get rid of it. And there was much rejoicing! Well, no, not really. Because some people were angry that they now had to write:
s = 0for i = 1:10 global s += iend
Do you see thatglobal
annotation in there? Hideous. Obviously this situation could not be tolerated. But seriously, there are two main issues with requiringglobal
for this kind of top-level code:
It's no longer convenient to copy and paste the code from inside a function body into the REPL to debug it—you have to addglobal
annotations and then remove them again to go back;
Beginners will write this kind of code without theglobal
and have no idea why their code doesn't work—the error that they get is thats
is undefined, which does not seem to enlighten anyone who happens to make this mistake.
As of Julia 1.5, this code works without theglobal
annotation in interactive contexts like the REPL or Jupyter notebooks (just like Julia 0.6) and in files and other non-interactive contexts, it prints this very direct warning:
Assignment to
s
in soft scope is ambiguous because a global variable by the same name exists:s
will be treated as a new local. Disambiguate by usinglocal s
to suppress this warning orglobal s
to assign to the existing global variable.
This addresses both issues while preserving the "programming at scale" benefits of the 1.0 behavior: global variables have no spooky effect on the meaning of code that may be far away; in the REPL copy-and-paste debugging works and beginners don't have any issues; any time someone either forgets aglobal
annotation or accidentally shadows an existing global with a local in a soft scope, which would be confusing anyway, they get a nice clear warning.
An important property of this design is that any code that executes in a file without a warning will behave the same way in a fresh REPL. And on the flip side, if you take a REPL session and save it to file, if it behaves differently than it did in the REPL, then you will get a warning.
let
statements create a newhard scope block (see above) and introduce new variable bindings each time they run. The variable need not be immediately assigned:
julia> var1 = let x for i in 1:5 (i == 4) && (x = i; break) end x end4
Whereas assignments might reassign a new value to an existing value location,let
always creates a new location. This difference is usually not important, and is only detectable in the case of variables that outlive their scope via closures. Thelet
syntax accepts a comma-separated series of assignments and variable names:
julia> x, y, z = -1, -1, -1;julia> let x = 1, z println("x: $x, y: $y") # x is local variable, y the global println("z: $z") # errors as z has not been assigned yet but is local endx: 1, y: -1ERROR: UndefVarError: `z` not defined in local scope
The assignments are evaluated in order, with each right-hand side evaluated in the scope before the new variable on the left-hand side has been introduced. Therefore it makes sense to write something likelet x = x
since the twox
variables are distinct and have separate storage. Here is an example where the behavior oflet
is needed:
julia> Fs = Vector{Any}(undef, 2); i = 1;julia> while i <= 2 Fs[i] = ()->i global i += 1 endjulia> Fs[1]()3julia> Fs[2]()3
Here we create and store two closures that return variablei
. However, it is always the same variablei
, so the two closures behave identically. We can uselet
to create a new binding fori
:
julia> Fs = Vector{Any}(undef, 2); i = 1;julia> while i <= 2 let i = i Fs[i] = ()->i end global i += 1 endjulia> Fs[1]()1julia> Fs[2]()2
Since thebegin
construct does not introduce a new scope, it can be useful to use a zero-argumentlet
to just introduce a new scope block without creating any new bindings immediately:
julia> let local x = 1 let local x = 2 end x end1
Sincelet
introduces a new scope block, the inner localx
is a different variable than the outer localx
. This particular example is equivalent to:
julia> let x = 1 let x = 2 end x end1
In loops andcomprehensions, new variables introduced in their body scopes are freshly allocated for each loop iteration, as if the loop body were surrounded by alet
block, as demonstrated by this example:
julia> Fs = Vector{Any}(undef, 2);julia> for j = 1:2 Fs[j] = ()->j endjulia> Fs[1]()1julia> Fs[2]()2
Afor
loop or comprehension iteration variable is always a new variable:
julia> function f() i = 0 for i = 1:3 # empty end return i end;julia> f()0
However, it is occasionally useful to reuse an existing local variable as the iteration variable. This can be done conveniently by adding the keywordouter
:
julia> function f() i = 0 for outer i = 1:3 # empty end return i end;julia> f()3
A common use of variables is giving names to specific, unchanging values. Such variables are only assigned once. This intent can be conveyed to the compiler using theconst
keyword:
julia> const e = 2.71828182845904523536;julia> const pi = 3.14159265358979323846;
Multiple variables can be declared in a singleconst
statement:
julia> const a, b = 1, 2(1, 2)
Theconst
declaration should only be used in global scope on globals. It is difficult for the compiler to optimize code involving global variables, since their values (or even their types) might change at almost any time. If a global variable will not change, adding aconst
declaration solves this performance problem.
Local constants are quite different. The compiler is able to determine automatically when a local variable is constant, so local constant declarations are not necessary, and in fact are currently not supported.
Special top-level assignments, such as those performed by thefunction
andstruct
keywords, are constant by default.
Note thatconst
only affects the variable binding; the variable may be bound to a mutable object (such as an array), and that object may still be modified. Additionally when one tries to assign a value to a variable that is declared constant the following scenarios are possible:
julia> const x = 1.01.0julia> x = 1ERROR: invalid redefinition of constant x
julia> const y = 1.01.0julia> y = 2.0WARNING: redefinition of constant y. This may fail, cause incorrect answers, or produce other errors.2.0
julia> const z = 100100julia> z = 100100
The last rule applies to immutable objects even if the variable binding would change, e.g.:
julia> const s1 = "1""1"julia> s2 = "1""1"julia> pointer.([s1, s2], 1)2-element Array{Ptr{UInt8},1}: Ptr{UInt8} @0x00000000132c9638 Ptr{UInt8} @0x0000000013dd3d18julia> s1 = s2"1"julia> pointer.([s1, s2], 1)2-element Array{Ptr{UInt8},1}: Ptr{UInt8} @0x0000000013dd3d18 Ptr{UInt8} @0x0000000013dd3d18
However, for mutable objects the warning is printed as expected:
julia> const a = [1]1-element Vector{Int64}: 1julia> a = [1]WARNING: redefinition of constant a. This may fail, cause incorrect answers, or produce other errors.1-element Vector{Int64}: 1
Note that although sometimes possible, changing the value of aconst
variable is strongly discouraged, and is intended only for convenience during interactive use. Changing constants can cause various problems or unexpected behaviors. For instance, if a method references a constant and is already compiled before the constant is changed, then it might keep using the old value:
julia> const x = 11julia> f() = xf (generic function with 1 method)julia> f()1julia> x = 2WARNING: redefinition of constant x. This may fail, cause incorrect answers, or produce other errors.2julia> f()1
Support for typed globals was added in Julia 1.8
Similar to being declared as constants, global bindings can also be declared to always be of a constant type. This can either be done without assigning an actual value using the syntaxglobal x::T
or upon assignment asx::T = 123
.
julia> x::Float64 = 2.7182.718julia> f() = xf (generic function with 1 method)julia> Base.return_types(f)1-element Vector{Any}: Float64
For any assignment to a global, Julia will first try to convert it to the appropriate type usingconvert
:
julia> global y::Intjulia> y = 1.01.0julia> y1julia> y = 3.14ERROR: InexactError: Int64(3.14)Stacktrace:[...]
The type does not need to be concrete, but annotations with abstract types typically have little performance benefit.
Once a global has either been assigned to or its type has been set, the binding type is not allowed to change:
julia> x = 11julia> global x::IntERROR: cannot set type for global x. It already has a value or is already set to a different type.Stacktrace:[...]
Settings
This document was generated withDocumenter.jl version 1.8.0 onWednesday 9 July 2025. Using Julia version 1.11.6.