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.

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. They will simply 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.