How to Stand Out in a Python Coding Interview - Functions, Data Structures & Libraries

Read it in 10 Mins

Last updated on
27th Apr, 2022
Published
19th Sep, 2019
Views
6,861
How to Stand Out in a Python Coding Interview - Functions, Data Structures & Libraries

Any coding interview is a test that primarily focuses on your technical skills and algorithm knowledge. However, if you want to stand out among the hundreds of interviewees, you should know how to use the common functionalities of Python in a convenient manner.

The type of interview you might face can be a remote coding challenge, a whiteboard challenge or a full day on-site interview. So, if you can prove your coding skills learnt in your python programming classes in the interview. You may go through some of the top Python interview questions and answers provided by experts which are divided into three levels- beginner, intermediate and advanced. A thorough practice of these questions and answers on Python will definitely help you achieve your dream job as a Python Developer, Full Stack engineer, and other top profiles.

A Python coding interview is basically a technical interview. They are not just about solving problems, they are more about how technically sound you are and how you can write clean productive Python code. This will show your depth of knowledge about Python and how you can use Python’s built-in functions and libraries to implement your code. Go through our best advanced python course to learn more about concepts related to Python. Let us look into some of the built-in functions provided by Python and how to select the correct one, learn about the effective use of data structures, how standard libraries in Python can be utilized and so on.

How to Select the Correct Built-in Function?

Python’s library of built-in functions is small as compared to the standard library. The built-in functions are always available and are not needed to be imported. It is suggested to learn each function before sitting for the interview. Till then, let us learn a few built-in functions and how to use them and also what alternatives can be used.

Perform iteration with enumerate() instead of range() 

Consider a situation during a coding interview: You have a list of elements and you have to iterate over the list with the access to both the indices and values. 

To differentiate between iteration with enumerate()  and iteration with range(), let us take a look at the classic coding interview question FizzBuzz. It can be solved by iterating over both indices and values. You will be given a list of integers and your task will be as follows:

  1. Replace all integers that are evenly distributed by 3 with “fizz”.
  2. Replace all integers divisible by 5 with “buzz”.
  3. Replace all integers divisible by 3 and 5 with “fizzbuzz”.

Developers make use of range() in these situations which can access the elements by index:

>>> list_num = [30, 29, 10, 65, 95, 99]
>>> for i in range(len(list_num)):
      if list_num[i] % 3 == 0 and list_num[i] % 5 == 0:
          list_num[i] = 'fizzbuzz'
      elif list_num[i] % 3 == 0:
          list_num[i] = 'fizz'
      elif list_num[i] % 5 == 0:
          list_num[i] = 'buzz'
 
>>> list_num
['fizzbuzz', 22, 14, 'buzz', 97, 'fizz']

Though range() can be used in a lot of iterative methods, it is better to use enumerate() in this case since it can access the element’s index and value at the same time:

>>> list_num = [30, 29, 10, 65, 95, 99]
>>> for i,num in enumerate(list_num):
      if list_num[i] % 3 == 0 and list_num[i] % 5 == 0:
          list_num[i] = 'fizzbuzz'
      elif list_num[i] % 3 == 0:
          list_num[i] = 'fizz'
      elif list_num[i] % 5 == 0:
          list_num[i] = 'buzz'

>>> list_num
['fizzbuzz', 22, 14, 'buzz', 97, 'fizz']

The enumerate() function returns a counter and the element value for each element. The counter is set to 0 by default which is also the element’s index.  

However, if you are not willing to start your counter from 0, you can set an offset using the start parameter:

>>> list_num = [30, 29, 10, 65, 95, 99]
>>> for i, num in enumerate(list_num, start=11):
      print(i, num) 
11 30
12 29
13 10
14 65
14 95
16 99

You can access all of the same elements using the start parameter. However, the count will start from the specified integer value.

Using List Comprehensions in place of map() and filter()

Python supports list comprehensions which are easier to read and are analogous in functionality as map() and filter(). This is one of the reasons why Guido van Rossum, the creator of Python felt that dropping map() and filter() was quite uncontroversial.

An example to show  map() along with this equivalent list comprehension:

>>> list_num = [1, 2, 3, 4, 5, 6]
>>> def square_num(z):
...    return z*z
...
>>> list(map(square_num, list_num))
[1, 4, 9, 16, 25, 36]

>>> [square_num(z) for z in numbers]
[1, 4, 9, 16, 25, 36]

Though map() and list comprehension return the same values the list comprehension part is easier to read and understand.

An example to show  filter() and its equivalent list comprehension:

>>> def odd_num_check(z):
      return bool(z % 2)
 
>>> list(filter(odd_num_check, num_list))
[1, 3, 5]

>>> [z for z in numbers if odd_num_check(z)]
[1, 3, 5]

It is the same with filter()as it was with map(). The return values are the same but the list comprehension is easier to follow.

List comprehensions are easier to read and beginners are able to catch it more intuitively.

Though other programming language developers might argue the fact but if you make use of list comprehensions during your coding interview, it is more likely to communicate your knowledge about the common functionalities to the recruiter.

Debugging With breakpoint() instead of print() 

Debugging is an essential part of writing software and it shows your knowledge of Python tools which will be useful in developing quickly in your job in the long run. However, using print() to debug a small problem might be good initially but your code will become clumsy. On the other hand, if you use a debugger like breakpoint(), it will always act faster than print().

If you’re using Python 3.7, you can simply call breakpoint() at the point in your code where you want to debug without the need of importing anything:

# Complicated Code With Bugs
...
...
...
breakpoint()

Whenever you call breakpoint(), you will be put into The Python Debugger - pdb. However, if you’re using Python 3.6 or older, you can perform an explicit importing which will be exactly like calling breakpoint():

import pdb; pdb.set_trace()

In this example, you’re being put into the pdb by the pdb.set_trace().  Since it’s a bit difficult to remember, it is recommended to use breakpoint() whenever a debugger is needed. There are also other debuggers that you can try. Getting used to debuggers before your interview would be a great advantage but you can always come back to pdb since it’s a part of the Python Standard Library and is always available. 

Formatting Strings with the help of f-Strings

It can be confusing to know what type of string formatting should we use since Python consists of a number of different string formatting techniques. However, it is a good approach and is suggested to use Python’s f-strings during a coding interview for Python 3.6 or greater.

Literal String Interpolation or f-strings is a powerful string formatting technique that is more readable, more concise, faster and less prone to error than other formatting techniques. It supports the string formatting mini-language which makes string interpolation simpler. You also have the option of adding new variables and Python expressions and they can be evaluated before run-time:

>>> def name_and_age(name, age):
      return f"My name is {name} and I'm {age / 10:.5f} years old."
 
>>> name_and_age("Alex", 21)
My name is Alex and I'm 2.10000 years old.

The f-string allows you to add the name Alex into the string and his corresponding age with the type of formatting you want in one single operation.

Note that it is suggested to use Template Strings if the output consists of user-generated values.

Sorting Complex Lists with sorted()

There are a lot of interview questions that are mostly based on sorting and it is one of the most important concepts you should be clear about before you sit for a coding interview. However, it is always a better option to use sorted() unless you are asked to make your own sorting algorithm by the interviewer.

Example code to illustrate simple uses of sorting like sorting numbers or strings:

>>> sorted([6,5,3,7,2,4,1])
[1, 2, 3, 4, 5, 6, 7]

>>> sorted(['IronMan', 'Batman', 'Thor', 'CaptainAmerica', 'DoctorStrange'], reverse=False)
['Batman', 'CaptainAmerica', 'DoctorStrange', 'IronMan', 'Thor']

sorted() performs sorting in ascending order by default and also when the reverse argument is set to False. 

If you sorting complex data types, you might want to add a function that allows custom sorting rules:

>>> animal_list = [
...    {'type': 'bear', 'name': 'Stephan', 'age': 9},
...    {'type': 'elephant', 'name': 'Devory', 'age': 5},
...    {'type': 'jaguar', 'name': 'Moana', 'age': 7},
... ]
>>> sorted(animal_list, key=lambda animal: animal['age'])
[
    {'type': 'elephant', 'name': 'Devory', 'age': 5},
    {'type': 'jaguar', 'name': 'Moana', 'age': 7},
    {'type': 'bear, 'name': 'Stephan, 'age': 9},
]

You can easily sort a list of dictionaries using the lambda keyword. In the example above, the lambda returns each element’s age and the dictionary is sorted in ascending order by age.

Effective Use of Data Structures

Data Structures are one of the most important concepts you should know before getting into an interview and if you choose the perfect data structure during an interviewing context, it will certainly impact your performance. 

Python’s standard data structure implementations are incredibly powerful and give a lot of default functionalities which will surely be helpful in coding interviews.

Storing Values with Sets

Make use of sets instead of lists whenever you want to remove duplicate elements from an existing dataset.

Consider a function random_word that always returns a random word from a set of words:

>>> import random
>>> words = "all the words in the world".split()
>>> def random_word():
      return random.choice(words)

