bmad-code-org/BMAD-METHOD · critical · SystemExit

error: Python 3.11+ is required (stdlib `tomllib` not found)

Error message

error: Python 3.11+ is required (stdlib `tomllib` not found).

What it means

Identical mechanism to its sibling in resolve_config.py: a runtime interpreter-version guard. This script imports `load_customization` from the local `config_utils` module, which performs `import tomllib` (config_utils.py:5). `tomllib` is standard-library only on Python 3.11+. On older interpreters the import raises `ModuleNotFoundError(name='tomllib')`; the wrapper matches `error.name == 'tomllib'`, writes the same one-line message, and exits with `SystemExit(3)`. The guard exists because the project has no `pyproject.toml` / `requires-python` declaration, so this import-time check is the only enforcement of the minimum interpreter.

Source

Thrown at src/scripts/resolve_customization.py:21

# requires-python = ">=3.11"
# ///
"""Resolve a skill's default, team, and user TOML customization layers."""

import argparse
import json
import sys
from pathlib import Path

# Installed scripts are consumer files, not a location for interpreter caches.
sys.dont_write_bytecode = True

try:
    from config_utils import ConfigError, load_customization
except ModuleNotFoundError as error:
    if error.name != "tomllib":
        raise
    sys.stderr.write("error: Python 3.11+ is required (stdlib `tomllib` not found).\n")
    raise SystemExit(3) from None


_MISSING = object()


def find_project_root(start: Path) -> Path | None:
    current = start.resolve()
    while True:
        if (current / "_bmad").exists() or (current / ".git").exists():
            return current
        if current.parent == current:
            return None
        current = current.parent


def extract_key(data, dotted_key: str):
    current = data
    for part in dotted_key.split("."):

View on GitHub (pinned to b70486b9bd)

Solutions

  1. Run the script with Python 3.11+: `python3.11 src/scripts/resolve_customization.py` (or `python3.12`). Confirm with `python3 --version` first.
  2. Recreate the virtualenv on a 3.11+ base: `python3.12 -m venv .venv && source .venv/bin/activate`, then re-run.
  3. Pin the interpreter via your version manager: `pyenv local 3.12` / `mise use python@3.12` / `asdf local python 3.12.x`.
  4. If locked to < 3.11 and upgrading is impossible, install the `tomli` backport and add `if sys.version_info < (3,11): import tomli as tomllib` in config_utils.py — only with the project owner's sign-off, since this is a code change, not configuration.
  5. On CI, raise the setup-python version: `python-version: '3.11'` or newer, or switch the Docker base to `python:3.11-slim`+.

Example fix

# before — shebang floats to whatever 'python3' resolves to
#!/usr/bin/env python3

# after — pin to a 3.11+ interpreter explicitly
#!/usr/bin/env python3.12

# or, in CI (.github/workflows/*.yml)
# before
- uses: actions/setup-python@v5
  with:
    python-version: '3.10'
# after
- uses: actions/setup-python@v5
  with:
    python-version: '3.12'
Defensive patterns

Strategy: validation

Validate before calling

import sys

MIN_PY = (3, 11)

if sys.version_info < MIN_PY:
    sys.exit(
        f"error: Python {MIN_PY[0]}.{MIN_PY[1]}+ is required "
        f"(running {sys.version_info.major}.{sys.version_info.minor})."
    )

# Safe to invoke resolve_customization / config_utils here.
import subprocess, sys
subprocess.run([sys.executable, "src/scripts/resolve_customization.py"], check=True)

Try / catch

# SystemExit(3) fires at import time; catch it only at a subprocess boundary,
# never around the import, or you hide a genuinely broken environment.
import subprocess, sys

proc = subprocess.run(
    [sys.executable, "src/scripts/resolve_customization.py"],
    capture_output=True, text=True,
)
if proc.returncode == 3 and "tomllib" in proc.stderr:
    raise RuntimeError(
        "resolve_customization needs Python 3.11+; current interpreter is "
        f"{sys.version.split()[0]}."
    )
raise SystemExit(proc.returncode)

Prevention

When it happens

Trigger: Invoking `resolve_customization.py` (directly or via a wrapper) with an interpreter where `sys.version_info < (3, 11)` — e.g. `python3.10 src/scripts/resolve_customization.py`, or a `#!/usr/bin/env python3` shebang resolving to an older `python3` on PATH; running the customization-resolution test suite under the wrong interpreter; a pre-3.11 venv; a CI image pinned to 3.9/3.10. The error is raised at import time before any `_bmad`/`.git` project-root lookup or customization file is touched, so customization-file contents are NOT a cause.

Common situations: 1) Default `python3` on older LTS distros (Ubuntu 20.04 → 3.8, Debian 11 → 3.9) satisfying the shebang. 2) A venv built from a pre-3.11 base interpreter. 3) pyenv / asdf / mise shims still pointing at an older Python after an upgrade. 4) CI matrix that still lists 3.9/3.10, or a Docker base like `python:3.10-slim`. 5) A poetry/uv/conda env materialised against an older default Python. 6) A shell alias or PATH ordering that shadows the 3.11+ interpreter with an older one.

Related errors


AI-assisted analysis of bmad-code-org/BMAD-METHOD@b70486b9bd (2026-08-13). Data as JSON: /api/errors/5e0ea0d47892d7cb. Report an issue: GitHub.