pypa/pip · error · InvalidLicenseExpression

Unknown license exception

Error message

Unknown license exception: {token!r}

What it means

Raised during the final token pass of canonicalize_license_expression (licenses/__init__.py:167-168). When the previous normalized token is WITH, the current token is looked up in the SPDX EXCEPTIONS table; if it is not a registered SPDX license-exception identifier, InvalidLicenseExpression is raised. The input is lowercased first, so casing of the identifier does not matter, but the spelling must match an SPDX-listed exception.

Solutions

  1. Use the exact SPDX exception identifier, e.g. 'Classpath-exception-2.0', 'LLVM-exception', 'Linux-syscall-note'.
  2. Remove the WITH clause if you are unsure which exception applies.

Example fix

# before
canonicalize_license_expression('mit with linux-syscall')
# after
canonicalize_license_expression('apache-2.0 with linux-syscall-note')
Defensive patterns

Strategy: try-catch

Validate before calling

from pip._vendor.packaging.licenses._spdx import EXCEPTIONS

def is_known_exception(token: str) -> bool:
    return token.lower() in EXCEPTIONS

Try / catch

from packaging.licenses import canonicalize_license_expression, InvalidLicenseExpression

try:
    canonicalize_license_expression(expr)
except InvalidLicenseExpression as e:
    # surface to user / config validation
    ...

Prevention

When it happens

Trigger: Passing 'mit with foo' (foo not an exception), 'apache-2.0 with linux-syscall' (correct id is 'Linux-syscall-note'), or 'mit with apache-2.0' (a license id used where an exception id belongs).

Common situations: Guessing or abbreviating exception identifiers; using a license id after WITH; an older packaging vendoring an older SPDX list that lacks a newer exception id.

Related errors


AI-assisted analysis of pypa/pip@f399c37189 (2026-08-08). Data as JSON: /api/errors/551fb2a19df735d2. Report an issue: GitHub.

Appendix: source

Thrown at src/pip/_vendor/packaging/licenses/__init__.py:168

    normalized_tokens = []
    last_license_start = False
    for index, token in enumerate(tokens):
        if token in {"or", "and", "with", "(", ")"}:
            if token == "with" and (
                not last_license_start
                or index + 1 == len(tokens)
                or tokens[index + 1] in {"or", "and", "with", "(", ")"}
            ):
                message = f"Invalid license expression: {raw_license_expression!r}"
                raise InvalidLicenseExpression(message)
            normalized_tokens.append(token.upper())
            last_license_start = False
            continue

        if normalized_tokens and normalized_tokens[-1] == "WITH":
            if token not in EXCEPTIONS:
                message = f"Unknown license exception: {token!r}"
                raise InvalidLicenseExpression(message)

            normalized_tokens.append(EXCEPTIONS[token]["id"])
            last_license_start = False
        else:
            if token.endswith("+"):
                final_token = token[:-1]
                suffix = "+"
            else:
                final_token = token
                suffix = ""

            if final_token.startswith("licenseref-"):
                license_ref_id = final_token[len("licenseref-") :]
                if suffix or not license_ref_allowed.match(license_ref_id):
                    message = f"Invalid licenseref: {token!r}"
                    raise InvalidLicenseExpression(message)
                normalized_tokens.append(license_refs[final_token])
            else:

View on GitHub (pinned to f399c37189)