Package development
Last updated on 2026-04-14 | Edit this page
Overview
Questions
- How do I generate a new Julia package?
- What is the best workflow for developing a Julia package?
- How can I prevent having to recompile every time I make a change?
Objectives
- Quick start with using
Pkg.generateor the BestieTemplate to generate a new package - Basic structure of a package
- Revise.jl
Generating a fresh new Julia package
Structure of your new (empty) package
Activating the generated package
Now that we have generated our new package and inspected its contents and structure, we would like to use it.
In the shell, let’s enter the the directory of the new package, and
open the Julia REPL:
cd Newton.jl/
julia
We will start by checking what environment we are actually currently
using. We do this with the pkg> status command:
julia> # press ]
pkg> status
The output will show something like the following:
OUTPUT
Status `~/.julia/environments/v1.12/Project.toml`
This tells us that we are currently using the base environment for
the currently installed Julia version (in the above case,
v1.12. In a sense, you can think of this as the “default”
or “global” environment used by Julia, if no other has been
specified by the user.
The output of pkg> status will also output the
dependencies (and precise versions) that are currently installed in that
environment. If you used a package template like the
BestieTemplate, you will see that is already listed
here.
But we don’t currently want this environment. We would like to use, and work on, our new project. We do this by “activating” it:
pkg> activate .
Now let us check the pkg status again:
pkg> status
The output should now show Project Newton v0.1.0,
indicating that we are indeed using our new package. The “Status” line
will also now give the path to the Newton.jl’s
Project.toml, which is another sign that everything is in
order. However, the end of the line will say
(empty project), because there are currently no
dependencies of our package.
Adding dependencies
As before, we can try adding the Random package.
pkg> add Random
Checking the new contents of Project.toml, we see that a
[deps] section has appeared:
OUTPUT
[deps]
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
This shows that the Random package is a dependency of
our package, and also specifies the UUID that precisely identifies the
package. Remember that our package, Newton.jl also has such
a UUID.
Running pkg> status again will now also list this
dependency, instead of “(empty project)”.
You should now see that a Manifest.toml file is also in
the package directory. In this file, you should see something like the
following:
OUTPUT
# This file is machine-generated - editing it directly is not advised
julia_version = "1.12.5"
manifest_format = "2.0"
project_hash = "455df57f797e33b4c447167f16ca7912fdf88c81"
[[deps.Newton]]
path = "."
uuid = "1005e0b6-1dc9-4f2b-8c05-1b27f43311c6"
version = "0.1.0"
[[deps.Random]]
deps = ["SHA"]
uuid = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
version = "1.11.0"
[[deps.SHA]]
uuid = "ea8e919c-243c-51af-8825-aaa63cd721ce"
version = "0.7.0"
As before, we can now remove this Random package because
we do not need it.
pkg> remove Random
If we check pkg> status again, we see that we have
returned to the “(empty project)” state. Similarly, looking inside
Project.toml we see that the dependency has indeed
disappeared from the project.
Developing the package
Now that we have our empty package set up, we would like to develop some code for it!
In the REPL we can try running this:
julia> using Newton
julia> Newton.hello_world()
OUTPUT
"Hello, World!"
Note that we needed using Newton to tell Julia to make
the name Newton available for us to refer to. We then
called the hello_world function that is part of that
module.
The world is not enough
But perhaps the “World” is not inclusive enough. Let’s try saying hello to the whole universe. Try modifying the function to say “Hello, Universe!” then call it in the REPL again.
Before doing this - what do you think the result will be?
julia> Newton.hello_world()
OUTPUT
"Hello, World!"
Why did this happen? The answer is that Julia is using the version of the package as it existed when it was first loaded. The modifications you have made have not been tracked or recompiled, so the original function is still being called.
If you reload Julia (exit then open the REPL again) and try again, you will see the result now says “Universe” as desired.
Change the message back to Hello, World! for now.
Julia uses the package Newton in whatever state it is
when using is first called. Subsequent changes to the
source code do not trigger automatic recompilation, unless e.g. Julia is
restarted. This is problematic since, during development, we often want
to make changes to our code without restarting the Julia session to
check it. We can achieve this with Revise.jl.
Installing Revise.jl
Make sure you are in the default environment when you install
Revise.jl, as we generally do not want developer
dependencies to be a part of the package. Anything you install in the
default shared environment will be available in specific environments
too due to what is called “environment stacking”.
pkg> activate # No argument, so as to pick the default environment
pkg> status
pkg> add Revise
Trying out Revise.jl
Now we are ready to try out Revise. Exit the Julia REPL
and reload it, then indicate we wish to use Revise.
julia
julia> using Revise
pkg> activate . # Start using our local package environment again
You must load Revise before loading any
packages you want it to track.
Try loading and using our package again.
julia> using Newton
julia> Newton.hello_world()
OUTPUT
"Hello, World!"
Now try editing the message to say Goodbye, World!.
Remember to save your changes.
julia> Newton.hello_world()
OUTPUT
"Goodbye, World!"
Now, thanks to Revise, the change to the package’s code
was being tracked and was automatically recompiled. This means you can
make changes to the package and have them be active without needing to
reload the Julia REPL.
While Revise does its best to track any changes made,
there are some limits to what can be done in a single Julia session. For
example, changes to type definitions or consts (among
others) will probably still necessitate restarting your Julia
session.
- You can use either the built-in
Pkg.generatefunction or a third party template (such as theBestieTemplate) to generate a new package structure. - The
Revise.jlmodule can automatically reload parts of your code that have changed. - Best practice: file names should reflect module names.