{"record":{"id":"42a3d41bf8f6c5c9","repo":"hiyouga/LlamaFactory","slug":"empty-formatter-should-not-contain-any-placeholder","errorCode":null,"errorMessage":"Empty formatter should not contain any placeholder.","messagePattern":"Empty formatter should not contain any placeholder\\.","errorType":"exception","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"src/llamafactory/data/formatter.py","lineNumber":53,"sourceCode":"\n    def extract(self, content: str) -> str | list[\"FunctionCall\"]:\n        r\"\"\"Extract a list of tuples from the response message if using tools.\n\n        Each tuple consists of function name and function arguments.\n        \"\"\"\n        raise NotImplementedError\n\n\n@dataclass\nclass EmptyFormatter(Formatter):\n    def __post_init__(self):\n        has_placeholder = False\n        for slot in filter(lambda s: isinstance(s, str), self.slots):\n            if re.search(r\"\\{\\{[a-zA-Z_][a-zA-Z0-9_]*\\}\\}\", slot):\n                has_placeholder = True\n\n        if has_placeholder:\n            raise ValueError(\"Empty formatter should not contain any placeholder.\")\n\n    @override\n    def apply(self, **kwargs) -> SLOTS:\n        return self.slots\n\n\n@dataclass\nclass StringFormatter(Formatter):\n    def __post_init__(self):\n        has_placeholder = False\n        for slot in filter(lambda s: isinstance(s, str), self.slots):\n            if re.search(r\"\\{\\{[a-zA-Z_][a-zA-Z0-9_]*\\}\\}\", slot):\n                has_placeholder = True\n\n        if not has_placeholder:\n            raise ValueError(\"A placeholder is required in the string formatter.\")\n\n    @override","sourceCodeStart":35,"sourceCodeEnd":71,"githubUrl":"https://github.com/hiyouga/LlamaFactory/blob/f28afaf6355af515454dfb16c97d728307c93897/src/llamafactory/data/formatter.py#L35-L71","documentation":"EmptyFormatter is meant to be a constant slot list (e.g. an empty user prompt or a fixed eos token). Its __post_init__ scans string slots for the {{placeholder}} regex and raises ValueError if any is found — placeholders make no sense in a formatter whose apply() ignores kwargs and returns slots verbatim.","triggerScenarios":"Defining a custom template whose element uses EmptyFormatter but the slot text contains something like {{query}} or {{system}}; usually a copy-paste from a StringFormatter-based template where the placeholder was left behind.","commonSituations":"Writing/registering a custom template and mistaking which formatter class to use; refactoring a template and switching StringFormatter to EmptyFormatter without removing the placeholder.","solutions":["Remove the {{...}} placeholder from the EmptyFormatter slots, or","Switch to StringFormatter (or a subclass) if the slot genuinely needs substitution."],"exampleFix":"# before\nEmptyFormatter(slots=[\"Answer: {{response}}\"])\n\n# after\nStringFormatter(slots=[\"Answer: {{response}}\"])","handlingStrategy":"validation","validationCode":"import re\nph = any(re.search(r\"\\{\\{[a-zA-Z_][a-zA-Z0-9_]*\\}\\}\", s) for s in slots if isinstance(s, str))\nif ph:\n    assert formatter_cls is not EmptyFormatter, \"EmptyFormatter cannot hold placeholders — use StringFormatter\"","typeGuard":null,"tryCatchPattern":null,"preventionTips":["When authoring custom templates, choose formatter by 'does this slot substitute data?'.","Unit-test custom templates: instantiate all formters in __post_init__ during CI."],"tags":["formatter","template","config","custom-template"],"backgroundTag":null,"analyzedSha":"f28afaf6355af515454dfb16c97d728307c93897","analyzedAt":"2026-08-14T21:57:28.298Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}