Python Interview Questions and Answers

Python is a versatile language for web development, data analysis, artificial intelligence, scientific computing, and more. Known for its simplicity, readability, and ease of use, it allows developers to write code more quickly and efficiently. Here are the top Python interview questions and answers that cover a wide base of topics associated with Python, such as the use of the assert statement, the difference between Shallow copy vs deep copy, and decorators in Python. Go through these Python interview question-answer sets and land your dream job as a Python Developer, Full Stack engineer, and other top profiles as a fresher, intermediate or expert applicant. Practice these interview questions in Python with answers provided by experts and be fully prepared for your next Python interview. Be smarter with every interview.

  • 4.5 Rating
  • 100 Question(s)
  • 30 Mins of Read
  • 10093 Reader(s)

Beginner

There are a couple of ways to check if a file exists or not. First method is use open() function on a file and see if it raises any exception. If the file is indeed existing, the open() function will be executed correctly. However, if it doesn’t, an exception will be raised.

>>> try:
file=open(filename)
print ('the file exists')
file.close()
except:
print ('file is not existing')

You can also use exists() method of path object defined in os module.

>>> from os import path
>>> path.exists(filename)

Here filename should be a string containing name along with file’s path. Result will be True if file exists otherwise false.

Python library consists of pathlib module. You can also check if file exists or not by using exists() method in this module as follows:

>>> import pathlib
>>> file=pathlib.Path(filename)
>>> file.exists()

Computer languages are generally classified as statically or dynamically typed. Examples of statically typed languages are C/C++, Java, C# etc. Python, along with JavaScript, PHP etc are dynamically typed languages.  

First we have to understand the concept of variable. In C/C++/Java, variable is a user defined convenience name given to a memory location. Moreover, compilers of these languages require prior declaration of name of variable and type of data that can be stored in it before actually assigning any value. Typical variable declaration and assignment statement in C/C++/Java would be as follows:

int x;
x=10;

Here, the variable x is permanently bound to int type. If data of any other type is assigned, compiler error will be reported. Type of variable decides what can be assigned to it and what can’t be assigned.

In Python on the other hand, the object is stored in a randomly chosen memory location, whereas variable is just an identifier or a label referring to it. When we assign 10 to x in Python, integer object 10 is stored in memory and is labelled as x. Python’s built-in type() function  tells us that it stores object of int type.

>>> x=10
>>> type(x)
<class 'int'>

However, we can use same variable x as a label to another object, which may not be of same type. 

>>> x='Hello'
>>> type(x)
<class 'str'>

Python interpreter won’t object if same variable is used to store reference to object of another type. Unlike statically typed languages, type of variable changes dynamically as per the object whose reference has been assigned to it. Python is a dynamically typed language because type of variable depends upon type of object it is referring to.

Depending upon how strict its typing rules are, a programming language is classified as strongly typed or weakly (sometimes called loosely) typed.

Strongly typed language checks the type of a variable before performing an operation on it. If an operation involves incompatible types, compiler/interpreter rejects the operation. In such case, types must be made compatible by using appropriate casting techniques.

A weakly typed language does not enforce type safety strictly. If an operation involves two incompatible types, one of them is coerced into other type by performing implicit casts. PHP and JavaScript are the examples of weakly typed languages.

Python is a strongly typed language because it raises TypeError if two incompatible types are involved in an operation

>>> x=10
>>> y='Python'
>>> z=x+y
Traceback (most recent call last):
 File "<pyshell#2>", line 1, in <module>
   z=x+y
TypeError: unsupported operand type(s) for +: 'int' and 'str'

In above example, attempt to perform addition operation on one integer object and another string object raises TypeError as neither is implicitly converted to other. If however, the integer object “I” converted to string then concatenation is possible.

>>> x=10
>>> y='Python'
>>> z=str(x)+y
>>> z
'10Python'

In a weakly typed language such as JavaScript, the casting is performed implicitly.

<script>
var x = 10;
var y = "Python";
var z=x+y;
document.write(z);
</script>
Output:
10Python

Literal representation of Python string can be done using single, double or triple quotes. Following are the different ways in which a string object can be declared:

>>> s1='Hello Python'
>>> s2="Hello Python"
>>> s3='''Hello Python'''
>>> s4="""Hello Python"""

However, if a string contains any of the escape sequence characters, they are embedded inside the quotation marks. The back-slash character followed by certain alphabets carry a special meaning. Some of the escape characters are:

  • \n : newline – causing following characters printed in next line
  • \t : tab – resulting in a fixed space between characters
  • \\ : prints backslash character itself
  • \b : effects pressing backspace key – removes previous character
>>> s1='Hello\nPython'
>>> sprint (s1)
Traceback (most recent call last):
 File "<pyshell#6>", line 1, in <module>
   sprint (s1)
NameError: name 'sprint' is not defined
>>> print (s1)
Hello
Python
>>> s2='Hello\tPython'
>>> print (s2)
Hello Python

In case of a raw string on the other hand the escape characters don’t get translated while printing. Such raw string is prepared by prefixing ‘r’ or ‘R’ to the leading quotation mark(single, double or triple)

>>> r1=r'Hello\nPython'
>>> print (r1)
Hello\nPython
>>> r2=R"Hello\tPython"
>>> print (r2)
Hello\tPython

Python doesn’t allow any undefined escape sequence to be embedded in the quotation marks of a normal string.

>>> s1='Hello\xPython'
SyntaxError: (unicode error) 'unicodeescape' codec can't decode bytes in position 5-6: truncated \xXX escape

However, in a raw string, any character followed by backslash may be used because anyway it is not going to be interpreted for its meaning!

>>> s1=r'Hello\xPython'
>>> print (s1)
Hello\xPython

Raw strings are used in building regular expressions (called regex). Python’s re module provides functions to process the regular expressions. The re module assigns its own escape characters. Some of them are listed below:

\dMatches any decimal digit
\DMatches any non-digit character
\sMatches any whitespace character
\SMatches any non-whitespace character
\wMatches any alphanumeric character
\WMatches any non-alphanumeric character.
\bboundary between word and non-word and /B is opposite of /b

Some examples of above escape characters are given below:

>>> import re
>>> string='ab12cd34ef'
>>> #find all digits
>>> x = re.findall("\d", string)
>>> print (x)
['1', '2', '3', '4']
>>> #find all non-digit characters
>>> x = re.findall("\D", string)
>>> print (x)
['a', 'b', 'c', 'd', 'e', 'f']

Built-in pow() function has two variations  - with two arguments and three arguments.

The pow() function with two arguments returns first argument raised to second argument. That means pow(x,y) results in x to the power y

>>> pow(10,2)
100

The two-argument form pow(x, y) is equivalent to using the power operator: x**y.

The pow() function can take three arguments. In that case, pow(x,y,z) returns pow(x,y)%z. It returns modulo division of x to the power y and z.

>>> pow(10,2)
100
>>> pow(10,2,3)
1

This is equivalent to10**2%3 which is 100%3 = 1

The pow() function from math module only has a two argument version. Both the arguments are treated as float. Hence the result is always float even if arguments are int.

>>> math.pow(10,2)
100.0
>>> pow(10,2)
100

Because of the floating point representation in the memory, result of math.pow() may be inaccurate for integer arguments. Hence for integers it is advised to use built-in pow() function or ** operator.

Python has a built-in round() function that rounds given number to nearest integer.

>>> round(3.33)
3
>>> round(3.65)
4

Sometimes, you may need a largest integer smaller than given number, or a smallest integer, that is larger than given number. This is where floor() and ceil() functions in math module are used.

As the name suggests, ceil() stands for ceiling. It returns nearest integer greater than given number or numeric expression.

>>> import math
>>> math.ceil(3.33)
4
>>> math.ceil(3.65)
4

The floor() function on the other hand returns integer that is smaller than given number or numeric expression indicating that given number is larger than a certain fraction from the resulting integer.

>>> import math
>>> math.floor(3.33)
3
>>> math.floor(3.65)
3

The floor() function shows a peculiar behaviour when the number or numeric expression is negative. In such case, the result is floored away from 0.

>>> math.floor(-3.65)
-4

A class has two types of attributes – instance attributes and class atributes. Each object of class may have different values for instance attributes. However class attribute is same for each object of the class.

Instance attributes are normally defined and initialized through __init__() method which is executed when object is declared.

>>> class MyClass:
def __init__(self, x,y):
self.x=x
self.y=y
>>> obj1=MyClass(10,20)
>>> obj1.x, obj1.y
(10, 20)
>>> obj2=MyClass(100,200)
>>> obj2.x, obj2.y
(100, 200)

In above example MyClass defines two instance attributes x and y. These attributes are initialized through __init__() method when object is declared. Each object has different values of x and y attributes.

Class attribute is defined outside __init__() method (in fact outside any method of the class). It can be assigned value in definition or from within __init__() or any method. However, it’s value is not different for each object.

>>> class MyClass:
z=10
def __init__(self, x,y):
self.x=x
self.y=y

Here z is not an instance attribute but class attribute defined outside __init__() method. Value of z is same for each object. It is accessed using name of the class. However, accessing with object also shows same value

>>> obj1=MyClass(10,20)
>>> obj2=MyClass(100,200)
>>> MyClass.z
10
>>> obj1.x, obj1.y, obj1.z
(10, 20, 10)
>>> obj2.x, obj2.y, obj2.z
(100, 200, 10

Class has instance methods as well as class methods. An instance method such as __init__() always has one of the arguments as self which refers to calling object. Instance attributes of calling object are accessed through it. A class method is defined with @classmethod decorator and received class name as argument.

>>> class MyClass:
z=10
def __init__(self, x,y):
self.x=x
self.y=y
@classmethod
def classvar(cls):
print (cls.z)

The class variable processed inside class method using class name as well as object as reference. It cannot access or process instance attributes.

>>> obj1=MyClass(10,20)
>>> MyClass.classvar()
10
>>> obj1.classvar()
10

Class can have a static method which is defined with @staticmethod decorator. It takes neither a self nor a cls argument. Class attributes are accessed by providing name of class explicitly.

>>> class MyClass:
z=10
def __init__(self, x,y):
self.x=x
self.y=y
@classmethod
def classvar(cls):
print (cls.z)
@staticmethod
def statval():
print (MyClass.z)
>>> obj1=MyClass(10,20)
>>> MyClass.statval()
10
>>> obj1.statval()
10

Python is an interpreted language. Its source code is not converted to a self-executable as in C/C++ (Although certain OS specific third party utilities make it possible). Hence the standard way to execute a Python script is issuing the following command from command terminal.

$ python hello.py

Here $ (in case of Linux) or c:\> in case of Windows is called command prompt and text in front of it is called command line contains name of Python executable followed by Python script.

From within script, user input is accepted with the help of built-in input() function

x=int(input('enter a number'))
y=int(input('enter another number'))
print ('sum=',x+y)

Note that input() function always reads user input as string. If required it is converted to other data types.

#example.py
x=int(input('enter a number'))
y=int(input('enter another number'))
print ('sum=',x+y)

Above script is run from command line as:

$ python example.py
enter a number10
enter another number20
sum= 30

However, it is also possible to provide data to the script from outside by entering values separated by whitespace character after its name. All the segments of command line (called command line arguments) separated by space are stored inside the script in the form of a special list object as defined in built-in sys module. This special list object is called sys.argv

Following script collects command line arguments and displays the list.

import sys
print ('arguments received', sys.argv)

Run above script in command terminal as:

$ python example.py 10 20
arguments received ['example.py', '10', '20']

Note that first element in sys.argv sys.argv[0] is the name of Python script. Arguments usable inside the script are sysargv[1:] Like the input() function, arguments are always strings. They may have to be converted appropriately.

import sys
x=int(sys.argv[1])
y=int(sys.argv[2])
print ('sum=',x+y)
$python example.py 10 20
sum= 30

The command line can pass variable number of arguments to the script. Usually length sys.argv is checked to verify if desired number of arguments are passed. For above script, only 2 arguments need to be passed. Hence the script should report error if number of arguments is not as per requirement.

import sys
x=int(sys.argv[1])
y=int(sys.argv[2])
print ('sum=',x+y)
$python example.py 10 20
sum= 30
import sys
if len(sys.argv)!=3:
 print ("invalid number of arguments")
else:
 x=int(sys.argv[1])
 y=int(sys.argv[2])
 print ('sum=',x+y)

Output:

$ python example.py 10 20 30
invalid number of arguments
$ python example.py 10
invalid number of arguments
$ python example.py 10 20
sum= 30

A tuple object is a collection data type. It contains one or more items of same or different data types. Tuple is declared by literal representation using parentheses to hold comma separated objects.

>>> tup=(10,'hello',3.25, 2+3j)
Empty tuple is declared by empty parentheses
>>> tup=()

However, single element tuple should have additional comma in the parentheses otherwise it becomes a single object.

>>> tup=(10,)
>>> tup=(10)
>>> type(tup)
<class 'int'>

Using parentheses around comma separatedobjects is optional.

>>> tup=10,20,30
>>> type(tup)
<class 'tuple'>

Assigning multiple objects to tuple is called packing. Unpacking on the other hand is extracting objects in tuple into individual objects. In above example, the tuple object contains three int objects. To unpack, three variables on left hand side of assignment operator are used

>>> x,y,z=tup
>>> x
10
>>> y
20
>>> z
30

Number of variables on left hand side must be equal to length of tuple object.

>>> x,y=tup
Traceback (most recent call last):
 File "<pyshell#12>", line 1, in <module>
   x,y=tup
ValueError: too many values to unpack (expected 2)

However, we can use variable prefixed with * to create list object out of remaining values

>>> x,*y=tup
>>> x
10
>>> y
[20, 30]

In order to construct loop, Python language provides two keywords, while and for. The loop formed with while is a conditional loop. The body of loop keeps on getting executed till the boolean expression in while statement is true.

>>> while expr==True:
statement1
statement2
...
...

The ‘for’ loop on the other hand traverses a collection of objects. Body block inside the ‘for’ loop executes for each object in the collection

>>> for x in collection:
expression1
expression2
...
...

So both types of loops have a pre-decided endpoint. Sometimes though, an early termination of looping is sought by abandoning the remaining iterations of loop. In some other cases, it is required to start next round of iteration before actually completing entire body block of the loop.

Python provides two keywords for these two scenarios. For first, break keyword is used. When encountered inside looping body, program control comes out of the loop, abandoning remaining iterations of current loop. This situation is diagrammatically represented by following flowchart:difference between break and continueUse of break in while loop:

>>> while expr==True:
statement1
if statement2==True:
break
...
...

Use of break in ‘for’ loop:

>>> for x in collection:
expression1
if expression2==True:
break
...
...

On the other hand continue behaves almost opposite to break. Instead of bringing the program flow out of the loop, it is taken to the beginning of loop, although remaining steps in the current iteration are skipped. The flowchart representation of continue is as follows:flowchart representation of continue is as followsUse of continue in while loop:

>>> while expr==True:
statement1
if statement2==True:
continue
...
...

Use of continue in ‘for’ loop:

>>> for x in collection:
expression1
if expression2==True:
continue
...
...

Following code is a simple example of use of both break and continue keywords. It has an infinite loop within which user input is accepted for password. If incorrect password in entered, user is asked to input it again by continue keyword. Correct password terminates infinite loop by break

#example.py

while True:
   pw=input('enter password:')
   if pw!='abcd':
       continue
   if pw=='abcd':
       break

Output:

enter password:11
enter password:asd
enter password:abcd

Unlike C/C++, a variable in Python is just a label to object created in memory. Hence when it is assigned to another variable, it doesn’t copy the object, but rather it acts as another reference to the same object. The built-in id() function brings out this behaviour.

>>> num1=[10,20,30] 
>>> num2=num1 
>>> num1 
[10, 20, 30] 
>>> num2 
[10, 20, 30] 
>>> id(num1), id(num2) 
(140102619204168, 140102619204168)

The id() function returns the location of object in memory. Since id() for both the list objects is the same, both refer to the same object in memory.

The num2 is called as a shallow copy of num1. Since both refer to the same object, any change in either will reflect in the other object.

>>> num1=[10,20,30]
>>> num2=num1
>>> num2[2]=5
>>> num1
[10, 20, 5]
>>> num2
[10, 20, 5]

In the above example, the item at index no. 2 of num2 is changed. We see this change appearing in both.

A deep copy creates an entirely new object and copies of nested objects too are recursively added to it.

The copy module of the Python standard library provides two methods:

  • copy.copy() – creates a shallow copy
  • copy.deepcopy() – creates a deep copy

A shallow copy creates a new object and stores the reference of the original elements but doesn't create a copy of nested objects. It just copies the reference of nested objects. As a result, the copy process is not recursive.

>>> import copy
>>> num1=[[1,2,3],['a','b','c']]
>>> num2=copy.copy(num1)
>>> id(num1),id(num2)
(140102579677384, 140102579676872)
>>> id(num1[0]), id(num2[0])
(140102579676936, 140102579676936)
>>> num1[0][1],id(num1[0][1])
(2, 94504757566016)
>>> num2[0][1],id(num2[0][1])
(2, 94504757566016)

Here num1 is a nested list and num2 is its shallow copy. Ids of num1 and num2 are different, but num2 doesn’t hold physical copies of internal elements of num1, but holds just the ids.

As a result, if we try to modify an element in nested element, its effect will be seen in both lists.

>>> num2[0][2]=100
>>> num1
[[1, 2, 100], ['a', 'b', 'c']]
>>> num2
[[1, 2, 100], ['a', 'b', 'c']]

However, if we append a new element to one list, it will not reflect in the other list.

>>> num2.append('Hello')
>>> num2
[[1, 2, 100], ['a', 'b', 'c'], 'Hello']
>>> num1
[[1, 2, 100], ['a', 'b', 'c']]

A deep copy on the other hand, creates a new object and recursively adds the copies of nested objects too, present in the original elements.

In following example, num2 is a deep copy of num1. Now if we change any element of the inner list, this will not show in the other list.

>>> import copy
>>> num1=[[1,2,3],['a','b','c']]
>>> num2=copy.deepcopy(num1)
>>> id(num1),id(num2)
(140102641055368, 140102579678536)
>>> num2[0][2]=100
>>> num2
[[1, 2, 100], ['a', 'b', 'c']]
>>> num1 #not changed
[[1, 2, 3], ['a', 'b', 'c']]

After you install Python software on your computer, it is desired that you add the installation directory in your operating system’s PATH environment variable. Usually the installer program does this action by default. Otherwise you have to perform this from the control panel.

In addition to updating PATH, certain other Python-specific environment variables should be set up. These environment variables are as follows:

  • PYTHONPATH

This environment variable plays a role similar to PATH. It tells the Python interpreter where to locate the module files imported into a program. It includes the Python source library directory and  other directories containing Python source code. PYTHONPATH is usually preset by the Python installer.

  • PYTHONSTARTUP

It denotes the path of an initialization file containing Python source code. This file is executed every time the Python interpreter starts. It is named as .pythonrc.py and it contains commands that load utilities or modify PYTHONPATH.

  • PYTHONCASEOK

In Windows, it enables Python to find the first case-insensitive match in an import statement. Set this variable to any value to activate it.

  • PYTHONHOME

It is an alternative module search path. It is usually embedded in the PYTHONSTARTUP or PYTHONPATH directories to make switching module libraries easy. It may refer to zipfiles containing pure Python modules (in either source or compiled form)

  • PYTHONDEBUG

If this is set to a non-empty string it turns on parser debugging output.

  • PYTHONINSPECT

If this is set to a non-empty string it is equivalent to specifying the -i option. When a script is passed as the first argument or the -c option is used, enter interactive mode after executing the script or the command.

  • PYTHONVERBOSE

If this is set to a non-empty string it is equivalent to specifying the -v option causes printing a message each time a module is initialized, showing the place from which it is loaded.

  • PYTHONEXECUTABLE

If this environment variable is set, sys.argv[0] will be set to its value instead of the value got through the C runtime. Only works on Mac OS X.

Python’s sequence data types (list, tuple or string) are indexed collection of items not necessarily of the same type. Index starts from 0 – as in C/C++ or Java array (although these languages insist that an array is a collection of similar data types). Again like C/C++, any element in sequence can be accessed by its index.

>>> num=[10,20,25,15,40,60,23,90,50,80]
>>> num[3]
15

However, C/C++/Java don’t allow negative numbers as index. Java throws NegativeArraySizeException. C/C++ produces undefined behaviour. However, Python accepts negative index for sequences and starts counting index from end. Consequently, index -1 returns the last element in the sequence. 

>>> num=[10,20,25,15,40,60,23,90,50,80]
>>> num[-1]
80
>>> num[-10]
10

However, using negative index in slice notation exhibits some peculiar behaviour. The slice operator accepts two numbers as operands. First is index of the beginning element of the slice and second is the index of the element after slice. Num[2:5] returns elements with index 2, 3 and 4

>>> num=[10,20,25,15,40,60,23,90,50,80]
>>> num[2:5]
[25, 15, 40]

Note that default value of first operand is 0 and second is length+1

>>> num=[10,20,25,15,40,60,23,90,50,80]
>>> num[:3]
[10, 20, 25]
>>> num[0:3]
[10, 20, 25]
>>> num[8:]
[50, 80]
>>> num[8:10]
[50, 80]

Hence using a negative number as the first or second operand gives the results accordingly. For example using -3 as the first operand and ignoring the second returns the last three items. However, using  -1 as second operand results in leaving out the last element

>>> num[-3:]
[90, 50, 80]
>>> num[-3:-1]
[90, 50]

Using -1 as first operand without second operand is equivalent to indexing with -1

>>> num[-1:]
[80]
>>> num[-1]
80

Yes, the list and tuple objects are very similar in nature. Both are sequence data types being an ordered collection of items, not necessarily of the same type. However, there are a couple of subtle differences between the two.

First and foremost, a tuple is an immutable object while a list is mutable. Which simply means that once created, a tuple cannot be modified in place (insert/delete/update operations cannot be performed) and the list can be modified dynamically. Hence, if a collection is unlikely to be modified during the course of the program, a tuple should be used. For example price of items.

>>> quantity=[34,56,45,90,60]
>>> prices=(35.50, 299,50, 1.55, 25.00,99)

Although both objects can contain items of different types, conventionally a list is used generally to hold similar objects – similar to an array in C/C++ or Java. Python tuple is preferred to set up a collection of heterogenous objects – similar to a struct in C. Consequently, you would use a list to store marks obtained by students, and a tuple to store coordinates of a point in cartesian system.

>>> marks=[342,516,245,290,460]
>>> x=(10,20)

Internally too, Python uses tuple a lot for a number of purposes. For example, if a function returns more than one value, it is treated as a tuple.

>>> def testfunction():
x=10
y=20
return x,y
>>> t=testfunction()
>>> t
(10, 20)
>>> type(t)
<class 'tuple'>

Similarly if a function is capable of receiving multiple arguments in the form of *args, it is parsed as a tuple.

>>> def testfunction(*args):
print (args)
print (type(args))
>>> testfunction(1,2,3)
(1, 2, 3)
<class 'tuple'>

Python uses tuple to store many built-in data structures. For example time data is stored as a tuple.

>>> import time
>>> time.localtime()
time.struct_time(tm_year=2019, tm_mon=5, tm_mday=28, tm_hour=9, tm_min=20, tm_sec=0, tm_wday=1, tm_yday=148, tm_isdst=0)

Because of its immutable nature, a tuple can be used as a key in a dictionary, whereas a list can’t be used as key. Also, if you want to iterate over a large collection of items, tuple proves to be faster than a list.

An exception is a type of run time error reported by a Python interpreter when it encounters a situation that is not easy to handle while executing a certain statement. Python’s library has a number of built-in exceptions defined in it. Both TypeError and valueError are built-in exceptions and it may at times be confusing to understand the reasoning behind their usage.

For example, int(‘hello’) raises ValueError when one would expect TypeError. A closer look at the documentation of these exceptions would clear the difference.

>>> int('hello')
Traceback (most recent call last):
 File "<pyshell#26>", line 1, in <module>
   int('hello')
ValueError: invalid literal for int() with base 10: 'hello'

Passing arguments of the wrong type (e.g. passing a list when an int is expected) should result in a TypeError which is also raised when an operation or function is applied to an object of inappropriate type.

Passing arguments with the wrong value (e.g. a number outside expected boundaries) should result in a ValueError. It is also raised when an operation or function receives an argument that has the right type but an inappropriate value.

As far as the above case is concerned the int() function can accept a string argument so passing ‘hello’ is not valid hence it is not a case of TypeError. Alternate signature of int() function with two arguments receives string and base of number system

int(string,base)
>>> int('11', base=2)
3

Second argument if ignored defaults to 10

>>> int('11')
11

If you give a string which is of inappropriate ‘value’ such as ‘hello’ which can’t be converted to a decimal integer, ValueError is raised.

The built-in functions make it possible to retrieve the value of specified attribute of any object (getattr) and add an attribute to given object.

Following is definition of a test class without any attributes.

>>> class test:
Pass

Every Python class is a subclass of ‘object’ class. Hence it inherits attributes of the class which can be listed by dir() function:

>>> dir(test)
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__']

The setattr() function can be used to add a class level attribute to our test class.

>>> setattr(test,'name','Ravi')

To retrieve its value use getattr() function.

>>> getattr(test,'name')
'Ravi'

You can of course access the value by dot (.) notation

>>> test.name
'Ravi'

Similarly, these functions add/retrieve attributes to/from an object of any class. Let us declare an instance of test class and add age attribute to it.

>>> x=test()
>>> setattr(x,'age',21)

Obviously, ‘age’ becomes the instance attribute and not class attribute. To retrieve, use getattr() or ‘.’ operator.

>>> getattr(x,'age')
21
>>> x.age
21

Use dir() function to verify that ‘age’ attribute is available.

>>> dir(x)
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'age', 'name']

