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 8 – Modules, packages and reproducible environments

Modules, using, import, export and public

Organise Julia code into modules and files, choose between using and import, mark an API with export and public, and read settings in __init__.

  • Intermediate
  • 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

  • Organise code into modules, submodules and files, and reach names by qualified and relative paths
  • Choose between using and import, and add a method to another module's function the right way
  • Declare a module's API with export and public, and check it with names and Base.ispublic
  • Read settings at load time in __init__, not in top-level code that runs at precompile time

Before you start

What you’ll learn · Before you start

What you will learn

  • Organise code into modules, submodules and files, and reach names by qualified and relative paths
  • Choose between using and import, and add a method to another module's function the right way
  • Declare a module's API with export and public, and check it with names and Base.ispublic
  • Read settings at load time in __init__, not in top-level code that runs at precompile time

Before you start

On this page

Two files of the same analysis both define normalise(x), one for a rain series and one for station names, and the script that includes both keeps whichever came last. A module stops that: each has its own global scope, so Rain.normalise and Names.normalise are different functions, and a module says which of its names others should use.

This lesson builds a module and its submodules, chooses between using and import, marks an API with export and public, and reads a setting in __init__, the one place that runs every time a package loads. The sessions run Julia 1.13.1, and two of them run again on the 1.10 LTS, where the same files behave differently.

A module is a namespace, and a qualified name reaches inside it

module Geometry ... end creates a module: A named namespace of Julia code with its own global scope, written module Name ... end. Names inside it are reached from outside as Name.x, or brought in with using and import.: a namespace with its own global scope. Inside, export area offers area to anyone who writes using, public perimeter marks perimeter as part of the API without offering it, and check is an internal helper:

geometry.jl: an exported name, a public one and a helper Julia · geometry.jl
# A module is a namespace: what export, public and an internal helper each make visible.
module Geometry
export area
public perimeter

area(r::Real) = (check(r); π * r^2)
perimeter(r::Real) = (check(r); 2π * r)
check(r) = r >= 0 || throw(DomainError(r, "a radius cannot be negative"))
end

using .Geometry
println("area(3.0) = ", area(3.0))
println("Geometry.perimeter(3.0) = ", Geometry.perimeter(3.0))
println("Geometry.check(3.0) = ", Geometry.check(3.0), "  (internal, yet a qualified name reaches it)")
for name in (:area, :perimeter, :check)
    println(rpad(name, 10), "exported: ", rpad(Base.isexported(Geometry, name), 6),
            "public: ", rpad(Base.ispublic(Geometry, name), 6), "usable without Geometry.: ", isdefined(Main, name))
end
println("names(Geometry) = ", names(Geometry))
try
    perimeter(3.0)
catch err
    println(first(split(sprint(showerror, err), '\n')))
end
geometry.jl, run with Julia 1.13.1 Python · geometry_session.py
from session import files, run

# One module with an exported name, a public one and an internal helper.
files("geometry.jl")
run("""
julia geometry.jl
""")

Output

$ julia geometry.jl
area(3.0) = 28.274333882308138
Geometry.perimeter(3.0) = 18.84955592153876
Geometry.check(3.0) = true  (internal, yet a qualified name reaches it)
area      exported: true  public: true  usable without Geometry.: true
perimeter exported: false public: true  usable without Geometry.: false
check     exported: false public: false usable without Geometry.: false
names(Geometry) = [:Geometry, :area, :perimeter]
UndefVarError: `perimeter` not defined in `Main`

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 geometry_session.py

After using .Geometry, the dot meaning “a module defined here, not a package”, plain area(3.0) worked and gave 28.27, while plain perimeter(3.0) was an UndefVarError in Main. Yet Geometry.perimeter(3.0) and even Geometry.check(3.0) worked: a qualified name: A name written with its module in front, such as Geometry.perimeter or Base.sqrt. It always reaches the name, whether the module exports it, marks it public or neither. always reaches a module’s names. The manual is direct about it: Julia has no way to truly hide a module’s internals. export and public say what you should use, not what you can reach. Step through what using changes:

