sqlalchemy/alembic · error · RevisionError

Character(s) ' ' not allowed in revision identifier

Error message

Character(s) '%s' not allowed in revision identifier '%s'

What it means

Raised by Revision.verify_rev_id when a revision identifier contains characters from the illegal set ['@', '-', '+', ':']. These characters are reserved by Alembic's revision syntax: '@' separates branch labels, '+'/'-' express relative offsets, and ':' separates revision ranges. Allowing them in identifiers would make revision references ambiguous.

Solutions

  1. Replace dashes in revision ids with underscores: use '550e8400_e29b_41d4_a716_446655440000' instead of dashes.
  2. Remove any '@', '+', ':' characters from the revision identifier in the migration script's revision = '...' line.
  3. If using a custom revision id generator (revision_environment=True or script.py.x), strip or replace illegal characters before assignment.
  4. Use Alembic's default rev_id generator which produces safe hex strings.

Example fix

# before
revision = '550e8400-e29b-41d4-a716-446655440000'
down_revision = '3f4d2a1b'

# after
revision = '550e8400e29b41d4a716446655440000'
down_revision = '3f4d2a1b'
Defensive patterns

Strategy: validation

Validate before calling

_ILLEGAL = {'@', '-', '+', ':'}

def is_valid_rev_id(rev_id):
    return bool(rev_id) and not set(rev_id).intersection(_ILLEGAL)

def sanitize_rev_id(rev_id):
    for ch in _ILLEGAL:
        rev_id = rev_id.replace(ch, '_')
    return rev_id

Type guard

def is_valid_revision_id(rev_id: str) -> bool:
    """Type guard: True if rev_id contains no illegal characters."""
    illegal = {'@', '-', '+', ':'}
    return isinstance(rev_id, str) and len(rev_id) > 0 and not set(rev_id) & illegal

Prevention

When it happens

Trigger: Creating or registering a Revision (or Script) whose 'revision' string contains '@', '-', '+', or ':'. Happens at revision.py:1577-1582 during Revision.__init__ via verify_rev_id. Common when a custom revision id generator or hand-edited migration script uses an illegal character.

Common situations: Using a UUID with dashes as revision id (e.g. '550e8400-e29b-41d4-a716-446655440000'). Setting revision = 'feat@v2' thinking '@' is a valid separator. Using '+1' or ':3' style strings. Custom revision id callbacks that don't sanitize characters.

Related errors


AI-assisted analysis of sqlalchemy/alembic@5551b5d35f (2026-08-11). Data as JSON: /api/errors/3f7bdc2a4eb9a9a6. Report an issue: GitHub.

Appendix: source

Thrown at alembic/script/revision.py:1579

    From a migration standpoint, these dependencies are added to the
    down_revision to form the full iteration.  However, the separation
    of down_revision from "dependencies" is to assist in navigating
    a history that contains many branches, typically a multi-root scenario.

    """

    branch_labels: set[str] = None  # type: ignore[assignment]
    """Optional string/tuple of symbolic names to apply to this
    revision's branch"""

    _resolved_dependencies: tuple[str, ...]
    _normalized_resolved_dependencies: tuple[str, ...]

    @classmethod
    def verify_rev_id(cls, revision: str) -> None:
        illegal_chars = set(revision).intersection(_revision_illegal_chars)
        if illegal_chars:
            raise RevisionError(
                "Character(s) '%s' not allowed in revision identifier '%s'"
                % (", ".join(sorted(illegal_chars)), revision)
            )

    def __init__(
        self,
        revision: str,
        down_revision: str | tuple[str, ...] | None,
        dependencies: str | tuple[str, ...] | None = None,
        branch_labels: str | tuple[str, ...] | None = None,
    ) -> None:
        if down_revision and revision in util.to_tuple(down_revision):
            raise LoopDetected(revision)
        elif dependencies is not None and revision in util.to_tuple(
            dependencies
        ):
            raise DependencyLoopDetected(revision)

View on GitHub (pinned to 5551b5d35f)