{"record":{"id":"0b36bc0c4256302c","repo":"Textualize/textual","slug":"can-t-animate-attribute-attribute-r-on-obj-r","errorCode":null,"errorMessage":"Can't animate attribute {attribute!r} on {obj!r}; attribute does not exist","messagePattern":"Can't animate attribute (.+?) on (.+?); attribute does not exist","errorType":"exception","errorClass":"AttributeError","httpStatus":null,"severity":"error","filePath":"src/textual/_animator.py","lineNumber":380,"sourceCode":"        easing: EasingFunction | str = DEFAULT_EASING,\n        on_complete: CallbackType | None = None,\n        level: AnimationLevel = \"full\",\n    ) -> None:\n        \"\"\"Animate an attribute to a new value.\n\n        Args:\n            obj: The object containing the attribute.\n            attribute: The name of the attribute.\n            value: The destination value of the attribute.\n            final_value: The final value, or ellipsis if it is the same as ``value``.\n            duration: The duration of the animation, or ``None`` to use speed.\n            speed: The speed of the animation.\n            easing: An easing function.\n            on_complete: Callback to run after the animation completes.\n            level: Minimum level required for the animation to take place (inclusive).\n        \"\"\"\n        if not hasattr(obj, attribute):\n            raise AttributeError(\n                f\"Can't animate attribute {attribute!r} on {obj!r}; attribute does not exist\"\n            )\n        assert (duration is not None and speed is None) or (\n            duration is None and speed is not None\n        ), \"An Animation should have a duration OR a speed\"\n\n        # If an animation is already scheduled for this attribute, unschedule it.\n        animation_key = (id(obj), attribute)\n        try:\n            del self._scheduled[animation_key]\n        except KeyError:\n            pass\n\n        if final_value is ...:\n            final_value = value\n\n        start_time = self._get_time()\n        easing_function = EASING[easing] if isinstance(easing, str) else easing","sourceCodeStart":362,"sourceCodeEnd":398,"githubUrl":"https://github.com/Textualize/textual/blob/06dbeef4bb70fb718236aa418ed658ef4667a126/src/textual/_animator.py#L362-L398","documentation":"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.","triggerScenarios":"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.","commonSituations":"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__.","solutions":["Fix the attribute name spelling to match a real attribute of the object","Set the attribute on the object before animating (e.g. self.my_val = 0 in __init__)","Animate supported widget attributes such as 'offset', 'size', 'opacity' (on styles) via widget.animate with correct target","Pass the styles object when animating CSS properties, e.g. widget.animate('opacity', 0.5, on=widget.styles)"],"exampleFix":"# before\nwidget.animate(\"opactiy\", 0.0)  # typo -> AttributeError\n\n# after\nwidget.styles.animate(\"opacity\", 0.0)","handlingStrategy":"validation","validationCode":"if not hasattr(widget, attr) and not hasattr(widget.styles, attr):\n    raise ValueError(f'cannot animate {attr!r}')\nwidget.animate(attr, value)","typeGuard":"def is_animatable_target(obj: object, attribute: str) -> bool:\n    return hasattr(obj, attribute) or hasattr(getattr(obj, 'styles', None), attribute)","tryCatchPattern":"try:\n    widget.animate(attr, value)\nexcept AttributeError:\n    # fall back to instant set\n    setattr(widget, attr, value)","preventionTips":["Initialize attributes you plan to animate in __init__","Animate style properties via widget.styles.animate('opacity', ...)","Double-check attribute spelling against the widget API"],"tags":["python","textual","animation","attribute-access"],"backgroundTag":"animate-missing-attribute","analyzedSha":"06dbeef4bb70fb718236aa418ed658ef4667a126","analyzedAt":"2026-08-27T02:36:57.214Z","schemaVersion":2},"datasetVersion":"2026-08-27T03:17:27.898Z"}