Interactive · step by step What using .Geometry makes visible in Main Step through the calls of geometry.jl and watch which names Main reaches plainly and which only by a qualified name.
What using .Geometry makes visible in Main, step 5 of 5. Showing module Geometry, area, perimeter, check, Main (sees Geometry and area), area(3.0) (returned 28.27), perimeter(3.0) (UndefVarError), Geometry.check(3.0) (returned true). In focus: Geometry.check(3.0), check.using .Geometrymodule Geometrythree names insideareaexportedperimeterpubliccheckinternalMainsees Geometry and areaarea(3.0)returned 28.27perimeter(3.0)UndefVarErrorGeometry.check(3.0)returned true

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

Every step as text
  1. The module. Geometry defines three names. area is exported, perimeter is marked public, and check is a helper with no mark at all.
  2. using .Geometry. The dot means a module defined in this file, not a package. using makes the module name and its exported names usable in Main: here Geometry and area.
  3. A plain call. area(3.0) needs no prefix because area is exported. It returned 28.27, the area of a circle of radius 3.
  4. Public, not exported. perimeter is part of the API, but public does not bring it into Main, so plain perimeter(3.0) threw UndefVarError: perimeter not defined in Main.
  5. Nothing is hidden. Geometry.check(3.0) returned true. A qualified name reaches even an internal helper: export and public say what to use, not what can be reached.

names(Geometry) listed the module itself, area and perimeter, the names that are public; check appears only with names(Geometry; all = true).

Checkpoint

Check: Modules and qualified names

  1. Question 1 of 2 After using .Geometry, where Geometry exports area and marks perimeter public, which call works without the Geometry. prefix?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: area(3.0)

    using makes the module name and its exported names usable. Public and internal names need the module's name in front.

  2. Question 2 of 2 check is neither exported nor public. What does Geometry.check(3.0) do?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: It works and returns true

    export and public declare intent, not access. Anything defined in a module can be reached as Module.name, which is why unmarked names are internal by convention only.

using brings exported names in, and import lets you add methods

using Geometry makes the exported names usable, and import Geometry only the module’s name. Both accept a list, using Geometry: area or import Geometry: area, and the difference between those two decides how you add a method to area. Three modules try, each with a shape of its own:

extend.jl: three modules add a method to Geometry.area Julia · extend.jl
# Adding a method to a function that another module owns: two ways that work, and one that does not.
module Geometry
export area
area(r::Real) = π * r^2
end

module Squares                     # the function's module name in front: always extends it
using ..Geometry
struct Square; side::Float64; end
Geometry.area(s::Square) = s.side^2
end

module Discs                       # import brings the name in so that it can be extended
import ..Geometry: area
struct Disc; r::Float64; end
area(d::Disc) = area(d.r)
end

println("Geometry.area has ", length(methods(Geometry.area)), " methods: ",
        Geometry.area(Squares.Square(3.0)), " and ", round(Geometry.area(Discs.Disc(1.0)), digits = 4))

try
    @eval module Hexagons          # a plain definition after `using`, once the name has been used
    using ..Geometry
    const UNIT = area(1.0)
    struct Hexagon; side::Float64; end
    area(h::Hexagon) = 3 * sqrt(3) / 2 * h.side^2
    end
    f = Hexagons.area
    println("Hexagons: area is now ", parentmodule(f), ".area, a new function with ", length(methods(f)), " method")
    println("Hexagons.area(1.0): ", try f(1.0) catch err; nameof(typeof(err)) end)
catch err
    println("Hexagons: ", first(split(sprint(showerror, err), '\n')))
end
extend.jl, run with Julia 1.13.1 and 1.10.12 Python · extend_session.py
from session import files, run, use

# Three modules add a method to Geometry.area, on Julia 1.13.1 and on the 1.10.12 LTS.
files("extend.jl")
run("""
julia --version
julia extend.jl
""")
use("1.10.12")
run("""
julia --version
julia extend.jl
""")

Output

