Textualize/textual · error · AttributeError

Can't animate attribute {attribute!r} on {obj!r}; attribute

Error message

Can't animate attribute {attribute!r} on {obj!r}; attribute does not exist

What it means

Textual's animator refuses to animate an attribute that does not exist on the target object. Before creating the animation, _animate checks hasattr(obj, attribute) and raises AttributeError immediately, because there is no starting value to blend from.

Source

Thrown at src/textual/_animator.py:380

        easing: EasingFunction | str = DEFAULT_EASING,
        on_complete: CallbackType | None = None,
        level: AnimationLevel = "full",
    ) -> None:
        """Animate an attribute to a new value.

        Args:
            obj: The object containing the attribute.
            attribute: The name of the attribute.
            value: The destination value of the attribute.
            final_value: The final value, or ellipsis if it is the same as ``value``.
            duration: The duration of the animation, or ``None`` to use speed.
            speed: The speed of the animation.
            easing: An easing function.
            on_complete: Callback to run after the animation completes.
            level: Minimum level required for the animation to take place (inclusive).
        """
        if not hasattr(obj, attribute):
            raise AttributeError(
                f"Can't animate attribute {attribute!r} on {obj!r}; attribute does not exist"
            )
        assert (duration is not None and speed is None) or (
            duration is None and speed is not None
        ), "An Animation should have a duration OR a speed"

        # If an animation is already scheduled for this attribute, unschedule it.
        animation_key = (id(obj), attribute)
        try:
            del self._scheduled[animation_key]
        except KeyError:
            pass

        if final_value is ...:
            final_value = value

        start_time = self._get_time()
        easing_function = EASING[easing] if isinstance(easing, str) else easing

View on GitHub (pinned to 06dbeef4bb)

Solutions

  1. Fix the attribute name spelling to match a real attribute of the object
  2. Set the attribute on the object before animating (e.g. self.my_val = 0 in __init__)
  3. Animate supported widget attributes such as 'offset', 'size', 'opacity' (on styles) via widget.animate with correct target
  4. Pass the styles object when animating CSS properties, e.g. widget.animate('opacity', 0.5, on=widget.styles)

Example fix

# before
widget.animate("opactiy", 0.0)  # typo -> AttributeError

# after
widget.styles.animate("opacity", 0.0)
Defensive patterns

Strategy: validation

Validate before calling

if not hasattr(widget, attr) and not hasattr(widget.styles, attr):
    raise ValueError(f'cannot animate {attr!r}')
widget.animate(attr, value)

Type guard

def is_animatable_target(obj: object, attribute: str) -> bool:
    return hasattr(obj, attribute) or hasattr(getattr(obj, 'styles', None), attribute)

Try / catch

try:
    widget.animate(attr, value)
except AttributeError:
    # fall back to instant set
    setattr(widget, attr, value)

Prevention

When it happens

Trigger: Calling widget.animate(...) or animator.animate(obj, ...) with an attribute name that is misspelled or not set on the object yet, e.g. `widget.animate('opactiy', 1.0)` or animating 'color' on an object that has no color attribute. Also animating an attribute that is only set inside styles, not as a Python attribute.

Common situations: Typos in the attribute name; assuming CSS-style properties (e.g. 'opacity' from stylesheet) exist as Python attributes of the widget; animating a custom attribute before initializing it in __init__.

Related errors


AI-assisted analysis of Textualize/textual@06dbeef4bb (2026-08-27). Data as JSON: /api/errors/0b36bc0c4256302c. Report an issue: GitHub.