Incidentally, Python also provides ‘hasattr()’ function to check if an object possesses the given attribute.

>>> hasattr(x,'age')
True
>>> hasattr(test, 'age')
False

These two functions have exactly the opposite behaviour to each other. The chr() function returns a string representing a character to an integer argument which is a Unicode code point.

>>> chr(65)
'A'
>>> chr(51)
'3'
>>> chr(546)
'Ȣ'
>>> chr(8364)
'€'

The chr() function returns corresponding characters for integers between 0 to 1114111 (0x110000). For a number outside this range, Python raises ValuError.

>>> chr(-10)
   chr(-10)
ValueError: chr() arg not in range(0x110000)

On the other hand ord() function returns an integer corresponding to Unicode code point of a character.

>>> ord('A')
65
>>> ord('a')
97
>>> ord('\u0222')
546
>>> ord('€')
8364

Note that ord() function accepts a string of only one character otherwise it raises TypeError as shown below:

>>> ord('aaa')
   ord('aaa')
TypeError: ord() expected a character, but string of length 3 found

These two functions are the inverse of each other as can be seen from the following

>>> ord(chr(65))
65
>>> chr(ord('A'))
'A'

This is a common yet one of the most important Python coding interview questions and answers for experienced professionals, don't miss this one.

Python’s built-in function slice() returns Slice object. It can be used to extract slice from a sequence object list, tuple or string) or any other object that implements sequence protocol supporting _getitem__() and __len__() methods.

The slice() function is in fact the constructor in slice class and accepts start, stop and step parameters similar to range object. The start and step parameters are optional. Their default value is 0 and 1 respectively.

Following statement declares a slice object.

>>> obj=slice(1,6,2)

We use this object to extract a slice from a list of numbers as follows:

>>> numbers=[7,56,45,21,11,90,76,55,77,10]
>>> numbers[object]
Traceback (most recent call last):
 File "<stdin>", line 1, in <module>
TypeError: list indices must be integers or slices, not type
>>> numbers[obj]
[56, 21, 90]

You can see that elements starting from index 1 upto 5 with step 2 are sliced away from the original list.

Slice can also receive negative index.

>>> obj=slice(-1,3,-1)
>>> numbers[obj]
[10, 77, 55, 76, 90, 11]

In general, view gives a representation of a particular object in a certain direction or perspective. In Python’s dictionary, an object has items(), keys() and values() methods that return view objects. These are objects of dict_items, dict_keys and dict_values classes respectively. All of them are view types.

>>> dct=dct={'1':'one', '2':'two', '3':'three'}
>>> v1=dct.items()
>>> class(v1)
SyntaxError: invalid syntax
>>> type(v1)
<class 'dict_items'>
>>> v2=dct.keys()
>>> type(v2)
<class 'dict_keys'>
>>> v3=dct.values()
>>> type(v3)
<class 'dict_values'>

These view objects can be cast to list or tuples

>>> list(v1)
[('1', 'one'), ('2', 'two'), ('3', 'three')]
>>> tuple(v1)
(('1', 'one'), ('2', 'two'), ('3', 'three'))
>>> list(v2)
['1', '2', '3']
>>> list(v3)
['one', 'two', 'three']

It is possible to run a for loop over these views as they can return an iterator object.

>>> #using for loop over items() view
>>> for i in v1:
print (i)
('1', 'one')
('2', 'two')
('3', 'three')

As you can see each item in the view is a tuple of key–value pair. We can unpack each tuple in separate k-v objects

>>> for k,v in v1:
print ('key:{} value:{}'.format(k,v))
key:1 value:one
key:2 value:two
key:3 value:three

A view object also supports membership operators.

>>> '2' in v2
True
>>> 'ten' in v3
False

The most important feature of view objects is that they are dynamically refreshed as any add/delete/modify operation is performed on an underlying dictionary object. As a result, we need not constructs views again.

>>> #update dictionary
>>> dct.update({'4':'four','2':'twenty'})
>>> dct
{'1': 'one', '2': 'twenty', '3': 'three', '4': 'four'}
>>> #automatically refreshed views
>>> v1
dict_items([('1', 'one'), ('2', 'twenty'), ('3', 'three'), ('4', 'four')])
>>> v2
dict_keys(['1', '2', '3', '4'])
>>> v3
dict_values(['one', 'twenty', 'three', 'four'])

Python identifiers with leading and/or single and/or double underscore characters i.e. _ or __ are used for giving them a peculiar meaning.

Unlike C++ or Java, Python doesn’t restrict access to instance attributes of a class. In C++ and Java, access is controlled by public, private or protected keywords. These keywords or their equivalents are not defined in Python. All resources of class are public by default.

However, Python does allow you to indicate that a variable is private by prefixing the name of the variable by double underscore __. In the following example, Student class has ‘name’ as private variable.

>>> class Student:
def __init__(self):
self.__name='Amar'

Here __name acts as a private attribute of object of Student class. If we try to access its value from outside the class, Python raises AttributeError.

>>> x=Student()
>>> x.__name
Traceback (most recent call last):
  File "<pyshell#37>", line 1, in <module>
    x.__name
AttributeError: 'Student' object has no attribute '__name'

However, Python doesn’t restrict access altogether. Python only internally renames such attribute in the form _classname__attribute. Here __name is renamed as _Student__name. The mechanism is called name mangling

>>> x._Student__name
'Amar'

An attribute with single underscore prefix emulates the behaviour of a protected data member indicating that it is available only to subclass. 

Python uses attributes and methods with double underscore character before and after the name to form magic methods. Examples are __init__() and __dir__() etc.

A staple in Python basic interview questions, be prepared to answer this one using your hands-on experience. Here is an standard response -

Python is a completely object oriented language. It has ‘class’ keyword using which a new user defined class can be created. 

>>> class Myclass:
Pass

In object oriented programming, an object of a class is initialized by automatically invoking a certain method when it is declared. In C++ and Java, a method of the same name as the class acts as a constructor. However, Python uses a special method named as __init__() as a constructor.

>>> class Myclass:
def __init__(self):
print ("new object initialized")
>>> x=Myclass()
new object initialized

Here x is declared as a new object of Myclass and __init__() method is called automatically. 

The constructor method always has one mandatory argument carrying reference of the object that is calling. It is conventionally denoted by ‘self’ identifier although you are free to use any other. Instance attributes are generally initialized within the __init__() function.

>>> class Myclass:
def __init__(self):
self.name='Mack'
self.age=25
>>> x=Myclass()
>>> x.name
'Mack'
>>> x.age
25

In addition to self, __init__() method can have other arguments using which instance attributes can be initialized by user specified data.

>>> class Myclass:
def __init__(self, name, age):
self.name=name
self.age=age
>>> x=Myclass('Chris',20)
>>> x.name
'Chris'
>>> x.age
20

However, Python class is not allowed to have overloaded constructor as in C++ or Java. Instead you can use default values to arguments of __init__() method.

>>> class Myclass:
def __init__(self, name='Mack', age=20):
self.name=name
self.age=age
>>> x=Myclass()
>>> x.name
'Mack'
>>> x.age
20
>>> y=Myclass('Nancy', 18)
>>> y.name
'Nancy'
>>> y.age
18

In object oriented programming, destructor is a method of class which will be automatically called when its object is no longer in use. Python provides a special (magic) method named __del__() to define destructor. Although destructor is not really required in Python class because Python uses the principle of automatic garbage collection, you can still provide __del__() method for explicit deletion of object memory.

>>> class Myclass:
def __init__(self):
print ('object initialized')
def __del__(self):
print ('object destroyed')
>>> x=Myclass()
object initialized
>>> del x
object destroyed

In the above example , ‘del’ keyword in Python is invoked to delete a certain object. As __del__() method is exclusively provided, it is being called.

Python uses reference counting method for garbage collection. As per this method, an object is eligible for garbage collection if its reference count becomes zero.

In the following example, first obj becomes object of Myclass so its reference count is 1. But when we assign another object to obj, reference count of Myclass() becomes 0 and hence it is collected, thereby calling __del__() method inside the class.

>>> class Myclass:
def __init__(self):
print ('object initialized')
def __del__(self):
print ('object destroyed')
>>> obj=Myclass()
object initialized
>>> obj=10
object destroyed

Python uses if keyword to implement decision control. Python’s syntax for executing block conditionally is as below:

if expr==True:
    stmt1
    stmt2
    ..
stmtN

Any Boolean expression evaluating to True or False appears after if keyword. Use : symbol and press Enter after the expression to start a block with increased indent. One or more statements written with the same level of indent will be executed if the Boolean expression evaluates to True. To end the block, press backspace to de-dent. Subsequent statements after the block will be executed if the expression is false or after the block if expression is true.

Along with if statement, else clause can also be optionally used to define alternate block of statements to be executed if the Boolean expression (in if statement) is not true. Following code skeleton shows how else block is used.

if expr==True:
    stmt1
    stmt2
    ..
else:
    stmt3
    stmt4
    ..
stmtN

There may be situations where cascaded or nested conditional statements are required to be used as shown in the following skeleton:

if expr1==True:
    stmt1
    stmt2
    ..
else:
    if expr2==True:
        stmt3
        stmt4
        ...
    else:
        if expr3==True:
            stmt5
            stmt6
            ..
stmtN

As you can see, the indentation level of each subsequent block goes on increasing because if block starts after empty else. To avoid these clumsy indentations, we can combine empty else and subsequent if by elif keyword. General syntax of if – elif – else usage is as below:

if expr1==True:
    #Block To Be Executed If expr1 is True
elif expr2==True:
     #Block To Be Executed If expr1 is false and expr2 is true
elif expr3==True:
     #Block To Be Executed If expr2 is false and expr3 is true
elif expr4==True:
     #Block To Be Executed If expr3 is false and expr4 is true
else: 
    #Block To Be Executed If all preceding expressions false

In this code, one if block, followed by one or more elif blocks and one else block at the end will appear. Boolean expression in front of elif is evaluated if previous expression fails. Last else block is run only when all previous expressions turn out to be not true. Importantly all blocks have the same level of indentation.

Here is a simple example to demonstrate the use of elif keyword. The following program computes discount at different rates if the amount is in different slabs.

price=int(input("enter price"))
qty=int(input("enter quantity"))
amt=price*qty
if amt>10000:
    print ("10% discount applicable")
    discount=amt*10/100
    amt=amt-discount
elif amt>5000:
    print ("5% discount applicable")
    discount=amt*5/100
    amt=amt-discount
elif amt>2000:
    print ("2% discount applicable")
    discount=amt*2/100
    amt=amt-discount
elif amt>1000:
    print ("1% discount applicable")
    discount=amt/100
    amt=amt-discount
else:
    print ("no discount applicable")
print ("amount payable:",amt)

Output:

enter price1000
enter quantity11
10% discount applicable
amount payable: 9900.0
========================
enter price1000
enter quantity6
5% discount applicable
amount payable: 5700.0
========================
enter price1000
enter quantity4
2% discount applicable
amount payable: 3920.0
======================== 
enter price500
enter quantity3
1% discount applicable
amount payable: 1485.0
======================== 
enter price250
enter quantity3
no discount applicable
amount payable: 750

Syntax of while loop:

while expr==True:
    stmt1
    stmt2
    ..
    ..
stmtN

The block of statements with uniform indent is repeatedly executed till the expression in while statement remains true. Subsequent lines in the code will be executed after loop stops when the expression ceases to be true.

Syntax of for loop:

for x in interable:
    stmt1
    stmt2
    ..
stmtN

The looping block is executed for each item in the iterable object like list, or tuple or string. These objects have an in-built iterator that fetches one item at a time. As the items in iterable get exhausted, the loop stops and subsequent statements are executed.

The construction of while loop requires some mechanism by which the logical expression becomes false at some point or the other. If not, it will constitute an infinite loop. The usual practice is to keep count of the number of repetitions.

Both types of loops allow use of the else block at the end. The else block will be executed when stipulated iterations are over.

for x in "hello":
    print (x)
else:
    print ("loop over")
print ("end")

Both types of loops can be nested. When a loop is placed inside the other, these loops are called nested loops.

#nested while loop
x=0
while x<3:
    x=x+1
    y=0
    while y<3:
        y=y+1
        print (x,y)
#nested for loop
for x in range(1,4):
    for y in range(1,4):
        print (x,y)

Both versions of nested loops print the following output:

1 1
1 2
1 3
2 1
2 2
2 3
3 1
3 2
3 3
1 1
1 2
1 3
2 1
2 2
2 3
3 1
3 2
3 3

A staple in senior Python interview questions with answers, be prepared to answer this one using your hands-on experience. This is also one of the top interview questions to ask a Python developer.

Python interpreter is invoked from command terminal of operating system (Windows or Linux). Two most common ways are starting interactive console and running script

For interactive console
$ python
Python 3.6.6 |Anaconda custom (64-bit)| (default, Oct  9 2018, 12:34:16) 
[GCC 7.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>>

For scripting mode

$ python hello.py

In addition to these common ways, Python command line can have different options. They are listed below:

-c cmd : program passed in as string. Interpreter executes the Python code in string which can be one or more statements separated by newlines.

$ python -c "print ('hello')"
Hello

-m mod : run library module as a script. the named module and execute its contents as the __main__ module. The following command creates a new virtual environment

$ python -m venv myenv

-i: inspect interactively after running script or code with -c option. The interactive prompt appears after the output. This can be useful to inspect global variables or a stack trace when a script raises an exception.

$ python -i -c "print ('hello')"
hello
$ python -i temp.py
Hello world
>>>

Other options are:

  • B: don't write .pyc files on import; also PYTHONDONTWRITEBYTECODE=x
  • d: debug output from parser; also PYTHONDEBUG=x
  • E: ignore PYTHON* environment variables (such as PYTHONPATH)
  • h: print this help message and exit (also --help)
  • q: don't print version and copyright messages on interactive startup
  • v: verbose (trace import statements) can be supplied multiple times to increase verbosity
  • V: print the Python version number and exit (also --version)
  • x: skip first line of source, allowing use of non-Unix forms of #!cmd shebang
  • file: program read from script file

Python’s object hierarchy has a built-in zip object. It is an iterator of tuples containing items on the same index stored in one or more iterables. The zip object is returned by zip() function. With no arguments, it returns an empty iterator.

>>> a=zip()
>>> type(a)
<class 'zip'>
>>> list(a)
[]

When one iterable is provided, zip() function returns an iterator of one element tuples.

>>> a=zip('abcd')
>>> list(a)
[('a',), ('b',), ('c',), ('d',)]

When the function has multiple iterables as arguments, each tuple in zip object contains items at similar index. Following snippet gives two lists to zip() function and the result is an iterator of two element tuples.

>>> l1=['pen', 'computer', 'book']
>>> l2=[100,20000,500]
>>> a=zip(l1,l2)
>>> list(a)
[('pen', 100), ('computer', 20000), ('book', 500)]

Iterable arguments may be of different length. In that case the zip iterator stops when the shortest input iterable is exhausted.

>>> string="HelloWorld"
>>> lst=list(range(6))
>>> tup=(10,20,30,40,50)
>>> a=zip(string, lst, tup)
>>> list(a)
[('H', 0, 10), ('e', 1, 20), ('l', 2, 30), ('l', 3, 40), ('o', 4, 50)]

The zip object can be unpacked in separate iterables by prefixing * to zipped object.

>>> a=['x', 'y', 'z']
>>> b=[10,20,30]
>>> c=zip(a,b)
>>> result=list(c)
>>> result
[('x', 10), ('y', 20), ('z', 30)]
>>> x,y=zip(*result)
>>> x
('x', 'y', 'z')
>>> y
(10, 20, 30)

These two built-in functions execute logical or / and operators successively on each of the items in an iterable such as list, tuple or string.

The all() function returns True only if all items in the iterable return true. For empty iterable, all() function returns true. Another feature of all() function is that the evaluation stops at the first instance of returning false, abandoning the remaining items in the sequence.

On the other hand any() function returns True even if one item in the sequence returns true. Consequently, any() function returns false only if all items evaluate to false (or it is empty).

To check the behaviour of these functions, let us define a lambda function and subject each item in the list to it. The lambda function itself returns true if the number argument is even.

>>> iseven=lambda x:x%2==0
>>> iseven(100)
True
>>> iseven(101)
False
>>> lst=[50,32,45,90,60]
>>> all(iseven(x) for x in lst)
False
>>> any(iseven(x) for x in lst)
True

A complex number is made up of a real and an imaginary component. In mathematics, an imaginary number is defined as the square root of (-1) denoted by j. The imaginary component is multiplied by j. Python’s built-in complex object is represented by the following literal expression.

>>> x=3+2j

Python’s built-in complex() function also returns complex object using to float objects, first as a real part and second as an imaginary component.

>>> x=complex(3,2)
>>> x
(3+2j)

Addition and subtraction of complex numbers is the straightforward addition of the respective real and imaginary components.

The process of multiplying these two complex numbers is very similar to multiplying two binomials. Multiply each term in the first number by each term in the second number.

a=6+4j
b=3+2j
c=a*b
c=(6+4j)*(3+2j)
c=(18+12j+12j+8*-1)
c=10+24j

Verify this result with Python interpreter

>>> a=6+4j
>>> b=3+2j
>>> a*b
(10+24j) 

To obtain division of two complex numbers, multiply both sides by the conjugate of the denominator, which is a number with the same real part and the opposite imaginary part.

a=6+4j
b=3+2j
c=a/b
c=(6+4j)*(3-2j)/(3+2j)(3-2j)
c=(18-12j+12j-8*-1)/(9-6j+6j-4*-1)
c=26/13
c=2+0j

Verify this with Python interpreter

>>> a=6+4j
>>> b=3+2j
>>> a/b
(2+0j)

This, along with other Python advanced interview questions, is a regular feature in Python interviews. Be ready to tackle it with the approach mentioned.

Python’s exception handling technique involves four keywords: try, except, else and finally. The try block is essentially a script you want to check if it contains any runtime error or exception. If it does, the exception is raised and except block is executed. 

The else block is optional and will get run only if there is no exception in try block. Similarly, finally is an optional clause meant to perform clean up operations to be undertaken under all circumstances, whether try block encounters an exception or not. A finally clause is always executed before leaving the try statement, whether an exception has occurred or not. 

>>> try:
raise TypeError
finally:
print ("end of exception handling")
end of exception handling
Traceback (most recent call last):
  File "<pyshell#28>", line 2, in <module>
    raise TypeError
TypeError

When an unhandled exception occurs in the try block (or it has occurred in an except or else clause), it is re-raised after the finally block executes. The finally clause is also executed when either try, except or else block is left via a break, continue or return statement.

>>> def division():
try:
a=int(input("first number"))
b=int(input("second number"))
c=a/b
except ZeroDivisionError:
print ('divide by 0 not allowed')
else:
print ('division:',c)
finally:
print ('end of exception handling')

Let us call above function number of times to see how finally block works:

>>> division()
first number10
second number2
division: 5.0
end of exception handling
>>> division()
first number10
second number0
divide by 0 not allowed
end of exception handling
>>> division()
first number10
second numbertwo
end of exception handling
Traceback (most recent call last):
  File "<pyshell#47>", line 1, in <module>
    division()
  File "<pyshell#42>", line 4, in division
    b=int(input("second number"))
ValueError: invalid literal for int() with base 10: 'two'

Note how exception is re-raised after the finally clause is executed. The finally block is typically used for releasing external resources such as file whether or not it was successfully used in previous clauses.

The hash() functions hash value of given object. The object must be immutable. Hash value is an integer specific to the object. These hash values are used during dictionary lookup. 

Two objects may hash to the same hash value. This is called Hash collision. This means that if two objects have the same hash code, they do not necessarily have the same value.

>>> #hash of a number
>>> hash(100)
100
>>> hash(100.00)
100
>>> hash(1E2)
100
>>> hash(100+0j)
100

You can see that hash value of an object of same numeric value is the same.

>>> #hash of string
>>> hash("hello")
-1081986780589020980
>>> #hash of tuple
>>> hash((1,2,3))
2528502973977326415
>>> #hash of list
>>> hash([1,2,3])
Traceback (most recent call last):
  File "<pyshell#23>", line 1, in <module>
    hash([1,2,3])
TypeError: unhashable type: 'list'

Last case of hash value of list results in TypeError as list is not immutable /not hashable.

The hash() function internally calls __hash__() magic method. To obtain a hash value of object of user defined class, the class itself should provide overridden implementation of __hash__()

class User:
    def __init__(self, age, name):
        self.age = age
        self.name = name
    def __hash__(self):
        return hash((self.age, self.name))
x = User(23, 'Shubham')
print("The hash is: %d" % hash(x))

Output:

The hash is: -1916965497958416005

Intermediate

The built-in next() function calls __next__() method of iterator object to retrieve next item in it. So the next question would be – what is an iterator?

An object representing stream of data is an iterator. One element from the stream is retrieved at a time. It follows iterator protocol which requires it to support  __iter__() and __next__() methods. Python’s built-in method iter() implements __iter__() method and next() implements __next__() method. It receives an iterable and returns iterator object.

Python uses iterators  implicitly while working with collection data types such as list, tuple or string. That's why these data types are called iterables. We normally use ‘for’ loop to iterate through an iterable as follows:

>>> numbers=[10,20,30,40,50]
>>> for num in numbers:
print (num)
10
20
30
40
50

We can use iter() function to obtain iterator object underlying any iterable.

>>> iter('Hello World')
<str_iterator object at 0x7f8c11ae2eb8>
>>> iter([1,2,3,4])
<list_iterator object at 0x7f8c11ae2208>
>>> iter((1,2,3,4))
<tuple_iterator object at 0x7f8c11ae2cc0>
>>> iter({1:11,2:22,3:33})
<dict_keyiterator object at 0x7f8c157b9ea8>

Iterator object has __next__() method. Every time it is called, it returns next element in iterator stream. When the stream gets exhausted, StopIteration error is raised.

>>> numbers=[10,20,30,40,50]
>>> it=iter(numbers)
>>> it.__next__()
10
>>> next(it)
20
>>> it.__next__()
30
>>> it.__next__()
40
>>> next(it)
50
>>> it.__next__()
Traceback (most recent call last):
 File "<pyshell#16>", line 1, in <module>
   it.__next__()
StopIteration

Remember that next() function implements __next__() method. Hence next(it) is equivalent to it.__next__()

When we use a for/while loop with any iterable, it actually implements iterator protocol as follows:

>>> while True:
try:
num=it.__next__()
print (num)
except StopIteration:
break
10
20
30
40
50

A disk file, memory buffer or network stream also acts as an iterator object. Following example shows that each line from a file can be printed using next() function.

>>> file=open('csvexample.py')
>>> while True:
tr
KeyboardInterrupt
>>> while True:
try:
line=file.__next__()
print (line)
except StopIteration:
break

This is one of the most frequently asked Python interview questions for freshers in recent times. Make sure you get this one correct.

List comprehension technique is similar to mathematical set builder notation. A classical for/while loop is generally used to traverse and process each item in an iterable. List comprehension is considerably more efficient than processing a list by ‘for’ loop. It is a very concise mechanism of creating new list by performing a certain process on each item of existing list.

Suppose we want to compute square of each number in a list and store squares in another list object. We can do it by a ‘for’ loop as shown below:

squares=[]
for num in range(6):
       squares.append(pow(num,2))
print (squares)

The squares list object is displayed as follows:

[0, 1, 4, 9, 16, 25]

Same result is achieved by list comprehension technique much more efficiently. List comprehension statement uses following syntax:

newlist = [x for x in sequence]

We use above format to construct list of squares using list comprehension.

>>> numbers=[6,12,8,5,10]
>>> squares = [x**2 for x in numbers]
>>> squares
[36, 144, 64, 25, 100]

We can even generate a dictionary or tuple object as a result of list comprehension.

>>> [{x:x*10} for x in range(1,6)]
[{1: 10}, {2: 20}, {3: 30}, {4: 40}, {5: 50}]

Nested loops can also be used in a list comprehension expression. To obtain list of all combinations of items from two lists (Cartesian product):

>>> [{x:y} for x in range(1,3) for y in (11,22,33)]
[{1: 11}, {1: 22}, {1: 33}, {2: 11}, {2: 22}, {2: 33}]

The resulting list stores all combinations of one number from each list

We can even have if condition in list comprehension. Following statement will result in list of all non-vowel alphabets in a string.

>>> odd=[x for x in range(1,11) if x%2==1]
>>> odd
[1, 3, 5, 7, 9]

Python’s yield keyword is typically used along with a generator. A generator is a special type of function that returns an iterator object returning stream of values. Apparently it looks like a normal function , but it doesn’t return a single value. Just as we use return keyword in a function, in a generator yield statement is used.

A normal function, when called, executes all statements in it and returns back to calling environment. The generator is called just like a normal function. However, it pauses on encountering yield keyword, returning the yielded value to calling environment. Because execution of generator is paused, its local variables and their states are saved internally. It resumes when __next__() method of iterator is called. The function finally terminates when __next__() or next() raises StopIteration.

In following example function mygenerator() acts as a generator. It yields one character at a time from the string sequence successively on every call of next()

>>> def mygenerator(string):
for ch in string:
print ("yielding", ch)
yield ch

The generator is called which builds the iterator object

>>> it=mygenerator("Hello World")

Each character from string is pushed in iterator every time next() function is called.

>>> while True:
try:
print ("yielded character", next(it))
except StopIteration:
print ("end of iterator")
break

Output:

yielding H
yielded character H
yielding e
yielded character e
yielding l
yielded character l
yielding l
yielded character l
yielding o
yielded character o
yielding  
yielded character  
yielding W
yielded character W
yielding o
yielded character o
yielding r
yielded character r
yielding l
yielded character l
yielding d
yielded character d
end of iterator

In case of generator, elements are generated dynamically. Since next item is generated only after first is consumed, it is more memory efficient than iterator.

Each Python object, whether representing a built-in class or a user defined class, is stored in computer’s memory at a certain randomly chosen location which is returned by the built-in id() function.

>>> x=10
>>> id(x)
94368638893568

Remember that variable in Python is just a label bound to the object. Here x represents the integer object 10 which is stored at a certain location.

Further, if we assign x to another variable y, it is also referring to the same integer object.

>>> y=x
>>> id(y)
94368638893568

Let us now change value of x with expression x=x+1. As a result a new integer 11 is stored in memory and that is now referred to by x. The object 10 continues to be in memory which is bound to y.

>>> x=x+1
>>> id(x)
94368638893600
>>> id(y)
94368638893568

Most striking feature is that the object 10 is not changed to 11. A new object 11 is created. Any Python object whose value cannot be changed after its creation is immutable. All number type objects (int, float, complex, bool, complex) are immutable.

String object is also immutable. If we try to modify the string by replacing one of its characters, Python interpreter doesn’t allow this, raising TypeError thus implying that a string object is immutable.

>>> string='Python'
>>> string[2]='T'
Traceback (most recent call last):
 File "<pyshell#9>", line 1, in <module>
   string[2]='T'
TypeError: 'str' object does not support item assignment

Same thing is true with tuple which is also immutable.

>>> tup=(10,20,30)
>>> tup[1]=100
Traceback (most recent call last):
 File "<pyshell#11>", line 1, in <module>
   tup[1]=100
TypeError: 'tuple' object does not support item assignment

However, list and dictionary objects are mutable. These objects can be updated in place.

>>> num=[10,20,30]
>>> num[1]=100
>>> num
[10, 100, 30]
>>> dct={'x':10, 'y':20, 'z':30}
>>> dct['y']=100
>>> dct
{'x': 10, 'y': 100, 'z': 30}

Set is a collection data type in Python. It is a collection of unique and immutable objects, not necessarily of same types. A set object can be created by a literal representation as well as using built-in set() function.

Comma separated collection of items enclosed in curly brackets is a set object.

>>> s={10,'Hello', 2.52}
>>> type(s)
<class 'set'>

You can also use built-in set() function which in fact is constructor of set class. It constructs a set object from an iterable argument such as list, tuple or string.

>>> s1=set([10,20,30])
>>> s1
{10, 20, 30}
>>> s2=set((100,200,300))
>>> s2
{200, 100, 300}
>>> s3=set('Hello')
>>> s3
{'o', 'e', 'l', 'H'}

Items in the iterable argument may not appear in the same order in set collection. Also, set is a collection of unique objects. Therefore, even if the iterable contains repeated occurrence of an item, it will appear only one in set. You can see just one presence of ‘l’ even if it appears twice in the string.

Items in the set collection must be mutable. It means a list or a dict object cannot be one of the items in a set although tuple is allowed.

>>> s={1,(2,3)}
>>> s
{1, (2, 3)}
>>> s={1,[2,3]}
Traceback (most recent call last):
 File "<pyshell#16>", line 1, in <module>
   s={1,[2,3]}
TypeError: unhashable type: 'list'

Even though set can contain only immutable objects such as number, string or tuple, set itself is mutable. It is possible to perform add/remove/clear operations on set.

>>> s1=set([10,20,30])
>>> s1.add(40)
>>> s1
{40, 10, 20, 30}
>>> s1.remove(10)
>>> s1
{40, 20, 30}

Frozenset object on the other hand is immutable. All other characteristics of frozenset are similar to normal set. Because it is immutable add/remove operations are not possible.

>>> f=frozenset([10,20,30])
>>> f
frozenset({10, 20, 30})
>>> f.add(40)
Traceback (most recent call last):
 File "<pyshell#24>", line 1, in <module>
   f.add(40)
AttributeError: 'frozenset' object has no attribute 'add'

Python’s set data type is implementation of set as in set theory of Mathemetics. Operations such as union, intersection, difference etc. can be done on set as well as frozenset.

Certain non-alphanumeric characters are defined to perform a specified operation. Such characters are called operators. For example the characters +, -, * and / are defined to perform arithmetic operations on two numeric operands. Similarly <, > == and != perform comparison of two numeric operands by default.

Some of built-in classes of Python allow certain operators to be used with non-numeric objects too. For instance the + operator acts as concatenation operator with two strings. We say that + operator is overloaded. In general overloading refers to attaching additional operation to the operator.

>>> #default addition operation of +
>>> 2+5
7
>>> #+operator overloaded as concatenation operator
>>> 'Hello'+'Python'
'HelloPython'
>>> [1,2,3]+[4,5,6]
[1, 2, 3, 4, 5, 6]
>>> #default multiplication operation of *
>>> 2*5
10
>>> #overloaded * operator as repetition operation with sequences
>>> [1,2,3]*3
[1, 2, 3, 1, 2, 3, 1, 2, 3]
>>> 'Hello'*3
'HelloHelloHello'

For user defined classes, operator overloading is achieved by overriding relevant magic methods (methods with two underscores before and after name) from the object class. For example, if a class contains overridden definition of __add__() method, it is implemented when + operator is used with objects of that class. Following table shows magic methods and the arithmetic operator they implement:

OperatorMethod
+object.__add__(self, other)
-object.__sub__(self, other)
*object.__mul__(self, other)
//object.__floordiv__(self, other)
/object.__div__(self, other)
%object.__mod__(self, other)
**object.__pow__(self, other[, modulo])

Following script contains a class that overrides __add__() method. It causes + operator overloading.

>>> class MyClass:
def __init__(self, x,y):
self.x=x
self.y=y
def __add__(self, obj):
x=self.x+obj.x
y=self.y+obj.y
print ('x:{} y:{}'.format(x,y))

We now have two objects of above class and use + operator with them. The __add__() method will implement overloaded behaviour of + operator as below:

>>> m1=MyClass(5,7)
>>> m2=MyClass(3,8)
>>> m1+m2
x:8 y:15

Similarly comparison operators can also be overloaded by overriding following magic methods:

<
object.__lt__(self, other)
<=
object.__le__(self, other)
==
object.__eq__(self, other)
!=
object.__ne__(self, other)
>=
object.__ge__(self, other)

Complex number z=x+yj is a Cartesian (also called rectangular) representation. It is internally represented in polar coordinates with its modulus r (as returned by built-in abs() function) and the phase angle θ (pronounced as theta) which is counter clockwise angle in radians, between the x axis and line joining x with the origin. Following diagram illustrates polar representation of complex number:

polar coordinates

Functions in cmath module allow conversion of Cartesian representation to polar representation and vice versa.

polar() : This function returns polar representation of a Cartesian notation of complex number. The return value is a tuple consisting of modulus and phase.

>>> import cmath
>>> a=2+4j
>>> cmath.polar(a)
(4.47213595499958, 1.1071487177940904)

Note that the modulus is returned by abs() function

>>> abs(a)
4.47213595499958

phase(): This function returns counter clockwise angle between x axis and segment joining a with origin. The angle is represented in radians and is between π and -π

>>> cmath.phase(a)
1.1071487177940904

rect(): This function returns Cartesian representation of complex number represented in polar form i.e. in modulus and phase

>>> cmath.rect(4.47213595499958, 1.1071487177940904)
(2.0000000000000004+4j)

Function is said to be recursive if it calls itself. Recursion is used when a process is defined in terms of itself. A body of recursive function which is executed repeatedly is a type of iteration.

The difference between a recursive function and the one having a loop such as ‘while’ or ‘for’ loop is that of memory and processor overhead. Let us try to understand with the help of iterative and recursive functions that calculate factorial value of a number.

Iterative factorial function

>>> def factorial(x):
f=1
for i in range(1, x+1):
f=f*i
return f
>>> factorial(5)
120

Recursive factorial function

>>> def factorial(n):    
   if n == 1:
       print (n)
       return 1    
   else:
       return n * factorial(n-1)
>>> factorial(5)
120

For a function that involves a loop, it is called only once. Therefore only one copy is created for all the variables used in it. Also, as function call uses stack to return function value to calling environment, only limited stack operations are performed.

In case of recursion, repeated calls to same function with different arguments are initiated. Hence that many copies of local variables are created in the memory and that many stack operations are needed.  

Obviously, a recursive call by function to itself causes an infinite loop. Hence recursive call is always conditional. Recursive approach provides a very concise solution to complex problem having many iterations.

In case of iterative version of factorial function, it is a straightforward case of cumulative multiplication of numbers in a range. However, in case of recursive function it is implementation of mathematical definition of factorial as below:

n! = n X (n-1)!

Here we have used factorial itself to define factorial. Such problems are ideally suited to be expressed recursively.

We can perform this calculation using a loop as per following code:

Now we shall try to write recursive equivalent of above loop. Look at mathematical definition of factorial once again.

n!=nX(n-1)!

Substitute n with 5. The expression becomes

5!=5X4! = 5X4X3! = 5X4X3X2! = 5X4X3X2X1! = 5X4X3X2X1 = 120

Let us see graphically the step by step process of computing factorial value of 5.

The diagram illustrates how successively performing factorial calculation of number by decrementing the number till it reaches 1. This involves more cost of computing.

Hence it can be seen that recursion is more expensive in terms of resource utilization. It is also difficult to write a recursive function as compared to a straightforward repetitive or iterative solution.

Having said that, recursion is sometimes preferred especially in cases where iterative solution becomes very big, complex and involves too many conditions to keep track of. There are some typical applications where we have to employ recursive solution. Traversal of binary tree structure, sorting algorithms such as heap sort, finding traversal path etc are typical applications of recursion.

On a lighter note, to be able to write a recursive solution gives a lot of satisfaction to the programmer!

‘assert’ is one of 33 keywords in Python language. Essentially it checks whether given boolean expression is true. If expression is true, Python interpreter goes on to execute subsequent code. However, if the expression is false, AssertionError is raised bringing execution to halt.

To demonstrate usage of assert statement, let us consider following function:

>>> def testassert(x,y):
assert y!=0
print ('division=',x/y)

Here, the division will be performed only if boolean condition is true, but AssertionError raised if it is false

>>> testassert(10,2)
division= 5.0
>>> testassert(10,0)
Traceback (most recent call last):
 File "<pyshell#8>", line 1, in <module>
   testassert(10,0)
 File "<pyshell#6>", line 2, in testassert
   assert y!=0
AssertionError

AssertionError is raised with a string in assert statement as custom error message

>>> def testassert(x,y):
assert y!=0, 'Division by Zero not allowed'
print ('division=',x/y)
>>> testassert(10,0)
Traceback (most recent call last):
 File "<pyshell#11>", line 1, in <module>
   testassert(10,0)
 File "<pyshell#10>", line 2, in testassert
   assert y!=0, 'Division by Zero not allowed'
AssertionError: Division by Zero not allowed

Instead of letting the execution terminate abruptly, the AssertionError is handled with try – except construct as follows:

>>> def testassert(x,y):
try:
assert y!=0
print ('division', x/y)
except AssertionError:
print ('Division by Zero not allowed')
>>> testassert(10,0)
Division by Zero not allowed

An object that can invoke a certain action or process is callable. The most obvious example is a function. Any function, built-in or user defined or a method in built-in or user defined class is an object of Function class

>>> def myfunction():
print ('Hello')
>>> type(myfunction)
<class 'function'>
>>> type(print)
<class 'builtin_function_or_method'>

Python’s standard library has a nuilt-in callable() function that returns true if object is callable. Obviously a function returns true.

>>> callable(myfunction)
True
>>> callable(print)
True

A callable object with parentheses (having arguments or not) invokes associated functionality. In fact any object that has access to __call__() magic method is callable. Above function is normally called by entering myfunction(). However it can also be called by using __call_() method inherited from function class

>>> myfunction.__call__()
Hello
>>> myfunction()
Hello

Numbers, string, collection objects etc are not callable because they do not inherit __call__() method

>>> a=10
>>> b='hello'
>>> c=[1,2,3]
>>> callable(10)
False
>>> callable(b)
False
>>> callable(c)
False

In Python, class is also an object and it is callable.

>>> class MyClass:
def __init__(self):
print ('called')
>>> MyClass()
called
<__main__.MyClass object at 0x7f37d8066080>
>>> MyClass.__call__()
called

<__main__.MyClass object at 0x7f37dbd35710>

However, object of MyClass is not callable.

>>> m=MyClass()
called
>>> m()
Traceback (most recent call last):
 File "<pyshell#23>", line 1, in <module>
   m()
TypeError: 'MyClass' object is not callable

In order that object of user defined be callable, it must override __call__() method

>>> class MyClass:
def __init__(self):
print ('called')
def __call__(self):
print ('this makes object callable')
>>> m=MyClass()
called
>>> m()
this makes object callable

In Python, single and double asterisk (* and **) symbols are defined as multiplication and exponentiation operators respectively. However, when prefixed to a variable, these symbols carry a special meaning. Let us see what that means.

A Python function is defined to receive a certain number of arguments. Naturally the same number of arguments must be provided while calling the function, otherwise Python interpreter raises TypeError as shown below.

>>> def add(x,y):
return x+y
>>> add(11,22)
33
>>> add(1,2,3)
Traceback (most recent call last):
 File "<pyshell#8>", line 1, in <module>
   add(1,2,3)
TypeError: add() takes 2 positional arguments but 3 were given

This is where the use of formal argument prefixed with single * comes. In a function definition, an argument prefixed with * is able to receive variable number of values from the calling environment and stores the values in a tuple object.

>>> def add(*numbers):
s=0
for n in numbers:
s=s+n
return s
>>> add(11,22)
33
>>> add(1,2,3)
6
>>> add(10)
10
>>> add()
0

Python allows a function to be called by using the formal arguments as keywords. In such a case, the order of argument definition need not be followed.

>>> def add(x,y):
return x+y
>>> add(y=10,x=20)
30

However, if you want to define a function which should be able to receive variable number of keyword arguments, the argument is prefixed by ** and it stores the keyword: values passed as a dictionary.

>>> def add(**numbers):
s=0
for k,v in numbers.items():
s=s+v
return s
>>> add(a=1,b=2,c=3,d=4)
10

In fact, a function can have positional arguments, variable number of arguments, keyword arguments with defaults and variable number of keyword arguments. In order to use both variable arguments and variable keyword arguments, **variable should appear last in the argument list.

>>> def add(x,y,*arg, **kwarg):
print (x,y)
print (arg)
print (kwarg)
>>> add(1,2,3,4,5,6,a=10,b=20,c=30)
1 2
(3, 4, 5, 6)
{'a': 10, 'b': 20, 'c': 30}

Calling a function by value is found to be used in C/C++ where a variable is actually a named location in computer memory. Hence the passed expression is copied into the formal argument which happens to be a local variable of the called function. Any manipulation of that local variable doesn’t affect the variable that was actually passed.

The following C program has square() function. It is called from the main by passing x which is copied to the local variable in square(). The change inside called function doesn’t have impact on x in main()

#include <stdio.h>
int main()
{
   int square(int);
int x=10;
printf("\nx before passing: %d",x);
square(x);
printf("\nx after passing: %d",x);
}
int square(int x)
{
x=x*x;
}
result:
x before passing: 10
x after passing: 10

In Python though, a variable is just a label of an object in computer memory. Hence formal as well as actual argument variables in fact are two different labels of same object – in this case 10. This can be verified by id() function.

>>> def square(y):
print (id(y))
>>> x=10
>>> id(x)
94172959017792
>>> square(x)
94172959017792

Hence we can infer that Python calls a function by passing reference of actual arguments to formal arguments. However, what happens when the function manipulates the received object depends on the type of object.

When a function makes modifications to the immutable object received as an argument,  changes do not reflect in the object that was passed.

>>> def square(y):
print ('received',id(y))
y=y**2
print ('changed',id(y),'y=',y)
>>> x=10
>>> id(x)
94172959017792
>>> square(x)
received 94172959017792
changed 94172959020672 y= 100
>>> x
10

It can be seen that x=10 is passed to the square() function. Inside, y is changed. However, y becomes a label of another object 100, which is having different id(). This change doesn’t reflect in x – it being an immutable object. The same thing happens to a string or tuple.

However, if we pass a mutable object, any changes by called function will also have effect on that object after returning from the function.

In the example below, we pass a list (it is a mutable object) to a function and then add some more elements to it. After returning from the function, the original list is found to be modified.

>>> def newlist(list):
print ('received', list, id(list))
list.append(4)
print ('changed', list, id(list))
>>> num=[1,2,3]
>>> id(num)
139635530080008
>>> newlist(num)
received [1, 2, 3] 139635530080008
changed [1, 2, 3, 4] 139635530080008
>>> num
[1, 2, 3, 4]

Hence we can say that a mutable object, passed to a function by reference gets modified by any changes inside the called function.

A must-know for anyone looking for top Python interview questions, this is one of the frequently asked Python programming interview questions.

As per the principles of object oriented programming, data encapsulation is a mechanism by which instance attributes are prohibited from direct access by any environment outside the class. In C++ / Java, with the provision of access control keywords such as public, private and protected it is easy to enforce data encapsulation. The instance variables are usually restricted to have private access, though the methods are publicly accessible.

However, Python doesn’t follow the doctrine of controlling access to instance or method attributes of a class. In a way, all attributes are public by default. So in a strict sense, Python doesn’t support data encapsulation. In the following example, name and age are instance attributes of User class. They can be directly manipulated from outside the class environment.

>>> class User:
def __init__(self):
self.name='Amar'
self.age=20
>>> a=User()
>>> a.name
'Amar'
>>> a.age
20
>>> a.age=21

Python even has built-in functions getattr() and setattr() to fetch and set values of an instance attribute.

>>> getattr(a, 'name')
'Amar'
>>> setattr(a,'name','Ravi')
>>> a.name
'Ravi'

Having said that, Python does have means to emulate private keywords in Java/C++. An instance variable prefixed with ‘__’ (double underscore) behaves as a private variable – which means direct access to it will raise an exception as below:

>>> class User:
def __init__(self):
self.__name='Amar'
self.__age=20
>>> a=User()
>>> a.__name
Traceback (most recent call last):
 File "<pyshell#18>", line 1, in <module>
   a.__name
AttributeError: 'User' object has no attribute '__name'

However, the double underscore prefix doesn’t make price truly private (as in case of Java/C++). It merely performs name mangling by internally renaming the private variable by adding the "_ClassName" to the front of the variable. In this case, variable named "__name" in Book class will be mangled in “_User__name” form.

>>> a._User__name
'Amar'

Protected member is (in C++ and Java) accessible only from within the class and its subclasses. Python accomplishes this behaviour by convention of prefixing the name of your member with a single underscore. You’re telling others “don’t touch this, unless you’re a subclass”.

The def keyword is used to define a new function with a user specified name. Lambda keyword on the other hand is used to create an anonymous (un-named) function. Usually such a function is created on the fly and meant for one-time use. The general syntax of lambda is as follows:

lambda arg1, arg2… : expression

A lambda function can have any number of arguments but there’s always a single expression after : symbol. The value of the expression becomes the return value of the lambda function. For example

>>> add=lambda a,b:a+b

This is an anonymous function declared with lambda keyword. It receives a and b and returns a+b.. The anonymous function object is assigned to identifier called add. We can now use it as a regular function call

>>> print (add(2,3))
5

The above lambda function is equivalent to a normal function declared using def keyword as below:

>>> def add(a,b):
return a+b
>>> print (add(2,3))
5

However, the body of the lambda function can have only one expression. Therefore it is not a substitute for a normal function having complex logic involving conditionals, loops etc.

In Python, a function is a called a first order object, because function can also be used as argument, just as number, string, list etc. A lambda function is often used as argument function to functional programming tools such as map(), filter() and reduce() functions.

The built-in  map() function subjects each element in the iterable to another function which may be either a built-in function, a lambda function or a user defined function and returns the mapped object. The map() function needs two arguments:

map(Function, Sequence(s))

Next example uses a lambda function as argument to map() function. The lambda function itself takes two arguments taken from two lists and returns the first number raised to second. The resulting mapped object is then parsed to output list.

>>> powers=map(lambda x,y: x**y, [10,20,30], [1,2,3])
>>> list(powers)
[10, 400, 27000]

The filter() function also receives two arguments, a function with Boolean return value and an iterable. Only those items for whom the function returns True are stored in a filter object which can be further cast to a list.

Lambda function can also be used as a filter. The following program uses lambda function that filters all odd numbers from a given range.

>>> list(filter(lambda x: x%2 == 1,range(1,11)))
[1, 3, 5, 7, 9]

The intention of ‘with’ statement in Python is to make the code cleaner and much more readable. It simplifies the management of common resources like file streams.

The ‘with’ statement establishes a context manager object that defines the runtime context. The context manager handles the entry into, and the exit from, the desired runtime context for the execution of the block of code. Context managers are normally invoked using the ‘with’ statement.

Here is a typical use of with statement. Normally a file object is declared with built-in open() function. After performing file read/write operations the file object must be relieved from memory by its close() method failing which the disk file/stream underneath the object may be corrupted.

>>> f=open('file.txt','w')
>>> f.write('Hello')
>>> f.close()

Use of ‘with’ statement sets up a context for the following block of statements. As the block finishes, the context object also automatically goes out of scope. It need not be explicitly destroyed.

>>> with open('file.txt','w') as f:
f.write('Hello')

Note that the object f is no longer alive after the block and you don’t have to call close() method as earlier. Hence, the code becomes clean and easy to read.

The file object acts as a context manager object by default. To define a custom context manager, the class must have __enter__() and __exit__() methods.

class Mycontext:
   def __init__(self):
       print ('object initialized')
   def __enter__(self):
       print ('context manager entered')
   def __exit__(self, type, value, traceback):
       print ('context manager exited')
with Mycontext() as x:
   print ('context manager example')

Output:

object initialized
context manager entered
context manager example
context manager exited

A module in Python is a Python script that may have definitions of class, functions, variables etc. having certain similar nature or intended for a common purpose. For example math module in Python library contains all frequently required mathematical functions. This modular approach is taken further by the concept of package. A package may contain multiple modules or even subpackages which may be a part of resources required for a certain purpose. For example Python library’s Tkinter package contains various modules that contain functionality required for building GUI.

Hierarchical arrangement of modules and subpackages is similar to the operating system’s file system. Access to a certain file in the file tree is obtained by using ‘/’. For example a file in a certain folder may have its path as c:\newdir\subdir\file.py. Similarly a module in newdir package will be named as newdir.subdir.py

For Python to recognize any folder as a package, it must have a special file named __init__.py which may or may not be empty and also serves as a packaging list by specifying resources from its modules to be imported.

Modules and their contents in a package can be imported by some script which is at the same level. If we have a.py, b.py and c.py modules and __init__.py file in a test folder under newdir folder, one or more modules can be imported by test.py in newdir as under:

#c:/newdir/test.py
from test import a
a.hello()
#to import specific function
from test.a import hello
hello()

The __init__.py file can be customized to allow package level access to functions/classes in the modules under it:

#__init__.py
from .a import hello
from .b import sayhello

Note that hello() function can now be access from the package instead of its module

#test.py
import test
test.hello()

The test package is now being used by a script which is at the same level as the test folder. To make it available for system-wide use, prepare the following setup script:

#setup.py
from setuptools import setup
setup(name='test',
     version='0.1',
     description='test package',
     url='#',
     author='....',
     author_email='...@...',
     license='MIT',
     packages=['test'],
     zip_safe=False)

To install the test package:

C:\newdir>pip install .

Package is now available for import from anywhere in the file system. To make it publicly available, you have to upload it to PyPI repository.

Python’s built-in open() function returns a file object representing a disk file although it can also be used to open any stream such as byte stream or network stream. The open() function takes two arguments:

f=open(“filename”, mode)

Default value of mode parameter is ‘r’ which stands for reading mode. To open a file for storing data, the mode should be set to ‘w’

f=open(‘file.txt’,’w’)
f.write(‘Hello World’)
f.close()

However, ‘w’ mode causes overwriting file.txt if it is earlier present, thereby the earlier saved data is at risk of being lost. You can of course check if the file already exists before opening, using os module function:

import os
if os.path.exists('file.txt')==False:
   f=open('file.txt','w')
   f.write('Hello world')
   f.close()

This can create a race condition though because of simultaneous conditional operation and open() functions. To avoid this ‘x’ mode has been introduced, starting from Python 3.3. Here ‘x’ stands for exclusive. Hence, a file will be opened for write operation only if it doesn’t exist already. Otherwise Python raises FileExistsError

try:
   with open('file.txt','x') as f:
       f.write("Hello World")
   except FileExistsError:
       print ("File already exists")

Using ‘x’ mode is safer than checking the existence of file before writing and guarantees avoiding accidental overwriting of files.

Python offers more than one way to check if a certain class is inherited from the other. In Python each class is a subclass of object class. This can be verified by running built-in issubclass() function on class A as follows:

>>> class A:
pass
>>> issubclass(A,object)
True

We now create B class that is inherited from A. The issubclass() function returns true for A as well as object class.

>>> class B(A):
pass
>>> issubclass(B,A)
True
>>> issubclass(B,object)
True

You can also use __bases__ attribute of a class to find out its super class.

>>> A.__bases__
(<class 'object'>,)
>>> B.__bases__
(<class '__main__.A'>,)

Finally the mro() method returns method resolution order of methods in an inherited class. In our case B is inherited from A, which in turn is a subclass of object class.

>>> B.mro()
[<class '__main__.B'>, <class '__main__.A'>, <class 'object'>]

Depending upon where in the program a certain variable is declared, it is classified as a global, local or nonlocal variable. As the name suggests, any variable accessible to any function is a global variable. Any variable declared before a call to function (or any block of statements) is having global scope.

def square():
   print (x*x)
x=10
square()
output:
100

Here, x is a global variable hence it can be accessed from anywhere, including from square() function. If we modify it in the function, its effect is persistent to global variable.

def square():
   x=20
   print ('in function',x)
x=10
square()
print ('outside function',x)
output:
in function 20
outside function 10

However, if we try to increment x in the function, it raises UnBoundLocalError

def square():
   x=x+1
   print ('in function',x)
x=10
square()
print ('outside function',x)
output:
   x=x+1
UnboundLocalError: local variable 'x' referenced before assignment

A local variable is one which is declared inside any block of statements such as a function. Such a variable has only local presence which means, if we try to access outside its scope, an error will be reported.

def square():
   x=10
   x=x**2
   print ('in function',x)
square()
print ('outside function',x)
output:
   print ('outside function',x)
NameError: name 'x' is not defined

Note that if we use a variable of the same name as that available in the global namespace inside a function, it becomes a local variable as far as the local namespace is concerned. Hence, it will not modify the global variable.

def square():
   x=5
   x=x**2
   print ('in function',x)
x=100
square()
print ('outside function',x)
output:
in function 25
outside function 100

If however, you insist that your function needs to access global and local variables of the same name, you need to refer the former with ‘global’ keyword.

def square():
   global x
   x=x**2
   print ('global x in function',x)
x=10
square()
print ('global x outside function',x)
output:
global x in function 100
global x outside function 100

Python's global() function returns a dictionary object of all global variables keys and their respective values.

def square():
   print ('global x in function',globals()['x'])
x=10
square()
output:
global x in function 10

Python 3 introduced nonlocal keyword. It should be used to refer to a variable in the scope of an outer function from an inner function. Python does allow definition of nested functions. This nonlocal variable is useful in this context.

In following program function2 is nested inside function1 which is having var1 as its local variable. For function2 to refer to var1 it has to use a nonlocal keyword because it is neither a local nor a global variable.

def function1():
   var1=20
   def function2():
       nonlocal var1
       var1=50
       print (var1)
   print ('nonlocal var1 before inner function',var1)
   function2()
   print ('nonlocal var1 after inner function',var1)
var1=10
function1()
print ('global var1 after outer function unchanged',var1)
output:
nonlocal var1 before inner function 20
50
nonlocal var1 after inner function 50
global var1 after outer function unchanged 10

Format specification symbols (%d, %f, %s etc) are popularly used in C/C++ for formatting strings. These symbols can be used in Python also. Just as in C, a string is constructed by substituting these symbols with Python objects.

In the example below, we use the symbols %s and %d in the string and substitute them by values of objects in tuple outside the string, prefixed by % symbol

>>> name='Ravi'
>>> age=21
>>> string="Hello. My name is %s. I am %d years old" %(name,age)
>>> string
'Hello. My name is Ravi. I am 21 years old'

In numeric formatting symbols, the width of number can be specified before and/or after a decimal point.

>>> price=50
>>> weight=21.55
>>> string="price=%3d weight=%3.3f" %(price,weight)
>>> string
'price= 50 weight=21.550'

Since Python 3.x a new format() method has been added in built-in string class which is more efficient and elegant, and is prescribed to be the recommended way of formatting with variable substitution.

Instead of  % formatting operator, {} symbols are used as place holders.

>>> name='Ravi'
>>> age=21
>>> string="Hello. My name is {}. I am {} years old".format(name,age)
>>> string
'Hello. My name is Ravi. I am 21 years old'

Format specification symbols are used with : instead of %. So to insert a string in place holder use {:s} and for integer use {:d}. Width of numeric and string variables is specified as before.

>>> price=50
>>> weight=21.55
>>> string="price={:3d} quantity={:3.3f}".format(price,weight)
>>> string
'price= 50 quantity=21.550'

At first, it gives the impression that given expression is invalid because of railing comma. But it is perfectly valid and Python interpreter doesn’t raise any error.

As would be expected, expression without railing comma declares an int object.

>>> num=100
>>> type(num)
<class 'int'>

Note that use of parentheses in the representation of tuple is optional. Just comma separated values form a tuple.

>>> x=(10,20)
>>> type(x)
<class 'tuple'>
>>> #this is also a tuple even if there are no parentheses
... x=10,20
>>> type(x)
<class 'tuple'>

Hence a number (any object for that matter) followed by comma, and without parentheses forms a tuple with single element.

>>> num=100,
>>> type(num)
<class 'tuple'>

As mentioned above omitting comma is a regular assignment of int variable.

While the two names sound similar, they are in fact entirely different.

Python is a general purpose programming language that is on top of the popularity charts among programmers worldwide. IPython, on the other hand, is an interactive command-line terminal for Python. 

Standard Python runs in the interactive mode by invoking it from the command terminal to start a REPL (Read, Evaluate, Print and Loop) with Python prompt >>>

IPython is an enhanced Python REPL with much more features as compared to standard Python shell. Some of the features are given below:

  • Syntax highlighting
  • Stores the history of interactions
  • Offers features such as Tab completion of keywords, variables and function names
  • Has object introspection feature
  • Has magic command system for controlling Python environment
  • Can be embedded in other Python programs
  • Acts as a main kernel for Jupyter notebook.

IPython has been developed by Fernando Perez in 2001. Its current version is IPython 7.0.1, that runs on Python 3.4 version or higher. IPython has spun off into Project Jupyter that provides a web based interface for the IPython shell.

IPython is bundled with Anaconda distribution. If you intend to install IPython with standard Python, use pip utility

pip install ipython

Or better still, you can install Jupyter. IPython, being one of its dependencies, will be installed automatically.

pip install jupyter

The following diagram shows the IPython console.

The Jupyter notebook frontend of IPython looks as below:

In Python, range is one of the built-in objects. It is an immutable sequence of integers between two numbers separated by a fixed interval. The range object is obtained from built-in range() function.

The range() function has two signatures as follows:

range(stop)
range(start, stop[, step])

The function can have one, two or three integer arguments named as start, stop and step respectively. It returns an object that produces a sequence of integers from start (inclusive) to stop (exclusive) by step.

In case only one argument is given it is treated as stop value and start is 0 by default, step is 1 by default.

>>> #this will result in range from 0 to 9
>>> range(10)
range(0, 10)

If it has two arguments, first is start and second is stop. Here step is 1 by default.

>>> #this will return range of integers between 1 to 9 incremented by 1
>>> range(1,10)
range(1, 10)

If the function has three arguments, they will be used as start, stop and step parameters. The range will contain integers from start to step-1 separated by step.

>>> #this will return range of odd numbers between 1 to 9
>>> range(1,10,2)
range(1, 10, 2)

Elements in the range object are not automatically displayed. You can cast range object to list or tuple.

>>> r=range(1,10,2)
>>> list(r)
[1, 3, 5, 7, 9]

The most common use of range is to traverse and process periodic sequence of integers using ‘for’ loop. The following statement produces the square of all numbers between 1 to 10.

>>> for i in range(11):
print ('square of {} : {}'.format(i,i**2))
square of 0 : 0
square of 1 : 1
square of 2 : 4
square of 3 : 9
square of 4 : 16
square of 5 : 25
square of 6 : 36
square of 7 : 49
square of 8 : 64
square of 9 : 81
square of 10 : 100

The range object has all methods available to a sequence, such as len(), index() and count()

>>> r=range(1,10,2)
>>> len(r)
5
>>> r.index(7)
3
>>> r.count(5)
1

It is also possible to obtain an iterator out of the range object and traverse using next() function.

>>> it=iter(r)
>>> while True:
try:
print(next(it))
except StopIteration:
break
1
3
5
7
9

The acronym JSON stands for JavaScript Object Notation based on a subset of the JavaScript language. It is a lightweight data interchange format which is easy for humans to use and easy for machines to parse.

Python library provides json module for converting Python objects to JSON format and vice versa. It contains dumps() and loads() functions to serialize and de-serialize Python objects.

>>> import json
>>> #dumps() converts Python object to JSON
>>> data={'name':'Raju', 'subjects':['phy', 'che', 'maths'], 'marks':[50,60,70]}   
>>> jso=json.dumps(data)
>>> #loads() converts JSON string back to Python object
>>> Pydata=json.loads(jso)
>>> Pydata
{'name': 'Raju', 'subjects': ['phy', 'che', 'maths'], 'marks': [50, 60, 70]}

The json module has dump() and load() functions that perform serialization and deserialization of Python object to a File like object such as disk file or network stream. In the following example, we store JSON string to a file using load() and retrieve Python dictionary object from it. Note that File object must have relevant ‘write’ and ‘read’ permission.

>>> data={'name':'Raju', 'subjects':['phy', 'che', 'maths'], 'marks':[50,60,70]}   
>>> file=open('testjson.txt','w')
>>> json.dump(data, file)   
>>> file.close()

The ‘testjson.txt’ file will show following contents:

{"name": "Raju", "subjects": ["phy", "che", "maths"], "marks": [50, 60, 70]}

To load the json string from file to Python dictionary we use load() function with file opened in ‘r’ mode.

>>> file=open('testjson.txt','r')
>>> data=json.load(file)
>>> data
{'name': 'Raju', 'subjects': ['phy', 'che', 'maths'], 'marks': [50, 60, 70]}
>>> file.close()

The json module also provides object oriented API for the purpose with the help of JSONEncode and JSONDecoder classes.

The module also defines the relation between Python object and JSON data type for interconversion, as in the following table:

Python
JSON
dict
object
list, tuple
Array
str
String
int, float, int- & float-derived Enums
Number
True
True
False
False
None
Null

A Python script has .py extension and can have definition of class, function, constants, variables and even some executable code. Also, any Python script can be imported in another script using import keyword.

However, if we import a module having certain executable code, it will automatically get executed even if you do not intend to do so. That can be avoided by identifying __name__ property of module.

When Python is running in interactive mode or as a script, it sets the value of this special variable to '__main__'. It is the name of the scope in which top level is being executed.

>>> __name__
'__main__'

From within a script also, we find value of __name__ attribute is set to '__main__'. Execute the following script.

#example.py
print ('name of module:',__name__)
C:\users>python example.py
name of module: __main__

However, for imported module this attribute is set to the name of the Python script. (excluding the extension .py)

>>> import example
>>> messages.__name__
'example'

If we try to import example module in test.py script as follows:

#test.py
import example
print (‘name of module:’,__name__)
output:
c:\users>python test.py
name of module:example
name of module: __main__

However we wish to prevent the print statement in the executable part of example.py. To do that, identify the __name__ property of the module. If it is __main__ then only the print statement will be executed if we modify example.py as below:

#example.py
if __name__==”__main__”:
     print (‘name of module:,__name__)

Now the output of test.py will not show the name of the imported module.

Python’s import mechanism makes it possible to use code from one script in another. Any Python script (having .py extension) is a module. Python offers more than one way to import a module or class/function from a module in other script.

Most common practice is to use ‘import’ keyword. For example, to import functions in math module and call a function from it:

>>> import math
>>> math.sqrt(100)
10.0

Another way is to import specific function(s) from a module instead of populating the namespace with all contents of the imported module. For that purpose we need to use ‘from module import function’ syntax as below:

>>> from math import sqrt, exp
>>> sqrt(100)
10.0
>>> exp(5)
148.4131591025766

Use of wild card ‘*’ is also permitted to import all functions although it is discouraged; instead, basic import statement is preferred.

>>> from math import *

Python library also has __import__() built-in function. To import a module using this function:

>>> m=__import__('math')
>>> m.log10(100)
2.0

Incidentally, import keyword internally calls __import__() function.

Lastly we can use importlib module for importing a module. It contains __import__() function as an alternate implementation of built-in function. The import_module() function for dynamic imports is as below:

>>> mod=input('name of module:')
name of module:math
>>> m=importlib.import_module(mod)
>>> m.sin(1.5)
0.9974949866040544

Polymorphism is an important feature of object oriented programming. It comes into play when there are commonly named methods across subclasses of an abstract base class. 

Polymorphism is the ability to present the same interface for differing underlying forms. This allows functions to use objects of any of these polymorphic classes without needing to be aware of distinctions across the classes.

If class B inherits from class A, it doesn’t have to inherit everything about class A, it can do some of the things that class A does differently. It is most commonly used while dealing with inheritance. Method in Python class is implicitly polymorphic. Unlike C++ it need not be made explicitly polymorphic.

In the following example, we first define an abstract class called shape. It has an abstract method area() which has no implementation.

import abc
class Shape(metaclass=abc.ABCMeta):
    def __init__(self,tp):
        self.shtype=tp
        @abc.abstractmethod
        def area(self):
                pass

Two subclasses of shape - Rectangle and Circle are then created. They provide their respective implementation of inherited area() method.

class Rectangle(Shape):
        def __init__(self,nm):
            super().__init__(nm)
            self.l=10
            self.b=20
        def area(self):
                return self.l*self.b
class Circle(Shape):
    def __init__(self,nm):
        super().__init__(nm)
        self.r=5
    def area(self):
        import math
        return math.pi*pow(self.r,2)

Finally we set up objects with each of these classes and put in a collection. 

r=Rectangle('rect')
c=Circle('circle')
shapes=[r,c]
for sh in shapes:
    print (sh.shtype,sh.area())

Run a for loop over a collection. A generic call to area() method calculates the area of respective shape.

Output:
rect 200
circle 78.53981633974483

IP address is a string of decimal (or hexadecimal) numbers that forms a unique identity of each device connected to a computer network that uses Internet Protocol for data communication. The IP address has two purposes: identifying host and location addressing.

To start with IP addressing scheme uses 32 bits comprising of four octets each of 8 bits having a value equivalent to a decimal number between 0-255. The octets are separated by dot (.). For example 111.91.41.196

This addressing scheme is called Ipv4 scheme.  As a result of rapid increase in the number of devices directly connected to the internet, this scheme is being gradually replaced by Ipv6 version.

An IPv6 address uses hexadecimal digits to represent a string of unique 128 bit number. Each position in an IPv6 address represents four bits with a value from 0 to f. The 128 bits are divided into 8 groupings of 16 bits each separated by colons.

Example: 2001:db8:abcd:100::1/64

The IPv6 address always uses CIDR notation to determine how many of the leading bits are used for network identification and rest for host/interface identification.

Python's ipaddress module provides the capability to create, manipulate and operate on IPv4 and IPv6 addresses and networks. Following factory functions help to conveniently create IP addresses, networks and interfaces:

ip_address():

This function returns an IPv4Address or IPv6Address object depending on the IP address passed as argument. 

>>> import ipaddress
>>> ipaddress.ip_address('192.168.0.1')
IPv4Address('192.168.0.1')
>>> ipaddress.ip_address('2001:ab7::')
IPv6Address('2001:ab7::')

ip_network():

This function returns an IPv4Network or IPv6Network object depending on the IP address passed as argument.

>>> ipaddress.ip_network('192.168.100.0/24')
IPv4Network('192.168.100.0/24')
>>> ipaddress.ip_network('2001:db8:abcd:100::/64')
IPv6Network('2001:db8:abcd:100::/64')

ip_interface():

This function returns IPv4Interface or IPv6Interface object .
>>> ipaddress.ip_interface('192.168.100.10/24')
IPv4Interface('192.168.100.10/24')
>>> ipaddress.ip_interface('2001:db8:abcd:100::1/64')
IPv6Interface('2001:db8:abcd:100::1/64')

The pip is a very popular package management utility used to install and manage Python packages especially those hosted on Python’s official package repository called Python Package index(PyPI).

The pip utility is distributed by default in standard Python’s distributions with versions 2.7.9 or later or 3.4 or later. For other Python versions, you may have to obtain it by first downloading get-pip.py from https://bootstrap.pypa.io/get-pip.py and running it.

First, check the installed version of pip on your system.

$ pip –version

Note that for Python 3.x, the pip utility is named as pip3.

Most common usage of pip is to install a certain package using the following syntax:

$ pip install nameofpackage
#for example
$ pip install flask

In order to install a specific version of the package, version number is followed by name

$ pip install flask==1.0

Use --upgrade option to upgrade the local version of the package to the latest version on PyPi.

$ pip --upgrade flask

Many a time, a project needs many packages and they may be having a lot of dependencies. In order to install all of them in one go, first prepare a list of all the packages needed in a file called ‘requirements.txt’ and ask pip to use it for batch installation.

$ pip -r requirements.txt

Uninstalling a package is easy

$ pip uninstall packagename

If the package needs to be installed from a repository other than PyPI, use –index-url option.

$ pip --index-url path/to/package/ packagename

There is ‘search’ option to search for a specific package available.

$ pip search pyqt

The show option displays additional information of a package

$ pip show sqlalchemy
Name: SQLAlchemy
Version: 1.1.13
Summary: Database Abstraction Library
Home-page: http://www.sqlalchemy.org
Author: Mike Bayer
Author-email: mike_mp@zzzcomputing.com
License: MIT License
Location: /home/lib/python3.6/site-packages

The freeze option shows installed packages and their versions.

$ pip freeze

Unit testing is one of the various software testing methods where individual units/ components of software are tested. Unit testing refers to tests that verify the functionality of a specific section of code, usually at the function level. 

The unittest module is a part of Python’s standard library. This unit testing framework was originally inspired by Junit. Individual units of source code, such as functions, methods, and class are tested to determine whether they are fit for use. Intuitively, one can view a unit as the smallest testable part of an application. Unit tests are short code fragments created by programmers during the development process.

Test case is the smallest unit of testing. This checks for a specific response to a particular set of inputs. unittest provides a base class, TestCase, which may be used to create new test cases.

test suite is a collection of test cases, test suites, or both and is used to aggregate tests that should be executed together. Test suites are implemented by the TestSuite class.

Following code is a simple example of unit test using TestCase class.

First let us define a function to be tested. In the following example, add() function is to be tested.

Next step is to Create a testcase by subclassing unittest.TestCase.

Inside the class, define a test as a method with name starting with 'test'.

Each test call assert function of TestCase class. The assertEquals() function compares result of add() function with arg2 argument and throws assertionError if comparison fails.

import unittest
def add(x,y):
   return x + y
class SimpleTest(unittest.TestCase):
   def testadd1(self):
      self.assertEquals(add(10,20),30)
if __name__ == '__main__':
   unittest.main()

Run above script. Output shows that the test is passed.

Ran 1 test in 0.060s
OK

Possible outcomes are:

  • OK: test passes
  • FAIL: test doesn’t pass and AssertionError exception is raised.
  • ERROR: test raises exception other than AssertionError

The unittest module also has a command line interface to be run using Python’s -m option.

Python -m unittest example.py

Python script containing a definition of function, class or module usually contains a ‘docstring’ which appears as a comment but is treated as a description of the corresponding object.

The doctest module uses the docstring. It searches docstring text for interactive Python sessions and verifies their output if it is exactly as the output of the function itself.

The doctests are useful to check if docstrings are up-to-date by verifying that all interactive examples still work as documented. They also perform regression testing by verifying interactive examples from a test file or a test object. Lastly they serve as an important tool to write tutorial documentation for a package.

Following code is a simple example of doctest. The script defines add() function and embeds various test cases in docstring in interactive manner.  The doctest module defines testmod() function which runs the interactive examples and checks output of function for mentioned test cases with the one mentioned in docstring. If the output matches, test is passed otherwise it is failed. 

def add(x,y):
   """Return the factorial of n, an exact integer >= 0.
   >>> add(10,20)
   30
   >>> add('aa','bb')
   'aabb'
   >>> add('aaa',20)
   Traceback (most recent call last):
   ...
   TypeError: must be str, not int
   """
   return x+y
if __name__ == "__main__":
   import doctest
   doctest.testmod()

Save the above script as example.py and run it from command line. To get verbose output of doctests use -v option.

$ python example.py ##shows no output
$ python example.py -v
Trying:
    add(10,20)
Expecting:
    30
ok
Trying:
    add('aa','bb')
Expecting:
    'aabb'
ok
Trying:
    add('aaa',20)
Expecting:
    Traceback (most recent call last):
    ...
    TypeError: must be str, not int
ok
1 items had no tests:
    __main__
1 items passed all tests:
   3 tests in __main__.add
3 tests in 2 items.
3 passed and 0 failed.
Test passed

Test examples can be put in a separate textfile and subjected as argument to testfile() function instead of using testmod() function as above.

There are three distinct situations in Python script where ‘as’ keyword is appropriately used.

  1. to define alias for imported package, module or function.
Alias for module in a package
>>> from os import path as p
alias for a module
>>> import math as m
>>> m.sqrt(100)
10.0
alias for imported function
>>> from math import sqrt as underroot
>>> underroot(100)
10.0
  1. to define a context manager variable:
>>> with open("file.txt",'w') as f:
f.write("Hello")
  1. as argument of exception:

When an exception occurs inside the try block, it may have an associated value known as the argument defined after exception type with ‘as’ keyword. The type of the argument depends on the exception type. The except clause may specify a variable after the exception name. The variable is bound to an exception instance with the arguments stored in instance.args.

>>> def divide(x,y):
try:
z=x/y
print (z)
except ZeroDivisionError as err:
print (err.args)
>>> divide(10,0)
('division by zero',)

Errors detected during execution are called exceptions. In Python, all exceptions must be instances of a class that derives from BaseException. Python library defines many subclasses of BaseException. They are called built-in exceptions. Examples are TypeError, ValueError etc.

When user code in try: block raises an exception, program control is thrown towards except block of corresponding predefined exception type. However, if the exception is different from built-in ones, user can define a customized exception class. It is called user defined exception.

Following example defines MyException as a user defined exception.

class MyException(Exception):
   def __init__(self, num):
      self.num=num
   def __str__(self):
      return "MyException: invalid number"+str(self.num)

Built-in exceptions are raised implicitly by the Python interpreter. However, exceptions of user defined type must be raised explicitly by using raise keyword.

The try: block accepts a number from user and raises MyException if the number is beyond the range 0-100. The except block then processes the exception object as defined.

try:
   x=int(input('input any number: '))
   if x not in range(0,101):
      raise MyException(x)
   print ("Number is valid")
except MyException as m:
   print (m)

Output:

input any number: 55
Number is valid
input any number: -10
MyException: invalid number-10

This, along with other Python advanced interview questions, is a regular feature in Python interviews. Be ready to tackle it with the approach mentioned.

Obviously, print() is most commonly used Python statements and is used to display the value of one or more objects. However, it accepts other arguments other than objects to be displayed.

print(*objects, sep=' ', end='\n', file=sys.stdout, flush=False)

The function can have any number of objects as positional arguments, all separated by commas. Other arguments are optional but must be used as keyword arguments.

By default, the non-keyword arguments that is the list of objects is displayed with whitespace as a separator. However, you can define separator symbol with the help of sep argument.

>>> x=10
>>> y=20
>>> z=30
>>> print (x,y,z)
10 20 30
>>> #using comma as separator
>>> print (x,y,z, sep=',')
10,20,30

Second keyword argument controls the end of line character. By default output of each print() statement terminates with newline character or ‘\n’. If you wish you can change it to any desired character.

print ("Hello World")
print ("Python programming")
#this will cause output of next statement in same line
print ("Hello World", end=' ')
print ("Python programming")
output:
Hello World
Python programming
Hello World Python programming

The file argument decides the output stream to which result of print() is directed. By default it is sys.stdout which is standard output 

  • device – the primary display device of the system. It can be changed to any file like object such as a disk file, bytearray, network stream 
  • etc – any object possessing write() method.

The buffer argument is false by default, but if the flush keyword argument is true, the stream is forcibly flushed.

Advanced

Any class in Python code, whether built-in or user defined, is an object of ‘type’ class which is called a 'metaclass'.  A metaclass is a class whose objects are also classes. This relationship can be understood by the following diagram:

Metaclass in Python

Python library's built-in function type() returns class to which an object belongs.

>>> num=50
>>> type(num)
<class 'int'>
>>> numbers=[11.,22,33]
>>> type(numbers)
<class 'list'>
>>> lang='Python'
>>> type(lang)
<class 'str'>

Same thing applies to object of a user defined class

>>> class user:
def __init__self():
self.name='xyz'
self.age=21
>>> a=user()
>>> type(a)
<class '__main__.user'>

Interestingly, class is also an object in Python. Each class, both built-in class and a user defined class is an object of type class

>>> type(int)
<class 'type'>
>>> type(list)
<class 'type'>
>>> type(str)
<class 'type'>
>>> type(user)
<class 'type'>

Can be rephrased and mentioned at the start along with the diagram and the tables above can be used as an example instead.

The type class is the default metaclass. The type() function used above takes an object as argument. The type() function's three argument variation is used to define a class dynamically. Its prototype is as follows:

newclass=type(name, bases, dict)

The function returns class that has been created dynamically. Three arguments of type function are:

namename of the class which becomes __name__ attribute of new class
basestuple consisting of parent classes. Can be blank if not a derived class
dictdictionary forming namespace of the new class containing attributes and methods and their values.

Following statement creates user dynamically using type() function:

>>> user=type('user',(), {'name':'xyz', 'age':21})

We can now use this class in the same manner as we do when it is defined using class statement:

>>> a=user()
>>> a.name,a.age
('xyz', 21)
>>> type(a)
<class '__main__.user'>

Similarly you can add instance method dynamically.

>>> def userdata(self):
print('name:{}, age:{}'.format(self.name, self.age))
>>> user=type('user',(), {'name':'xyz', 'age':21,'userdata':userdata})
>>> a=user()
>>> a.userdata()
name:xyz, age:21

Moreover, this class can be used to create an inherited class as well.

>>> newuser=type('newuser',(user,),{})
>>> b=newuser()
>>> b.userdata()
name:xyz, age:21

As mentioned above, type is the default metaclass. A class that it inherited from type is a custom metaclass.

>>> class NewMetaClass(type):
pass

Using this custom metaclass to create new class as follows:

>>> class newclass(metaclass=NewMetaClass):
pass

Newly created class can even be used as base for other.

>>> class subclass(newclass):
pass

The custom metaclass is a type of both the classes. 

>>> type(NewMetaClass)
<class 'type'>
>>> type(newclass)
<class '__main__.NewMetaClass'>
>>> type(subclass)
<class '__main__.NewMetaClass'>

Construction of subclass of user defined metaclass can be customized by overriding __new__ magic method.

In following example, we declare  a metaclass (NewMetaClass) having __new__() method. It returns instance of new class which in turn is initialized by __init__() method as usual in the derived class.

lass NewMetaClass(type):
   def __new__(cls, name, bases, dict):
       print ('name:',name)
       print ('bases:',bases)
       print ('dict:',dict)        
       inst = super().__new__(cls, name, bases, dict)
       return inst
class myclass(metaclass=NewMetaClass):
   pass
class newclass(myclass,metaclass=NewMetaClass):
   def __init__(self):
       self.name='xyz'
       self.age=21

When we declare an object of newclass and print its attributes, following output is displayed.

x=newclass()
print('name:',x.name, 'age:',x.age)
print ('type:',type(x))

Output:

name: myclass
bases: ()
dict: {'__module__': '__main__', '__qualname__': 'myclass'}
name: newclass
bases: (<class '__main__.myclass'>,)
dict: {'__module__': '__main__', '__qualname__': 'newclass', '__init__': <function newclass.__init__ at 0x7f65d8dfcf28>}
name: xyz age: 21
type: <class '__main__.newclass'>

We can thus customize the metaclass and creation of its subclasses.

Array is a collection of more than one element of similar data type in C/C++/Java etc. Python doesn't have any built-in equivalent of array. Its List/tuple data types are collections, but items in it may be of different types.

Python's array module emulates C type array. The module defines 'array' class whose constructor requires two arguments:

array(typecode, initializer)

The typecode determines the type of array. Initializer should be a sequence with all elements of matching type.

Type codeC TypePython Type
'b'signed charint
'B'unsigned charint
'u'Py_UNICODEUnicode character
'h'signed shortint
'H'unsigned shortint
'i'signed intint
'I'unsigned intint
'I'signed longint
'L'unsigned longint
'q'signed long longint
'Q'unsigned long longint
'f'Floatfloat
'd'Doublefloat

The array module defines typecodes attribute which returns a string. Each character in the string represents a type code.

>>> array.typecodes
'bBuhHiIlLqQfd'

Following statement creates an integer array object:

>>> import array
>>> arr=array.array('i', range(5))
>>> arr
array('i', [0, 1, 2, 3, 4])
>>> type(arr)
<class 'array.array'>

The initializer may be a byte like object. Following example builds an array from byte representation of string.

>>> arr=array.array('b', b'Hello')
>>> arr
array('b', [72, 101, 108, 108, 111])

Some of the useful methods of the array class are as follows:

extend():

This method appends items from list/tuple to the end of the array. Data type of array and iterable list/tuple must match; if not, TypeError will be raised.

>>> arr=array.array('i', [0, 1, 2, 3, 4])
>>> arr1=array.array('i',[10,20,30])
>>> arr.extend(arr1)
>>> arr
array('i', [0, 1, 2, 3, 4, 10, 20, 30])

fromfile():

This method reads data from the file object and appends to an array.

>>> a=array.array('i')
>>> file=open('test.txt','rb')
>>> a.fromfile(file,file.tell())
>>> a
array('i', [1819043144, 2035294319, 1852794996])

tofile():

This method write all items to the file object which must have write mode enabled.

>>> a=array.array('i', [10, 20, 30, 40, 50])
>>> file=open("temp.txt","wb")
>>> a.tofile(file)
>>> file.close()
>>> file=open("temp.txt","rb")
>>> file.read()
b'\n\x00\x00\x00\x14\x00\x00\x00\x1e\x00\x00\x00(\x00\x00\x002\x00\x00\x00'

append():

This method appends a new item to the end of the array

fromlist():

This method appends items from the list to array. This is equivalent to for x in list: a.append(x)

>>> a=array.array('i')
>>> a.append(10)
>>> a
array('i', [10])
>>> num=[20,30,40,50]
>>> a.fromlist(num)
>>> a
array('i', [10, 20, 30, 40, 50])

insert():

Insert a new item in the array before specified position

>>> a=array.array('i', [10, 20, 30, 40, 50])
>>> a.insert(2,25)
>>> a
array('i', [10, 20, 25, 30, 40, 50])

pop():

This method returns item at given index after removing it from the array.

>>> a=array.array('i', [10, 20, 30, 40, 50])
>>> x=a.pop(2)
>>> x
30
>>> a
array('i', [10, 20, 40, 50])

remove():

This method removes first occurrence of given item from the array.

>>> a=array.array('i', [10, 20, 30, 40, 50])
>>> a.remove(30)
>>> a
array('i', [10, 20, 40, 50])

In programming language context, a first-class object is an entity that can perform all the operations generally available to other scalar data types such as integer, float or string.

As they say, everything in Python is an object and this applies to function (or method) as well.

>>> def testfunction():
print ("hello")
>>> type(testfunction)
<class 'function'>
>>> type(len)
<class 'builtin_function_or_method'>

As you can see, a built-in function as well as user defined function is an object of function class.

Just as you can have built-in data type object as argument to a function, you can also declare a function with one of its arguments being a function itself. Following example illustrates it.

>>> def add(x,y):
return x+y
>>> def calladd(add, x,y):
z=add(x,y)
return z
>>> calladd(add,5,6)
11

Return value of a function can itself be another function. In following example, two functions add() and multiply() are defined. Third function addormultiply() returns one of the first two depending on arguments passed to it.

>>> def add(x,y):
return x+y
>>> def multiply(x,y):
return x*y
>>> def addormultiply(x,y,op):
if op=='+':
return add(x,y)
else:
if op=='*':
return multiply(x,y)
>>> addormultiply(5,6,'+')
11
>>> addormultiply(5,6,'*')
30

Thus a function in Python is an object of function class and can be passed to other function as argument or a function can return other function. Hence Python function is a high order object.

This is one of the most frequently asked Python interview questions for Data Science in recent times. Practice this with other data types as well.

Python has a built-in file object that represents a disk file. The file API has write() and read() methods to store data in Python objects in computer files and read it back.

To save data in a computer file, file object is first obtained from built-in open() function.

>>> fo=open("file.txt","w")
>>> fo.write("Hello World")
11
>>> fo.close()

Here ‘w’ as mode parameter of open() function indicates that file is opened for writing data. To read back the data from file open it with ‘r’ mode.

>>> fo=open("file.txt","r")
>>> fo.read()
'Hello World'

However, the file object can store or retrieve only string data. For other type of objects, they must be either converted to string or their byte representations and store in binary files opened with ‘wb’ mode.

This type of manual conversion of objects in string or byte format (and vice versa) is very cumbersome. Answer to this situation is object serialization.

Object serialization means transforming it in a format that can be stored, so as to be able to deserialize it later, recreating the original object from the serialized format. Serialization may be done disk file, byte string or can be transmitted via network sockets. When serialized data is brought back in a form identical to original, the mechanism is called de-serialization.

Pythonic term for serialization is pickling while de-serialization is often referred to as unpickling. Python’ library contains pickle module that provides dumps() and loads() functions to serialize and deserialize Python objects.

Following code shows a dictionary object pickled using dumps() function in pickle module.

>>> import pickle
>>> data={'No':1, 'name':'Rahul', 'marks':50.00}
>>> pckled=pickle.dumps(data)
>>> pckled
b'\x80\x03}q\x00(X\x02\x00\x00\x00Noq\x01K\x01X\x04\x00\x00\x00nameq\x02X\x05\x00\x00\x00Rahulq\x03X\x05\x00\x00\x00marksq\x04G@I\x00\x00\x00\x00\x00\x00u.'

Original state of object is retrieved from pickled representation using loads() function.

>>> data=pickle.loads(pckled)
>>> data
{'No': 1, 'name': 'Rahul', 'marks': 50.0}

The serialized representation of data can be persistently stored in disk file and can be retried back using dump() and load() functions.

>>> import pickle
>>> data={'No':1, 'name':'Rahul', 'marks':50.00}
>>> fo=open('pickledata.txt','wb')
>>> pickle.dump(data, fo)
>>> fo.close()
>>> #unpickle
>>> fo=open('pickledata.txt','rb')
>>> data=pickle.load(fo)
>>> data
{'No': 1, 'name': 'Rahul', 'marks': 50.0}

Pickle data format is Python-specific in nature. To serialize and deserialize data to/from other standard formats Python library provides json, csv and xml modules.

As we can see, serialized data can be persistently stored in files. However, file created using write() method of file object does store data persistently but is not in serialized form. Hence we can say that serialized data can be stored persistently, but converse is not necessarily true.

This module implements pseudo-random number generator (PRNG) technique that uses the Mersenne Twister algorithm. It produces 53-bit precision floats and has a period of 2**19937-1. The module functions depend on the basic function random(), which generates a random float uniformly in the range 0.0 to 1.0.  

The Mersenne Twister is one of the most extensively tested random number generators in existence. However, being completely deterministic, it is not suitable for cryptographic purposes.

Starting from Python 3.6, the secrets module has been added to Python standard library for generating cryptographically strong random numbers (CSPRNG) suitable for managing data such as passwords, account authentication, security tokens, etc.

It is recommended that, secrets should be used in preference to the default pseudo-random number generator in the random module, which is designed for modelling and simulation, not security or cryptography.

Here’s how a secure password can be generated using secrets module. Let the password have one character each from group of uppercase characters, lowercase characters, digits and special characters.

Characters in these groups can be obtained by using following attributes defined in string module.

>>> import string
>>> upper=string.ascii_uppercase
>>> lower=string.ascii_lowercase
>>> dig=string.digits
>>> sp=string.punctuation

We now randomly choose one character from each group using choice() function.

>>> a=secrets.choice(upper)
>>> a
'E'
>>> b=secrets.choice(lower)
>>> b
'z'
>>> c=secrets.choice(dig)
>>> c
'2'
>>> d=secrets.choice(sp)
>>> d
':'

Finally these four characters are shuffled randomly and joined to produce a cryptographically secure password.

>>> pwd=[a,b,c,d]
>>> secrets.SystemRandom().shuffle(pwd)
>>> ''.join(pwd)
':Ez2'

SQL injection is the technique of inserting malicious SQL statements in the user input thereby gaining unauthorized access to database and causing harm in an interactive data driven application.

Most computer applications including Python applications that involve database interaction use relational database products that work on SQL queries. Python has standardized database adapter specifications by DB-API standard. Python standard distribution has sqlite3 module which is a DB-API compliant SQLite driver. For other databases the module must be installed. For example, if using MySQL, you will have to install PyMySQL module.

Standard procedure of executing SQL queries is to first establish connection with database server, obtain cursor object and then execute SQL query string. Following describes the sequence of steps for SQLite database

con=sqlite3.connect(‘testdata.db’)
cur=con.cursor()
cur.execute(SQLQueryString)

For example, following code displays all records in products table whose category is ‘Electronics’

qry=”SELECT * from products where ctg==’Electronics’;”

However, if price is taken as user input then the query string will have to be constructed by inserting user input variable.

catg=input(‘enter category:’)
qry=”select * from prooducts where ctg={};”.format(catg)

if input is Electronics, the query will be built as select * from products where ctg=’Electronics’But a malicious input such as following is given enter category:electronics;drop table products;Now this will cause the query to be as follows:

  • select * from products where ctg=’Electronics’;drop table products;
  • Subsequent cur.execute(qry) statemnt will inadvertently delete products table as well!
  • To prevent this DB-API recommends use of parameterized queries using ? Placeholder.
  • cur.execute(‘select * from products where ctg=?’, (catg))
  • In some database adapter modules such as mysql, other placeholder may be used
  • cur.execute(‘select * from products where ctg=%s’, (catg))

In such prepared statements, database engine checks validity of interpolated data and makes sure that undesirable query is not executed, preventing SQL injection attack.

The dataclasses module is one of the most recent modules to be added in Python's standard library. It has been introduced since version 3.7 and defines @dataclass decorator that automatically generates following methods in a user defined class:

constructor method __init__()
string representation method __repr__()
__eq__() method which overloads == operator

The dataclass function decorator has following prototype:

@dataclass(init=True, repr=True, eq=True, order=False, unsafe_hash=False, frozen=False)

All the arguments are of Boolean value. Each decides whether a corresponding magic method will be automatically generated or not.

The 'init' parameter is True by default which will automatically generate __init__() method for the class.

Let us define Employee class using dataclass decorator as follows:

#data_class.py
from dataclasses import dataclass
@dataclass
class Employee(object):
     name : str
     age : int
     salary : float

The auto-generated __init__() method is like:

def __init__(self, name: str, age: int, salary: float):
       self.name = name
       self.age = age
       self.salary = salary

Auto-generation will not happen if the class contsins explicit definition of __init__() method.

The repr argument is also true by default.  A __repr__() method will be generated. Again the repr string will not be auto-generated if class provides explicit definition.

The eq argument forces __eq__() method to be generated. This method gets called in response to equals comparison operator (==). Similarly other operator overloading magic methods will be generated if the order argument is true (the default is False), __lt__(), __le__(), __gt__(), and __ge__() methods will be generated, they implement comparison operators < <= > ans >= respectively.

If unsafe_hash argument is set to False (the default), a __hash__() method is generated according to how eq and frozen are set.

frozen argument: If true (the default is False), it emulates read-only frozen instances.

>>> from data_class import Employee
>>> e1=Employee('XYZ', 21, 2150.50)
>>> e2=Employee('xyz', 20, 5000.00)
>>> e1==e2
False

Other useful functions in this module are as follows:

asdict():

This function converts class instance into a dictionary object.

>>> import dataclasses
>>> dataclasses.asdict(e1)
{'name': 'XYZ', 'age': 21, 'salary': 2150.5}

astuple():

This function converts class instance into a tuple object.

>>> dataclasses.astuple(e2)
('xyz', 20, 5000.0)

make_dataclass():

This function creates a new dataclass from the list of tuples given as fields argument.

>>> NewClass=dataclasses.make_dataclass('NewClass', [('x',int),('y',float)])
>>> n=NewClass(10,20)
>>> n
NewClass(x=10, y=20)

Function is a reusable block of statements. Whenever called, it performs a certain process on parameters or arguments if passed to it. Following function receives two integers and returns their division

>>> def division(x,y):
return x/y
>>> division(10,2)
5.0

While calling the function, arguments must be passed in the same sequence in which they have been defined. Such arguments are called positional arguments. If it is desired to provide arguments in any other order, you must use keyword arguments as follows:

>>> def division(x,y):
return x/y
>>> division(y=2, x=10)
5.0

The formal names of arguments are used as keywords while calling the function. However, using keyword arguments is optional. Question is how to define a function that can be called only by using keywords arguments?

To force compulsory use of keyword arguments, use '*' as one parameter in parameter list. All parameters after * become keyword only parameters. Any parameters before * continue to be positional parameters.

>>> def division(*,x,y):
return x/y
>>> division(10,2)
Traceback (most recent call last):
 File "<pyshell#7>", line 1, in <module>
   division(10,2)
TypeError: division() takes 0 positional arguments but 2 were given
>>> division(y=2, x=10)
5.0
>>> division(x=10, y=2)
5.0

In above example, both arguments are keyword arguments. Hence keyword arguments must be used even if you intend to pass values to arguments in the same order in which they have been defined.

A function can have mix of positional and keyword arguments. In such case, positional arguments must be defined before ‘*’

In following example, the function has two positional and two keyword arguments. Positional arguments may be passed using keywords also. However, positional arguments must be passed before keyword arguments.

>>> def sumdivide(a,b,*,c,d):
return (a+b)/(c-d)
>>> sumdivide(10,20,c=10, d=5)
6.0
>>> sumdivide(c=10, d=5,a=10,b=20)
6.0
>>> sumdivide(c=10, d=5,10,20)
SyntaxError: positional argument follows keyword argument

Expect to come across this, one of the most important Python interview questions for experienced professionals in data science, in your next interviews.

When you install Python and add Python executable to PATH environment variable of your OS, it is available for use from anywhere in the file system. Thereafter, while building a Python application, you may need Python libraries other than those shipped with the standard distribution (or any other distribution you may be using – such as Anaconda). Those libraries are generally installed using pip installer or conda package manager. These packages are installed in site-packages directory under Python’s installation folder.

However, sometimes, requirement of a specific version of same package may sometimes be conflicting with other application’s requirements. Suppose you have application A that uses library 1.1 and application B requires 1.2 version of the same package which may have modified or deprecated certain functionality of its older version. Hence a system-wide installation of such package is likely to break application A.

To avoid this situation, Python provides creation of virtual environment. A virtual environment is a complete but isolated Python environment having its own library, site-packages and executables. This way you can work on dependencies of a certain application so that it doesn’t clash with other or system wide environment.

For standard distribution of Python, virtual environment is set up using following command:

c:\Python37>python -m venv myenv

Here myenv is the name of directory/folder (along with its path. If none, it will be created under current directory) in which you want this virtual environment be set up. Under this directory, include, lib and scripts folders will be created. Local copy of Python executable along with other utilities such as pip are present in scripts folder. Script older also has activate and deactivate batch files.

To activate newly created environment, run activate.bat in Scripts folder.

C:\myenv>scripts\activate
(myenv)c:\python37>

Name of the virtual environment appears before command prompt. Now you can invoke Python interpreter which create will local copy in the new environment and not system wide Python.

To deactivate the environment, simply run deactivate.bat

If using Anaconda distribution, you need to use conda install command.

conda create --name myenv

This will create new environment under Anaconda installation folder’s envs directory.

Python’s built-in function library contains exec() function that is useful when it comes to dynamic execution of Python code. The prototype syntax of the function is as follows:

exec(object, globals, locals)

The object parameter can be a string or code object. The globals and locals parameters are optional. If only globals is provided, it must be a dictionary, which will be used for both the global and the local variables. If both are given, they are used for the global and local variables, respectively. The locals can be any mapping object.

In first example of usage of exec(), we have a string that contains a Python script.

>>> string='''
x=int(input('enter a number'))
y=int(input('enter another number'))
print ('sum=',x+y)
'''
>>> exec(string)
enter a number2
enter another number4
sum= 6

Secondly, we have a Python script saved as example.py

#example.py
x=int(input('enter a number'))
y=int(input('enter another number'))
print ('sum=',x+y)

The script file is read using built-in open() function as a string and then exec() function executes the code

>>> exec(open('example.py').read())
enter a number1
enter another number2
sum= 3

As a third example, the string containing Python code is first compiled to code object using compile() function

>>> string='''
x=int(input('enter a number'))
y=int(input('enter another number'))
print ('sum=',x+y)
'''
>>> obj=compile(string,'example', 'exec')
>>> exec(obj)
enter a number1
enter another number2
sum= 3

Thus we can see how exec() function is useful for dynamic execution of Python code.

Python is an interpreter based language. Hence, a Python program is run by executing one statement at a time. Object version of the entire script is not created as in C or C++. As a result, Python interpreter must be available on the machine on which you intend to run Python program. On a Windows machine, a program is executed by following the command line:

C:\users>python example.py

The Python executable must be available in the system’s path.

Although the same command line can be used for running a program on Linux, there is one more option. On Linux, the Python script can be given execute permission by invoking chmod. However, for that to work, the first line in the script must specify the path to Python executable. This line, though it appears as a comment, is called shebang.

#!/usr/bin/python
def add(x,y):
   return x+y
x=10
y=20
print ('addition=',add(x,y))

The first line in above script is the shebang line and mentions the path to the Python executable. This script is now made executable by chmod command:

$ chmod +x example.py

The Python script now becomes executable and doesn’t need executable’s name for running it.

$ ./example.py

However, the system still needs Python installed. So in that sense this is not self-executable. For this purpose, you can use one of the following utilities:

py2exe

cx-freeze

py2app

pyinstaller

The pyinstaller is very easy to use. First install pyinstaller with the help of pip

$ pip install pyinstaller

Then run the following command from within the directory in which your Python script is present:

~/Desktop/example$ pyinstaller example.py

In the same directory (here Desktop/example) a dist folder will be created. Inside dist, there is another directory with name of script (in this case example) in which the executable will be present. It can now be executed even if the target system doesn’t have Python installed.

~/Desktop/example/dist/example$ ./example

This is how a self-executable Python application is bundled and distributed. The dist folder will contain all modules in the form of libraries.

Instructions written in C/C++ are translated directly in a hardware specific machine code. On the other hand, Java/Python instructions are first converted in a hardware independent bytecode and then in turn the virtual machine which is specific to the hardware/operating system converts them in corresponding machine instructions.

The official distribution of Python hosted by https://www.python.org/ is called Cpython. The Python interpreter (virtual machine) software is written in C. We can call it as a C implementation of Python.

There are many alternatives to this official implementation. They are explained as below:

  • Jython: Jython is a JRE implementation of Python. It is written in Java. It is designed to run on Java platform. Just as a regular Python module/package, a Jython program can import and use any Java class. Jython program also compiles to bytecode. One of the main advantages is that a user interface designed in Python can use GUI elements of AWT, swing or SWT package.
  • Jython follows closely the standard Python implementation. Jython was created in 1997 by Jim Hugunin. Jython 2.0 was released in 1999. Current Jython 2.7.0 released in May 2015, corresponds to CPython 2.7. Development of Jython 3.x is under progress. It can be downloaded from https://www.jython.org/downloads.html
  • IronPython: IronPython is a .NET implementation of Python. It is completely written in C#. Power of .NET framework can be easily harnessed by Python programs. Python’s rapid development tools are also easily embeddable in .NET applications. Similar to JVM, IronPython runs on Microsoft’s Common Language Runtime (CLR)
  • IronPython’s current release, version 2.7.9 can be downloaded from https://ironpython.net/download/
  • PyPy: PyPy is a fast, compliant alternative implementation of the Python language. It uses Just-in-Time compiler, because of which Python programs often run faster on PyPy. PyPy programs also consume less space than they do in CPython. PyPy is highly compatible with existing Python code.  PyPy comes by default with support for stackless mode, providing micro-threads for massive concurrency.
  • Both CPython 2.x and 3.x compatible versions of PyPy are available for download on https://www.pypy.org/download.html

Although Python is predominantly an object oriented programming language, it does have important functional programming capabilities included in functools module.

One such function from the functools module is partial() function which returns a partial object. The object itself behaves like a function. The partial() function receives another function as argument and freezes some portion of a function’s arguments resulting in a new object with a simplified signature.

Let us consider the signature of built-in int() function which is as below:

int(x, base=10) 

This function converts a number or string to an integer, or returns 0 if no arguments are given.  If x is a number, returns x.__int__(). For floating point numbers, this truncates towards zero. If x is not a number or if base is given, then x must be a string.

>>> int(20)
20
>>> int('20', base=8)
16

We now use partial() function to create a callable that behaves like the int() function where the base argument defaults to 8.

>>> from functools import partial
>>> partial_octal=partial(int,base=8)
>>> partial_octal('20')
16

In the following example, a user defined function power() is used as argument to a partial function square() by setting a default value on one of the arguments of the original function.

>>> def power(x,y):
return x**y
>>> power(x=10,y=3)
1000
>>> square=partial(power, y=2)
>>> square(10)
100

The functools module also defines partialmethod() function that returns a new partialmethod descriptor which behaves like partial except that it is designed to be used as a method definition rather than being directly callable.

We normally come across login page of web application where a secret input such as a password or PIN is entered in a text field that masks the actual keys. How do we accept user input in a console based program? This is where the getpass module proves useful.

The getpass module provides a portable way to handle such password inputs securely so that the program can interact with the user via the terminal without showing what the user types on the screen.

The module has the getpass() function which prompt the user for a password without echoing. The user is prompted using the string prompt, which defaults to 'Password: '.

import getpass
pwd=getpass.getpass()
print ('you entered :', pwd)

Run above script as example.py from command line:

$ python example.py
Password:
you entered: test

You can change the default prompt by explicitly defining it as argument to getpass() function

pwd=getpass.getpass(prompt='enter your password: ')
$ python example.py
enter your password:
you entered test

The getpass() function can also receive optional stream argument to redirect the prompt to a file like object such as sys.stderr – the default value of stream is sys.stdout.

There is also a getuser() function in this module. It returns the “login name” of the user by checking the environment variables LOGNAME, USER, LNAME and USERNAME.

The mechanism of execution of a Python program is similar to Java. A Python script is first compiled to a bytecode which in turn is executed by a platform specific Python virtual machine i.e. Python interpreter. However, unlike Java, bytecode version is not stored as a disk file. It is held temporarily in the RAM itself and discarded after execution.

However, things are different when a script imports one or more modules. The imported modules are compiled to bytecode in the form of .pyc file and stored in a directory named as __pycache__.

Following Python script imports another script as module.

#hello.py
def hello():
   print ("Hello World")
#example.py
import hello
hello.hello()

Run example.py from command line

Python example.py

You will find a __pycache__ folder created in current working directory with hello.cpython-36.pyc file created in it.

The advantage of this compiled file is that whenever hello module is imported, it need not be converted to bytecode again as long as it is not modified.

As mentioned above, when executed, .pyc file of the .py script is not created. If you want it to be explicitly created, you can use py_compile and compileall modules available in standard library.

To compile a specific script

import py_compile
py_compile.compile(‘example.py’, ‘example.pyc’)

This module can be directly used from command line

python -m py_compile example.py

To compile all files in a directory

import compileall
compileall.compile_dir(‘newdir’)

Following command line usage of compileall module is also allowed:

python -m compileall newdir

Where newdir is a folder under current working directory.

Don't be surprised if this question pops up as one of the top Python interview questions and answers in your next interview. It has all the elements of a standard interview question.

Garbage collection is the mechanism of freeing up a computer’s memory by identifying and disposing of those objects that are no longer in use. C and C++ language compilers don’t have the capability of automatically disposing memory contents. That’s why disposal of memory resources has to be manually performed. Runtime environments of Java and Python have automatic garbage collection feature hence they provide efficient memory management.

Garbage collection is achieved by using different strategies. Java uses a tracing strategy wherein it determines which objects should be added to the garbage, by tracing the objects that are reachable by a chain of references from certain root objects. Those that are not reachable are considered as garbage and collecting them.

Python on the other hand uses a reference counting strategy. In this case, Python keeps count of the number of references to an object. The count is incremented when a reference to it is created, and decremented when a reference is destroyed. Garbage is identified by having a reference count of zero. When the count reaches zero, the object's memory is reclaimed.

For example 0 is stored as int object and is assigned to variable a, thereby incrementing its reference count to 1. However, when a is assigned to another object, the reference count of 10 becomes 0 and is eligible for garbage collection.

Python’s garbage collector works periodically. To identify and collect garbage manually, we have gc module in Python’s library.  You can also set the collection threshold value. Here, the default threshold is 700. This means when the number of allocations vs. the number of deallocations is greater than 700, the automatic garbage collector will run.

>>> import gc
>>> gc.get_threshold()
(700, 10, 10)

The collect() function identifies and collects all objects that are eligible.

The term Monkey patching is used to describe a technique used to dynamically modify the behavior of a piece of code at run-time. This technique allows us to modify or extend the behavior of libraries, modules, classes or methods at runtime without actually modifying the source code.

Monkey patching technique is useful in the following scenarios:

  1. When we want to extend or modify the behavior of third-party or built-in libraries or methods at runtime without actually modifying the original code.
  1. It is often found to be convenient when writing unit tests.

In Python, classes are just like any other mutable objects. Hence we can modify them or their attributes including functions or methods at runtime.

In the following demonstration of monkey patching, let us first write a simple class with a method that adds two operands.

#monkeytest.py
class MonkeyTest:
   def add(self, x,y):
       return x+y

We can call add() method straightaway

from monkeytest import MonkeyTest
a=MonkeyTest()
print (a.add(1,2))

However, we now need to modify add() method in the above class to accept a variable number of arguments. What do we do? First define a newadd() function that can accept variable arguments and perform addition.

def newadd(s,*args):
   s=0
   for a in args:s=s+int(a)
   return s

Now assign this function to the add() method of our MonkeyTest class. The modified behaviour of add() method is now available for all instances of your class.

MonkeyTest.add=newadd
a=MonkeyTest()
print (a.add(1,2,3))

Python doesn’t implement data encapsulation principle of object oriented programming methodology in the sense that there are no access restrictions like private, public or protected members. Hence a traditional method (as used in Java) of accessing instance variables using getter and setter methods is of no avail in Python.

Python recommends use of property object to provide easy interface to instance attributes of a class. The built-in property() function uses getter , setter and deleter methods to return a property attribute corresponding to a specific instance variable.

Let us first write a Student class with name as instance attributes. We shall provide getname() and setname() methods in the class as below:

class User:
   def __init__(self, name='Test'):
       self.__name=name
   def getname(self):
       return self.__name
   def setname(self,name):
       self.__name=name

We can declare its object and access its name attribute using the above methods.

>>> from example import User
>>> a=User()
>>> a.getname()
'Test'
>>> a.setname('Ravi')

Rather than calling setter and getter function explicitly, the property object calls them whenever the instance variable underlying it is either retrieved or set. The property() function uses the getter and setter methods to return the property object.

Let us add name as property object in the above User class by modifying it as follows:

class User:
   def __init__(self, name='Test'):
       self.__name=name
   def getname(self):
       print ('getname() called')
       return self.__name
   def setname(self,name):
       print ('setname() called')
       self.__name=name
   name=property(getname, setname)

The name property, returned by property() function hides private instance variable __name.  You can use property object directly. Whenever, its value is retrieved, it is internally called getname() method. On the other hand when a value is assigned to name property, internally setname() method is called.

>>> from example import User
>>> a=User()
>>> a.name
getname() called
'Test'
>>> a.name='Ravi'
setname() called

The property() function also can have deleter method. A docstring can also be specified in its definition.

property(getter, setter, deleter, docstring)

Python also has @property decorator that encapsulates property() function.

Python’s built-in function library provides input() and print() functions. They are default functions for console IO. The input() function receives data received from standard input stream i.e. keyboard and print() function sends data to standard output stream directed to standard output device i.e. display monitor device.

In addition to these, we can make use of standard input and output stream objects defined in sys module. The sys.stdin object is a file like object capable of reading data from a console device. It in fact is an object of TextIOWrapper class capable of reading data. Just as built-in File object, read() and readline() methods are defined in TextIOWrapper.

>>> import sys
>>> a=sys.stdin
>>> type(a)
<class '_io.TextIOWrapper'>
>>> data=a.readline()
Hello Python
>>> data
'Hello Python\n'

Note that unlike input() function, the trailing newline character (\n) is not stripped here.

On the other hand sys.stdout object is also an object of TextIOWrapper class that is configured to write data in standard output stream and has write() and writelines() methods.

>>> b.write(data)
Hello Python
13

The eval() function evaluates a single expression embedded in a string. The exec() function on the other hand runs the script embedded in a string. There may be more than one Python statement in the string.

Simple example of eval():

>>> x=5
>>> sqr=eval('x**2')
>>> sqr
25

Simple example of exec()

>>> code='''
x=int(input('enter a number'))
sqr=x**2
print (sqr)
'''
>>> exec(code)
enter a number5
25

eval() always returns the result of evaluation of expression or raises an error if the expression is incorrect. There is no return value of exec() function as it just runs the script.

Use of eval() may pose a security risk especially if user input is accepted through string expression, as  any harmful code may be input by the user.

Both eval() and exec() functions can perform operations on a code object returned by another built-in function compile(). Depending on mode parameter to this function, the code object is prepared for evaluation or execution.

According to Python’s standard documentation, compile() function returns code object from a string containing valid Python statements, a byte string or AST object.

A code object represents byte-compiled executable version of Python code or simply a bytecode. AST stands for Abstract syntax tree. The AST object is obtained by parsing a string containing Python code.

Syntax of compile() function looks as follows:

compile(source, filename, mode, flags=0, dont_inherit=False, optimize=-1)

The source parameter can be a string, bytestring or AST object. Filename parameter is required if you are reading code from a file. The code object can be executed by exec() or eval() functions which is mode parameter. Other parameters are optional.

Thee following example compiles given string to a code object for it to be executed using exec() function.

>>> string='''
x=10
y=20
z=x+y
print (z)
'''
>>> obj=compile(string,'addition', 'exec')
>>> exec(obj)
30

The following example uses eval as mode parameter in compile() function. Resulting code object is executed using eval() function.

>>> x=10
>>> obj=compile('x**2', 'square','eval')
>>> val=eval(obj)
>>> val
100

The AST object is first obtained by parse() function which is defined in ast module. It in turn is used to compile a code object.

>>> import ast
>>> astobj=parse(string)
>>> astobj=ast.parse(string)
>>> obj=compile(astobj,'addition', 'exec')
>>> exec(obj)
30

Whereas print() is a built-in function that displays value of one or more arguments on the output console, pprint() function is defined in the pprint built-in module. The name stands for pretty print. The pprint() function produces an aesthetically good looking output of Python data structures. Any data structure that is properly parsed by the Python interpreter is elegantly formatted. It tries to keep the formatted expression in one line as far as possible, but generates into multiple lines if the length exceeds the width parameter of formatting. 

In order to use it, first it must be imported from pprint module.

from pprint import pprint

One of the unique features of pprint output is that it automatically sorts dictionaries before the display representation is formatted. 

>>> from pprint import pprint
>>> ctg={'Electronics':['TV', 'Washing machine', 'Mixer'], 'computer':{'Mouse':200, 'Keyboard':150,'Router':1200},'stationery':('Book','pen', 'notebook')}
>>> pprint(ctg)
{'Electronics': ['TV', 'Washing machine', 'Mixer'],
 'computer': {'Keyboard': 150, 'Mouse': 200, 'Router': 1200},
 'stationery': ('Book', 'pen', 'notebook')}

The pprint module also has definition of PrettyPrinter class, and pprint() method belongs to it. 

>>> from pprint import PrettyPrinter
>>> pp=PrettyPrinter()
>>> pp.pprint(ctg)
{'Electronics': ['TV', 'Washing machine', 'Mixer'],
 'computer': {'Keyboard': 150, 'Mouse': 200, 'Router': 1200},
 'stationery': ('Book', 'pen', 'notebook')}

One of the most frequently posed Python technical interview questions, be ready for this conceptual question.

CGI, which stands for Common Gateway Interface, is a set of standards for the HTTP server to render the output of executable scripts. CGI is one of the earliest technologies to be used for rendering dynamic content to the client of a web application. All major http web server software, such as Apache, IIS etc are CGI aware. With proper configuration, executable scripts written C/C++, PHP, Python, Perl etc can be executed on the server and the output is dynamically rendered in the HTML form to the client’s browser.

One important disadvantage of CGI is that the server starts a new process for every request that it receives from its clients. As a result, the server is likely to face severe bottlenecks and is slow.

WSGI (Web Server Gateway Interface) on the other hand prescribes a set of standards for communication between web servers and web applications written in Python based web application frameworks such as Django, Flask etc. WSGI specifications are defined in PEP 3333 (Python enhanced Proposal)

WSGI enabled servers are designed to handle multiple requests concurrently. Hence a web application hosted on a WSGI server is faster.

There are many self-contained WSGI containers available such as Gunicorn and Gevent. You can also configure any Apache server for WSGI by installing and configuring mod_wsgi module. Many shared hosting services are also available for deploying Python web applications built with Django, Flask or other web frameworks. Examples are Heroku, Google App Engine, PythonAnywhere etc.

FieldStorage class is defined in cgi module of Python’s standard library. 

A http server can be configured to run executable scripts and programs stored in the cgi-bin directory on the server and render response in HTML form towards client. For example a Python script can be executed from cgi-bin directory.

However, the input to such a script may come from the client in the form of HTTP request either via its GET method or POST method.

In following HTML script, a form with two text input elements are defined. User data is sent as a HTTP request to a script mentioned as parameter of ‘action’ attribute with GET method.

<form action = "/cgi-bin/test.py" method = "get">
Name: <input type = "text" name = "name">  <br />
address: <input type = "text" name = "addr" />
<input type = "submit" value = "Submit" />
</form>

In order to process the user input, Python’s CGI script makes use of FieldStorage object. This object is actually a dictionary object with HTML form elements as key and user data as value.

#!/usr/bin/python
# Import CGI module 
import cgi
# Create instance of FieldStorage 
fso = cgi.FieldStorage()

The fso object now contains form data which can be retrieved using usual dictionary notation.

nm = fso[‘name’]
add=fso[‘addr’]

FieldStorage class also defines the following methods:

  • getvalue(): This method retrieves value associated with form element. 
  • getfirst(): This method returns only one value associated with form field name. The method returns only the first value in case more values were given under such name.
  • getlist():This method always returns a list of values associated with form field name.

Both are built-in functions in standard library. Both functions return sequence objects. However, bytes() returns an immutable sequence, whereas bytearray is a mutable sequence.

The bytes() function returns a bytes object that is an immutable sequence of integers between 0 to 255.

>>> #if argument is an int, creates an array of given size, initialized to null
>>> x=10
>>> y=bytes(x)
>>> y
b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
>>> type(y)
<class 'bytes'>

If string object is used with bytes() function, you should use encoding parameter, default is ‘utf-8’

>> string='Hello'
>>> x='Hello'
>>> y=bytes(x,'utf-8')
>>> y
b'Hello'

Note that the bytes object has a literal representation too. Just prefix the string by b

>>> x=b'Hello'
>>> type(x)
<class 'bytes'>

On the other hand bytearray() returns a mutable sequence. If argument is an int, it initializes the array of given size with null.

>>> x=10
>>> y=bytearray(x)
>>> y
bytearray(b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')
>>> type(y)
<class 'bytearray'>

However, if the argument is an iterable sequence, array size is equal to iterable count and items are initialized by elements of iterable. Following example shows bytearray object obtained from a list.

>>> x=[2,4,6]
>>> y=bytearray(x)
>>> y
bytearray(b'\x02\x04\x06')

Unlike bytes object, a bytearray object doesn’t have literal representation.

Both bytes and bytearray objects support all normal sequence operations like find and replace, join and count etc. 

>>> x=b'Hello'
>>> x.replace(b'l', b'k')
b'Hekko'

Both objects also support logical functions such as isalpha(), isalphanum(), isupper(), islower() etc.

>>> x=b'Hello'
>>> x.isupper()
False

Case conversion functions are also allowed.

>>> x=b'Hello'
>>> x.upper()
b'HELLO'

A class having one or more abstract methods is an abstract class. A method on the other hand is called abstract if it has a dummy declaration and carries no definition. Such a class cannot be instantiated and can only be used as a base class to construct a subclass. However, the subclass must implement the abstract methods to be a concrete class.

Abstract base classes (ABCs) introduce virtual subclasses. They don’t inherit from any class but are still recognized by isinstance() and issubclass() built-in functions.

For example, numbers module in standard library contains abstract base classes for all number data types. The int, float or complex class will show numbers. Number class is its super class although it doesn’t appear in the __bases__ attribute.

>>> a=10
>>> type(a)
<class 'int'>
>>> int.__bases__
(<class 'object'>,)
#However, issubclass() function returns true for numbers.Number
>>> issubclass(int, numbers.Number)
True
#also isinstance() also returns true for numbers.Number
>>> isinstance(a, numbers.Number)
True

The collections.abc module defines ABCs for Data structures like Iterator, Generator, Set, mapping etc.

The abc module has ABCMeta class which is a metaclass for defining custom abstract base class. In the following example Shape class is an abstract base class using ABCMeta. The shape class has area() method decorated by @abstractmethod decorator.

import abc
class Shape(metaclass=abc.ABCMeta):
        @abc.abstractmethod
        def area(self):
                pass

We now create a rectangle class derived from shape class and make it concrete by providing implementation of abstract method area()

class Rectangle(Shape):
        def __init__(self, x,y):
                self.l=x
                self.b=y
        def area(self):
                return self.l*self.b
r=Rectangle(10,20)
print ('area: ',r.area())

If the abstract base class has more than one abstract method, the child class must implement all of them otherwise TypeError will be raised.

Hashing algorithm is a technique of obtaining an encrypted version from a string using a certain mathematical function. Hashing ensures that the message is securely transmitted if it is intended for a particular recipient only.

Several hashing algorithms are currently in use. Secure hash algorithms SHA1, SHA224, SHA256, SHA384, and SHA512 are defined by Federal Information Processing Standard (FIPS). 

RSA algorithm defines md5 algorithm.

The hashlib module in standard library provides a common interface to many different secure hash and message digest algorithms. 

hashlib.new(name,[data]) function in the module is a generic constructor that takes the string name of the desired algorithm as its first parameter.

>>> import hashlib
>>> hash=hashlib.new('md5',b'hello')
>>> hash.hexdigest()
'5d41402abc4b2a76b9719d911017c592'

The hexdigest() function returns a hashed version of string in hexadecimal symbols.

Instead of using new() as generic constructor, we can use a named constructor corresponding to hash algorithm. For example for md5 hash,

>>> hash=hashlib.md5(b'hello')
>>> hash.hexdigest()
'5d41402abc4b2a76b9719d911017c592'

The hashlib module also defines algorithms_guaranteed and algorithms_available attributes.

The algorithms_guaranteed returns a set containing the names of the hash algorithms guaranteed to be supported by this module on all platforms. 

algorithms_available is a set containing the names of the hash algorithms that are available in the running Python interpreter.

>>> hashlib.algorithms_guaranteed
{'sha3_512', 'md5', 'sha3_256', 'sha224', 'sha384', 'sha3_224', 'sha256', 'shake_128', 'blake2b', 'sha1', 'sha3_384', 'blake2s', 'shake_256', 'sha512'}
>>> hashlib.algorithms_available
{'mdc2', 'SHA512', 'SHA384', 'dsaWithSHA', 'md4', 'shake_256', 'SHA224', 'sha512', 'DSA', 'RIPEMD160', 'md5', 'sha224', 'sha384', 'sha3_224', 'MD5', 'ripemd160', 'whirlpool', 'MDC2', 'sha', 'blake2b', 'SHA', 'sha1', 'DSA-SHA', 'sha3_512', 'sha3_256', 'sha256', 'SHA1', 'sha3_384', 'ecdsa-with-SHA1', 'shake_128', 'MD4', 'SHA256', 'dsaEncryption', 'blake2s'}

The vars() function can be operated upon those objects which have __dict__ attribute. It returns the __dict__ attribute for a module, class, instance, or any other object having a __dict__ attribute. If the object doesn’t have the attribute, it raises a TypeError exception. 

The dir() function returns list of the attributes of any Python object. If no parameters are passed it returns a list of names in the current local scope.  For a class , it returns a list of its attributes as well as those of the base classes recursively. For a module, a list of names of all the attributes, contained is returned.

When you access an object's attribute using the dot operator, python does a lot more than just looking up the attribute in that objects dictionary.

Let us take a look at the following class:

class myclass:
    def __init__(self):
        self.x=10
    def disp(self):
        print (self.x)

The vars() of above class is similar to its __dict__  value

>>> vars(myclass)
mappingproxy({'__module__': '__main__', '__init__': <function myclass.__init__ at 0x7f770e520950>, 'disp': <function myclass.disp at 0x7f770e5208c8>, '__dict__': <attribute '__dict__' of 'myclass' objects>, '__weakref__': <attribute '__weakref__' of 'myclass' objects>, '__doc__': None})
>>> myclass.__dict__
mappingproxy({'__module__': '__main__', '__init__': <function myclass.__init__ at 0x7f770e520950>, 'disp': <function myclass.disp at 0x7f770e5208c8>, '__dict__': <attribute '__dict__' of 'myclass' objects>, '__weakref__': <attribute '__weakref__' of 'myclass' objects>, '__doc__': None})

However, vars() of its object just shows instance attributes

>>> a=myclass()
>>> vars(a)
{'x': 10}
>>> x.__dict__
{'x': 10}

The dir() function returns the attributes and methods of myclass as well as object class, the base class of all Python classes.

>>> dir(myclass)
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'disp']

Whereas dir(a), the object of myclass returns instance variables of the object, methods of its class and object base class

>>> a=myclass()
>>> dir(a)
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'disp', 'x']