$ julia --version
julia version 1.13.1
$ julia extend.jl
Geometry.area has 3 methods: 9.0 and 3.1416
Hexagons: area is now Main.Hexagons.area, a new function with 1 method
Hexagons.area(1.0): MethodError
$ julia --version
julia version 1.10.12
$ julia extend.jl
Geometry.area has 3 methods: 9.0 and 3.1416
Hexagons: invalid method definition in Hexagons: function Geometry.area must be explicitly imported to be extended

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 extend_session.py

Squares wrote Geometry.area(s::Square) = …, which extends the function wherever it lives. Discs wrote import ..Geometry: area, after which a plain area(d::Disc) = … extends it too; Geometry.area ended with three methods on both versions. Hexagons wrote a plain definition after using, and the versions part ways. On 1.10.12 the definition stopped with function Geometry.area must be explicitly imported to be extended. On 1.13.1 it made a new function, Main.Hexagons.area, with one method, so inside Hexagons the name area no longer meant the function it had just used, and area(1.0) was a MethodError. The manual’s section on the precedence of definitions explains it: a name brought in by plain using is the weakest kind of binding, and a definition of your own replaces it.

You write What it brings in Can a plain area(…) = … add a method?
using Geometry Geometry and every exported name No: on 1.12 and later it makes a new function
using Geometry: area Only area No: an error, explicitly imported to be extended
import Geometry Only Geometry Write Geometry.area(…) = …
import Geometry: area Only area Yes

Extending a function after a plain using

Wrong: using Geometry and then area(s::Square) = …, which on 1.12 and later quietly defines your own area and hides the one you meant. Right: Geometry.area(s::Square) = …, which reads as what it is, or import Geometry: area at the top of the file.

For code that others depend on, the manual recommends listing what you use, using Foo: Foo, f, rather than plain using Foo: a new export in a later version of Foo then cannot collide with your own names.

Checkpoint

Check: using, import and adding methods

  1. Question 1 of 2 Which of these add a method to Geometry.area for a new type Square? Choose every one that does.

    Choose every answer that is right.

    Show the answer: Question 1 of 2

    Answer:

    • import .Geometry: area, then area(s::Square) = s.side^2
    • using .Geometry, then Geometry.area(s::Square) = s.side^2

    Extend with the module's name in front, or after import M: f. A plain definition after using never extends the other module's function.

  2. Question 2 of 2 Hexagons writes using ..Geometry, calls area(1.0), then defines area(h::Hexagon) = .... What happened on Julia 1.13.1?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: It made a new function Hexagons.area, so area(1.0) inside Hexagons became a MethodError

    The same file behaves differently on 1.10 and 1.13, and neither result is the extension the author wanted: write Geometry.area(...) or import the name first.

export puts a name in scope, and public only marks it as API

Both keywords declare public API: The names a module promises to keep working across versions: the names it exports and the names it marks public. Its other names are internal and may change in any release., the names a module promises to keep working across versions; only export also hands the name to using. public is new in Julia 1.11, and the same three files show what that means on the 1.10 LTS:

Three small modules that declare an API

public_keyword.jl

# The public keyword: Julia 1.11 and later.
module Stations
export lookup
public describe
lookup(code) = code
describe(code) = "station $code"
end
println("public works: ", Base.ispublic(Stations, :describe))

public_compat.jl

# The form the manual gives for code that must also load on Julia 1.10 and earlier.
module Stations
export lookup
VERSION >= v"1.11.0-DEV.469" && eval(Meta.parse("public describe"))
lookup(code) = code
describe(code) = "station $code"
end
println("loaded on ", VERSION, "; describe is public: ",
        isdefined(Base, :ispublic) ? Base.ispublic(Stations, :describe) : "(1.10 has no public)")

public_and_export.jl

# One name declared both exported and public.
module Stations
export describe
public describe
describe(code) = "station $code"
end
The three files, run with Julia 1.13.1 and 1.10.12 Python · api_versions_session.py
from session import files, run, use

# public on 1.13.1 and on the 1.10.12 LTS, and a name that is both exported and public.
files("public_keyword.jl", "public_compat.jl", "public_and_export.jl")
run("""
julia --version
julia public_keyword.jl
julia public_compat.jl
!julia public_and_export.jl
""")
use("1.10.12")
run("""
julia --version
!julia public_keyword.jl
julia public_compat.jl
""")

Output

$ julia --version
julia version 1.13.1
$ julia public_keyword.jl
public works: true
$ julia public_compat.jl
loaded on 1.13.1; describe is public: true
$ julia public_and_export.jl
ERROR: LoadError: cannot declare Stations.describe public; it is already declared exported
Stacktrace:
 [1] include(mod::Module, _path::String)
   @ Base ./Base.jl:309
 [2] exec_options(opts::Base.JLOptions)
   @ Base ./client.jl:344
 [3] _start()
   @ Base ./client.jl:577
in expression starting at ~/demo/public_and_export.jl:2
[exit status 1]
$ julia --version
julia version 1.10.12
$ julia public_keyword.jl
ERROR: LoadError: ParseError:
# Error @ ~/demo/public_keyword.jl:4:8
export lookup
#      ┌───────
public describe
lookup(code) = code
#─────────┘ ── Expected `end`
Stacktrace:
 [1] top-level scope
   @ ~/demo/public_keyword.jl:4
in expression starting at ~/demo/public_keyword.jl:4
[exit status 1]
$ julia public_compat.jl
loaded on 1.10.12; describe is public: (1.10 has no public)

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 api_versions_session.py

On 1.13.1, public describe worked and Base.ispublic(Stations, :describe) was true. Declaring one name both export and public stopped the file: cannot declare Stations.describe public; it is already declared exported, an error since 1.12, because an exported name is public already. On 1.10.12 the keyword itself was a ParseError, so a package that still supports the LTS uses the guarded form from the manual, VERSION >= v"1.11.0-DEV.469" && eval(Meta.parse("public describe")), which loaded on both. The keywords arrived in this order:

Timeline · 3 events in order How a module declares its API, version by version Walk through the releases in order; each one changed what export and public mean for your code.
  1. 1.0

    export, using and import

    export lists the names that using brings into the caller's scope. import M: f brings one name in so that methods can be added to it. A qualified name such as M.f reaches every name, marked or not.

  2. 1.11

    The public keyword new keyword

    public marks a name as API without bringing it into anyone's scope, and Base.ispublic tests it. On 1.10 the keyword is a parse error, so code for both uses the manual's VERSION-guarded form.

  3. 1.12

    Public or exported, never both new error

    Declaring one name both public and exported became an error. names gained usings = true, and the REPL warns when a name is reached through a module that neither defines it nor makes it public.

The timeline has 3 events, from 1.0 to 1.12.

The REPL of 1.12 and later also helps keep to the API. It warns when you reach a name through a module that neither defines it nor declares it public:

A REPL session with Julia 1.13.1 Python · repl_warning_session.py
from session import repl

# The REPL of Julia 1.12 and later warns when a name is reached through a module that neither defines it nor
# declares it public.
repl([
    "module Geometry; check(r) = r >= 0; end",
    "Geometry.check(1.0)",
    "Geometry.sqrt(16.0)",
    "Base.sqrt(16.0)",
])

Output

$ julia --startup-file=no -q
julia> module Geometry; check(r) = r >= 0; end
Main.Geometry

julia> Geometry.check(1.0)
true

