Extreme Python Coaching

Logo

An assessment based python coaching session designed to fill gaps in your understanding extremely quickly.

Python Quiz: names and types

by Alan

In this sample quiz you’re going to be tested on your understanding of the fundamentals of name-bindings and the type system. I encourage you to think not just about what the answer to each question is, but why because if you can’t explain it, then you don’t really know it!

My answers and explanations are included at the end of the post.

Number of questions: 5

Question 1

x = 7 
y = x 
x = 11

What’s the final value of y?

Question 2

def foo(x):
  if x: 
      return 1 + 1
  return 1 + 'NaN'

What’s the output of foo(True)?

Question 3

numbers  = [1, 1, 2, 3, 5]
for x in numbers:
    x = x * x 

Once the loop terminates, what’s the value of x? What’s the value of numbers?

Question 4

Which one of the following creates a new name-binding? If it does, can it be reassigned?

# A
import x
# B
from x import y
# C
def foo(): 
    pass
# D
class Boo():  
    pass 
# E
for x in some_list:
    pass
# F
x = 5

Question 5

class Lamp:
    def turn_on():
        print("Turning on the Lamp") 

class Computer:
    def turn_on():
    	print("Turning on the Computer")
  
x = Lamp() 
x.turn_on()
x = Computer() 
x.turn_on() 

Will this program run without any errors? Why or why not?

Answers

Question 1

The final value of y is 7.

One misconception about why the value is 7 is that x is initially storing the value 7 and y = x causes 7 to be copied to y. And since the variables store separate copies of the integer, any further assignments to x will not affect the value of y. While it’s true that variable assignments in python happen independently, it’s not due to value copying.

So lets first get one thing straight: names in python do not store values (any python object) - they reference values. What’s really happening is:

  1. The name x is initially referencing the integer object 7
  2. y = x makes y reference whatever x is refrencing, which is 7
  3. x = 11 updates x to reference the new integer object 11

The names have no reference of each other. Assigning variables to each other just makes it so that they reference the same object. Since the names reference the objects directly, any updates to those references will happen independently.

FAQS

But how do I know for sure values aren’t being copied tho?

Ok fine lets just do a simple test using some basic python tools: id() and sys.getrefcount().

id pretty much returns the address of the object. It will be unique for every object. When you call id on a name, it gets you the address of the object it’s referencing (not the address of the name!).

getrefcount will get the number of names referring to an object. I’m going to demonstrate how the refcount for an integer object changes as we add more assignments.

Okay, on to the test.

If data is being copied, then the reference count of that data should not be increasing (because copies of it will have its own identifier). We’ll see that it doesn’t happen.

First, lets import sys so that we can use it to invoke getrefcount

>>> import sys

Now lets print out the address of the integer object 1 and its starting reference count.

>>> id(1)
10910400
>>> sys.getrefcount(1)
806

Great, now we know the identity of our number as well as its base reference count.

Note: You might be wondering why the refcount is such a high number. For the sake of this test, it doesn’t matter why but my guess is that it’s not just counting the references in code we’ve written but rather references to the object globally in the current runtime environment.

Now we’re going to write an assignment statement and see the reference count increment.

>>> x = 1
>>> id(x)
10910400
>>> sys.getrefcount(1)
807

Great, so we’ve verified with the call to id that we’re dealing with the same integer and that the reference count increased by 1!

Finally, lets prove that no value is being copied when we assign x to y. If a value is being copied, then we should expect to see two things. First is that the id of y will not equal 10910400. Second, the reference count will remain 807.

>>> y = x
>>> id(y)
10910400
>>> sys.getrefcount(1)
808

… and neither of those things happened. It looks like y is referencing the same object and that new reference incremented the reference count of 1.

What you’re describing sounds a lot like pointers to me - are python variables basically C pointers?

The short answer is …kind of, but not really. For a longer, better answer check out this answer on quora. It really helped me understand.

If you know a bit of C, I wrote an example of something you can do with C pointers that you won’t be able to do with python variables.

int five = 5; // Store 5 
int seven = 7; // Store 7
int *x = &five; // Store address of five in pointer x
int **y = &x; // Store address of pointer x in pointer y
x = &seven; // Update pointer x to store the address of seven
printf("%d\n", **y); 

The result of the print statement is 7.

Why’s that significant? Well, reassigning one variable actually changed the value you get from a different variable. You cannot do that in python. In python, names do not refer to other names. They refer directly to values.

Put another way, they behave like very limited pointers whereby names point to other addresses, but those addresses cannot be that of another name.

Question 2

The return value of foo(True) is 2.

Python performs dynamic type checking. What that means is that it associates type information with values at runtime and thus only cares about the type of a value when it tries to use it. Since the unsupported operation is never used, the function returns without complaining.

Now lets execute the unsupported operations in the REPL and see what happens:

>>> if True: 
...     1 + 'NaN'
... else:
...     1 + 1
... 
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
TypeError: unsupported operand type(s) for +: 'int' and 'str'

Now that we’re actually executing the line where we’re performing an unsupported mix-type operation, the type error is thrown.

FAQ

Does this mean I can type anything in regions of code that won’t be executed?

Not quite - you can’t run a program if it contains syntax errors even if you put them where they won’t be executed.

For example, this program is correct by pythons syntax:

if True:
    1 + 1
else:
    icandowhateveriwanthereright+=1

Even though icandowhateveriwanthereright might not be defined, it’s a syntactically correct expression so this program will execute without error.

Now lets look at a syntactically incorrect program:

if True:
    1 + 1
else:
    icandowhat   ++ everiwanthereright+=1

icandowhat ++ everiwanthereright is not a valid identifier by any stretch. So what happens? We get this error if we try to run it.

...
  icandowhat   ++ everiwanthereright+=1
SyntaxError: can't assign to operator

Lets be clear about one thing: no expressions in this program got executed. Is that obvious? Maybe not. Lets dig into it a bit though.

When we try running a python file, we kick off a compilation process. Syntax errors are actually caught at a different stage in the process from type errors. Specifically, they’re caught at the parsing stage and not the stage where your expressions are being executed (final stage).

Lets break that down a bit more.

Generally speaking, python compilation involves:

  1. Lexing (breaking up text into meaningful tokens)
  2. Parsing (using grammar to organize tokens into an abstract syntax tree)
  3. Translation of AST into bytecode
  4. Bytecode execution

Step 1 and 2 is about where syntax errors are caught. Step 4 is where type errors are caught. This makes sense, because if you have characters in your program that python can’t even recognize, you probably don’t want to execute that program.

Extra credit experiment? Give it a try - add some print statements in a program containing syntax errors and try to get them to print something.

Question 3

The final value of x is 25 and numbers remains unchanged [1, 1, 2, 3, 5].

The reason numbers remains unchanged is that at no point do we change the reference to integers in the list numbers. Our loop created a new name called x, and then for each expression x = x * x it bound that name to a new integer object (the value of x * x)

Another way to think about it is that this is actually just another case of the following:

number = 5
x = number 
x = x * x

Does number change when we reassign x? No, it doesn’t because x = x * x updates x to reference the new object 25. But that does nothing to change the fact that number is still referencing the 5. If you want to change number, you need to reassign it to something else.

So if we want to actually change the integers in the list numbers, we have two options:

Option 1

We update the references to integers within the existing list.

>>> numbers = [1, 1, 2, 3, 5]
>>> for idx, x in enumerate(numbers): 
...     numbers[idx] = x * x 
... 
>>> numbers
[1, 1, 4, 9, 25]

Option 2

We update the reference of numbers to a new list that we append the new values to.

>>> numbers = [1, 1, 2, 3, 5]
>>> numbers_squared = []
>>> for x in numbers:
...     numbers_squared.append(x * x)
... 
>>> numbers = numbers_squared
>>> numbers
[1, 1, 4, 9, 25]

Or more idiomatically, as a list comprehension:

>>> numbers = [1, 1, 2, 3, 5]
>>> numbers = [x * x for x in numbers]
>>> numbers
[1, 1, 4, 9, 25]

Question 4

They all create name bindings! Every introduction of an identifier introduces a new name into memory that points to an object.

Ok lets go through each:

import x binds the name x to a module object

from x import y binds the name y to an object belonging to the module x

def foo() binds foo to an object function

class Boo() binds Boo to a class object

... for x in some_list... binds x to objects in some_list

x = 5 binds x to the integer object 5

And like any name binding, they can be reassigned to any other value.

Here’s an example where I define a class object referenced by A and then update the reference to an integer:

>>> class A():
...     pass
>>> A
<class '__main__.A'>
>>> A = 5
>>> A
5

Why would you ever do that? You probably wouldn’t :) but you can!

Question 5

Both statements will be printed successfully.

Python uses a duck type system. Names are not declared with types in Python - once you have a name, it can reference any other object. What matters is not so much what the type of an object is, but what it can do.

Java, as an counter-example, uses a nominal type system. This means the variables are declared with types and trying to reassign a name to a different type will yield errors. If you declare a name to be a Lamp, you can’t make it a Computer even if they know how to do the same thing.

class Lamp {
  boolean isOn;
  void turnOn() {
    System.out.println("Turning on lamp");
  }
}

class Computer { 
  void turnOn() { 
    System.out.println("Turning on computer");
  }
}

class Example {
public static void main(String[] args) {
    Lamp x = new Lamp();
    x.turnOn();
    x = new Computer();
    x.turnOn();
  }
}

Trying to compile this program raises:

Main.java:18: error: incompatible types: Computer cannot be converted to Lamp

Did you find that useful? Sign up for assessment based coaching! You can find more information about it here.

tags: