# Modules, Packages & Environments

Julia's package manager is, in my honest opinion, the best of any language I've used. That's a strong claim about a boring topic, so let me justify it early: **Julia environments are reproducible by default and cost nothing to create.** No virtualenv to remember to activate, no dependency resolution that quietly installs a different version than your colleague got. Two commands and you have an exactly reproducible project.

## Modules: organizing code within a project

A **module** is a namespace. It groups related definitions and controls what's visible from outside.

```{code-block} julia
module Ledger

export post_entry, balance_of

struct Entry
    account::String
    amount::Float64
end

post_entry(account, amount) = Entry(account, amount)

balance_of(entries, account) =
    sum(e.amount for e in entries if e.account == account; init = 0.0)

# Not exported — internal helper
normalize_account(s) = uppercase(strip(s))

end # module
```

Using it:

```{code-block} julia
using .Ledger              # the dot means "a module defined right here"

e = post_entry("GL-4001", 1200.0)     # exported, available directly
Ledger.normalize_account(" gl-4001 ") # not exported, but still reachable
```

Two things to note.

**`export` is a suggestion, not a wall.** Anything in the module can be reached with `Ledger.name`. Julia doesn't have private members. The convention is that unexported names are internal and may change without warning — an honour system, and it works fine in practice.

:::{note} The `public` keyword
Julia 1.11 added a `public` keyword to fill a real gap. Sometimes you want to say "this name is part of my stable API" without *exporting* it into everyone's namespace. `public foo` marks `foo` as officially supported API that users should still access as `MyModule.foo`.

```{code-block} julia
module Ledger
export post_entry           # comes into scope with `using`
public normalize_account    # stable API, but call it as Ledger.normalize_account
...
end
```

This is available on both the current stable line and the 1.10 LTS via version-guarded code, though you'll mostly see it in newer packages.
:::

### `using` vs `import`

```{code-block} julia
using Statistics            # brings exported names into scope: mean, std, ...
using Statistics: mean      # brings in exactly `mean`, nothing else
import Statistics           # brings nothing into scope; use Statistics.mean
import Statistics: mean     # brings in `mean`, AND lets you add methods to it
```

The distinction that actually matters day to day: **if you want to add a method to a function from another module, you must `import` it** (or write the fully qualified name `Base.:+`). `using` gives you read access; `import` gives you extension rights. That's a deliberate safety rail — you can't accidentally hijack someone's function by defining a similarly-named one.

## Environments: the part you should care about

Every Julia project gets its own environment, defined by two files:

- **`Project.toml`** — your direct dependencies, and the version ranges you'll accept. You write this (via the package manager). It's short and human-readable.
- **`Manifest.toml`** — the *exact* version of every package in the entire dependency tree, including transitive ones. Generated for you. Never edit it by hand.

Commit both to git and anyone who clones your repo can reproduce your exact stack with one command. This is the whole ballgame, and it's why I lead with it.

### Creating and using an environment

In the REPL, press `]` to enter package mode. The prompt changes to `pkg>`.

```{code-block} julia
(@v1.12) pkg> activate .          # create/use an environment in this directory
(myproject) pkg> add DataFrames CSV Statistics
(myproject) pkg> status
```

Or from the shell, which is the habit I'd recommend:

```{code-block} bash
julia --project=.
```

That starts Julia with the current directory's environment already active. Put it in an alias, or let VS Code do it for you (the Julia extension detects `Project.toml` automatically).

:::{warning} The most common beginner mistake
If you just run `julia` and start `add`ing packages, they go into your **global** environment (`@v1.12`). Everything works, right up until you have two projects that need different versions of the same package, or you try to give your code to someone else and discover you have no idea what it depends on.

Get in the habit now: **one directory, one project, `--project=.`**. It costs you nothing and it's the difference between "works on my machine" and "works."
:::

### Package mode commands you'll actually use

