Search code examples
pythondjangopython-decorators

Local variable unreferenced in decorator definition


My code is behaving weird, and I can't get why.

Here is the code:

from django.urls import path


app_name = 'portal'
urlpatterns = []

def route(url, name=""):

    def dec(f):
        f_name = name or f.__name__
        urlpatterns.append(
            path(url, f, name=f_name)
        )   
        return f
    return dec 

from . import views                  
# 2) The decorator call in the other file
from . import urls

@urls.route("/my_function")
def my_function():
    print("Hello world")

I get an UnboundLocalError on name

 File "urls.py", line 10, in dec
    if name == "":
UnboundLocalError: local variable 'name' referenced before assignment

name should be set to "" by default, I don't understand where the problem is. The weird thing is if I run the same code and change the decorator to this:

urlpatterns = []

def route(url, name=""):

    def dec(f):
        if name == "":
            print("I work!")
        urlpatterns.append(
            path(url, f, name=name)
        )
        return f
    return dec

It works perfectly and output:

I work ! 

while the problem was supposed to come from the line if name == ""

PS: I'm programming on django, this line is in the urls.py file.


Solution

  • The answer is in the part you didn't posted. Your real code actually looks something like

    def route(url, name=""):    
        def dec(f):
            if name == "":
                # here's the real issue
                name = "something_" + f.__name__
    
            urlpatterns.append(
                path(url, f, name=name)
            )
            return f
        return dec
    

    Assigning to name makes it a local variable - Python has not variable declaration, so it's the place where a name is bound that defines its scope. In your case, assigning to name in dec makes the name "name" local to dec, so it's NOT looked up in the enclosing scopes. And since you test it ("reference") before you assign to it, you get the very obvious (nah, just kidding) " local variable 'name' referenced before assignment" error.

    The solution here is to declare name as "nonlocal" at the top of your decfunction, so Python knows it has to be looked up in the enclosing scope (or, more exactly, in the closure's cells which capture the dec function's environment):

    def route(url, name=""):    
        def dec(f):
            nonlocal name
    
            if name == "":
                # here's the real issue
                name = "something_" + f.__name__
    
            urlpatterns.append(
                path(url, f, name=name)
            )
            return f
        return dec
    

    Note that this only works for Python3 - if using Python2, you'll have to resort to a hack to emulate this behaviour:

    def route(url, name=""):    
        # Py2 hack: wrap the "nonlocal" variable in
        # a mutable container
    
        name = [name]
    
        def dec(f):
    
            if name[0] == "":
                name[0] = "something_" + f.__name__
    
            urlpatterns.append(
                path(url, f, name=name[0])
            )
            return f
        return dec
    

    The hack here bing that you mutate name instead of rebinding it, so Python doesn't mark it as a local variable.

    As a side note, I wouldn't recommand trying to port this @route(url) idiom (Flask anyone ?) to Django. First because separating the view definitions from the url mapping is a deliberate design decision, which allow to remap a third-part apps urls to whatever we want without hacks nor forks etc, and also because most Django devs expect urls to be explicitely defined in the urls.py module and will hate you for not following the convention. Now you're of course free to write your project as you want, but following conventions makes it easier for everyone. My 2 cents...