Python Library Dependencies
This page goes into more detail about configuring package-index, wheel, and local-project dependencies for `PythonModule`s.
Adding Dependencies
package build
import mill.*, pythonlib.*
object `package` extends PythonModule {
def pythonDeps = Seq(
"numpy==2.1.2",
"pandas~=2.2.3",
"jinja2 @ https://github.com/pallets/jinja/releases/download/3.1.4/jinja2-3.1.4-py3-none-any.whl"
)
}
You can define the pythonDeps field to add dependencies to your module, which will be installed
via uv. Dependencies can include
standard dependency specifications, such as <package>==<version>
constraints, or even direct references to wheels.
> ./mill run
[10 20 30 40 50]
Adding Dependencies via requirements.txt files
You can also read dependencies from requirements.txt files. This can be
useful if you’re migrating an existing project to mill.
package build
import mill.*, pythonlib.*
object `package` extends PythonModule {
def pythonRequirementFiles = Task.Sources("requirements.txt")
}
> ./mill run
[10 20 30 40 50]
> ./mill show bundle
".../out/bundle.dest/bundle"
> out/bundle.dest/bundle
[10 20 30 40 50]
Unmanaged Wheels
In most scenarios you should rely on pythonDeps/moduleDeps and let Mill
manage the downloading and caching of wheels for you. But in the rare case
you receive a wheel or folder-full-of-wheels from somewhere and need to
include it in your project, unmanagedWheels is the way to do it.
package build
import mill.*, pythonlib.*
object `package` extends PythonModule {
def unmanagedWheels: T[Seq[PathRef]] = Task.Input {
Seq.from(os.list(moduleDir / "lib").map(PathRef(_)))
}
}
You can override unmanagedWheels to point it at a wheel (.whl file) or
source distribution (.tar.gz with a pyproject.toml file) you place on the
filesystem, e.g. in the above snippet any files that happen to live in the
lib/ folder.
> ./mill run
Hello, world!
> ./mill show bundle
".../out/bundle.dest/bundle"
> out/bundle.dest/bundle
Hello, world!
Downloading Unmanaged Wheels
You can also override unmanagedWheels to point it at wheels that you want to
download from arbitrary URLs.
requests.get comes from the Requests-Scala
library, one of Mill’s Bundled Libraries.
package build
import mill.*, pythonlib.*
object `package` extends PythonModule {
def unmanagedWheels = Task {
if (Task.offline) Task.fail("Cannot download classpath when in offline-mode")
else {
val name = "jinja2-3.1.4-py3-none-any.whl"
val url = s"https://github.com/pallets/jinja/releases/download/3.1.4/$name"
os.write(Task.dest / name, requests.get.stream(url))
Seq(PathRef(Task.dest / name))
}
}
}
> ./mill run
Hello, world!
Tasks like unmanagedWheels and pythonDeps are cached, so your wheel is downloaded only
once and re-used indefinitely after that. This is usually not a problem, because usually URLs
follow the rule that Cool URIs don’t change, and so files
downloaded from the same URL will always contain the same contents.
> ./mill --offline run
Hello, world!
An unmanaged wheel downloaded via requests.get is still unmanaged: even though you
downloaded it from somewhere, requests.get does not know how to pull in transitive
dependencies or de-duplicate different versions on the classpath. All the same caveats you need
to worry about when dealing with unmanaged wheels apply here as well. In
case you do want mill to take care of managing dependencies of a package which is not
available on PyPI, you shouldn’t get that package in unmanagedWheels (like we did in the
example above). Instead, you can declare the dependency as an entry in pythonDeps
using the standard direct-URL syntax.
|
Using Custom Package Indexes
By default, dependencies are resolved from the Python
Package Index (PyPI), the standard package index for python projects. You
can also add your own package indexes by overriding the indexes task in
the module:
package build
import mill.*, pythonlib.*
object foo extends PythonModule {
def pythonDeps = Seq(
"testpkg-jodersky==0.0.1" // a test package, only available on test.pypi.org
)
// override this task to add or replace the package indexes
def indexes = super.indexes() ++ Seq("https://test.pypi.org/simple/")
}
Mill uses uv to find and install dependencies.
You can configure uv through uv.toml files and environment variables.
Private indexes
You can read up in more detail on how to configure uv to authenticate to private indexes. Name the index in the URL and provide credentials through uv’s environment variables so that secrets never become Mill task values or cached task output:
object bar extends PythonModule {
def indexes = Seq(
"company=https://pypi.company.com/simple",
"https://pypi.org/simple"
)
}
export UV_INDEX_COMPANY_USERNAME=username export UV_INDEX_COMPANY_PASSWORD="$COMPANY_PASSWORD"
More advanced authentication techniques are available through uv’s named-index credentials.
> ./mill foo.run
2
Depending on Local Python Projects
package build
import mill.*, pythonlib.*
import mill.api.BuildCtx
object `package` extends PythonModule {
def pythonProjectDeps = Task.Sources(BuildCtx.workspaceRoot / "raw-project")
object test extends PythonTests, TestModule.Unittest
}
pythonProjectDeps adds cache-tracked Python projects that are not Mill
modules. uv reads the standard project metadata in each directory, installs
the project into this module’s virtual environment, and rebuilds the
environment when that project changes. The dependency is inherited by
downstream modules and included in standalone bundles. Because this setting
is part of the shared uv environment configuration, it works for regular and
bare modules as well as their test, lint, coverage, and publishing mixins.
Local filesystem paths are not portable package metadata, so a
PublishModule should separately declare any published dependency in
pythonDeps.
[project]
name = "raw-uv-project"
version = "0.1.0"
[build-system]
requires = ["uv_build>=0.12.12,<0.13"]
build-backend = "uv_build"
[tool.uv.build-backend]
module-name = "raw_uv_project"
def raw_message() -> str:
return "Hello from a raw uv project!"
import unittest
from raw_uv_project import raw_message
class RawProjectTests(unittest.TestCase):
def test_dependency_is_inherited(self) -> None:
self.assertEqual(raw_message(), "Hello from a raw uv project!")
> ./mill run
Hello from a raw uv project!
> ./mill test
...test_dependency_is_inherited...ok...
...Ran 1 test...
...OK...
> ./mill show bundle
".../out/bundle.dest/bundle"
> out/bundle.dest/bundle
Hello from a raw uv project!
Debugging
In case anything goes wrong, or if you’re just curious, you can see what
arguments Mill passes to uv pip install by looking at the output of the
uvInstallArgs task.
package build
import mill.*, pythonlib.*
object `package` extends PythonModule {
def pythonDeps = Seq(
"numpy==2.1.2",
"pandas~=2.2.3",
"jinja2 @ https://github.com/pallets/jinja/releases/download/3.1.4/jinja2-3.1.4-py3-none-any.whl"
)
def indexes = Seq("invalid_index")
}
> ./mill show uvInstallArgs
{
"args": [
"--default-index",
"invalid_index",
"numpy==2.1.2",
"pandas~=2.2.3",
"jinja2 @ https://github.com/pallets/jinja/releases/download/3.1.4/jinja2-3.1.4-py3-none-any.whl"
],
"sig": ...
}