Python 3 was introduced in 2008. It was meant to be backward incompatible although some features have later been backported to Python 2.7. Still because of vast differences in specifications, Python 2.x code won’t run on Python 3.x environment. 

However, Python 3 library comes with a utility to convert Python 2.x code. This module is called 2to3. Command Line usage of this tool is as below:

$ 2to3 py2.py

To test this usage, save following code as py2.py

def sayhello(nm):
    print "Hello", nm
nm=raw_input("enter name")
sayhello(nm)

Above code uses Python 2.x syntax. The print statement doesn’t require parentheses. Also raw_input() function is not available in Python 3.x. Hence when we convert above code, print() function will have parentheses and raw_input will change to input()

As we run above command, a diff against the original source file is printed as below:

$ 2to3 py2.py
RefactoringTool: Skipping optional fixer: buffer
RefactoringTool: Skipping optional fixer: idioms
RefactoringTool: Skipping optional fixer: set_literal
RefactoringTool: Skipping optional fixer: ws_comma
RefactoringTool: Refactored py2.py
--- py2.py (original)
+++ py2.py (refactored)
@@ -1,5 +1,5 @@
 def sayhello(nm):
-    print "Hello", nm
+    print("Hello", nm)
-nm=raw_input("enter name")
+nm=input("enter name")
 sayhello(nm)