julia> Geometry.sqrt(16.0)
[ Warning: sqrt is defined in Base and is not public in Main.Geometry
4.0

julia> Base.sqrt(16.0)
4.0

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 repl_warning_session.py

Geometry.check(1.0) passed quietly, since Geometry defines check. Geometry.sqrt(16.0) worked too, but with a warning that sqrt belongs to Base: code that reaches names that way breaks if the module stops using them.

Key idea

Export the few names users call all the time, mark the rest of your API public, and leave helpers unmarked: qualified names still reach everything, but only the public names are a promise.

Checkpoint

Check: export and public

  1. Question 1 of 2 You want users to write Stations.describe(code) and promise that describe stays in the API, without adding describe to their namespace. What do you write?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: public describe

    export for the names users call all the time, public for the rest of the API, no mark for helpers.

  2. Question 2 of 2 A package must load on the 1.10 LTS and on 1.13. How should it mark describe public?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: VERSION >= v"1.11.0-DEV.469" && eval(Meta.parse("public describe"))

    public arrived in Julia 1.11. Code that supports 1.10 uses the guarded form, or the @compat macro of the Compat package, which the manual also names.

A module can span files, and submodules find each other by relative paths

include("units.jl") runs a file as if its text stood at that point of the module, so one module can be split across files. Modules nest too, and a submodule reaches its siblings with a relative path: .Units is a module inside the current one, and each further dot goes up one level. Lab is made of three files:

A module in three files, and a script that uses it

lab/Lab.jl · lab/Lab.jl

# The top module: one file per part, each included into the module that needs it.
module Lab
export summary_line
include("units.jl")        # defines the submodule Units
include("report.jl")       # defines the submodule Report, which uses Units
using .Report: summary_line
end

lab/units.jl · lab/units.jl

module Units
export mm_to_cm
mm_to_cm(x) = x / 10
const SYMBOL = "cm"
end

lab/report.jl · lab/report.jl

module Report
using ..Units              # a sibling module: up one level to Lab, then down to Units
export summary_line
summary_line(station, mm) = string(station, ": ", mm_to_cm(mm), " ", Units.SYMBOL)
end

use_lab.jl

# Load a module made of three files, and reach its parts by relative and full paths.
include("lab/Lab.jl")
using .Lab
println(summary_line("Pune", 722.0))
println("Lab.Units.mm_to_cm(150.0) = ", Lab.Units.mm_to_cm(150.0))
println("fullname(Lab.Report) = ", fullname(Lab.Report), ",  parentmodule(summary_line) = ", parentmodule(summary_line))
println("names(Lab) = ", names(Lab))
use_lab.jl, run with Julia 1.13.1 Python · lab_session.py
from session import files, run

# A module split into three files with include, and a script that uses it.
files("use_lab.jl", "lab/Lab.jl", "lab/units.jl", "lab/report.jl")
run("""
julia use_lab.jl
""")

Output

$ julia use_lab.jl
Pune: 72.2 cm
Lab.Units.mm_to_cm(150.0) = 15.0
fullname(Lab.Report) = (:Main, :Lab, :Report),  parentmodule(summary_line) = Main.Lab.Report
names(Lab) = [:Lab, :summary_line]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 lab_session.py

The diagram shows where each module lives and what each using reaches:

Main holds Lab, from lab/Lab.jl; Lab holds Units, from units.jl, and Report, from report.jl, which uses ..Units.Main: use_lab.jlLab: lab/Lab.jlUnits: lab/units.jlexports mm_to_cmReport: lab/report.jlusing ..Unitsexports summary_lineusing ..Units

The modules of the lab folder, each with the file it lives in

Text description of the diagram

Each box is a module, drawn inside the module that contains it, with the file its code comes from.

  1. Main is the script use_lab.jl, which includes lab/Lab.jl and writes using .Lab.
  2. Lab is defined in lab/Lab.jl. It includes units.jl and report.jl, so both submodules live inside Lab.
  3. Units, from lab/units.jl, exports mm_to_cm.
  4. Report, from lab/report.jl, writes using ..Units: two dots go up one level to Lab and then down to its sibling Units. It exports summary_line, which Lab takes with using .Report: summary_line and exports again.

Report wrote using ..Units, up one level to Lab and down to Units, so mm_to_cm worked inside it and summary_line("Pune", 722.0) printed 72.2 cm. fullname(Lab.Report) traced the path from Main, and names(Lab) listed summary_line, which Lab took from Report and exported again. Files and modules are independent: one module may span many files and one file may hold several modules, but a package keeps one top-level module, in src/<Name>.jl.

Checkpoint

Check: Files, submodules and relative paths

  1. Question 1 of 2 Inside Lab.Report, which statement reaches its sibling Lab.Units?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: using ..Units

    Relative paths start at the current module: .X is a child, ..X a sibling, ...X a sibling of the parent.

  2. Question 2 of 2 What does include("units.jl") inside module Lab do?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Evaluates the file's code in the global scope of Lab, as if it were written there

    One module can span many files with include, and one file can hold several modules.

The init function runs at every load, and top-level code only at precompile time

A package’s module is compiled once and saved in the depot: A folder where Julia keeps packages, registries, compiled code and logs. Pkg writes to the first entry of DEPOT_PATH, which is ~/.julia by default. More in “Install Julia with juliaup and manage versions”’s cache; later sessions load the cache instead of running the source again, and any value top-level code computes is saved with it. A function named __init__: A function named __init__ in a module. Julia calls it every time the module is loaded, after a precompiled module has been read from its cache, so it suits work that depends on the session. is the exception, run at every load. Settings reads the environment variable RAIN_UNITS twice, at top level and in __init__:

Settings/src/Settings.jl: one setting, read in two places Julia · Settings/src/Settings.jl
module Settings
export units_at_precompile, units_at_load

# Top-level code runs while the package is precompiled; the value is saved in the cache with the code.
const UNITS_AT_PRECOMPILE = get(ENV, "RAIN_UNITS", "mm")

# __init__ runs every time the package is loaded, after the cache has been read.
const UNITS_AT_LOAD = Ref("")
function __init__()
    UNITS_AT_LOAD[] = get(ENV, "RAIN_UNITS", "mm")
end

units_at_precompile() = UNITS_AT_PRECOMPILE
units_at_load() = UNITS_AT_LOAD[]
end
Settings, loaded twice with Julia 1.13.1 Python · init_session.py
from session import files, run

# A package that reads RAIN_UNITS twice: in top-level code and in __init__. The first run precompiles it.
files("Settings/Project.toml", "Settings/src/Settings.jl")
run("""
julia --project=Settings -e 'using Settings; println(units_at_precompile(), " ", units_at_load())'
RAIN_UNITS=cm julia --project=Settings -e 'using Settings; println(units_at_precompile(), " ", units_at_load())'
ls ~/.julia/compiled/v1.13
""")

Output

$ julia --project=Settings -e 'using Settings; println(units_at_precompile(), " ", units_at_load())'
mm mm
$ RAIN_UNITS=cm julia --project=Settings -e 'using Settings; println(units_at_precompile(), " ", units_at_load())'
mm cm
$ ls ~/.julia/compiled/v1.13
Settings

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 init_session.py

The first run compiled the package, with RAIN_UNITS unset, and both readings said mm. The second run set RAIN_UNITS=cm. __init__ read cm, because Julia calls a module’s __init__ every time the module is loaded. The top-level constant still said mm: it was computed during precompilation and loaded from the cache, and changing an environment variable does not make Julia precompile again. Follow both runs:

Interactive · message by message Top-level code at precompile time, __init__ at every load Step through the two runs of Settings and watch which reading of RAIN_UNITS each value comes from.
Top-level code at precompile time, __init__ at every load, step 6 of 6. Messages so far: First run to The cache: Settings, with "mm"; The cache to Second run: cached module.First runprinted mm mmThe cache~/.julia/compiled/v1.13Second runprinted mm cmSettings, with "mm"cached moduleconstant stays "mm" until recompiled

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

Every step as text
  1. No cache yet. The first using Settings finds no compiled copy, so Julia precompiles the package: it runs the module's top-level code once, in a separate process.
  2. Top-level code runs. const UNITS_AT_PRECOMPILE = get(ENV, "RAIN_UNITS", "mm") reads the variable now. It is unset, so the constant is "mm".
  3. Saved with the code. The compiled module goes into the cache, and the value of the constant goes with it.
  4. __init__ at load. Loading the module calls __init__, which reads RAIN_UNITS again: still unset, so "mm". The first run printed mm mm.
  5. A new setting. The second run sets RAIN_UNITS=cm. The source has not changed, so the cache is still valid and Julia loads it; the top-level code does not run again.
  6. Two different answers. __init__ runs at this load and reads "cm". The constant was computed in the first run and still says "mm", so the second run printed mm cm.

The modules chapter of the manual makes it a rule: state that belongs to the machine the code runs on, such as the set-up of a C library and the pointers it returns, is computed in __init__, because a cache does not preserve external state. Environment variables, open files and the current time are state of the same kind.

Checkpoint

Check: __init__ and precompile time

  1. Question 1 of 2 Settings was precompiled with RAIN_UNITS unset. A later run sets RAIN_UNITS=cm. What do units_at_precompile() and units_at_load() return?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: "mm" and "cm"

    Top-level code of a package runs when it is precompiled, and its results are cached. __init__ runs every time the module is loaded.

  2. Question 2 of 2 Which of these belong in a package's __init__ rather than in its top-level code? Choose every one that does.

    Choose every answer that is right.

    Show the answer: Question 2 of 2

    Answer:

    • Recording the time the package was loaded
    • Reading an environment variable that selects units

    Anything that depends on the machine or the moment, environment variables, time, open files, pointers from C libraries, goes in __init__.

Practise: give a script an API

Turn a script into a module with a declared API

Take a script of your own with five or six functions and put them in a module. Export the two names users call most, mark two more public, and leave the rest unmarked. Check on your machine that names(YourModule) lists exactly the four API names, that Base.ispublic is false for the helpers, and that a file which adds a method to one of your functions after a plain using fails a test you write for it.

The self-check covers namespaces, using and import, export and public, submodules and __init__. Its first questions come from the part you have answered least well so far.

Practice that adapts to you

Practise this lesson

12 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 12 What did names(Geometry) return for a module that exports area, marks perimeter public and also defines check?

    Choose one answer.

    Show the answer: Question 1 of 12

    Answer: [:Geometry, :area, :perimeter]

    names(M) lists M's public names; all = true adds the internal ones.

  2. Question 2 of 12 Why does the script write using .Geometry with a dot, and not using Geometry?

    Choose one answer.

    Show the answer: Question 2 of 12

    Answer: Geometry is a module defined in this file, not a package in the environment

    using Name looks for a package; using .Name looks for a module inside the current one.

  3. Question 3 of 12 A colleague wants to make a helper impossible to call from outside its module. What is true?

    Choose one answer.

    Show the answer: Question 3 of 12

    Answer: Julia cannot do that; leave it unmarked and document that only public names are API

    Julia's API is a promise, not a wall: public and exported names are supported, the rest may change in any release.

  4. Question 4 of 12 What does import .Geometry alone bring into scope?

    Choose one answer.

    Show the answer: Question 4 of 12

    Answer: Only the name Geometry

    import M brings M; using M brings M and its exports; import M: f and using M: f bring f.

  5. Question 5 of 12 The manual recommends using Foo: Foo, f over plain using Foo for code that others depend on. Why?

    Choose one answer.

    Show the answer: Question 5 of 12

    Answer: A later version of Foo may export a new name that clashes with yours, and an explicit list cannot change

    When two modules export the same name, a plain reference becomes an error until you qualify it. Explicit lists avoid the surprise.

  6. Question 6 of 12 A file adds six methods to Base.show for its own types. Which style does the manual describe as making it clearest that methods go into another module's function?

    Choose one answer.

    Show the answer: Question 6 of 12

    Answer: Writing Base.show(io::IO, x::MyType) = ... for each method

    Both Base.show(...) = ... and import Base: show work. The qualified form is explicit at every definition; the import is shorter when there are many.

  7. Question 7 of 12 What is Base.ispublic(Geometry, :area) when area is exported?

    Choose one answer.

    Show the answer: Question 7 of 12

    Answer: true

    Base.ispublic is true for exported and public names; Base.isexported only for exported ones.

  8. Question 8 of 12 In the 1.13 REPL, Geometry.sqrt(16.0) printed 4.0 with a warning. What did the warning say, and why?

    Choose one answer.

    Show the answer: Question 8 of 12

    Answer: sqrt is defined in Base and is not public in Geometry, so reaching it through Geometry relies on an accident

    From 1.12 the REPL warns when a name is reached through a module that neither defines it nor makes it public.

  9. Question 9 of 12 What did fullname(Lab.Report) return in the run?

    Choose one answer.

    Show the answer: Question 9 of 12

    Answer: (:Main, :Lab, :Report)

    Submodules are nested namespaces: Lab.Report lives inside Lab, which lives inside Main.

  10. Question 10 of 12 names(Lab) listed summary_line, which is defined in Lab.Report. How did it get there?

    Choose one answer.

    Show the answer: Question 10 of 12

    Answer: Lab wrote using .Report: summary_line and exported it

    A parent can take names from its submodules with using and export them again, so users need only using .Lab.

  11. Question 11 of 12 When does Julia call a module's __init__ function?

    Choose one answer.

    Show the answer: Question 11 of 12

    Answer: Every time the module is loaded, after its cached code has been read

    __init__ runs at load time, in every session that loads the module.

  12. Question 12 of 12 A package writes const START = time() at top level to measure how long a session has run. Users see durations of several days. What is wrong, and what fixes it?

    Choose one answer.

    Show the answer: Question 12 of 12

    Answer: START was computed during precompilation; set it in __init__ instead, in a Ref

    Store values that belong to a session in __init__, for example START[] = time() with const START = Ref(0.0).

Interview questions

Warm-up (fresher to 2–5 years): why mark a name public instead of exporting it?

Both make a name part of the module’s public API, the names it promises to keep working across releases. export also brings the name into the scope of everyone who writes using TheModule, so a common word such as normalise can collide with other packages’ exports, and every new export is a new chance of a clash. public keeps the promise without that effect: callers write TheModule.normalise. Export the few names users call all the time and mark the rest of the API public. public needs Julia 1.11 or later, and since 1.12 declaring one name both ways is an error.

Two design questions follow: an update that breaks a script with a name clash, and a setting that will not change until the cache is deleted. Say your first move for each, then open its first step:

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Fresher to senior

    An analysis script starts with using Smoothing, Stats. After an update of Stats, every call to smooth(x) fails with an UndefVarError whose hint says two modules export different bindings with this name. Nothing in your script changed. Explain what happened and how you would write imports that survive updates.

    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 staff

    A package reads the path of its data folder from an environment variable in a top-level const. Users report that changing the variable has no effect until they delete their compiled cache, and two machines that share a depot disagree about the folder. Explain why, and fix the package.

    Model answer, Pro. First step and follow-ups

Key takeaways

  • A module is a namespace with its own global scope; a qualified name such as Geometry.check reaches any of its names, and export and public only declare which ones to use.
  • using M brings M and its exported names, import M: f lets a plain definition add methods to f, and on 1.12 and later a plain definition after using makes a new function instead.
  • public (1.11 and later) marks API without exporting it, declaring a name both ways is an error from 1.12, and the 1.10 LTS needs the VERSION-guarded form.
  • include splits a module across files, and submodules reach siblings with relative paths such as ..Units.
  • Top-level code of a package runs when it is precompiled and its results are cached, while __init__ runs at every load, so settings from the environment are read in __init__.

Terms in this lesson

__init__
A function named __init__ in a module. Julia calls it every time the module is loaded, after a precompiled module has been read from its cache, so it suits work that depends on the session.
module
A named namespace of Julia code with its own global scope, written module Name ... end. Names inside it are reached from outside as Name.x, or brought in with using and import.
public API
The names a module promises to keep working across versions: the names it exports and the names it marks public. Its other names are internal and may change in any release.
qualified name
A name written with its module in front, such as Geometry.perimeter or Base.sqrt. It always reaches the name, whether the module exports it, marks it public or neither.

References

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.