```{code-block} julia
pkg> add DataFrames              # add a dependency
pkg> add DataFrames@1.6          # pin a version
pkg> rm DataFrames               # remove
pkg> update                      # update within the ranges in Project.toml
pkg> status                      # what's installed
pkg> instantiate                 # install exactly what Manifest.toml says
pkg> test                        # run the package's tests
pkg> gc                          # reclaim disk from old versions
```

`instantiate` is the one to remember. Clone a repo, run `julia --project=.` then `] instantiate`, and you have the author's exact environment. That's reproducibility that actually reproduces.

### Reproducing someone's work

The full ritual, start to finish:

```{code-block} bash
git clone https://github.com/someone/their-analysis
cd their-analysis
julia --project=. -e 'using Pkg; Pkg.instantiate()'
julia --project=. scripts/run_analysis.jl
```

Three commands. No conda, no Docker required, no version drift.

## Packages worth knowing

The ecosystem is smaller than Python's but strong where it's strong. A starting map for the kind of work this series is heading toward:

**Data**
- `DataFrames.jl` — tabular data, the pandas equivalent
- `CSV.jl` — fast CSV reading and writing
- `Arrow.jl`, `Parquet2.jl` — columnar formats
- `Query.jl` / `DataFramesMeta.jl` — query DSLs

**Numerics and stats**
- `Statistics`, `LinearAlgebra`, `Random` — in the standard library, no install needed
- `Distributions.jl` — probability distributions
- `StatsBase.jl`, `GLM.jl` — statistics and regression

**Plotting**
- `Plots.jl` — the general-purpose front end with swappable backends
- `Makie.jl` — high-performance, interactive, GPU-capable
- `AlgebraOfGraphics.jl` — grammar-of-graphics on top of Makie

**Machine learning**
- `Flux.jl` — neural networks, written in pure Julia
- `MLJ.jl` — a unified interface across many ML models
- `SciML` ecosystem (`DifferentialEquations.jl` and friends) — the crown jewel, honestly

**Development**
- `Revise.jl` — auto-reloads your code as you edit. Install this first, before anything else.
- `BenchmarkTools.jl` — reliable microbenchmarking
- `Test` — in the standard library
- `JET.jl` — static analysis that finds type instabilities and errors

:::{note} Set up Revise once, benefit forever
Create `~/.julia/config/startup.jl` with:

```{code-block} julia
try
    using Revise
catch e
    @warn "Revise not loaded"
end
```

Now every Julia session you start automatically picks up edits to your package code without a restart. Combined with 1.12's redefinable structs, this makes the development loop feel genuinely interactive.
:::

## Creating your own package

When your script grows into something you want to reuse or share:

```{code-block} julia
pkg> generate MyAnalysis
```

That scaffolds:

```
MyAnalysis/
├── Project.toml
└── src/
    └── MyAnalysis.jl
```

Add a `test/runtests.jl` and you have the standard layout the whole ecosystem uses:

```{code-block} julia
# test/runtests.jl
using MyAnalysis
using Test

@testset "MyAnalysis" begin
    @test net_pay(1000, 0.1) ≈ 900
    @test_throws ArgumentError net_pay(-1, 0.1)
end
```

Run it with `] test`. That's the entire testing setup — `Test` ships with Julia, there's nothing to install and no configuration file.

:::{note} Reproducible test failures
As of Julia 1.12, when a `@testset` fails, Julia prints the random number generator seed it was using. Paste that seed back into your test set and you reproduce the exact failing run. If you've ever chased a test that fails one time in fifty, you'll appreciate this more than the feature list suggests.
:::

## Try it yourself

1. Create a new directory, start Julia with `--project=.`, and add `Statistics` and `BenchmarkTools`. Look at the `Project.toml` and `Manifest.toml` that appear.
2. Write a small module with one exported function and one unexported helper. Confirm you can call the helper with the qualified name.
3. Run `] status` and then `] rm BenchmarkTools`, and watch the `Project.toml` change.
4. Delete `Manifest.toml`, run `] instantiate`, and confirm you get the same environment back.

You can now organize code and manage dependencies like a professional. Next we'll look at the feature that lets Julia programs write Julia programs: metaprogramming.
