Hello! I was reading the documentation on package extensions and felt that I needed some more detailed instructions. I wrote this tutorial based on what I learned (only today, so this is surely not an exhaustive tutorial).
Quick tutorial on package extensions
A package can have modules that are conditionally loaded based on what other packages are loaded. These are called extensions.
Goal
In the below text, there will be a package MyPackage
. It will have an extension module MyExt
which should only be loaded if the Example
package is loaded in the environment.
MyPackage
will define a method greet(::This)
, which prints a message “Hello world, from MyPackage!”. MyExt
will extend that method with greet(::That)
, which prints “Hello world, from MyExt!”.
A new environment will be created. If Example
is not installed, then we can run greet(This())
and see “Hello world, from MyPackage!”, but greet(That())
will not work.
If Example
is installed, then we can run greet(That())
and see “Hello world, from MyExt!”.
Source code
Here the package is MyPackage
, with a main module also called MyPackage
. The module MyExt
is an extension.
Here is the source code layout, with only relevant files shown.
MyPackage/ ->
src/ ->
MyPackage.jl
ext/ ->
MyExt.jl
Project.toml
The extension is expected to be in MyPackage/ext
. Note that this it outside of the MyPackage/src
directory.
Here is the main MyPackage/src/MyPackage.jl
file:
# MyPackage/src/MyPackage.jl
module MyPackage
export greet, This, That
struct This end
struct That end
greet(::This) = print("Hello World, from MyPackage!")
end # module MyPackage
Here is the extension file MyPackage/ext/MyExt.jl
file:
module MyExt
using MyPackage
MyPackage.greet(::That) = print("Hello world, from MyExt!")
end # module MyExt
Note that ext/MyExt.jl
is not included from src/MyPackage.jl
the way a normal sub-module file would be. Julia will know to find the MyExt
package under ext
.
Adding a weak dependency
Suppose you have a package MyPackage
with an extension module MyExt
. The module MyExt
should only be loaded if the package Example
is loaded in the same environment. Then MyPackage
will need to have the Example
package as a “weak dependency”. This allows a package to specify version limits for an optional package.
Adding a weak dependency requires (this is my understanding at least, at the time of writing in July 2025) manually modifying Project.toml
.
First, in the MyPackage
environment, add the Example
package as a normal dependency.
(MyPackage) pkg> add Example
This updates the Project.toml
file, so that it looks something like
name = "MyPackage"
uuid = "b6ab5532-f566-471e-8495-90ba1797e34a"
authors = ["Erik Edin <erikedin@users.noreply.github.com>"]
version = "0.1.0"
[deps]
Example = "7876af07-990d-54b4-ab0e-23690620f79a"
[compat]
Example = "0.5.5"
To make the Example
package into a weak dependency, move it from the section [deps]
to [weakdeps]
. Then it should look like
name = "MyPackage"
uuid = "b6ab5532-f566-471e-8495-90ba1797e34a"
authors = ["Erik Edin <erikedin@users.noreply.github.com>"]
version = "0.1.0"
[weakdeps]
Example = "7876af07-990d-54b4-ab0e-23690620f79a"
[compat]
Example = "0.5.5"
In this simple case, there are no other packages in the [deps]
section, so we just rename it to [weakdeps]
.
In Julia, in package mode, if you run
(MyPackage) pkg> status
then you will get a warning that Project.toml
has changed.
(MyPackage) pkg> status
Project MyPackage v0.1.0
Status `~/.julia/dev/MyPackage/Project.toml` (empty project)
Warning The project dependencies or compat requirements have changed since the manifest was last resolved. It is recommended to `Pkg.resolve()` or consider `Pkg.update()` if necessary.
Following the recommendation, run resolve
.
(MyPackage) pkg> resolve
No Changes to `~/.julia/dev/MyPackage/Project.toml`
Updating `~/.julia/dev/MyPackage/Manifest.toml`
[7876af07] - Example v0.5.5
Note that Project.toml
is unchanged and that Example
is still listed under [weakdeps]
.
When MyPackage
is added into an environment, then Example
will not be automatically installed.
Making MyExt
an extension
What we want here is for MyExt
to be loaded if and only if Example
is loaded in the environment.
This is done by adding MyExt
to the [extensions]
section of Project.toml
.
[extensions]
MyExt = "Example"
The complete file should look like
name = "MyPackage"
uuid = "b6ab5532-f566-471e-8495-90ba1797e34a"
authors = ["Erik Edin <erikedin@users.noreply.github.com>"]
version = "0.1.0"
[weakdeps]
Example = "7876af07-990d-54b4-ab0e-23690620f79a"
[extensions]
MyExt = "Example"
[compat]
Example = "0.5.5"
Using an extension
Here a new environment is created. Note that this is a different environment that the MyPackage
environment. Both the MyPackage
and the conditional dependency Example
are added.
$ mkdir newenv
$ cd newenv
$ julia --project=.
pkg> add Example
pkg> dev MyPackage
MyPackage
is added with dev
because it’s not a registered package.
First, load just MyPackage
and see that greet(This())
works. Also ensure that greet(That())
does not work, until the Example
package is loaded.
# Load package and ensure that greet(This()) works as expected.
julia> using MyPackage
julia> greet(This())
Hello World, from MyPackage!
# The Example package is not loaded, so the extension is also not
# loaded.
julia> greet(That())
ERROR: MethodError: no method matching greet(::That)
The function `greet` exists, but no method is defined for this combination of argument types.
Closest candidates are:
greet(::This)
@ MyPackage ~/.julia/dev/MyPackage/src/MyPackage.jl:8
Stacktrace:
[1] top-level scope
@ REPL[3]:1
Load the Example
package, and try running greet(That())
again.
julia> using Example
Precompiling MyExt...
1 dependency successfully precompiled in 1 seconds. 2 already precompiled.
julia> greet(That())
Hello world, from MyExt!
Now the extension to the greet
method is available.