Script metadata format#

conda-exec supports PEP 723 inline script metadata for declaring dependencies directly inside a Python script. When you run conda exec script.py, conda-exec parses any metadata block in the file and creates a cached environment with the declared dependencies.

Block syntax#

Metadata is embedded in a comment block delimited by # /// script and # ///. Each line inside the block must start with # (hash, space), and the content is parsed as TOML.

# /// script
# requires-python = ">=3.12"
# dependencies = ["requests", "rich"]
#
# [tool.conda]
# channels = ["conda-forge", "bioconda"]
# dependencies = ["samtools>=1.19"]
# ///

The block can appear anywhere in the file. Only the first # /// script block is used. Blank lines within the block use a bare # (no trailing space required).

Standard fields#

These fields follow the PEP 723 specification and are compatible with other tools that support inline script metadata (such as uv and pipx).

requires-python#

Python version constraint as a PEP 440 version specifier.

# /// script
# requires-python = ">=3.11"
# ///

Translated to a conda python spec in the environment solve. For example, requires-python = ">=3.11" becomes the spec python >=3.11. After the environment is created, conda-exec validates that the resolved Python version satisfies the constraint and reports a clear error if it does not.

dependencies#

A list of PyPI package names, following PEP 508 syntax.

# /// script
# dependencies = ["requests>=2.28", "rich"]
# ///

Requires conda-pypi to be installed. When PyPI dependencies are present, the conda-pypi channel is added to the channel list automatically. If conda-pypi is not installed, conda-exec raises a PyPIDependencyError.

Extension fields#

These fields are specific to conda-exec and live under the [tool.conda] TOML table, following PEP 723’s convention for tool-specific configuration.

[tool.conda].dependencies#

A list of conda package specs (match specs).

# /// script
# [tool.conda]
# dependencies = ["numpy>=1.24", "pandas", "scipy"]
# ///

These are passed directly to the conda solver. Any valid conda match spec syntax is accepted (e.g. numpy>=1.24,<2, python-dateutil).

[tool.conda].channels#

A list of conda channels to search for packages.

# /// script
# [tool.conda]
# channels = ["conda-forge", "bioconda"]
# dependencies = ["samtools>=1.19"]
# ///

If no channels are specified (neither in the metadata nor via --channel on the command line), conda-exec defaults to conda-forge.

Automatic Python spec#

If no dependency in the combined spec list (conda dependencies, PyPI dependencies, and --with specs) starts with python, conda-exec adds one automatically:

  • If requires-python is set, the spec is python <constraint> (e.g. python >=3.12).

  • Otherwise, a bare python spec is added, letting the solver pick the best available version.

This ensures that script environments always include a Python interpreter.

Field interactions#

The metadata fields combine with command-line options according to these rules:

  • Channels from the metadata and --channel flags are merged. If the combined list is empty, conda-forge is used as the default.

  • Conda dependencies from the metadata and --with specs are merged into a single spec list for the solver.

  • PyPI dependencies from dependencies are added to the spec list. The conda-pypi channel is appended automatically when PyPI dependencies are present.

  • If the script has no metadata block and no --with/--channel flags, conda-exec skips environment creation entirely and runs the script with the current Python interpreter.

File size limit#

conda-exec skips metadata parsing for files larger than 10 MB (MAX_SCRIPT_SIZE). Scripts exceeding this limit are treated as having no metadata block.

Note

The 10 MB limit exists to prevent memory exhaustion when conda-exec is pointed at large generated files or binaries that happen to have a .py extension. In practice, Python scripts with inline metadata are far smaller than this threshold.

Cache key computation#

Script environments use a cache key of the form script--{hash}, where {hash} is the first 16 hex characters of the SHA-256 digest computed from the dependency metadata. The inputs to the hash are:

  1. Sorted conda dependencies (joined with |)

  2. Sorted PyPI dependencies (joined with |)

  3. Sorted channels (joined with |)

  4. The requires-python value (or empty string if not set)

These four parts are joined with || to form the hash input.

The hash is derived from the metadata content, not the file path or script body. This means:

  • Changing only the script code (without changing dependencies) reuses the same cached environment.

  • Two different scripts with identical dependency declarations share the same cached environment.

  • Changing any dependency, channel, or the requires-python value produces a different cache key and a new environment.

Embedded lock block#

When conda exec --lock --embed script.py is used, conda-exec writes a generated # /// conda-exec-lock block into the script.

# /// conda-exec-lock
# ...generated lock data...
# ///

This block is not PEP 723 metadata. It is generated lock state. Keep dependency intent in the # /// script block and let conda-exec update the lock block.

When running a script, conda-exec checks embedded lock data before sibling sidecar lockfiles. If lock data is present, the cached environment key is derived from the lock content instead of the metadata block.

Generated lock data includes an input digest. conda-exec only uses it automatically when the digest matches the current dependency input. See Script lock reference for the exact discovery and invalidation rules.

Examples#

Conda dependencies only#

# /// script
# [tool.conda]
# channels = ["conda-forge"]
# dependencies = ["numpy>=1.24", "matplotlib"]
# ///

import numpy as np
import matplotlib.pyplot as plt

data = np.random.randn(1000)
plt.hist(data, bins=30)
plt.savefig("histogram.png")

PyPI dependencies only#

Requires conda-pypi to be installed.

# /// script
# dependencies = ["httpx", "rich"]
# ///

import httpx
from rich import print

resp = httpx.get("https://httpbin.org/json")
print(resp.json())

Mixed conda and PyPI dependencies#

# /// script
# requires-python = ">=3.11"
# dependencies = ["rich"]
#
# [tool.conda]
# channels = ["conda-forge"]
# dependencies = ["numpy>=1.24"]
# ///

import numpy as np
from rich import print

print(f"numpy version: {np.__version__}")

Python version constraint only#

# /// script
# requires-python = ">=3.12"
# ///

import tomllib
from pathlib import Path

data = tomllib.loads(Path("pyproject.toml").read_text())
print(data["project"]["name"])

No metadata (runs with current Python)#

# No metadata block, no dependencies needed
print("Hello from conda exec!")

Compatibility with uv#

The standard PEP 723 fields (requires-python and dependencies) are compatible with uv’s inline script metadata support. A script using only these fields works with both conda exec script.py and uv run script.py.

The [tool.conda] extension fields are ignored by uv (and other PEP 723 consumers), so scripts that include conda-specific configuration remain valid for other tools. Those tools will not install the conda-specific dependencies.

Tip

You can write scripts that work with both conda-exec and uv. Put PyPI-only dependencies in the standard dependencies field and conda-specific packages in [tool.conda].dependencies. Running the script with uv run installs the PyPI packages; running with conda exec installs both.