In the example above, you need to call random_word repeatedly to get 1000 random selections and then return a data structure that will contain every unique word.

Let us look at three approaches to execute this – two suboptimal approaches and one good approach.

Bad Approach 

An example to store values in a list and then convert them into a set:

>>> def unique_words():
      words = []
      for _ in range(1000):
          words.append(random_word())
      return set(words)
>>> unique_words()
{'planet', 'earth', 'to', 'words'}

In this example, creating a list and then converting it into a set is an unnecessary approach. Interviewers notice this type of design and questions about it generally.

Worse Approach

You can store values into a list to avoid the conversion from a list to a set. You can then check for uniqueness by comparing new values with all current elements in the list:

>>> def unique_words():
      words = []
      for _ in range(1000):
    word = unique_words()
    if word not in words:
    words.append(word)
      return words
>>> unique_words()
{'planet', 'earth', 'to', 'words'}

This approach is much worse than the previous one since you have to compare every word to every other word already present in the list. In simple terms, the time complexity is much greater in this case than the earlier example.

Good Approach

In this example, you can skip the lists and use sets altogether from the beginning:

>>> def unique_words():
      words = set()
      for _ in range(1000):
          words.add(random_word())
      return words
>>> unique_words()
{'planet', 'earth', 'to', 'words'}

This approach differs from the second approach as the storing of elements in this approach allows near-constant-time-checks whether a value is present in the set or not whereas linear time-lookups were required when lists were used. The time complexity for this approach is O(N) which is much better than the second approach whose time complexity grew at the rate of O(N²).

Saving Memory with Generators

Though lists comprehensions are convenient tools, it may lead to excessive use of memory.

Consider a situation where you need to find the sum of the first 1000 squares starting with 1 using list comprehensions:

>>> sum([z * z for z in range(1, 1001)])
333833500

Your solution returns the correct answer by making a list of every perfect square and then sums the values. However, the interviewer asks you to increase the number of perfect squares. 

Initially, your program might work well but it will gradually slow down and the process will be changed completely.  

However, you can resolve this memory issue just by replacing the brackets with parentheses:

>>> sum((z * z for z in range(1, 1001)))
333833500

When you make the change from brackets to parentheses, the list comprehension changes to generator expressions. It returns a generator object. The object calculates the next value only when asked. 

Generators are mainly used on massive sequences of data and in situations when you want to retrieve data from a sequence but don’t want to access all of it at the same time.

Defining Default Values in Dictionaries with .get() and .setdefault()

Adding, modifying, or retrieving an item from a dictionary is one of the most primitive tasks of programming and it is easy to perform with Python functionalities. However, developers often check explicitly for values even its not necessary.

Consider a situation where a dictionary named shepherd exists and you want to get that cowboy’s name by explicitly checking for the key with a conditional:

>>> shepherd = {'age': 20, 'sheep': 'yorkie', 'size_of_hat': 'large'}
>>> if 'name' in shepherd:
      name = shepherd['name']
    else:
      name = 'The Man with No Name'
 
>>> name

In this example, the key name is searched in the dictionary and the corresponding value is returned otherwise a default value is returned.

You can use .get() in a single line instead of checking keys explicitly:

>>> name = shepherd.get('name', 'The Man with No Name')

The get() performs the same operation as the first approach does, but they are now handled automatically. 

However, .get() function does not help in situations where you need to update the dictionary with a default value while still accessing the same key. In such a case, you again need to use explicit checking:

>>> if 'name' not in shepherd:
      shepherd['name'] = 'The Man with No Name'
 
>>> name = shepherd['name']

However, Python still offers a more elegant way of performing this approach using .setdefault():

>>> name = shepherd.setdefault('name', 'The Man with No Name')

The .setdefault() function performs the same operation as the previous approach did. If name exists in shepherd, it returns a value otherwise it sets shepherd[‘name’]  to The Man with No Name and returns a new value.

Taking Advantage of the Python Standard Library

Python functionalities are powerful on their own and all the things can be accessed just by using the import statement. If you know how to make good use of the standard library, it will boost your coding interview skills.

How to handle missing dictionaries?

You can use .get() and .setdefault() when you want to set a default for a single key. However, there will be situations where you will need to set a default value for all possible unset keys, especially during the context of a coding interview.

Consider you have a  group of students and your task is to keep track of their grades on assignments. The input value is a tuple with student_name and grade. You want to look upon all the grades for a single student without iterating over the whole list. 

An example to store grade data using a dictionary:

>>> grades_of_students = {}
>>> grades = [
      ('alex', 89),
      ('bob', 95),
      ('charles', 81),
      ('alex', 94),
      ]
>>> for name, grade in grades:
      if name not in grades_of_student:
          grades_of_student[name] = []
      grades_of_student[name].append(grade)

>>> student_grades
{'alex': [89, 94], 'bob': [95], 'charles': [81]}

In the example above, you iterate over the list and check if the names are already present in the dictionary or not. If it isn’t, then you add them to the dictionary with an empty list and then append their actual grades to the student’s list of grades.

However, the previous approach is good but there is a cleaner approach for such cases using the defaultdict:

>>> from collections import defaultdict
>>> student_grades = defaultdict(list)
>>> for name, grade in grades:
      student_grades[name].append(grade)

In this approach, a defaultdict is created that uses the list() with no arguments. The list()returns an empty list. defaultdict calls the list() if the name does not exist and then appends the grade.

Using the defaultdict, you can handle all the common default values at once and need not worry about default values at the key level. Moreover, it generates a much cleaner application code.

How to Count Hashable Objects?

Pretend you have a long string of words with no punctuation or capital letters and you are asked to count the number of the appearance of each word. In this case, you can use collections.Counter that uses 0 as the default value for any missing element and makes it easier and cleaner to count the occurrence of different objects:

>>> from collections import Counter
>>> words = "if I am there but if \
... he was not there then I was not".split()
>>> counts = Counter(words)
>>> counts
Counter({'if': 2, 'there': 2, 'was': 1, 'not': 2, 'but': 1, ‘I’: 2, ‘am’: 1, }

When the list is passed to Counter, it stores each word and also the number of occurrences of that word in the list.

If you want to know the two most common words in a list of strings like above, you can use .most_common() which simply returns the n most frequently inputs by count:

>>> counts.most_common(2)
[('if': 2), ('there': 2), ('not': 2), (‘I’: 2)]
How to Access Common String Groups?

If you want to check whether ‘A’ > ‘a’ or not, you have to do it using the ASCII chart. The answer will be false since the ASCII value for A is 65 and a is 97, which is clearly greater. 

However, it would be a difficult task to remember the ASCII code when it comes to lowercase and uppercase ASCII characters and also this method is a bit clumsy. You can use the much easier and more convenient constants which are a part of the string module

An example to check whether all the characters in a string are uppercase or not:

>>> import string
>>> def check_if_upper(word):
      for letter in word:
          if letter not in string.ascii_uppercase:
              return False
      return True
 
>>> check_if_upper('Thanks Alex')
False
>>> check_if_upper('ROFL')
True

The function check_if_upper iterates over the letters in words and checks whether the letters are part of string.ascii_uppercase. It is set to the literal ‘ABCDEFGHIJKLMNOPQRSTUVWXYZ’.

There are a number of string constants that are frequently used for referencing string values that are easy to read and use. Some of which are as follows:

  • string.ascii_letters
  • string.ascii_upercase
  • string.ascii_lowercase
  • string.ascii_digits
  • string.ascii_hexdigits
  • string.ascii_octdigits
  • string.ascii_punctuation
  • string.ascii_printable
  • string.ascii_whitespace

Conclusion

Clearing interview with confidence and panache is a skill. You might be a good programmer but it’s only a small part of the picture. You might fail to clear a few interviews, but if you follow a good process, it will certainly help you in the long run. Being enthusiastic is an important factor that will have a huge impact on your interview results. In addition to that is practice. Practice always helps. Brush up on all the common interview concepts and then head off to practicing different interview questions. Interviewers also help during interviews if you can communicate properly and interact. Ask questions and always talk through brute-force and optimized solution.

Let us now sum up what we have learned in this article so far:

  • To use enumerate() to iterate over both indices and values.
  • To debug problematic code with breakpoint().
  • To format strings effectively with f-strings.
  • To sort lists with custom arguments.
  • To use generators instead of list comprehensions to save memory.
  • To define default values when looking up dictionary keys.
  • To count hashable objects with collections.Counter class.

Hope you have learned about most of the powerful Python’s built-in functions, data structures, and standard library packages that will help you in writing better, faster, and cleaner code. Though there are a lot of other things to learn about the language, join Knowledgehut python programming classes to gain more skills and knowledge. 

Profile

Priyankur Sarkar

Data Science Enthusiast

Priyankur Sarkar loves to play with data and get insightful results out of it, then turn those data insights and results in business growth. He is an electronics engineer with a versatile experience as an individual contributor and leading teams, and has actively worked towards building Machine Learning capabilities for organizations.