Convenient way to mark any variable

Hi,

I’m using Python (3.12) with Flask to render web pages. So I can handle web requests in Python, and I choose which Python variables to make available in the HTML templates. Most endpoints look like this:

GLOBAL_CONSTANT = 123

@app.route('/endpoint_name')
def endpoint_name():
    render_vars = {'GLOBAL_CONSTANT': GLOBAL_CONSTANT}   # gather variables for export here

    example_name1 = compute_value()
    use_value(example_name1)
    example_name1 = update_value(example_name1)
    render_vars['example_name1'] = example_name1

    do_complicated_stuff(render_vars)  # this adds more variables to render_vars

    example_name2 = 'this one is just a literal'
    do_more_stuff(example_name2)
    render_vars['example_name2'] = example_name2
    
    # finally we can render our template, everything in render_vars is available there
    return render_template('endpoint_name.html', **render_vars)

This has worked well for years, even for fairly complicated web applications. My issue/question is around how variables are exported. As you can see above, each time I export a variable I need to type its name 3 times. I think there must be a more succinct way that’s still Pythonic, but I haven’t found a satisfying one yet.

I thought about renaming render_vars to something shorter like r and then using r['example_name1'] everywhere… but that is unsatisfying, doesn’t work with autocomplete in my IDE, etc. So the above code is the approach I’ve been using.

I learned about type annotations recently, and the syntax seemed perfect for my application here. This actually works fairly well for global variables. I can mark them with a special type (type RenderExport = Any) and then gather them by looking at the global __annotations__ variable. So my global variables are the somewhat satisfying:

GLOBAL_CONSTANT: RenderExport = 123

This doesn’t work at all for local variables though since (as I understand it) local variable type annotations aren’t made available at runtime. I read a bit about how type annotations evolved and I’d like to think this a great example of a non-type related application of the language feature, but I’m curious if people closer to the design of type annotations would agree.

I’ve experimented with a few things but haven’t found a way to mark local variables with a simple consistent syntax that feels Pythonic and lets me avoid typing their name more than just once. So I was wondering if anyone had any ideas (or feedback in general about how to approach this in a Pythonic manner).

Regards,
Jeremy

Generally I don’t think this is an intended use for type hints, but hey if it works: it works.

I think there is a typing.Annotated for a more general way of assigning a metadata to a variable. typing — Support for type hints — Python 3.12.2 documentation

One way to repeat a var less is to use it once and return changed/updated versions of it. Consider this example:

def update_val(v: str) -> str:
    # so stuff with v.. then at the end:
    return v

Now you have only one use of the var in your function:

exported = {}
exported['value'] = update_val('initial value')
...

Then have one update function per exported value (that needs modifications).

I can barely even begin to imagine trying to use annotations for this.

Assuming that the calculation for each render variable are independent of each other, one powerful technique for “collecting” results like this is to use generators that give key-value pairs, and just build the dictionary at the top level. This can also make it easier to split the work into functions, because individual functions don’t have to modify a passed-in dictionary nor do callers need to handle that updating - you do it all at the “end”/“top”, and it’s all in one place so you have a proper separation of responsibilities.

It could look like:

def _get_name_1():
    result = compute_value()
    use_value(result)
    yield ('example_name1', result)

def _get_name_2():
    result = 'this one is just a literal'
    do_more_stuff(result)
    yield ('example_name2', result)

def _endpoint_name_items():
    yield ('GLOBAL_CONSTANT', GLOBAL_CONSTANT)
    yield _get_name_1()
    # do_complicated_stuff is rewritten as a generator that yields
    # key-value pairs instead of inserting them into the dict.
    yield from _complicated_stuff()
    yield _get_name_2()

@app.route('/endpoint_name')
def endpoint_name():
    return render_template('endpoint_name.html', **dict(_endpoint_name_items()))

Sorry, I spotted the intermediate usage of render_vars too late. I now see the dict is still
needed in your case Jeremy.

However in a situation that doesn’t use the kwargs dict as a dict, it can be avoided using a wrapper and some Python black magic/witch craft boiler plate (introspection):

import  inspect

def call_with_only_supported_kwargs(func, *args, **kwargs):

    sig = inspect.signature(func)
    kw_arg_names = [x 
                    for x, p in sig.parameters.items() 
                    if p.default is not inspect.Parameter.empty
                   ]
    
    return func(*args, **{k : v 
                          for k, v in kwargs.items() 
                          if k in kw_arg_names
                         }
               )

GLOBAL_CONSTANT = 123

@app.route('/endpoint_name')
def endpoint_name():

    example_name1 = compute_value()
    use_value(example_name1)
    example_name1 = update_value(example_name1)

    # This example is for situations in which there are no steps 
    # using render_vars, such as the one below
    # do_complicated_stuff(render_vars)  # adds more variables to render_vars

    example_name2 = 'this one is just a literal'
    do_more_stuff(example_name2)
    
    return call_with_only_supported_kwargs(
                        'endpoint_name.html',
                        **locals(),
                        **globals(),
                        )

It’s messy, but if you have a heap of kwargs and local variables that all have the same name as, maintaining the boilerplate return calls and helper function could be easier.

I’ve tried getting rid of the boilerplate too, by directly inspecting the frame after the call to the ‘wrapped’ function.

Hi everyone, thanks for your responses. I was hoping to avoid refactoring my code around this - in reality the code gets quite involved, and I’d like to be able to write it in a natural style and then mark variables for export without repeating their names.

It seems like the only way to annotate or somehow mark a variable at declaration time is to use type annotations. So far all the other approaches I’ve seen require the variable name to be repeated.

I’ll see if I can inquire about how general the type annotation support is likely to be long-term. I know my use case is conceptually unrelated, but I’m really just trying to annotate variables which is exactly what type annotations do.

Jeremy

Python has function annotations and variable annotations (of which the above is an example). Function annotations were initially added without prescribing any particular semantics (though they were clearly designed with type hints in mind). PEP 484 started the process of providing a standard way to use function annotation for type hints, and PEP 526 added variable annotations to further support type hinting.

PEP 563 formally deprecates the use of either annotation for anything not compatible with type hinting as described in various PEPs. (Click on the link to read the relevant paragraph.)

With this in mind, uses for annotations incompatible with the aforementioned PEPs should be considered deprecated.

That makes sense - I’m not using any features that worked and were then deprecated. I just have a non-type application and I wish the type annotations were slightly more accommodating.

In particular I was thinking Annotated could offer a single-argument form which just attaches metadata to a variable without stating anything about its type.

This is mostly the same thematic I proposed a solution for in An alternative to Annotated, with the idea that Annotated is just annoying to use if you don’t want type hints at all, and even if you do, it’s very verbose.

Yes, you are. You’re annotating a variable with something that isn’t a type hint. That’s what has been deprecated. (Deprecation doesn’t necessarily mean it will stop working at runtime.)

Non-type applications are deprecated. The ship has sailed on using variable annotations for anything else.

Annotated is for attaching metadata to a type, not a variable.

1 Like