RefactoringTool: Files that need to be modified:
RefactoringTool: py2.py

2to3 can also write the needed modifications right back to the source file which is enabled with the -w flag. Changed version of script will be as shown.

$ 2to3 -w py2.py
RefactoringTool: Skipping optional fixer: buffer
RefactoringTool: Skipping optional fixer: idioms
RefactoringTool: Skipping optional fixer: set_literal
RefactoringTool: Skipping optional fixer: ws_comma
RefactoringTool: Refactored py2.py
--- py2.py (original)
+++ py2.py (refactored)
@@ -1,5 +1,5 @@
 def sayhello(nm):
-    print "Hello", nm
+    print("Hello", nm)
-nm=raw_input("enter name")
+nm=input("enter name")
 sayhello(nm)
RefactoringTool: Files that were modified:
RefactoringTool: py2.py

The py2.py will be modified to be compatible with python 3.x syntax

def sayhello(nm):
    print("Hello", nm)
nm=input("enter name")
sayhello(nm)

People often get confused between assertions and exceptions and often use the latter when the former should be used in a program (and vice versa).

The fundamental difference lies in the nature of the error to be caught and processed. Assertion is a statement of something that MUST be true. If not, then the program is terminated and you cannot recover from it. Exception on the other hand is a runtime situation that can be recovered from.

Python’s assert statement checks for a condition. If the condition is true, it does nothing and your program just continues to execute. But if it evaluates to false, it raises an AssertionError exception with an optional error message.

>>> def testassert(x,y):
try:
assert y!=0
print ('division', x/y)
except AssertionError:
print ('Division by Zero not allowed')
>>> testassert(10,0)
Division by Zero not allowed

The proper use of assertions is to inform developers about unrecoverable errors in a program. They’re not intended to signal expected error conditions, like “file not found”, where a user can take corrective action or just try again. The built-in exceptions like FileNotFoundError are appropriate for the same.

You can think of assert as equivalent to a raise-if statement as follows:

if __debug__:
    if not expression1:
        raise AssertionError(expression2)

__debug__ constant is true by default. 

Python’s assert statement is a debugging aid, not a mechanism for handling run-time errors for which exceptions can be used.

Assertion can be globally disabled by setting __debug__ to false or starting Python with -O option.

$python -O
>>> __debug__
False

Exceptions are generally fatal as the program terminates instantly when an unhandled exception occurs. Warnings are non-fatal. A warning message is displayed but the program won’t terminate. Warnings generally appear if some deprecated usage of certain programming element like keyword/function/class etc. occurs.

Python’s builtins module defines Warnings class which is inherited from Exception itself. However, custom warning messages can be displayed with the help of warn() function defined in built-in warnings module.

