Script variables - Locally scoped, final, shadowed - is that a fair summary?

Hey folks, I am a long time HA user, and I am finally digging in to native scripting. Not Node Red or the Python add ons - nothing wrong with those, I just want to understand the native scripting architecture. I have spent a bit of time trying to wrap my head around variables and I think they can be summarized as: Locally scoped to the current and descendant sequences, final, and shadowed. The first is I think fairly uncontroversial - it’s specifically documented that way, but the other two bear some clarification.

By final I mean the value of a variable will be calculated exactly once when it is defined, and never modified. If it is templated, changes that would modify the result of the template will not alter the variable. There is also no way to write a new value to an existing variable. This is the same as Java final.

By shadowed I mean the that a new variable with the same name as an existing one can be created even when the other variable is in scope. The value of the new variable will be used when the name is used, but the old value will return once the shadowing variable leaves scope. This is what appears to be happening in the variable scope example of the docs - people can be read in the sub-sequence, and a new variable called people can be defined, but when we return to the outer sequence the original value of people reappears.

Does this sound about right? Also note that I am not discussing the various variable add ons that exist, just the vanilla ones that are in the built in scripting language.

Assuming this is correct, I think it means you can’t modify variables in an if-then-else block, since the variable definitions will be locally scoped to the then or else blocks. As a work around, templates can be used instead, e.g.:

  - variables:
      foozle: |
        {% if is_state("light.kitchen_accent", "off") -%}
        dark
        {%- else -%}
        light
        {% endif %}

Yes. All correct.

Sure. Although you can definitely make that template shorter:

  - variables:
      foozle: "{{ 'dark' if is_state('light.kitchen_accent', 'off') else 'light' }}"

Or use the immediate if:

  - variables:
      foozle: "{{ is_state('light.kitchen_accent', 'off') | iif('dark', 'light') }}"

Understanding templates is on my todo list :slight_smile: That section made for some good bedtime reading! Thanks for the confirmation on the other items.

Yes and no. Have a look at “Scoping Behavior” in the documentation (sorry no direct link available) and specifically “namespace”. This makes the following very contrived code work:

{% set ns = namespace(test=0) %}
{% if 0 == 1 %}
  {% set ns.test = 1 %}
{% else %}
  {% set ns.test = 2 %}
{% endif %}
{{ ns.test }}

I could be wrong but I think you’re describing a different thing. I believe the OP was talking about an script if-then-else block and using that to change script variables. Which you can’t do for the reasons they described. Hence why they were showing using a jinja if-then-else as a workaround.

You’re showing how to make a jinja variable which can be modified within if-then-else blocks within the same template. Perhaps I misunderstood but they did link to this example to show the types of variables they want to set.

Thanks @michaelblight, that is actually really helpful! It looks like this would let me have variables that outlive the scope of the current template? And possibly even the current script? Something more to add to my reading list :slight_smile: Though, as @CentralCommand mentions, I don’t think this changes my understanding of HA Script variables, though it is pushing me toward doing more “complex” logic in Jinja. I still don’t have a good intuitive feel for when I should use script constructs vs template constructs. Heck, I don’t really understand how to talk about them as different things, since they share a surface.

No, it doesn’t do that.

Jinja also has final variables by default with shadowing between scopes. So for example if you had this template:

{% set test = 'hello' %}
{% if 0 == 0 %}
  {% set test = 'goodbye' %}
{% endif %}
{{ test }}

The result of the template would be the text hello. The test variable was shadowed when you set it within the if block, the original was not changed. So outside of the if block the value was still hello.

However unlike HA script variables, Jinja has a way to break out of this. You can set the value of a variable to a namespace object. And then when you change fields of that namespace object within scoped blocks those changes are visible in the outer scope. So this example:

{% set ns = namespace(test='hello') %}
{% if 0 == 0 %}
  {% set ns.test = 'goodbye' %}
{% endif %}
{{ ns.test }}

Would result in goodbye.

But this is all contained within a single template. After the template finishes rendering the namespace object is gone. And you also cannot set the value of an HA variable to a namespace object since it is not serializable.

Yes, my mistake. I do most automation in Node Red, so when I saw “{% %}”, I assumed Jinja.

Ah, ok, thanks! That makes sense, and could prove handy.