pypa/pip · error · SyntaxError

unterminated parenthesis: %s

Error message

unterminated parenthesis: %s

What it means

Raised by distlib's marker parser inside `marker_expr` when a parenthesised sub-expression was opened with `(` but the matching `)` is never found before input ends. The parser checks `remaining[0] != ')'` after recursing and raises `SyntaxError: unterminated parenthesis`.

Source

Thrown at src/pip/_vendor/distlib/util.py:104

                else:
                    m = STRING_CHUNK.match(remaining)
                    if not m:
                        raise SyntaxError('error in string literal: %s' % remaining)
                    parts.append(m.groups()[0])
                    remaining = remaining[m.end():]
            else:
                s = ''.join(parts)
                raise SyntaxError('unterminated string: %s' % s)
            parts.append(q)
            result = ''.join(parts)
            remaining = remaining[1:].lstrip()  # skip past closing quote
        return result, remaining

    def marker_expr(remaining):
        if remaining and remaining[0] == '(':
            result, remaining = marker(remaining[1:].lstrip())
            if remaining[0] != ')':
                raise SyntaxError('unterminated parenthesis: %s' % remaining)
            remaining = remaining[1:].lstrip()
        else:
            lhs, remaining = marker_var(remaining)
            while remaining:
                m = MARKER_OP.match(remaining)
                if not m:
                    break
                op = m.groups()[0]
                remaining = remaining[m.end():]
                rhs, remaining = marker_var(remaining)
                lhs = {'op': op, 'lhs': lhs, 'rhs': rhs}
            result = lhs
        return result, remaining

    def marker_and(remaining):
        lhs, remaining = marker_expr(remaining)
        while remaining:
            m = AND.match(remaining)

View on GitHub (pinned to d7d0d0a394)

Solutions

  1. Balance the parentheses: every `(` must have a matching `)`.
  2. Simplify the marker or split it into multiple requirements to avoid grouping.
  3. Validate the marker with `packaging.markers.Marker(...)` before committing the requirements file.

Example fix

# before
requests==2.28.0 ; (python_version >= '3.8' and platform_system == 'Linux'

# after
requests==2.28.0 ; (python_version >= '3.8' and platform_system == 'Linux')
Defensive patterns

Strategy: validation

Validate before calling

from packaging.markers import Marker, InvalidMarker

def validate_marker_parens(marker: str) -> None:
    try:
        Marker(marker)
    except InvalidMarker as e:
        raise ValueError(f"Marker parse error: {e}") from e

Type guard

def marker_parens_balanced(m: str) -> bool:
    return m.count("(") == m.count(")")

Try / catch

null

Prevention

When it happens

Trigger: Parsing a marker like `(python_version >= '3.8' and os_name == 'posix'` (no closing paren) with `parse_marker`. Triggered when pip parses a requirement whose marker groups sub-conditions with parens but omits the closer.

Common situations: Hand-written complex markers in `requirements.txt`/`pyproject.toml`; refactoring a marker and dropping a `)`; line wrapping that lost the trailing paren.

Related errors


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