Tricky Python questions from interviews
I think every programming language has its tricky moments. They require more concentration and practice for fluent usage. Python is not an exception. And today I want to tell you about its features which can look weird. Some of them I have met in everyday work, and others are only interview experience.
I want to let you think about every question, so I will share a little prompt and explanation before answering. Beware! The first explanation may be intuitive but wrong. To know the proper answer, read till the end of the example.
First example
It's very short.
11 > 0 is True
At first glance, one can think in the following way
11 > 0
- it isTrue
- Let's simplify the expression:
True is True
- OK, the result is
True
But actually this expression evaluates to False
.
There are two more similar expressions.
0 < 0 == 0 # False
1 in range(2) == True # False
Also, it's worth noting that using parentheses changes the result.
(11 > 0) is True # True
(0 < 0) == 0 # True
(1 in range(2)) == True # True
As always, there is no magic in programming. Provided expressions are chained comparisons. Such expression a op1 b op2 c ... y opN z
is equal to a op1 b and b op2 c and ... y opN z
.
There is the right explanation finally. The initial expression 11 > 0 is True
is equivalent to (11 > 0) and (0 is True)
. And it is obviously False
.
Do you still have a question about the parenthesis? Using parenthesis converts chained comparison to usual expression. So we get the simple expression with obvious operations order.
Second example
I think the following feature can't lead you to an error in your code unlike the others, but in my opinion, it's interesting to know about it (to know about).
a = 123
b = 123
a == b
a is b
The double equal sign checks the equality of objects, and 123 is equal to 123. But is
operator checks, that two variables reference the same object. We can see, that a
and b
are different objects, so a is b
returns False.
Actually Python has an optimization for small integers (from -5 to 256 inclusively). These objects are loaded into the interpreter's memory at its start. And we get a little cache. That is why there is only one "object 123" and the expression evaluates to True
.
A similar example for a value greater than 256 looks more familiar.
a = 257
b = 257
a == b # True
a is b # False
Second example. Continuation
Let's try to dig deeper and take a look at the extension of the previous example.
def test():
a = 257
b = 257
print(a is b)
test()
257 is not in the cache, so we must get False.
Here all instructions are inside the body of the function. So the interpreter gets all instructions at once. To understand what is going on let's explore bytecode.
import dis
dis.dis(test)
We will see the following.
2 0 LOAD_CONST 1 (257)
2 STORE_FAST 0 (a)
3 4 LOAD_CONST 1 (257)
6 STORE_FAST 1 (b)
4 8 LOAD_GLOBAL 0 (print)
10 LOAD_FAST 0 (a)
12 LOAD_FAST 1 (b)
14 COMPARE_OP 8 (is)
16 CALL_FUNCTION 1
18 POP_TOP
20 LOAD_CONST 0 (None)
22 RETURN_VALUE
The penultimate column contains arguments for operations. LOAD_CONST
's argument is an index in an array of constants (test.code.co_consts
). Since LOAD_CONST
gets the same index both times, there is only one "object 257" in bytecode.
You see, the interpreter can make such optimizations. It analyzes code preliminary and reuses some constants, i.e. floats too but not tuples.
So, the initial code prints True
.
Third example
This question I met during the first job interview. It is about classes and methods.
class C:
a = lambda self: self.b()
def __init__(self):
self.b = lambda self: None
c = C()
c.a()
Here we should carefully follow the chain of calls distinguishing bound class methods from usual functions.
Calling a class method on its instance c.method()
is the same thing as calling this method on the class itself with the instance as a first argument C.method(c)
.
The next step is inspecting the types of a
and b
attributes.
type(c.a) # <class 'method'>
type(c.b) # <class 'function'>
OK, b
is a normal function. But what is a
now? Like with the regular method definition via the 'def' keyword, a
became the method.
There is a little bit of python docs for you to look at technical details:
When a non-data attribute of an instance is referenced, the instance’s class is searched. If the name denotes a valid class attribute that is a function object, a method object is created by packing (pointers to) the instance object and the function object just found together in an abstract object: this is the method object.
b
is still a usual function because it is assigned to the instance, but not defined at the class level.
OK, it's time to return to the chain of calls. When we make c.a()
we really get C.a(c)
. Here in place of the self
argument, we get instance c
. Next inside the a
method b
function is called. We already know that it is an ordinary function. So there is no 'automatic' passing instance as the first argument. So the b
function doesn't get any arguments. But it requires argument because it is presented as lambda self: None
. Don't pay much attention to the name 'self' of the argument. It's just another edge of the trick.
So the final answer:
Traceback (most recent call last):
File "<input>", line 1, in <module>
File "<input>", line 2, in <lambda>
TypeError: <lambda>() missing 1 required positional argument: 'self'
We get this error because function b is called without the necessary argument.
Fourth example
It is about variables' behavior in closures. It is a common question from interviews, also you can find it on toptal.
def create_multipliers():
return [lambda x : i * x for i in range(5)]
for multiplier in create_multipliers():
print(multiplier(2))
There is nothing special at first glance. create_multipliers
returns a list with five functions (let's call this function multiplier
). Every multiplier
is multiplying its argument at its index in the result list. Expected result:
0
2
4
6
8
But we don't have simple questions here. You should be familiar with closures and nested functions for further analysis. There is a great article about it.
We should correct the initial explanation. Python closures are late binding: the values of variables used in closures are defined at the time the inner function is called. So variable i
is looked up when the multiplier
is called in the fifth row. But at this moment create_multipliers
has finished work and i
has a value 4. So all multipliers have i=4
.
There is the final answer
8
8
8
8
8
Fifth example
And the final example is from my last job interview.
{True : "true", 1 : "one", 1.0 : "double one"}
To predict the result we need two facts:
- any hashable object can be a key in a dictionary
- boolean in Python is an integer's subclass
Next, we can make sure, that all keys have the same hash:
hash(1) # 1
hash(True) # 1
hash(1.0) # 1
The last thing is to deal with repeated keys. As we know, setting a value to existed key rewrites an old value. So we get.
{True: 'double one'}
Conclusion
I have a good tip for you at the end of this article. If understanding any part of your code is exhausting, it's time to rewrite it. I will try to provide you with several examples and inspiration in one of my next articles.
I hope the reading was interesting for you. I'm waiting for your tricky questions in the comments impatiently. Together we can make a great collection that will help us to better understand python and pass interviews easier.