There are a number of built-in Warning subclasses. User defined subclass can also be defined.

WarningThis is the base class of all warning category classes
UserWarningThe default category for warn().
DeprecationWarningWarnings about deprecated features when those warnings are intended for developers
SyntaxWarningWarnings about dubious syntactic features
RuntimeWarningWarnings about dubious runtime features
FutureWarningWarnings about deprecated features when those warnings are intended for end users
PendingDeprecationWarningWarnings about features that will be deprecated in the future
ImportWarningWarnings triggered during the process of importing a module
UnicodeWarningWarnings related to Unicode
BytesWarningWarnings related to bytes and bytearray
ResourceWarningWarnings related to resource usage

Following code defines WarnExample class. When method1() is called, it issues a depreciation warning.

# warningexample.py 
import warnings
class WarnExample:
    def __init__(self):
        self.text = "Warning"
    def method1(self):
        warnings.warn(
            "method1 is deprecated, use new_method instead",
            DeprecationWarning
if __name__=='__main__':        
        e=WarnExample()
        e.method1()

Output:

$ python -Wd warningexample.py
warningexample.py:10: DeprecationWarning: method1 is deprecated, use new_method instead
  DeprecationWarning

Counter class is defined in collections module, which is part of Python’s standard library. It is defined as a subclass of built-in dict class. Main application of Counter object is to keep count of hashable objects. Counter object is a collection of key-value pairs, telling how many times a key has appeared in sequence or dictionary.

The Counter() function acts as a constructor. Without arguments, an empty Counter object is returned. When a sequence is given as argument, it results in a dictionary holding occurrences of each item.

>>> from collections import Counter
>>> c1=Counter()
>>> c1
Counter()
>>> c2=Counter('MALAYALAM')
>>> c2
Counter({'A': 4, 'M': 2, 'L': 2, 'Y': 1})
>>> c3=Counter([34,21,43,32,12,21,43,21,12])
>>> c3
Counter({21: 3, 43: 2, 12: 2, 34: 1, 32: 1})

Counter object can be created directly by giving a dict object as argument which itself should be k-v pair of element and count.

>>> c4=Counter({'Ravi':4, 'Anant':3, "Rakhi":3})

The Counter object supports all the methods that a built-in dict object has. One important method is elements() which returns a chain object and returns an iterator of element and value repeating as many times as value.

>>> list(c4.elements())
['Ravi', 'Ravi', 'Ravi', 'Ravi', 'Anant', 'Anant', 'Anant', 'Rakhi', 'Rakhi', 'Rakhi']

Python supports data compression using various algorithms such as zlib, gzip, bzip2 and lzma. Python library also has modules that can manage ZIP and tar archives.

Data compression and decompression according to zlib algorithm is implemented by zlib module. The gzip module provides a simple interface to compress and decompress files just as very popular GNU utilities GZIP and GUNZIP.

Following example creates a gzip file by writing compressed data in it.

>>> import gzip
>>> data=b'Python is Easy'
>>> with gzip.open("test.txt.gz", "wb") as f:
f.write(data)

This will create “test.txt.gz” file in the current directory.

In order to read this compressed file:

>>> with gzip.open("test.txt.gz", "rb") as f:
data=f.read()
>>> data
b'Python is Easy'

Note that the gz file should be opened in wb and rb mode respectively for writing and reading.

The bzip2 compression and decompression is implemented by bz2 module. Primary interface to the module involves following three functions:

  1. Open(): opens a bzip2 compressed file and returns a file object. The file can be opened as binary/text mode with read/write permissions. 
  2. write(): the file should be opened in ‘w’ or ‘wb’ mode. In binary mode, it writes compressed binary data to the file. In normal text mode, the file object is wrapped in TetIOWrapper object to perform encoding.
  3. read(): When opened in read mode, this function reads it and returns the uncompressed data.

Following code writes the compressed data to a bzip2 file

>>> f=bz2.open("test.bz2", "wb")        
>>> data=b'KnowledgeHut Solutions Private Limited'        
>>> f.write(data)
 >>> f.close()

This will create test.bz2 file in the current directory. Any unzipping tool will show a ‘test’ file in it. To read the uncompressed data from this test.bz2 file use the following code:

>>> f=bz2.open("test.bz2", "rb")        
>>> data=f.read()        
>>> data        
b'KnowledgeHut Solutions Private Limited'

The Lempel–Ziv–Markov chain algorithm (LZMA) performs lossless data compression with a higher compression ratio than other algorithms. Python’s lzma module consists of classes and convenience functions for this purpose.

Following code is an example of lzma compression/decompression:

>>> import lzma
>>> data=b"KnowledgeHut Solutions Private Limited"
>>> f=lzma.open("test.xz","wb")
>>>f.write(data)
>>>f.close()

A ‘test.xz’ file will be created in the current working directory. To fetch uncompressed data from this file use the following code:

>>> import lzma
>>> f=lzma.open("test.xz","rb")
>>> data=f.read()
>>> data
b'KnowledgeHut Solutions Private Limited'

The ZIP is one of the most popular and old file formats used for archiving and compression. It is used by famous PKZIP application.

The zipfile module in Python’s standard library provides ZipFile() function that returns ZipFile object. Its write() and read() methods are used to create and read archive.

>>> import zipfile
>>> newzip=zipfile.ZipFile('a.zip','w')
>>> newzip.write('abc.txt')
>>> newzip.close()

To  read  data from a particular file in the archive

>>> newzip=zipfile.ZipFile('a.zip','r')
>>> data=newzip.read('abc.txt')
>>> data

Finally, Python’s tarfile module helps you to create a tarball of multiple files by applying different compression algorithms.

Following example opens a tar file for compression with gzip algorithm and adds a file in it.

>>> fp=tarfile.open("a.tar.gz","w:gz")        
>>> fp.add("abc.txt")        
>>> fp.close()

Following code extracts the files from the tar archive, extracts all files and puts them in current folder. 

>>> fp=tarfile.open("a.tar.gz","r:gz")        
>>> fp.extractall()        
>>> fp.close()

Duck typing is a special case of dynamic polymorphism and a characteristic found in all dynamically typed languages including Python. The premise of duck typing is a famous quote: "If it walks like a duck and it quacks like a duck, then it must be a duck".

In programming, the above duck test is used to determine if an object can be used for a particular purpose. With statically typed languages, suitability is determined by an object's type. In duck typing, an object's suitability is determined by the presence of certain methods and properties, rather than the type of the object itself. Here, the idea is that it doesn't actually matter what type data is - just whether or not it is possible to perform a certain operation.  

Take a simple case of addition operation. Normally addition of similar objects is allowed, for example the addition of numbers or strings. In Python, addition of two integers, x+y actually does int.__add__(x+y) under the hood.

>>> x=10
>>> y=20
>>> x+y
30
>>> int.__add__(x,y)
30

Hence, addition operation can be done on any objects whose class supports __add__() method. Python’s data model describes protocols of data types. For example sequence protocol stipulates that any class that supports iterator, len() and __getitem__() methods is a sequence. 

The simple example of duck typing given below demonstrates how any object may be used in any context, up until it is used in a way that it does not support:

class Duck:
    def fly(self):
        print("Duck flies")
class Airplane:
    def fly(self):
        print("Airplane flies")
class Kangaroo:
    def swim(self):
        print("Kangaroo runs")
d=Duck()
a=Airplane()
k=Kangaroo()
for b in [d,a,k]:
   b.fly()
output:
Duck flies
Airplane flies
Traceback (most recent call last):
  File "example.py", line 18, in <module>
    b.fly()
AttributeError: 'Kangaroo' object has no attribute 'fly'

Duck typing is possible in certain statically typed languages by providing extra type annotations  that instruct the compiler to arrange for type checking of classes to occur at run-time rather than compile time, and include run-time type checking code in the compiled output.

Python allows a function to be defined inside another function. Such functions are called nested functions. In such case, a certain variable is located in the order of local, outer, global and lastly built-in namespace order. The inner function can access variable declared in nonlocal scope.

>>> def outer():
data=10
def inner():
print (data)
inner()
>>> outer()
10

Whereas the inner function is able to access the outer scope, inner function is able to use variables in the outer scope through closures. Closures maintain references to objects from the earlier scope. 

A closure is a nested function object that remembers values in enclosing scopes even if they are not present in memory.

Following criteria must be met to create a closure in Python:

  • We must have a nested function (function inside a function).
  • The nested function must refer to a value defined in the enclosing function.
  • The enclosing function must return the nested function.

Following code is an example of closure.

>>> def outer():
data=10
def inner():
print (data)
return inner
>>> closure=outer()
>>> closure()
10

A memory view object is returned by memoryview() a built-in function in standard library. This object is similar to a sequence but allows access to the internal data of an object that supports the buffer protocol without copying.

Just as in the C language, we access the memory using pointer variables; in Python; we use memoryview to access its referencing memory.

Buffer Protocol allows exporting an interface to access internal memory (or buffer). The built-in types bytes and bytearray supports this protocol and objects of these classes are allowed to expose internal buffer to allow to directly access the data. 

Memory view objects are required to access the memory directly instead of data duplication.

The memoryview() function has one object that supports the buffer protocol as argument.

>>> obj=memoryview(b'HEllo Python')
>>> obj
<memory at 0x7ffa12e2b048>

This code creates a memoryview of bytesarray

>>> obj1=memoryview(bytearray("Hello Python",'utf-8'))
>>> obj1
<memory at 0x7ffa12e2b108>

A memoryview supports slicing and indexing to expose its data.

>>> obj[7]
121

Slicing creates a subview

>>> v=obj[6:]
>>> bytes(v)
b'Python'

This, along with other Python advanced interview questions, is a regular feature in Python interviews. Be ready to tackle it with the approach mentioned.

Description

Python has turned out to be the most in-demand programming language in recent years. Many IT  companies that hunt for good Python programmers are ready to pay the best salaries to the eligible candidates. Hence, we have covered the top commonly asked Python interview questions to familiarize you with the knowledge and skills required to succeed in your next Python job interview.

It's time to view what companies use Python. Companies like Google, Facebook, Netflix etc uses Python. Among programming languages, Python is the most promising career option for techies. Companies today, both in India and the US, are looking out for the qualified and skilled workforce to meet the needs of customers. Python, SQL, Java, JavaScript, .NET, C, C#, AngularJS, C++, PHP, ReactJS, Android, iOS, Ruby, NodeJS, Go, and Perl is some of the hot skills that will help you rock for 2019 and beyond. According to GitHub and Stack Overflow, taking up Python Course will be an added advantage. A Python Developer will earn an average of 4,41,248  per year.

Interviews can be scary, but preparing these top 100 Python interview questions and answers will help you in pursuing your dream career. It’s important to be prepared to respond effectively to the questions that employers typically ask in an interview. Since these interview questions on Python are very common, your prospective recruiters will expect you to be able to answer. These Python interview questions and answers will increase your confidence that you need to ace the interview and motivation as well.

Going through these Python programming interview questions and answers will help you to land your dream job in Data Science, Machine Learning or just Python coding. These Python basic interview questions will surely boost your confidence to face an interview and will definitely prepare you to answer the toughest of questions in the best way possible. These Python developer interview questions are suggested by experts and have proven to have great value.

Not only the job aspirants but also the recruiters can also refer these Python basic interview questions to know the right set of questions to assess a candidate. It is easier to prepare for interviews while taking programming courses as well. Treat your next Python interview as an entrance to success. Give it your best and get the job. Wish you all the luck and confidence.

Read More
Levels