{"id":"58bf7aa91a5322bd","repo":"psycopg/psycopg2","slug":"cannot-switch-from-automatic-field-numbering-to-ma","errorCode":null,"errorMessage":"cannot switch from automatic field numbering to manual","messagePattern":"cannot switch from automatic field numbering to manual","errorType":"exception","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"lib/sql.py","lineNumber":241,"sourceCode":"            select * from \"people\" where \"id\" = %s\n\n        \"\"\"\n        rv = []\n        autonum = 0\n        for pre, name, spec, conv in _formatter.parse(self._wrapped):\n            if spec:\n                raise ValueError(\"no format specification supported by SQL\")\n            if conv:\n                raise ValueError(\"no format conversion supported by SQL\")\n            if pre:\n                rv.append(SQL(pre))\n\n            if name is None:\n                continue\n\n            if name.isdigit():\n                if autonum:\n                    raise ValueError(\n                        \"cannot switch from automatic field numbering to manual\")\n                rv.append(args[int(name)])\n                autonum = None\n\n            elif not name:\n                if autonum is None:\n                    raise ValueError(\n                        \"cannot switch from manual field numbering to automatic\")\n                rv.append(args[autonum])\n                autonum += 1\n\n            else:\n                rv.append(kwargs[name])\n\n        return Composed(rv)\n\n    def join(self, seq):\n        \"\"\"","sourceCodeStart":223,"sourceCodeEnd":259,"githubUrl":"https://github.com/psycopg/psycopg2/blob/3a6d9d6ddc6b53eaa80b712f5fa6b23abbdc38db/lib/sql.py#L223-L259","documentation":"Raised by SQL.format() (lib/sql.py:241) when a template mixes automatic ({}) and manual ({0}) field numbering, specifically when an auto-numbered placeholder was already seen (autonum counter > 0) and then a manual index appears. Mirrors the rule enforced by built-in str.format: a single template must use one style throughout. The library tracks the auto counter and, once started, forbids explicit indices to avoid ambiguous argument binding.","triggerScenarios":"A template like sql.SQL(\"select {} from {0}\").format(a, b) - the first {} starts automatic mode, then {0} is manual and trips the check. Also sql.SQL(\"{} {} {1}\").format(...).","commonSituations":"Refactoring or partially editing a template that ended up mixing styles; concatenating fragments built with different conventions; copy-pasting one placeholder from another query.","solutions":["Make all placeholders automatic ({}) and pass positional args in the right order.","Or make all placeholders explicitly numbered ({0}, {1}, ...) consistently across the whole template.","Best for readability: use named placeholders ({tbl}, {col}) with keyword arguments."],"exampleFix":"// before\nq = sql.SQL(\"select {} from {0}\").format(sql.Identifier('c'), sql.Identifier('t'))\n// after\nq = sql.SQL(\"select {0} from {1}\").format(sql.Identifier('c'), sql.Identifier('t'))","handlingStrategy":"validation","validationCode":"import string\n\ndef template_numbering_is_consistent(template: str) -> bool:\n    seen_auto = seen_manual = False\n    for _pre, name, _spec, _conv in string.Formatter().parse(template):\n        if not name:\n            continue\n        if name.isdigit():\n            seen_manual = True\n        else:\n            seen_auto = True\n        if seen_auto and seen_manual:\n            return False\n    return True\n\nassert template_numbering_is_consistent(tpl)","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Pick one numbering style per template and stick to it.","Prefer named placeholders ({tbl}, {col}) to avoid the auto/manual pitfall entirely.","When editing a template, re-read all placeholders rather than just the one you changed."],"tags":["psycopg2","sql-composition","format","valueerror"],"analyzedSha":"3a6d9d6ddc6b53eaa80b712f5fa6b23abbdc38db","analyzedAt":"2026-08-04T19:56:51.958Z","schemaVersion":2}