pypa/pip · error · InvalidLicenseExpression

Invalid license expression: {raw_license_expression!r}

Error message

Invalid license expression: {raw_license_expression!r}

What it means

Raised by `canonicalize_license_expression` when the input is empty/falsy. An empty license-expression is invalid because it carries no license information; the SPDX/PEP 639 grammar requires at least one license token. This is the earliest validation check in the canonicalizer.

Source

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

    .. doctest::

        >>> from packaging.licenses import canonicalize_license_expression
        >>> canonicalize_license_expression("mit")
        'MIT'
        >>> canonicalize_license_expression("mit and (apache-2.0 or bsd-2-clause)")
        'MIT AND (Apache-2.0 OR BSD-2-Clause)'
        >>> canonicalize_license_expression("(mit")
        Traceback (most recent call last):
          ...
        InvalidLicenseExpression: Invalid license expression: '(mit'
        >>> canonicalize_license_expression("Use-it-after-midnight")
        Traceback (most recent call last):
          ...
        InvalidLicenseExpression: Unknown license: 'Use-it-after-midnight'
    """
    if not raw_license_expression:
        message = f"Invalid license expression: {raw_license_expression!r}"
        raise InvalidLicenseExpression(message)

    # Pad any parentheses so tokenization can be achieved by merely splitting on
    # whitespace.
    license_expression = raw_license_expression.replace("(", " ( ").replace(")", " ) ")
    licenseref_prefix = "LicenseRef-"
    license_refs = {
        ref.lower(): "LicenseRef-" + ref[len(licenseref_prefix) :]
        for ref in license_expression.split()
        if ref.lower().startswith(licenseref_prefix.lower())
    }

    # Normalize to lower case so we can look up licenses/exceptions
    # and so boolean operators are Python-compatible.
    license_expression = license_expression.lower()

    tokens = license_expression.split()

    # Rather than implementing a parenthesis/boolean logic parser, create an

View on GitHub (pinned to d7d0d0a394)

Solutions

  1. Provide a real SPDX expression such as `license = "MIT"` in pyproject.toml
  2. If you genuinely have no license to declare, omit the field rather than passing empty
  3. Validate `if not raw_license_expression: raise` upstream of canonicalize

Example fix

// before
[project]
license = ""
// after
[project]
license = "MIT"
Defensive patterns

Strategy: validation

Validate before calling

def validate_license_expression(expr: str) -> str:
    if not expr:
        raise ValueError('license expression must not be empty')
    return expr

Type guard

def is_nonempty_license_expression(s: object) -> bool:
    return isinstance(s, str) and bool(s.strip())

Try / catch

from packaging.licenses import canonicalize_license_expression, InvalidLicenseExpression
try:
    norm = canonicalize_license_expression(expr)
except InvalidLicenseExpression as e:
    if 'Invalid license expression' in str(e) and not expr:
        raise ValueError('project license is required') from e
    raise

Prevention

When it happens

Trigger: Calling `canonicalize_license_expression('')`, `canonicalize_license_expression(None)` (after coercion), or passing a whitespace-only string that evaluates falsy. Hit by build backends (hatchling, setuptools) reading an empty `license = ""` from `pyproject.toml`.

Common situations: A `pyproject.toml` with `license = ""` or a missing license field that defaulted to empty; a tool that builds the license string from optional config and produces `''`; migration from the deprecated `license-file` table leaving an empty value.

Related errors


AI-assisted analysis of pypa/pip@d7d0d0a394 (2026-08-04). Data as JSON: /data/errors/3bf24b0167703c6a.json. Report an issue: GitHub.