Textualize/textual · error · BadIdentifier
{name!r} is an invalid {description}; identifiers must conta
Error message
{name!r} is an invalid {description}; identifiers must contain only letters, numbers, underscores, or hyphens, and must not begin with a number. What it means
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.
Source
Thrown at src/textual/dom.py:99
QueryOneCacheKey: TypeAlias = "tuple[int, str, Type[Widget] | None]"
"""The key used to cache query_one results."""
class BadIdentifier(Exception):
"""Exception raised if you supply a `id` attribute or class name in the wrong format."""
def check_identifiers(description: str, *names: str) -> None:
"""Validate identifier and raise an error if it fails.
Args:
description: Description of where identifier is used for error message.
*names: Identifiers to check.
"""
match = _re_identifier.fullmatch
for name in names:
if match(name) is None:
raise BadIdentifier(
f"{name!r} is an invalid {description}; "
"identifiers must contain only letters, numbers, underscores, or hyphens, and must not begin with a number."
)
class DOMError(Exception):
"""Base exception class for errors relating to the DOM."""
class NoScreen(DOMError):
"""Raised when the node has no associated screen."""
class _ClassesDescriptor:
"""A descriptor to manage the `classes` property."""
def __get__(
self, obj: DOMNode, objtype: type[DOMNode] | None = NoneView on GitHub (pinned to 06dbeef4bb)
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}'
Example fix
# before
widget.id = "#main"
widget.add_class("1st-row")
# after
widget.id = "main"
widget.add_class("row-1") Defensive patterns
Strategy: type-guard
Validate before calling
import re
_RE_ID = re.compile(r'[a-zA-Z_][a-zA-Z0-9_-]*')
def safe_id(value: str) -> str:
value = value.lstrip('#.')
return value if _RE_ID.fullmatch(value) else f'id-{value}' Type guard
_RE = re.compile(r'[a-zA-Z_][a-zA-Z0-9_-]*')
def is_valid_identifier(name: str) -> bool:
return _RE.fullmatch(name) is not None Try / catch
from textual.dom import BadIdentifier
try:
widget.add_class(cls)
except BadIdentifier:
cls = sanitize(cls); widget.add_class(cls) Prevention
- Never include '#' or '.' in widget ids/classes
- Prefix generated numeric ids with a letter
- Sanitize user-derived identifiers before assigning
When it happens
Trigger: widget.id = '1header', add_class('foo.bar'), classes like 'my class' (space), ids with '#' prefix accidentally included, or starting with a digit.
Common situations: 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.
Related errors
- {self.name} must be a str (e.g. '10%') or a float (e.g. 0.1)
- Expected a character or hatch value here; found {character!r
- No nodes match {self!r} on {self.node!r}
- No nodes match {self!r} on dom{self.node!r}
- {token!r} is not a valid scalar
AI-assisted analysis of Textualize/textual@06dbeef4bb (2026-08-27).
Data as JSON: /api/errors/0b0b8c3a5e085052.
Report an issue: GitHub.