A Dagger module for managing Dagger modules that use the Python SDK.
SDK-specific module authoring (scaffolding new modules, language build config,
codegen) lives in modules like this one. The engine drives the SDK
(dagger/dagger#13992): it records a module scope in dagger.toml, sets the
workspace cwd to it, and asks this module to generate the complete scope
through findClientRoot and generateScope. The module writes the manifest and
its own files; the engine owns the workspace bookkeeping.
It uses the engine's native Workspace and ModuleSource APIs. It uses
sdkHelpers.moduleManifest from dagger/sdk-helpers.
| Path | What it is |
|---|---|
python-sdk.dang, mod.dang, templates/ |
authoring: findClientRoot, generateScope, mod (generate, config), templates |
sdk/ |
the dagger-io client library and code generator |
runtime/ |
the module runtime the engine calls to run a module, and the container build the static entrypoint shares |
entrypoint/ |
the shared Dang ModuleEntrypoint, served from this repository to any module that names it |
Code generation happens at dagger generate, which calls generateScope for
every recorded scope. It runs the code generator in sdk/ and vendors the
result into the module. The runtime never generates: it builds a module from its
committed generated files, so there is no codegen step in a cold
dagger call, and a module that has not been generated fails with an
actionable error rather than being silently regenerated.
When a managed pre-1.0 dagger.json scope is generated, the SDK writes
dagger-module.toml and removes dagger.json. An unmanaged legacy module keeps
using the Python SDK that is built into the engine.
Python modules reach one of two implementations, and which one is decided by the module's config format:
- Legacy — an unmanaged
dagger.jsonwith"sdk": {"source": "python"}resolves to the runtime built into the engine (dagger/dagger'ssdk/python). It still generates bindings at module load. - Modern — a
dagger-module.tomlcan point[runtime] sourceat this repository'sruntime/, which is the no-codegen path above. Either a module ref or a path relative to the module works, for bothdagger generateanddagger call.
The engine resolves the short name python to exactly one target, the
engine-baked runtime, so the modern path is reached by module ref rather than
by name. The manifest generateScope writes for a new module therefore still
names python; it moves to github.com/dagger/python-sdk/runtime in a
follow-up, once runtime/ exists on the default branch for that ref to
resolve to. See
future/done/self-contained-python-sdk.md
for the full reasoning and for the engine change that would let one name serve
both.
A module created today names the python runtime, so it runs on the
engine's runtime. To move one onto this repository's runtime, point it there by
hand:
# <module>/dagger-module.toml
[runtime]
source = "github.com/dagger/python-sdk/runtime"Then dagger generate the module and dagger call it as usual. The generated
files are identical either way — generation is this SDK's regardless of which
runtime runs the module — so switching back is just editing the line again.
Within this repository, a path relative to the module works too, which is how the end-to-end fixture exercises the runtime before the ref exists.
entrypoint/ is one ModuleEntrypoint, written in Dang, that backs every
Python module at once, with nothing generated into the module. dagger generate names it in the manifest of every module that does not use
--dang-entrypoint, next to the builtin runtime:
# <module>/dagger-module.toml
name = "my-module"
engineVersion = "v1.0.0"
[runtime]
source = "python"
[entrypoint]
kind = "dang"
source = "dagger.io/sdk/python/entrypoint@v1"One manifest then loads on both kinds of engine. An engine that predates
entrypoints ignores the table and runs the module on the runtime. An engine
that loads manifest version 2 drives the module through the entrypoint and
ignores [runtime]; when the module has [[dependencies]] it reads the
manifest the old way instead, because manifest version 2 has no dependency
list, and the runtime runs the module.
A Dang entrypoint already in the manifest is kept as written, so a module can pin a version of the shared entrypoint or point at a fork. A static entrypoint is told from the shared one by its source, a path inside the module.
The entrypoint finds the module it serves through the workspace it is handed,
whose working directory is that module's directory. It reads the module's name
from that directory's manifest, builds the module's container with the same
build the runtime uses, and asks the module to describe itself
(python -m dagger.mod describe) or to run one call
(python -m dagger.mod call). The types it returns are rebuilt from that
description in the engine's own session.
| File | What it is |
|---|---|
main.dang |
the ModuleEntrypoint: types and call |
build.dang |
the container build, generated from runtime/build.dang |
build.dang is generated, not hand-edited: the engine copies only the .dang
files at the top of an entrypoint directory, and currentModule inside an
entrypoint is the module it serves, so a shared entrypoint can read none of its
own non-Dang files. The externals block that reads runtime/images/ is written
out into the copy. dagger check -m .dagger/modules/e2e fails when the copy
drifts; refresh it with
dagger call -m .dagger/modules/e2e shared-entrypoint-build export --path entrypoint/build.dang.
The address above only resolves once entrypoint/ is on this repository's
default branch and a v1 release is tagged: @v1 selects the greatest
entrypoint/v1.* tag, then the greatest plain v1.* tag. The Python process the entrypoint starts belongs to no module
on the engine's side: the core API works in it, and dag.current_module()
fails with "no current module". The static entrypoint has the same limit.
By default a module's types are discovered by running it: the engine builds
the module's container and starts Python once per session to register the
types, then again for every call. With --dang-entrypoint the types are
computed once, at dagger generate, and written into a generated entrypoint
the engine loads without running Python:
dagger module init python --name my-module --dang-entrypointGenerating the module then writes an entrypoint manifest instead of a runtime
manifest, and sdk/entrypoint/ next to the vendored library:
| File | What it is |
|---|---|
types.dang |
the module's types, as a literal list of TypeDef values |
main.dang |
the ModuleEntrypoint: returns the types and runs calls in the module's container |
build.dang |
the container build, copied from runtime/build.dang |
The types come from importing the module in its own container and reading
what its decorators registered, so they are the ones the runtime would
register. main.dang bakes a content digest of every source file that can
change them; a call after an edit is refused with a message to run
dagger generate. A lock file added afterwards is refused the same way, and
only file contents count, not permissions.
What the static path cannot do yet, and refuses at dagger generate:
module clients (an entrypoint manifest has no dependencies), any cache= value
on a function (the entrypoint's exec is content-cached and receives no
per-call signal), the legacy template, and a manifest with include,
disableDefaultFunctionCaching, a runtime other than python, a source
other than ., or codegen, clients or dependencies tables. Such
modules keep the default path. There is no debug terminal on the static
path, and a function error reaches the caller as the exec failure with the
process's stderr.
The setting is persisted on the scope. To switch an existing module either
way, re-run dagger module init python --path <module> with
--dang-entrypoint or --dang-entrypoint=false, or edit the scope's
settings in dagger.toml, then dagger generate. Generating one module
directly, with dagger call python-sdk mod --path <module> generate, keeps
the mode that module is in; switching modes goes through
dagger module init python --path <module> as above. Switching back removes
sdk/entrypoint/ and rewrites a runtime manifest with the generating
engine's version. Loading a static module needs an engine that reads an
entrypoint manifest (dagger/dagger#14038); see
future/done/static-module-entrypoint.md
for the design and the plan to make it the default.
From your workspace root:
dagger module install github.com/dagger/python-sdkThe engine recognizes the SDK interface and records the module as the python
SDK in dagger.toml. After install, the module is also available in
dagger call as python-sdk.
Calls that return a Changeset will print the diff and prompt you to confirm
before writing anything to your workspace.
dagger module init python --name my-moduleThe engine records the module scope in dagger.toml and calls this SDK's
generateScope, which renders the template, writes dagger-module.toml, and
generates the SDK bindings in one step.
The SDK settings below become typed flags on dagger module init python and
are persisted on the scope:
dagger module init python --name my-module --template legacy
dagger module init python --name my-module \
--python-version 3.13 \
--use-uv=false \
--base-image python:3.13-slim--template picks a starter template: default (a small working module) when
you pass nothing, empty for a bare object class, or legacy for a
container-echo example. The three pyproject.toml flags are optional; by
default the template's Python version is used, uv is enabled, and no base image
override is written.
Read the current configuration. Settings that are not explicitly written to
pyproject.toml are reported as null rather than guessed:
dagger call python-sdk mod --path my-module config getSelect a single value:
dagger call python-sdk mod --path my-module config get python-version
dagger call python-sdk mod --path my-module config get use-uv
dagger call python-sdk mod --path my-module config get base-imageChange one or more values at once (prints a diff to confirm before writing). Each flag is optional; omitting one leaves that setting untouched:
dagger call python-sdk mod --path my-module config set \
--python-version 3.13 \
--use-uv=false \
--base-image python:3.13-slimdagger generate regenerates every recorded scope. A recorded module can also
be generated on its own:
dagger call python-sdk mod --path my-module generatemod resolves recorded modules by default. For a module root that is not
recorded, pass the module root as --path and add --find-up=false:
dagger call python-sdk mod --path my-module --find-up=false generateModule dependencies are replaced by generated module clients
(dagger module client add). In a module scope the client set becomes the
module's dependency set: each client is recorded in dagger-module.toml and
its types are part of the generated bindings, and a removed client is dropped
again.
Standalone clients, in a scope without a module, are not generated yet; adding
one to a Python scope is refused and the workspace is left unchanged.
dagger checkengine-e-2-e:dev-sdk-check builds the pinned dagger/dagger#13992 engine. It
runs the SDK interface checks, initializes Python modules with default and
explicit settings, and calls a generated module.