{"record":{"id":"7339376c9970e3ad","repo":"pypa/pip","slug":"expected-extra-name-after-comma","errorCode":null,"errorMessage":"Expected extra name after comma","messagePattern":"Expected extra name after comma","errorType":"exception","errorClass":"ParserSyntaxError","httpStatus":null,"severity":"error","filePath":"src/pip/_vendor/packaging/_parser.py","lineNumber":245,"sourceCode":"    \"\"\"\n    extras: list[str] = []\n\n    if not tokenizer.check(\"IDENTIFIER\"):\n        return extras\n\n    extras.append(tokenizer.read().text)\n\n    while True:\n        tokenizer.consume(\"WS\")\n        if tokenizer.check(\"IDENTIFIER\", peek=True):\n            tokenizer.raise_syntax_error(\"Expected comma between extra names\")\n        elif not tokenizer.check(\"COMMA\"):\n            break\n\n        tokenizer.read()\n        tokenizer.consume(\"WS\")\n\n        extra_token = tokenizer.expect(\"IDENTIFIER\", expected=\"extra name after comma\")\n        extras.append(extra_token.text)\n\n    return extras\n\n\ndef _parse_specifier(tokenizer: Tokenizer) -> str:\n    \"\"\"\n    specifier = LEFT_PARENTHESIS WS? version_many WS? RIGHT_PARENTHESIS\n              | WS? version_many WS?\n    \"\"\"\n    with tokenizer.enclosing_tokens(\n        \"LEFT_PARENTHESIS\",\n        \"RIGHT_PARENTHESIS\",\n        around=\"version specifier\",\n    ):\n        tokenizer.consume(\"WS\")\n        parsed_specifiers = _parse_version_many(tokenizer)\n        tokenizer.consume(\"WS\")","sourceCodeStart":227,"sourceCodeEnd":263,"githubUrl":"https://github.com/pypa/pip/blob/f399c3718970b1b0e2478dac5296eb62679a9b86/src/pip/_vendor/packaging/_parser.py#L227-L263","documentation":"Raised as a ParserSyntaxError (surfacing as InvalidRequirement) at line 245 inside _parse_extras_list: after consuming a comma in an extras list (line 242), tokenizer.expect(\"IDENTIFIER\", expected=\"extra name after comma\") fails because no valid identifier follows the comma. The grammar (line 226) requires an identifier after each comma.","triggerScenarios":"A requirement with a trailing comma in extras, e.g. 'pkg[a,]', or an empty slot like 'pkg[a,,b]', or a non-identifier token after the comma, e.g. 'pkg[a,2]'. After tokenizer.read() consumes the comma (line 242), the next token is not an IDENTIFIER.","commonSituations":"Programmatically joining extras with a trailing comma (','.join(extras) + ','); user-edited extras strings with a stray comma; copy-paste leaving a dangling comma.","solutions":["Remove the trailing/empty comma: 'pkg[a,b]' instead of 'pkg[a,b,]'.","When building extras programmatically, filter empties and avoid a trailing comma: '[' + ','.join(filter(None, extras)) + ']'.","Validate the extras segment parses before using it."],"exampleFix":"// before\nRequirement('pkg[dev,test,]')\n\n// after\nRequirement('pkg[dev,test]')","handlingStrategy":"validation","validationCode":"def clean_extras(extras):\n    return [e for e in (x.strip() for x in extras) if e]\ndef extras_str(extras):\n    return '[' + ','.join(clean_extras(extras)) + ']' if clean_extras(extras) else ''","typeGuard":null,"tryCatchPattern":"from packaging.requirements import Requirement, InvalidRequirement\ntry:\n    req = Requirement(raw)\nexcept InvalidRequirement as e:\n    if 'extra name after comma' in str(e):\n        # strip trailing/empty commas in the extras bracket\n        raw = re.sub(r',+', ',', raw)\n    raise","preventionTips":["When joining extras programmatically, filter falsy values and never append a trailing comma.","Treat the extras segment as structured data, not free-form text."],"tags":["packaging","pep508","requirement","extras","parser","syntax-error"],"backgroundTag":null,"analyzedSha":"f399c3718970b1b0e2478dac5296eb62679a9b86","analyzedAt":"2026-08-08T23:01:42.227Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}