pypa/pip · error · SyntaxError

unexpected trailing data

Error message

unexpected trailing data: %s

What it means

Raised as SyntaxError inside parse_requirement when, after successfully parsing the name, version, extras, URL, and any environment marker, there is still leftover data that does not start with '#' (a comment). The PEP 508 grammar allows an optional trailing comment but nothing else; any other residue is rejected as malformed. This guards against requirement strings with duplicate or appended junk after a complete, valid spec.

Solutions

  1. Strip or split the line so only one requirement remains; trailing comments must start with '#'.
  2. Run a pre-pass that normalizes whitespace and removes anything after the first '#' before parsing.
  3. Use packaging's Requirement parser to localize where the surplus data begins.

Example fix

// before
parse_requirement('pkg==1.0 ; python_version<"3" ; extra')
// after
parse_requirement('pkg==1.0 ; python_version<"3"  # note')
Defensive patterns

Strategy: validation

Validate before calling

def strip_requirement_line(line):
    line = line.split('#', 1)[0].strip()  # drop comment + surrounding ws
    return line

Type guard

null

Try / catch

try:
    parse_requirement(line)
except SyntaxError as e:
    if 'unexpected trailing data' in str(e):
        line = line.split(';', 1)[0].strip()
        parse_requirement(line)

Prevention

When it happens

Trigger: parse_requirement('pkg>=1.0; python_version<"3" garbage'), parse_requirement('pkg==1.0 ;; marker'), or any string where a second ';' or extra token survives marker parsing. Also triggered by trailing characters after a valid marker expression that are not a '#' comment.

Common situations: Reading requirement lines from a file where multiple requirements got concatenated; requirements with double semicolons or stray punctuation after the marker; pasting specs that include inline annotations outside a comment.

Related errors


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

Appendix: source

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

                else:
                    m = VERSION_IDENTIFIER.match(s)
                    if not m:
                        raise SyntaxError('invalid constraint: %s' % s)
                    v = m.groups()[0]
                    s = s[m.end():].lstrip()
                    if s:
                        raise SyntaxError('invalid constraint: %s' % s)
                    versions = [('~=', v)]

    if remaining:
        if remaining[0] != ';':
            raise SyntaxError('invalid requirement: %s' % remaining)
        remaining = remaining[1:].lstrip()

        mark_expr, remaining = parse_marker(remaining)

    if remaining and remaining[0] != '#':
        raise SyntaxError('unexpected trailing data: %s' % remaining)

    if not versions:
        rs = distname
    else:
        rs = '%s %s' % (distname, ', '.join(['%s %s' % con for con in versions]))
    return Container(name=distname, extras=extras, constraints=versions, marker=mark_expr, url=uri, requirement=rs)


def get_resources_dests(resources_root, rules):
    """Find destinations for resources files"""

    def get_rel_path(root, path):
        # normalizes and returns a lstripped-/-separated path
        root = root.replace(os.path.sep, '/')
        path = path.replace(os.path.sep, '/')
        assert path.startswith(root)
        return path[len(root):].lstrip('/')

View on GitHub (pinned to f399c37189)