2

I am using python 2.7, but trying to make a code that checks if an object is a subclass of basestring compatible with python 3+ as well. I tried to follow the approach suggested here and found in the process a behavior that I do not understand

If I do:

def foo():
    try: basestring
    except NameError:
        print "a"
foo()

nothing happens.

If I slightly modify that code just inside the except:

def foo():
    try: basestring
    except NameError:
        print "a"
        basestring=str
foo()

Then "a" is printed.

I do not understand how adding something to the except block, can affect the triggering of the exception.

I checked the same code outside a function:

try:
    basestring
except NameError:
    print("a")
    basestring=str

but nothing gets printed in that case.

Community
  • 1
  • 1
alvarosg
  • 330
  • 2
  • 7

2 Answers2

5

When you add basestring = str to the function, you're telling python that basestring should be treated as a local variable. However, at the time that the first statement is executed, there is no local variable with the name basestring (only a global), so python raises an UnboundLocalError.

Since UnboundLocalError inherits from NameError your exception handling is triggered and you'll see a printed.


If you're interested in the nitty-gritty -- we can pull this apart using dis:

import dis

def foo():
    try:
        basestring
    except NameError:
        print("a")
        basestring=str

def bar():
    try:
        basestring
    except NameError:
        print("a")

dis.dis(foo)
print('--' * 20)
dis.dis(bar)

Note that for foo, basestring is retrieved using a LOAD_FAST op code (which means it's looking for a local variable). In bar however, basestring is retrieved using a LOAD_GLOBAL op code.

mgilson
  • 300,191
  • 65
  • 633
  • 696
3

In the first case it's easy, the name basestring is resolved on __builtins__.basestring. There is no Exception raised by the try block, so the behaviour should be as expected.

In the second case, it's tricky. Using the name basestring inside the function makes that name a local variable of the function. Note that the names which are local to a function are determined at function definition time. When executing the first line of the function, Python already knows that the name basestring is a local variable of the function.

>>> def foo():
...     basestring
...     potato
...     errorerrorerror
...     
>>> print foo.func_code.co_names
('basestring', 'potato', 'errorerrorerror')
>>> print foo.func_code.co_varnames
()

Calling foo() would NameError out on the line potato. Compare and contrast with bar() below, which would NameError out on the line basestring:

>>> def bar():
...     basestring
...     potato
...     errorerrorerror
...     basestring = "D'Addario EXL160 Medium"
...     
>>> print bar.func_code.co_names
('potato', 'errorerrorerror')
>>> print bar.func_code.co_varnames
('basestring',)

So, the exception raised is due to a name being used before it has been bound to an object, which is an error in Python. This is an error at runtime, not at definition time. The third case is just similar to the first case - the concept of 'local variable' doesn't apply at the global scope.

wim
  • 338,267
  • 99
  • 616
  • 750
  • 1
    "**Note that the names which are local to a function are determined at function definition time**" This is precisely what I was missing! Thank you! – alvarosg Nov 15 '16 at 23:13