On this page
- 1. What is the difference between a list, a tuple, a set and a dictionary?
- 2. What is the difference between == and is?
- 3. What is the mutable default argument trap?
- 4. What is a list comprehension?
- 5. What are *args and **kwargs?
- 6. What is the difference between a shallow copy and a deep copy?
- 7. How do you count how often each item appears?
- 8. How do you remove duplicates and keep the original order?
- 9. What is a generator, and why use one?
- 10. How do you handle errors?
- How to prepare
Fresher interviews do not ask about advanced Python. They ask the same 10 or so questions, to see whether you understand the basics well enough to explain them. This guide answers them with code you can run. Every output below came from really running the code, so you can trust what each snippet prints.
What is the difference between a list, a tuple, a set and a dictionary?
All four hold several values. They differ in whether they can change, whether they keep order, and whether they allow repeats.
colors = ["red", "green", "red"] # list: ordered, can change, repeats allowed
point = (10, 20) # tuple: ordered, cannot change
unique = {"red", "green", "red"} # set: no repeats, no order to rely on
person = {"name": "Asha", "age": 25} # dict: name -> value pairs
colors.append("blue")
print(colors)
print(len(unique))
print(person["name"])['red', 'green', 'red', 'blue'] 2 Asha
point[0] = 99TypeError: 'tuple' object does not support item assignment
The list grew, the set kept only two colours, and the dictionary was read by key. The tuple refused to change, because tuples are immutable (cannot be modified). Use a list for a collection that grows, a tuple for a fixed pair such as coordinates, a set to remove duplicates or test membership, and a dictionary to look values up by name.
What is the difference between == and is?
== asks "are the values equal?" and is asks "is this the very same object in memory?"
a = [1, 2, 3]
b = [1, 2, 3]
c = a
print(a == b)
print(a is b)
print(a is c)True False True
a and b hold equal values but are two separate lists, so == is True and is is False. c is another name for the same list as a, so is is True. Use == to compare values, and is only to compare with None, as in if x is None.
What is the mutable default argument trap?
A default value in a function definition is created once, when the function is defined, not each time it is called. If that default is a list, every call shares it.
def add_item(item, basket=[]):
basket.append(item)
return basket
print(add_item("pen"))
print(add_item("book"))['pen'] ['pen', 'book']
The second call returned ['pen', 'book'], although you might expect only ['book']. The same list was reused. The fix is to use None as the default and create a new list inside the function:
def add_item_safe(item, basket=None):
if basket is None:
basket = []
basket.append(item)
return basket
print(add_item_safe("pen"))
print(add_item_safe("book"))['pen'] ['book']
What is a list comprehension?
A short way to build a list from another list, with an optional filter.
amounts = [1200, 800, 450, 2500, 300]
big = []
for amount in amounts:
if amount > 500:
big.append(amount)
big_short = [amount for amount in amounts if amount > 500]
print(big)
print(big_short)
print(big == big_short)[1200, 800, 2500] [1200, 800, 2500] True
Both ways produce the same list, and the comprehension fits on one line. Use it for short, simple cases, and a normal loop when the logic needs several steps.
What are *args and **kwargs?
They let a function accept any number of extra values. *args collects extra positional values into a tuple, and **kwargs collects extra named values into a dictionary.
def describe(*args, **kwargs):
print("args:", args)
print("kwargs:", kwargs)
describe(1, 2, city="Pune", amount=1200)args: (1, 2)
kwargs: {'city': 'Pune', 'amount': 1200}The names args and kwargs are only a convention. The stars are what matter. You will see them in libraries that pass options through to other functions.
What is the difference between a shallow copy and a deep copy?
A shallow copy makes a new outer list but shares the inner objects. A deep copy copies everything, all the way down.
The outer list is new.
Both lists point to the same inner lists.
Result: [[1, 2, 99], [3, 4]]
Inner lists are copied too.
The original's change cannot reach it.
Result: [[1, 2], [3, 4]]
import copy
original = [[1, 2], [3, 4]]
shallow = copy.copy(original)
deep = copy.deepcopy(original)
original[0].append(99)
print("original:", original)
print("shallow: ", shallow)
print("deep: ", deep)original: [[1, 2, 99], [3, 4]] shallow: [[1, 2, 99], [3, 4]] deep: [[1, 2], [3, 4]]
Changing an inner list of the original also changed the shallow copy, because both still point to the same inner list. The deep copy is unaffected. Remember this when you copy a list of lists or a dictionary of dictionaries.
How do you count how often each item appears?
A very common small task. The standard library has Counter for exactly this.
from collections import Counter
words = "pune mumbai pune chennai pune mumbai".split()
counts = Counter(words)
print(counts)
print(counts.most_common(2))Counter({'pune': 3, 'mumbai': 2, 'chennai': 1})
[('pune', 3), ('mumbai', 2)]Pune appeared 3 times, Mumbai twice and Chennai once. If an interviewer asks you to do it without Counter, use a dictionary and counts[word] = counts.get(word, 0) + 1, as in the Python for data engineering guide.
How do you remove duplicates and keep the original order?
A set removes duplicates but loses order. A dictionary keeps insertion order, so its keys give you both.
cities = ["Pune", "Mumbai", "Pune", "Chennai", "Mumbai"]
print(list(set(cities)) == cities)
print(list(dict.fromkeys(cities)))False ['Pune', 'Mumbai', 'Chennai']
dict.fromkeys(cities) keeps each city once, in the order it first appeared. The first line printed False because a set does not keep the original sequence. Practise this with the free Remove Duplicates Preserving Order problem.
What is a generator, and why use one?
A generator produces values one at a time, only when asked, instead of building the whole list first. For large data this saves memory.
def squares(n):
for i in range(n):
yield i * i
gen = squares(5)
print(next(gen))
print(next(gen))
print(list(gen))0 1 [4, 9, 16]
The first 2 calls to next returned 0 and 1, and list(gen) collected the values that were still left (4, 9, 16). The function pauses at yield and continues from there next time. Generators are how Python reads a huge file line by line without loading it all.
How do you handle errors?
Put the risky code in try, handle the failure in except, and use finally for code that must always run.
def to_number(text):
try:
return int(text)
except ValueError:
return None
finally:
print("checked", repr(text))
print(to_number("42"))
print(to_number("n/a"))checked '42' 42 checked 'n/a' None
Both calls printed the "checked" line, because finally always runs. The second returned None instead of crashing. Catch the specific error you expect, here ValueError, rather than everything, so real bugs do not get hidden.
How to prepare
- Run every snippet above yourself and change a value to see what happens.
- Explain each answer out loud in two sentences, as if to a friend.
- Move on to problems: the free Python questions on Python practice check your code against hidden tests.
- For the next step, read pandas for beginners, because data engineering interviews move from plain Python to tables quickly.