In this post, you will learn about tools to create, run and debug Julia code.
TLDR: You're not alone!
Before you write any line of code, it's good to know where to find help. The official help page is a good place to start. In particular, the Julia community is always happy to guide beginners.
As a rule of thumb, the Discourse forum is where you should ask your questions to make the answers discoverable for future users. If you just want to chat with someone, you have a choice between the open source Zulip and the closed source Slack. Some of the vocabulary used by community members may appear unfamiliar, but don't worry: StartHere.jl gives you a good overview.
TLDR: Use juliaup
The most natural starting point to install Julia onto your system is the Julia downloads page, which will tell you to use juliaup
.
juliaup
together from the Windows Store.curl -fsSL https://install.julialang.org | sh
In both cases, this will make the juliaup
and julia
commands accessible from the terminal (or Windows Powershell).
On Windows this will also create an application launcher.
All users can start Julia by running
julia
Meanwhile, juliaup
provides various utilities to download, update, organize and switch between different Julia versions.
As a bonus, you no longer have to manually specify the path to your executable.
This all works thanks to adaptive shortcuts called "channels", which allow you to access specific Julia versions without giving their exact number.
For instance, the release
channel will always point to the current stable version, and the lts
channel will always point to the long-term support version.
Upon installation of juliaup
, the current stable version of Julia is downloaded and selected as the default.
Advanced: To use other channels, add them to juliaup
and put a +
in front of the channel name when you start Julia:
juliaup add lts
julia +lts
You can get an overview of the channels installed on your computer with
juliaup status
When new versions are tagged, the version associated with a given channel can change, which means a new executable needs to be downloaded. If you want to catch up with the latest developments, just do
juliaup update
TLDR: The Julia REPL has 4 modes: Julia, package (]
), help (?
) and shell (;
).
The Read-Eval-Print Loop (or REPL) is the most basic way to interact with Julia, check out its documentation for details.
You can start a REPL by typing julia
into a terminal, or by clicking on the Julia application in your computer.
It will allow you to play around with arbitrary Julia code:
julia> a, b = 1, 2;
julia> a + b
3
This is the standard (Julia) mode of the REPL, but there are three other modes you need to know.
Each mode is entered by typing a specific character after the julia>
prompt.
Once you're in a non-Julia mode, you stay there for every command you run.
To exit it, hit backspace after the prompt and you'll get the julia>
prompt back.
?
)By pressing ?
you can obtain information and metadata about Julia objects (functions, types, etc.) or unicode symbols.
The query fetches the docstring of the object, which explains how to use it.
help?> println
println([io::IO], xs...)
Print (using ) xs
to io
followed by a newline. If io
is not supplied, prints to the default output stream .
See also to add colors etc.
julia> println("Hello, world")
Hello, world
julia> io = IOBuffer();
julia> println(io, "Hello", ',', " world.")
julia> String(take!(io))
"Hello, world.\n"
If you don't know the exact name you are looking for, type a word surrounded by quotes to see in which docstrings it pops up.
]
)By pressing ]
you access Pkg.jl, Julia's integrated package manager, whose documentation is an absolute must-read.
Pkg.jl allows you to:
]activate
different local, shared or temporary environments;]instantiate
them by downloading the necessary packages;]add
, ]update
(or ]up
) and ]remove
(or ]rm
) packages;]status
(or ]st
) of your current environment.As an illustration, we download the package Example.jl inside our current environment:
(writing) pkg> add Example
┌ Warning: The Pkg REPL mode is intended for interactive use only, and should not be used from scripts. It is recommended to use the functional API instead.
└ @ Pkg.REPLMode /opt/hostedtoolcache/julia/1.11.2/x64/share/julia/stdlib/v1.11/Pkg/src/REPLMode/REPLMode.jl:388
Resolving package versions...
Installed Example ─ v0.5.5
Updating `~/work/modernjuliaworkflows.github.io/modernjuliaworkflows.github.io/writing/Project.toml`
[7876af07] + Example v0.5.5
Updating `~/work/modernjuliaworkflows.github.io/modernjuliaworkflows.github.io/writing/Manifest.toml`
[7876af07] + Example v0.5.5
Precompiling project...
266.4 ms ✓ Example
1 dependency successfully precompiled in 0 seconds
(writing) pkg> status
Status `~/work/modernjuliaworkflows.github.io/modernjuliaworkflows.github.io/writing/Project.toml`
[7876af07] Example v0.5.5
Note that the same keywords are also available in Julia mode:
julia> using Pkg
julia> Pkg.rm("Example")
Updating `~/work/modernjuliaworkflows.github.io/modernjuliaworkflows.github.io/writing/Project.toml`
[7876af07] - Example v0.5.5
Updating `~/work/modernjuliaworkflows.github.io/modernjuliaworkflows.github.io/writing/Manifest.toml`
[7876af07] - Example v0.5.5
The package mode itself also has a help mode, accessed with ?
, in case you're lost among all these new keywords.
;
)By pressing ;
you enter a terminal, where you can execute any command you want.
Here's an example for Unix systems:
shell> ls ./writing
Manifest.toml
Project.toml
index.md
TLDR: VSCode is the IDE with the best Julia support.
Most computer programs are just plain text files with a specific extension (in our case .jl
).
So in theory, any text editor suffices to write and modify Julia code.
In practice, an Integrated Development Environment (or IDE) makes the experience much more pleasant, thanks to code-related utilities and language-specific plugins.
The best IDE for Julia is Visual Studio Code, or VSCode, developed by Microsoft. Indeed, the Julia VSCode extension is the most feature-rich of all Julia IDE plugins. You can download it from the VSCode Marketplace and read its documentation.
VSCode: In what follows, we will sometimes mention commands and keyboard shortcuts provided by this extension.
But the only shortcut you need to remember is Ctrl + Shift + P
(or Cmd + Shift + P
on Mac): this opens the VSCode command palette, in which you can search for any command.
Type "julia" in the command palette to see what you can do.
Advanced: Assuming you want to avoid the Microsoft ecosystem, VSCodium is a nearly bit-for-bit replacement for VSCode, but with an open source license and without telemetry. If you don't want to use VSCode at all, other options include Emacs and Vim. Check out JuliaEditorSupport to see if your favorite IDE has a Julia plugin. The available functionalities should be roughly similar to those of VSCode, at least for the basic aspects like running code.
You may also want to download the JuliaMono font for esthetically pleasant unicode handling.
TLDR: Open a REPL and run all your code there interactively.
You can execute a Julia script from your terminal, but in most cases that is not what you want to do.
julia myfile.jl # avoid this
Julia has a rather high startup and compilation latency.
If you only use scripts, you will pay this cost every time you run a slightly modified version of your code.
That is why many Julia developers fire up a REPL at the beginning of the day and run all of their code there, chunk by chunk, in an interactive way.
Full files can be run interactively from the REPL with the include
function.
julia> include("myfile.jl")
Alternatively, includet
from the Revise.jl package can be used to "include and track" a file.
This will automatically update changes to function definitions in the file in the running REPL session.
VSCode: Running code is made much easier by the following commands:
Julia: Restart REPL
(shortcut Alt + J
then Alt + R
) - this will open or restart the integrated Julia REPL. It is different from opening a plain VSCode terminal and launching Julia manually from there.Julia: Execute Code in REPL and Move
(shortcut Shift + Enter
) - this will execute the selected code in the integrated Julia REPL, like a notebook.When keeping the same REPL open for a long time, it's common to end up with a "polluted" workspace where the definitions of certain variables or functions have been overwritten in unexpected ways.
This, along with other events like struct
redefinitions, might force you to restart your REPL now and again, and that's okay.
TLDR: Try either Jupyter or Pluto, depending on your reactivity needs.
Notebooks are a popular alternative to IDEs when it comes to short and self-contained code, typically in data science. They are also a good fit for literate programming, where lines of code are interspersed with comments and explanations.
The most well-known notebook ecosystem is Jupyter, which supports Julia, Python and R as its three core languages.
To use it with Julia, you will need to install the IJulia.jl backend.
Then, if you have also installed Jupyter with pip install jupyterlab
, you can run this command to launch the server:
jupyter lab
If you only have IJulia.jl on your system, you can run this snippet instead:
julia> using IJulia
julia> IJulia.notebook()
VSCode: Jupyter notebooks can be opened, modified and run directly from the editor. Thanks to the Julia extension, you don't even need to install IJulia.jl or Jupyter first.
A pure-Julia alternative to Jupyter is given by Pluto.jl. Unlike Jupyter notebooks, Pluto notebooks are
To try them out, install the package and then run
julia> using Pluto
julia> Pluto.run()
TLDR: Markdown is also a good fit for literate programming, and Quarto is an alternative to notebooks.
Markdown is a markup language used to add formatting elements to plaintext text files.
Plain text markdown files, which have the .md
extension, are not used for interactive programming, meaning one cannot run code written in the file.
As a result, plain text markdown files are usually rendered into a final product by other software.
This is an example of a plain text markdown file:
# Title
## Section Header
This is example text.
```julia
println("hello world")
```
Quarto "is an open-source scientific and technical publishing system."
Quarto makes a plain text markdown file (.md
) alternative called Quarto markdown file (.qmd
).
Quarto markdown files like plain text markdown files also integrate with editors, such as VSCode.
VSCode: Install the Quarto extension for a streamlined experience.
Unlike plain text markdown files, Quarto markdown files have executable code chunks.
These code chunks provide a functionality similar to notebooks, thus Quarto markdown files are an alternative to notebooks.
Additionally, Quarto markdown files give users additional control over output and styling via the YAML header at the top of the .qmd
file.
As of Quarto version 1.5, users can choose from two Julia engines to execute code - a native Julia engine and IJulia.jl. The primary difference between the native Julia engine and IJulia.jl is that the native Julia engine does not depend on Python and can utilize local environments. For this reason it's recommended to start with the native Julia engine. Learn more about the native Julia engine in Quarto's documentation.
Below is an example of a Quarto markdown file.
---
title: "My document"
format:
# renders a HTML document
html:
# table of contents
toc: true
execute:
# makes code chunks invisible in the output
# code output is still visible though
echo: false
# hides warnings in the output
warning: false
# native julia engine
engine: julia
---
# Title
## Section Header
Below is an executable code chunk.
If this file were opened in an editor such as VSCode one could execute the `println("hello world")` Julia code and view the output, like in a notebook.
```{julia}
println("hello world")
```
TLDR: Activate a local environment for each project with ]activate path
. Its details are stored in Project.toml
and Manifest.toml
.
As we have seen, Pkg.jl is the Julia equivalent of pip
or conda
for Python.
It lets you install packages and manage environments (collections of packages with specific versions).
You can activate an environment from the Pkg REPL by specifying its path ]activate somepath
.
Typically, you would do ]activate .
to activate the environment in the current directory.
Another option is to directly start Julia inside an environment, with the command line option julia --project=somepath
.
Once in an environment, the packages you ]add
will be listed in two files somepath/Project.toml
and somepath/Manifest.toml
:
Project.toml
contains general project information (name of the package, unique id, authors) and direct dependencies with version bounds.Manifest.toml
contains the exact versions of all direct and indirect dependenciesIf you haven't entered any local project, packages will be installed in the default environment, called @v1.X
after the active version of Julia (note the @
before the name).
Packages installed that way are available no matter which local environment is active, because of "environment stacking".
It is therefore recommended to keep the default environment very light, containing only essential development tools.
VSCode: You can configure the environment in which a VSCode Julia REPL opens.
Just click the Julia env: ...
button at the bottom.
Note however that the Julia version itself will always be the default one from juliaup
.
Advanced: You can visualize the dependency graph of an environment with PkgDependency.jl.
TLDR: A package makes your code modular and reproducible.
Once your code base grows beyond a few scripts, you will want to create a package of your own.
The first advantage is that you don't need to specify the path of every file: using MyPackage: myfunc
is enough to get access to the names you define.
Furthermore, you can specify versions for your package and its dependencies, making your code easier and safer to reuse.
To create a new package locally, the easy way is to use ]generate
(we will discuss a more sophisticated workflow in the next blog post).
julia> Pkg.generate(sitepath("MyPackage")); # ignore sitepath
Generating project MyPackage:
~/work/modernjuliaworkflows.github.io/modernjuliaworkflows.github.io/__site/MyPackage/Project.toml
~/work/modernjuliaworkflows.github.io/modernjuliaworkflows.github.io/__site/MyPackage/src/MyPackage.jl
This command initializes a simple folder with a Project.toml
and a src
subfolder.
As we have seen, the Project.toml
specifies the dependencies.
Meanwhile, the src
subfolder contains a file MyPackage.jl
, where a module called MyPackage
is defined.
It is the heart of your package, and will typically look like this when you're done:
module MyPackage
# imported dependencies
using OtherPackage1
using OtherPackage2
# files defining functions, types, etc.
include("file1.jl")
include("subfolder/file2.jl")
# names you want to make public
export myfunc
export MyType
end
TLDR: Use Revise.jl to track code changes while you play with your package in its own environment.
Once you have created a package, your daily routine will look like this:
MyPackage
MyPackage
For that to work well, you need code modifications to be taken into account automatically. That is why Revise.jl exists. In fact, it is used by so many Julia developers that some wish it were part of the core language: you can read its documentation for more details. If you start every REPL session by importing Revise.jl, then all the other packages you import after that will have their code tracked. Whenever you edit a source file and hit save, the REPL will update its state accordingly.
VSCode: The Julia extension imports Revise.jl by default when it starts a REPL.
The only remaining question is: in which environment should you work? In general, you can work within the environment defined by your package, and add all the dependencies you need there. To summarize, this is how you get started:
using Revise, Pkg
Pkg.activate("./MyPackage")
using MyPackage
MyPackage.myfunc()
Advanced: There are situations where the previous method does not work:
Then, you will need to use another environment as a playground, and ]develop
(or ]dev
) your package(s) into it.
Note the new Pkg.jl keyword: ]add PackageName
is used to download a fixed version of a registered package, while ]develop path
links to the current state of the code in a local folder.
To summarize, this is how you get started:
using Revise, Pkg
Pkg.activate("./MyPlayground")
Pkg.develop(path="./MyPackage")
using MyPackage
MyPackage.myfunc()
TLDR: Use the startup file to import packages as soon as Julia starts.
Julia accepts startup flags to handle settings such as the number of threads available or the environment in which it launches.
In addition, most Julia developers also have a startup file which is run automatically every time the language is started.
It is located at .julia/config/startup.jl
.
The basic component that everyone puts in the startup file is Revise.jl:
try
using Revise
catch e
@warn "Error initializing Revise"
end
In addition, users commonly import packages that affect the REPL experience, as well as esthetic, benchmarking or profiling utilities. A typical example is OhMyREPL.jl which is widely used for syntax highlighting in the REPL. More generally, the startup file allows you to define your own favorite helper functions and have them immediately available in every Julia session. StartupCustomizer.jl can help you set up your startup file.
Advanced: Here are a few more startup packages that can make your life easier once you know the language better:
TLDR: Explore source code from within the REPL.
The Julia REPL comes bundled with InteractiveUtils.jl, a bunch of very useful functions for interacting with source code.
Here are a few examples:
julia> supertypes(Int64)
(Int64, Signed, Integer, Real, Number, Any)
julia> subtypes(Integer)
Any[Bool, Signed, Unsigned]
julia> length(methodswith(Integer))
717
julia> @which exp(1)
exp(x::Real) @ Base.Math math.jl:1528
julia> apropos("matrix exponential")
Base.exp
Base.:^
When you ask for help on a Julia forum, you might want to include your local Julia information:
julia> versioninfo()
Julia Version 1.11.2
Commit 5e9a32e7af2 (2024-12-01 20:02 UTC)
Build Info:
Official https://julialang.org/ release
Platform Info:
OS: Linux (x86_64-linux-gnu)
CPU: 4 × AMD EPYC 7763 64-Core Processor
WORD_SIZE: 64
LLVM: libLLVM-16.0.6 (ORCJIT, znver3)
Threads: 1 default, 0 interactive, 1 GC (on 4 virtual cores)
Environment:
JULIA_PRE =
JULIA_POST =
JULIA_DEBUG =
Advanced: The following packages can give you even more interactive power:
TLDR: Logging macros are more versatile than printing.
When you encounter a problem in your code or want to track progress, a common reflex is to add print
statements everywhere.
function printing_func(n)
for i in 1:n
println(i^2)
end
end
julia> printing_func(3)
1
4
9
A slight improvement is given by the @show
macro, which displays the variable name:
function showing_func(n)
for i in 1:n
@show i^2
end
end
julia> showing_func(3)
i ^ 2 = 1
i ^ 2 = 4
i ^ 2 = 9
But you can go even further with the macros @debug
, @info
, @warn
and @error
.
They have several advantages over printing:
function warning_func(n)
for i in 1:n
@warn "This is bad" i^2
end
end
julia> warning_func(3)
┌ Warning: This is bad
│ i ^ 2 = 1
└ @ [Franklin]:3
┌ Warning: This is bad
│ i ^ 2 = 4
└ @ [Franklin]:3
┌ Warning: This is bad
│ i ^ 2 = 9
└ @ [Franklin]:3
Refer to the logging documentation for more information.
Advanced: In particular, note that @debug
messages are suppressed by default.
You can enable them through the JULIA_DEBUG
environment variable if you specify the source module name, typically Main
or your package module.
Beyond the built-in logging utilities, ProgressLogging.jl has a macro @progress
, which interfaces nicely with VSCode and Pluto to display progress bars.
And Suppressor.jl can sometimes be handy when you need to suppress warnings or other bothersome messages (use at your own risk).
TLDR: Infiltrator.jl and Debugger.jl allow you to peek inside a function while its execution is paused.
The problem with printing or logging is that you cannot interact with local variables or save them for further analysis.
The following two packages solve this issue, and they probably belong in your default environment @v1.X
, like Revise.jl.
Assume you want to debug a function checking whether the \(n\)-th Fermat number \(F_n = 2^{2^n} + 1\) is prime:
function fermat_prime(n)
k = 2^n
F = 2^k + 1
for d in 2:isqrt(F) # integer square root
if F % d == 0
return false
end
end
return true
end
julia> fermat_prime(4)
true
julia> fermat_prime(6)
true
Unfortunately, \(F_4 = 65537\) is the largest known Fermat prime, which means \(F_6\) is incorrectly classified. Let's investigate why this happens!
Infiltrator.jl is a lightweight inspection package, which will not slow down your code at all.
Its @infiltrate
macro allows you to directly set breakpoints in your code.
Calling a function which hits a breakpoint will activate the Infiltrator REPL-mode
and change the prompt to infil>
.
Typing ?
in this mode will summarize available commands.
For example, typing @locals
in Infiltrator-mode will print local variables:
using Infiltrator
function fermat_prime_infil(n)
k = 2^n
F = 2^k + 1
@infiltrate
for d in 2:isqrt(F)
if F % d == 0
return false
end
end
return true
end
What makes Infiltrator.jl even more powerful is the @exfiltrate
macro, which allows you to move local variables into a global storage called the safehouse
.
julia> fermat_prime_infil(6)
Infiltrating fermat_prime_infil(n::Int64)
at REPL[2]:4
infil> @exfiltrate k F
Exfiltrating 2 local variables into the safehouse.
infil> @continue
true
julia> safehouse.k
64
julia> safehouse.F
1
The diagnosis is a classic one: integer overflow. Indeed, \(2^{64}\) is larger than the maximum integer value in Julia:
julia> typemax(Int)
9223372036854775807
julia> 2^63-1
9223372036854775807
And the solution is to call our function on "big" integers with an arbitrary number of bits:
julia> fermat_prime(big(6))
false
Debugger.jl allows us to interrupt code execution anywhere we want, even in functions we did not write.
Using its @enter
macro, we can enter a function call and walk through the call stack, at the cost of reduced performance.
The REPL prompt changes to 1|debug>
, allowing you to use custom navigation commands to step into and out of function calls, show local variables and set breakpoints.
Typing a backtick `
will change the prompt to 1|julia>
, indicating evaluation mode.
Any expression typed in this mode will be evaluated in the local context.
This is useful to show local variables, as demonstrated in the following example:
julia> using Debugger
julia> @enter fermat_prime(6)
In fermat_prime(n) at REPL[7]:1
1 function fermat_prime(n)
>2 k = 2^n
3 F = 2^k + 1
4 for d in 2:isqrt(F) # integer square root
5 if F % d == 0
6 return false
About to run: (^)(2, 6)
1|debug> n
In fermat_prime(n) at REPL[7]:1
1 function fermat_prime(n)
2 k = 2^n
>3 F = 2^k + 1
4 for d in 2:isqrt(F) # integer square root
5 if F % d == 0
6 return false
7 end
About to run: (^)(2, 64)
1|julia> k
64
VSCode: VSCode offers a nice graphical interface for debugging.
Click left of a line number in an editor pane to add a breakpoint, which is represented by a red circle.
In the debugging pane of the Julia extension, click Run and Debug
to start the debugger.
The program will automatically halt when it hits a breakpoint.
Using the toolbar at the top of the editor, you can then continue, step over, step into and step out of your code.
The debugger will open a pane showing information about the code such as local variables inside of the current function, their current values and the full call stack.
The debugger can be sped up by selectively compiling modules that you will not need to step into via the +
symbol at the bottom of the debugging pane.
It is often easiest to start by adding ALL_MODULES_EXCEPT_MAIN
to the compiled list, and then selectively remove the modules you need to have interpreted.