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.generate or 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.

Challenge

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

function hello_world()
    return "Hello, Universe!"
end
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
Callout

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.

Callout

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.

Key Points
  • You can use either the built-in Pkg.generate function or a third party template (such as the BestieTemplate) to generate a new package structure.
  • The Revise.jl module can automatically reload parts of your code that have changed.
  • Best practice: file names should reflect module names.