pypa/pip · error · SyntaxError

unterminated string: %s

Error message

unterminated string: %s

What it means

Raised by distlib's PEP 508 marker parser when a quoted string literal in a marker is opened but never closed before the end of input. The parser's inner loop exits via the `else` clause of the while loop (no closing quote found) and raises `SyntaxError: unterminated string`.

Source

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

            oq = '\'"'.replace(q, '')
            remaining = remaining[1:]
            parts = [q]
            while remaining:
                # either a string chunk, or oq, or q to terminate
                if remaining[0] == q:
                    break
                elif remaining[0] == oq:
                    parts.append(oq)
                    remaining = remaining[1:]
                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]

View on GitHub (pinned to d7d0d0a394)

Solutions

  1. Add the missing closing quote that matches the opening one (`'...'` or `"..."`).
  2. Avoid shell-splitting marker strings — quote the whole requirement when installing from a shell.
  3. Lint requirements files for balanced quotes.

Example fix

# before
requests==2.28.0 ; python_version >= '3.8

# after
requests==2.28.0 ; python_version >= '3.8'
Defensive patterns

Strategy: validation

Validate before calling

from packaging.markers import Marker, InvalidMarker

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

Type guard

def marker_quotes_balanced(m: str) -> bool:
    return m.count("'") % 2 == 0 and m.count('"') % 2 == 0

Try / catch

null

Prevention

When it happens

Trigger: `parse_marker` encounters a marker like `python_version >= '3.8` (missing closing quote). Triggered when pip parses a requirement whose marker has an unbalanced quote.

Common situations: Truncated requirement lines; quotes stripped by shell expansion or by copy-paste; line continuations that broke the marker; requirements files edited with an editor that auto-paired quotes incorrectly.

Related errors


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