{"record":{"id":"0b0b8c3a5e085052","repo":"Textualize/textual","slug":"name-r-is-an-invalid-description-identifiers","errorCode":null,"errorMessage":"{name!r} is an invalid {description}; identifiers must contain only letters, numbers, underscores, or hyphens, and must not begin with a number.","messagePattern":"(.+?) is an invalid (.+?); identifiers must contain only letters, numbers, underscores, or hyphens, and must not begin with a number\\.","errorType":"validation","errorClass":"BadIdentifier","httpStatus":null,"severity":"error","filePath":"src/textual/dom.py","lineNumber":99,"sourceCode":"QueryOneCacheKey: TypeAlias = \"tuple[int, str, Type[Widget] | None]\"\n\"\"\"The key used to cache query_one results.\"\"\"\n\n\nclass BadIdentifier(Exception):\n    \"\"\"Exception raised if you supply a `id` attribute or class name in the wrong format.\"\"\"\n\n\ndef check_identifiers(description: str, *names: str) -> None:\n    \"\"\"Validate identifier and raise an error if it fails.\n\n    Args:\n        description: Description of where identifier is used for error message.\n        *names: Identifiers to check.\n    \"\"\"\n    match = _re_identifier.fullmatch\n    for name in names:\n        if match(name) is None:\n            raise BadIdentifier(\n                f\"{name!r} is an invalid {description}; \"\n                \"identifiers must contain only letters, numbers, underscores, or hyphens, and must not begin with a number.\"\n            )\n\n\nclass DOMError(Exception):\n    \"\"\"Base exception class for errors relating to the DOM.\"\"\"\n\n\nclass NoScreen(DOMError):\n    \"\"\"Raised when the node has no associated screen.\"\"\"\n\n\nclass _ClassesDescriptor:\n    \"\"\"A descriptor to manage the `classes` property.\"\"\"\n\n    def __get__(\n        self, obj: DOMNode, objtype: type[DOMNode] | None = None","sourceCodeStart":81,"sourceCodeEnd":117,"githubUrl":"https://github.com/Textualize/textual/blob/06dbeef4bb70fb718236aa418ed658ef4667a126/src/textual/dom.py#L81-L117","documentation":"check_identifiers raises BadIdentifier when a widget id, class, or similar identifier fails the regex: only letters, numbers, underscores, hyphens allowed, and it must not start with a number. Used by widget id setters, add_class/remove_class/toggle_class, etc.","triggerScenarios":"widget.id = '1header', add_class('foo.bar'), classes like 'my class' (space), ids with '#' prefix accidentally included, or starting with a digit.","commonSituations":"Prefixing an id with '#' out of CSS habit ('#main'), ids derived from user data or counters starting with digits, class strings containing spaces or dots.","solutions":["Strip leading '#'/'.': use 'main' not '#main'","Ensure ids/classes match [a-zA-Z_][a-zA-Z0-9_-]*","When generating ids from numbers, prefix a letter, e.g. f'row-{n}'"],"exampleFix":"# before\nwidget.id = \"#main\"\nwidget.add_class(\"1st-row\")\n# after\nwidget.id = \"main\"\nwidget.add_class(\"row-1\")","handlingStrategy":"type-guard","validationCode":"import re\n_RE_ID = re.compile(r'[a-zA-Z_][a-zA-Z0-9_-]*')\n\ndef safe_id(value: str) -> str:\n    value = value.lstrip('#.')\n    return value if _RE_ID.fullmatch(value) else f'id-{value}'","typeGuard":"_RE = re.compile(r'[a-zA-Z_][a-zA-Z0-9_-]*')\ndef is_valid_identifier(name: str) -> bool:\n    return _RE.fullmatch(name) is not None","tryCatchPattern":"from textual.dom import BadIdentifier\ntry:\n    widget.add_class(cls)\nexcept BadIdentifier:\n    cls = sanitize(cls); widget.add_class(cls)","preventionTips":["Never include '#' or '.' in widget ids/classes","Prefix generated numeric ids with a letter","Sanitize user-derived identifiers before assigning"],"tags":["textual","dom","identifier","validation","regex"],"backgroundTag":"invalid-identifier","analyzedSha":"06dbeef4bb70fb718236aa418ed658ef4667a126","analyzedAt":"2026-08-27T02:36:57.214Z","schemaVersion":2},"datasetVersion":"2026-08-27T03:17:27.898Z"}