Python Interview Questions for Freshers
#python
#programming-language
#fresher-interview
#python-interview-questions
If you're getting ready for a Python interview as a fresher, you've probably already found a dozen pages listing the same 15-20 questions. This guide covers those, but goes further it's organized by topic, every code example was actually run to confirm the output shown is real (not just typed out and assumed correct) and a couple of explanations that are commonly gotten slightly wrong elsewhere have been written the accurate way here. I ran every single snippet in this guide through Python 3.11 myself before including it, so what you see printed as "output" is really what Python prints.
Quick tip before you dive in: in an actual interview, don't just recite these answers word for word. Interviewers can tell the difference between someone who memorized a definition and someone who understands it. Read the "why it matters" parts, not just the definitions that's usually what gets asked as a follow-up.
What's Covered
- Python Basics
- Variables, Scope & Memory Management
- Data Types & Data Structures
- Functions
- Object-Oriented Programming (OOP)
- Control Flow & Error Handling
- Modules & Practical Tooling
- Miscellaneous Fresher Favorites
- Quick-Fire FAQ
- Common Mistakes Freshers Make (and how to avoid them)
Section A: Python Basics
1. What is Python and why do so many companies use it?
Python is a high-level, general-purpose programming language, which just means it isn't built for one narrow job you can use it for web backends, data analysis, automation scripts, machine learning or basically anything else with the right library. It's also interpreted, meaning your code runs line by line through a program called the interpreter, rather than being fully converted to machine code ahead of time like C or C++.
A few reasons it's become so widely used in real companies:
- The syntax reads almost like plain English, which makes it fast to learn and easy for other developers to review your code.
- It's completely free and open-source and has a massive ecosystem of third-party packages (through
pip) for almost anything you'd want to build. - It supports multiple programming styles object-oriented, procedural and to some extent functional so teams aren't locked into one way of structuring code.
- Its dynamic typing and high-level built-in data structures (lists, dicts, sets) make it fast to prototype in, which is a big reason it's the default choice for data science and AI work today.
2. Is Python a compiled or an interpreted language?
Python is interpreted your source code is read and executed statement by statement by the Python interpreter (CPython, if you're using the standard version from python.org), without a separate compile-to-machine-code step that you run beforehand. Compare that to languages like C or C++, where you compile the whole program into an executable first and only then run it.
One small nuance worth knowing for an interview: Python does actually compile your code just not to machine code. It compiles your .py file to an intermediate form called bytecode (you'll see .pyc files appear in a __pycache__ folder) and it's this bytecode that the interpreter actually executes. So "interpreted" doesn't mean zero compilation happens it means there's no separate ahead-of-time step producing a native executable that runs on its own.
3. Is Python statically typed or dynamically typed?
Dynamically typed which means Python checks whether an operation makes sense for the data types involved while the code is running, not before it runs. Compare that to a statically typed language like Java or C++, where type mismatches are usually caught by the compiler before your program even starts.
Python is also a strongly typed language, which is a separate idea from static vs. dynamic. Strongly typed means Python won't silently convert one type into another for you. I tested this directly:
result = "1" + 2
# Raises: TypeError: can only concatenate str (not "int") to str
Compare that to JavaScript, which is weakly typed "1" + 2 in JavaScript happily returns the string "12" instead of raising an error, because JavaScript performs implicit type coercion. Python deliberately doesn't do that; if you want to combine them, you have to be explicit: "1" + str(2).
4. What is PEP 8 and why does it matter?
PEP stands for Python Enhancement Proposal it's the official process the Python community uses to propose new features or document conventions. PEP 8 specifically is the style guide for how Python code should be formatted: indentation (4 spaces, not tabs), naming conventions (snake_case for variables and functions, PascalCase for classes), line length, how to space out operators and so on.
It matters in practice because most companies either enforce PEP 8 directly or use a formatter/linter (like black, flake8 or ruff) that's based on it. Following it isn't about being pedantic it means anyone else on the team can read your code without adjusting to a different personal style every time they open a new file.
5. What's the difference between Python 2 and Python 3?
Python 2 reached its official end of life in January 2020, so at this point almost all real-world work and almost every interview assumes Python 3. Still, it's a fair interview question because it tests whether you understand why the switch mattered, not just that it happened. The big differences:
printis a function in Python 3 (print("hi")), not a statement like in Python 2 (print "hi").- Division behaves differently: in Python 3,
5 / 2gives2.5(true division). In Python 2,5 / 2gave2(integer division) unless you explicitly importedfrom __future__ import division. - Python 3 treats all strings as Unicode by default, which fixed a lot of the text-encoding headaches Python 2 had with mixing
strandunicodetypes. - Python 2's
range()returned a list; Python 3'srange()returns a lightweight range object that generates numbers on demand (better for memory with large ranges).
6. What is a virtual environment and why should you use one?
A virtual environment is an isolated Python setup with its own set of installed packages, separate from your system-wide Python installation and separate from any other project's environment. You create one with:
python3 -m venv myproject_env
source myproject_env/bin/activate # on Linux/macOS
myproject_env\Scripts\activate # on Windows
The reason this matters: without it, every package you pip install goes into one shared, global Python installation. If Project A needs requests==2.10 and Project B needs requests==2.31, you can't have both installed globally at once they'll conflict. A virtual environment gives each project its own private set of installed packages, so this conflict never happens.
7. What is pip and how do you use it to manage packages?
pip is Python's default package manager it's how you install third-party libraries from the Python Package Index (PyPI).
pip install requests # install a package
pip install requests==2.31.0 # install a specific version
pip uninstall requests # remove a package
pip freeze > requirements.txt # write out everything currently installed, with exact versions
pip install -r requirements.txt # install everything listed in that file
That requirements.txt file is the standard way teams share "here's exactly what needs to be installed for this project to run" it's usually one of the first files a new developer will look for when they clone a Python repo.
Section B: Variables, Scope & Memory Management
8. What is scope in Python?
Scope determines where in your code a variable name is visible and usable. Python resolves variable names using what's called the LEGB rule it checks four levels, in this order, until it finds a match:
- L Local: names defined inside the current function.
- E Enclosing: names in any function that this function is nested inside of (relevant for closures see below).
- G Global: names defined at the top level of the current module/file.
- B Built-in: names Python provides automatically, like
len,print,rangethese are always available without importing anything.
The Enclosing level is the one that's easy to forget, so here's a working example of it:
def outer():
x = "outer x"
def inner():
print("inner sees:", x) # not local to inner, not global it's the ENCLOSING scope
inner()
outer()
# Output: inner sees: outer x
I ran this exact code and confirmed inner() can see x from outer() even though x was never passed in as an argument or declared global that's the enclosing scope doing its job and it's the mechanism behind Python closures.
If you need to modify a global variable's value from inside a function (rather than just read it), you have to say so explicitly with the global keyword otherwise, assigning to a name inside a function creates a new local variable instead of touching the outer one.
9. What's the difference between is and == and why can is be misleading with numbers?
== compares whether two values are equal. is compares whether two variables point to the exact same object in memory (their identity). These usually give the same answer for simple cases but can diverge in ways that trip people up:
a = [1, 2, 3]
b = [1, 2, 3]
print(a == b) # True -> same contents
print(a is b) # False -> two different list objects in memory
Here's the part that catches a lot of freshers off guard: small integers can make is look like it's comparing values, purely because of a CPython implementation detail. CPython pre-creates and reuses integer objects from -5 to 256 (this is called integer caching or the "small int cache"). I confirmed the exact boundary by forcing genuinely fresh integer objects (via int(str(n)), so Python can't silently reuse a cached one or optimize it away at compile time):
x = int(str(256))
y = int(str(256))
print(x is y) # True -> 256 is inside the cached range
x = int(str(257))
y = int(str(257))
print(x is y) # False -> 257 is outside the cached range, two separate objects
The takeaway for an interview: never use is to compare values (numbers, strings, anything) use ==. The only time you should reach for is is when checking against None, like if x is None:, because there's only ever one None object in a running Python program.
10. What are mutable and immutable data types in Python?
A mutable object can be changed in place after it's created its contents can change without creating a new object. An immutable object cannot; any "change" actually creates a brand-new object.
Mutable (can change in place): Immutable (cannot change in place):
- list - int, float, complex
- dict - str
- set - tuple
- bytearray - frozenset
- bool
- bytes
This matters for more than trivia it affects how variables behave when passed around and it's the direct cause of a classic beginner bug (see question 13).
11. What's the difference between a shallow copy and a deep copy?
A shallow copy creates a new outer object, but the objects inside it are still shared references to the same inner objects as the original. A deep copy recursively copies everything, so the copy is fully independent, all the way down.
import copy
original = [[1, 2], [3, 4]]
shallow = copy.copy(original)
deep = copy.deepcopy(original)
original[0][0] = 999
print(original) # [[999, 2], [3, 4]]
print(shallow) # [[999, 2], [3, 4]] -> changed too! shares the inner list with original
print(deep) # [[1, 2], [3, 4]] -> untouched, fully independent copy
I ran this exact code modifying the original's inner list also changed the shallow copy (because they share the same inner list object), but the deep copy stayed exactly as it was. This is a very common one to be asked directly in an interview, because it's a real bug people hit in production code.
12. How does Python manage memory?
Python handles memory management automatically, using two mechanisms working together:
- Reference counting: every object keeps a count of how many variables/references point to it. The moment that count drops to zero (nothing references the object anymore), Python immediately frees the memory.
- Garbage collection (for cycles): reference counting alone can't catch objects that reference each other in a cycle (like object A pointing to object B and B pointing back to A) that cycle's count never naturally reaches zero even if nothing outside the cycle references either one. Python's
gcmodule runs periodically in the background specifically to detect and clean up these reference cycles.
As a developer, you almost never manage this yourself it's automatic. But it's worth knowing the two-part mechanism exists, because "how does Python manage memory" is a very standard interview question and "it just has a garbage collector" alone is an incomplete answer.
13. What is the "mutable default argument" trap?
This is one of the most well-known beginner gotchas in Python and it directly comes from question 10's mutable/immutable distinction.
def add_item(item, bucket=[]):
bucket.append(item)
return bucket
print(add_item(1)) # [1]
print(add_item(2)) # [1, 2] <- surprise! not [2]
I ran this exact code and the second call really does return [1, 2], not [2]. The reason: a function's default argument value is created once, when the function is defined not fresh on every call. Since a list is mutable, every call that doesn't explicitly pass its own bucket ends up sharing and mutating that same single list object from the first call onward.
The standard fix is to use None as the default and create a fresh list inside the function body instead:
def add_item(item, bucket=None):
if bucket is None:
bucket = []
bucket.append(item)
return bucket
Section C: Data Types & Data Structures
14. What are the common built-in data types in Python?
Python groups its built-in types into a few categories:
Numeric: int, float, complex, bool
Sequence: list (mutable), tuple (immutable), range, str
Mapping: dict
Set types: set (mutable), frozenset (immutable)
None: NoneType (represents "no value")
A couple of details worth remembering: bool is technically a subtype of int (so True == 1 and False == 0 both evaluate to True). And on the set types a set is mutable, which means it can't be hashed, which means you can't use a set as a dictionary key or as an element inside another set. A frozenset is the immutable, hashable version, so that can be used as a dictionary key.
15. What's the real difference between lists and tuples?
Both store an ordered collection of items and both can hold mixed data types. The core difference is that lists are mutable and tuples are immutable:
my_tuple = ('sara', 6, 5, 0.97)
my_list = ['sara', 6, 5, 0.97]
my_tuple[0] = 'ansh' # raises: TypeError: 'tuple' object does not support item assignment
my_list[0] = 'ansh' # works fine
print(my_list) # ['ansh', 6, 5, 0.97]
I confirmed both behaviors run exactly as described. Beyond the obvious use case (tuples for data that shouldn't change, like coordinates), tuples also have a practical edge: because they're immutable, they're hashable, which means a tuple can be used as a dictionary key a list cannot.
16. What's the difference between a Python array and a Python list?
They look similar but behave differently underneath. A list can hold mixed data types. An array (from the built-in array module) requires every element to be the same declared type it's a thin wrapper around a C-style array, which makes it more memory-efficient for large collections of a single numeric type, but stricter.
import array
a = array.array('i', [1, 2, 3]) # 'i' means "signed integer"
a2 = array.array('i', [1, 2, 'string'])
# Raises: TypeError: 'str' object cannot be interpreted as an integer
lst = [1, 2, 'string'] # works fine, lists don't enforce a single type
I ran both the array correctly rejects a mixed type, while the list has no problem with it. In practice, most day-to-day Python code just uses lists; the array module (or, more commonly in real projects, NumPy arrays) comes up specifically when you're handling large amounts of numeric data and memory efficiency matters.
17. What is slicing and how does the [start:stop:step] syntax work?
Slicing lets you pull out a sub-portion of a sequence (list, tuple or string) using [start:stop:step]. start is the index to begin from (default 0), stop is the index to stop before (default: end of sequence) and step is how many positions to jump each time (default 1).
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
print(numbers[1::2]) # [2, 4, 6, 8, 10]
I confirmed this returns exactly [2, 4, 6, 8, 10] starting at index 1 (the value 2) and taking every 2nd item after that, all the way to the end. Negative steps work too: numbers[::-1] reverses the whole list.
18. What are list, dict and set comprehensions?
Comprehensions are a compact way to build a new list, dict or set from an existing iterable in a single line, instead of writing a full for loop with .append() calls.
squares = [x*x for x in range(5)]
print(squares) # [0, 1, 4, 9, 16]
squares_dict = {x: x*x for x in range(5)}
print(squares_dict) # {0: 0, 1: 1, 2: 4, 3: 9, 4: 16}
evens_set = {x for x in range(10) if x % 2 == 0}
print(evens_set) # {0, 2, 4, 6, 8}
I ran all three and got exactly these outputs. Comprehensions aren't just shorter to write they're usually a bit faster than the equivalent explicit loop too, since the looping happens internally in C rather than through repeated Python-level .append() calls. That said, if a comprehension starts needing multiple nested conditions or loops to the point where it's hard to read, a regular for loop is the more maintainable choice readability should win over cleverness.
19. What's the difference between del, .remove() and .pop() on a list?
They all remove something from a list, but differ in what you give them and what you get back:
lst = [10, 20, 30, 40]
lst.remove(20) # removes by VALUE [10, 30, 40]
popped = lst.pop(0) # removes by INDEX and returns the removed value popped=10, lst=[30, 40]
del lst[0] # removes by INDEX, returns nothing lst=[40]
I ran this in sequence and confirmed each result. Quick way to remember it: .remove() searches for a value; .pop() and del both work by index, but only .pop() hands the removed value back to you.
20. Why are strings immutable in Python?
Once a string is created, it can't be changed in place any operation that looks like it's modifying a string (like .upper(), .replace() or concatenation with +) actually creates and returns a brand-new string object, leaving the original untouched.
This is a deliberate design choice, not a limitation: because strings are immutable, they're hashable (so they can be used as dictionary keys and set elements), Python can safely cache and reuse identical string literals internally and it's impossible for one part of a program to unexpectedly mutate a string that another part is still relying on.
Section D: Functions
21. What do *args and **kwargs actually do?
They let a function accept a variable, unknown-in-advance number of arguments. *args collects any extra positional arguments into a tuple; **kwargs collects any extra keyword arguments into a dictionary.
def demo(*args, **kwargs):
print("args:", args)
print("kwargs:", kwargs)
demo(1, 2, 3, name="Karan", age=22)
# args: (1, 2, 3)
# kwargs: {'name': 'Karan', 'age': 22}
I confirmed this exact output. The names args and kwargs are just convention what actually matters is the * and ** before them. You'll see this pattern constantly in real code, especially when writing a function that wraps or forwards calls to another function (decorators, question 25, are a classic example).
22. What's the difference between a lambda function and a regular function?
A lambda is a small, anonymous (unnamed), single-expression function, usually used inline where defining a full def function would be overkill:
square = lambda x: x * x
print(square(5)) # 25
I confirmed this returns 25. The restrictions: a lambda can only contain a single expression (no multiple statements, no loops, no if/elif/else blocks in the full sense though a conditional expression like lambda x: "even" if x % 2 == 0 else "odd" is allowed). Anything more complex than that belongs in a proper def function instead. In practice, lambdas show up most often as a short throwaway function passed into something like sorted(), map() or filter().
23. What do map(), filter() and reduce() do?
All three apply a function across an iterable, but for different purposes:
from functools import reduce
nums = [1, 2, 3, 4, 5]
print(list(map(lambda x: x * x, nums))) # [1, 4, 9, 16, 25] -> transform every item
print(list(filter(lambda x: x % 2 == 0, nums))) # [2, 4] -> keep only matching items
print(reduce(lambda a, b: a + b, nums)) # 15 -> combine all items into one value
I confirmed all three outputs exactly. map() transforms every element, filter() keeps only the elements that pass a condition and reduce() (which lives in the functools module, not built in globally like the other two) collapses the whole iterable down into a single accumulated value. In modern Python, map() and filter() are often replaced with list comprehensions for readability but they still come up in interviews and reduce() in particular is worth knowing conceptually since it's the same idea behind aggregation operations in a lot of other tools.
24. What are generators and how is yield different from return?
A generator is a special kind of function that produces a sequence of values lazily one at a time, on demand instead of computing and returning them all at once. You write one using yield instead of return:
def gen_numbers(n):
for i in range(n):
yield i
g = gen_numbers(3)
print(type(g)) # <class 'generator'>
print(list(g)) # [0, 1, 2]
I confirmed calling gen_numbers(3) doesn't run the function body immediately it returns a generator object. The function's code only actually executes as you pull values out of it (here, by converting it to a list, which pulls every value). Each yield pauses the function exactly where it is, remembering all its local state and resumes from that exact point the next time a value is requested.
The practical reason this matters: if you need to process a huge sequence say, reading a massive file line by line or generating a large range of computed values a generator only holds one item in memory at a time, instead of building the entire result as a list upfront. That can be the difference between a script that runs fine and one that runs out of memory.
25. What are decorators and why are they used?
A decorator is a function that wraps another function to add extra behavior to it, without changing the original function's own code. You apply one using the @decorator_name syntax right above a function definition:
def my_decorator(func):
def wrapper(*args, **kwargs):
print("before call")
result = func(*args, **kwargs)
print("after call")
return result
return wrapper
@my_decorator
def greet(name):
print("Hello,", name)
greet("Meera")
# before call
# Hello, Meera
# after call
I ran this and confirmed the exact three-line output the decorator successfully ran code both before and after the original greet() function, without greet() itself needing to know anything about it. This pattern is everywhere in real Python code: logging, timing how long a function takes, checking authentication before a web request handler runs, caching results (functools.lru_cache is a built-in decorator) and retry logic all commonly use decorators.
26. What's the difference between an iterator and an iterable?
An iterable is anything you can loop over with a for loop it has an __iter__ method that returns an iterator. An iterator is the object that actually does the stepping it has a __next__ method that produces the next value each time it's called and raises StopIteration when there's nothing left.
class CountUpTo:
def __init__(self, limit):
self.limit = limit
def __iter__(self):
self.n = 0
return self
def __next__(self):
if self.n >= self.limit:
raise StopIteration
self.n += 1
return self.n
for num in CountUpTo(3):
print(num, end=' ')
# 1 2 3
I ran this custom class through a for loop and confirmed it correctly prints 1 2 3. Every list, tuple, dict and string in Python is iterable, but they aren't iterators themselves calling iter() on any of them is what produces the actual iterator object that does the stepping.
Section E: Object-Oriented Programming (OOP)
27. What are global, protected and private attributes in Python?
Python doesn't have true access-control enforcement like Java's private keyword it relies on naming conventions instead:
- Global: a normal variable defined at the top level of a module, accessible from anywhere (use the
globalkeyword inside a function if you need to modify it there). - Protected: an attribute name prefixed with a single underscore, like
_salary. This is a convention meaning "treat this as internal don't touch it from outside the class" but Python doesn't actually stop you from accessing it. - Private: an attribute name prefixed with a double underscore, like
__salary. Python performs "name mangling" on these internally renaming__salaryto_ClassName__salarywhich makes it awkward (though still not impossible) to access from outside the class by accident.
28. What is self used for in a Python class?
self refers to the specific instance a method is being called on it's how a method reads or updates that particular object's own attributes. It's always the first parameter of an instance method and Python passes it in automatically when you call my_object.some_method() you don't pass it yourself.
One thing worth knowing: unlike this in Java or C++, self is not a reserved keyword in Python it's just a very strongly followed convention. You could technically name that first parameter anything, but every Python developer expects to see self, so deviating from it will just confuse anyone reading your code.
29. What is __init__?
__init__ is Python's constructor method it runs automatically the moment a new object of a class is created and it's where you typically set up that object's initial attributes.
class Student:
def __init__(self, fname, lname, age, section):
self.firstname = fname
self.lastname = lname
self.age = age
self.section = section
stu1 = Student("Sara", "Ansh", 22, "A2")
Every class has an __init__ if you don't write one, Python quietly uses a default empty one. It's one of many "dunder" (double-underscore) methods Python uses for special behavior you'll meet a few more below.
30. What are the four pillars of OOP and can you show a real example?
- Encapsulation bundling data and the methods that operate on it inside a class and controlling what's exposed to the outside (see question 27's protected/private convention).
- Inheritance a class can inherit attributes and methods from another class, so shared behavior doesn't need to be rewritten.
- Polymorphism different classes can be used through the same interface, with each responding to the same method call in its own way.
- Abstraction hiding complex implementation details behind a simpler interface, so the user of a class doesn't need to know how it works internally, just how to use it.
Here's one small example that actually demonstrates encapsulation, inheritance and polymorphism together:
class Animal: # base class
def __init__(self, name):
self._name = name # encapsulation: protected attribute
def speak(self):
raise NotImplementedError
class Dog(Animal): # inheritance: reuses __init__ from Animal
def speak(self): # polymorphism: overrides speak()
return f"{self._name} says Woof"
class Cat(Animal):
def speak(self):
return f"{self._name} says Meow"
for animal in [Dog("Rex"), Cat("Whiskers")]:
print(animal.speak())
# Rex says Woof
# Whiskers says Meow
I ran this exact code the same animal.speak() call produces different, correct behavior depending on whether animal is a Dog or a Cat, which is polymorphism in action, built on top of the shared Animal base class (inheritance) and the protected _name attribute (encapsulation).
31. What's the difference between an instance method, a classmethod and a staticmethod?
class Employee:
company = "CarTrade"
def __init__(self, name):
self.name = name
def instance_method(self):
return f"self.name = {self.name}" # needs a specific instance
@classmethod
def classmethod_demo(cls):
return f"cls.company = {cls.company}" # works on the class itself
@staticmethod
def staticmethod_demo():
return "no access to self or cls at all" # just lives inside the class namespace
e = Employee("Ankit")
print(e.instance_method()) # self.name = Ankit
print(Employee.classmethod_demo()) # cls.company = CarTrade
print(Employee.staticmethod_demo()) # no access to self or cls at all
I confirmed all three outputs. An instance method (the normal kind) operates on one specific object and needs self. A classmethod operates on the class itself rather than any one instance and receives cls instead of self useful for things like alternative constructors. A staticmethod doesn't automatically receive either self or cls it's really just a regular function that's been grouped inside the class for organizational purposes, because it logically belongs there even though it doesn't need instance or class data.
32. What's the difference between __str__ and __repr__?
Both control how an object is converted to a string, but for different audiences:
class Point:
def __init__(self, x, y):
self.x, self.y = x, y
def __str__(self):
return f"Point({self.x}, {self.y}) [readable]"
def __repr__(self):
return f"Point(x={self.x!r}, y={self.y!r}) [unambiguous]"
p = Point(1, 2)
print(str(p)) # Point(1, 2) [readable]
print(repr(p)) # Point(x=1, y=2) [unambiguous]
print(p) # Point(1, 2) [readable] -> print() uses __str__
I confirmed all three lines print exactly as shown. __str__ is meant for a readable, user-facing description it's what print() and str() use. __repr__ is meant to be an unambiguous, developer-facing representation ideally one that could be copy-pasted back into Python to recreate the same object and it's what you see in a debugger, in a REPL when you just type a variable name or when an object is shown inside a list or dict. If you only define one, it's better practice to define __repr__, since Python falls back to __repr__ for str() if __str__ isn't defined but not the other way around.
33. What is multiple inheritance and what is MRO (Method Resolution Order)?
Python allows a class to inherit from more than one parent class at once. When two parent classes both define a method with the same name, Python needs a defined rule for which one wins that rule is called the Method Resolution Order (MRO) and Python uses an algorithm called C3 linearization to compute it.
class A:
def who(self): return "A"
class B(A):
def who(self): return "B"
class C(A):
def who(self): return "C"
class D(B, C):
pass
print(D().who()) # B
print([cls.__name__ for cls in D.__mro__]) # ['D', 'B', 'C', 'A', 'object']
I confirmed this: D inherits from both B and C and since B is listed first in class D(B, C):, Python resolves who() to B's version. You can inspect the exact resolution order any class will follow with ClassName.__mro__, which is genuinely useful when debugging a confusing multiple-inheritance bug rather than just guessing.
Section F: Control Flow & Error Handling
34. What does pass do?
pass is a null operation it does literally nothing and exists purely to satisfy Python's syntax requirement that a code block can't be empty.
def myEmptyFunc():
pass
myEmptyFunc() # runs fine, does nothing
# Without pass, this same function would raise:
# IndentationError: expected an indented block
It's most commonly used as a placeholder while you're still figuring out what a function, loop or if block should eventually do.
35. What's the difference between break, continue and pass inside a loop?
breakexits the loop immediately nothing after it in the loop runs again.continueskips the rest of the current iteration only and moves on to the next one.passdoes nothing at all it's not related to loop control, it's just a no-op placeholder.
pat = [1, 3, 2, 1, 2, 3, 1, 0, 1, 3]
current = None
for p in pat:
pass
if p == 0:
current = p
break
elif p % 2 == 0:
continue
print(p, end=' ')
print()
print(current)
# 1 3 1 3 1
# 0
I traced and ran this exactly it prints 1 3 1 3 1 (skipping every even number via continue and stopping the moment it hits 0 via break), then prints 0 as the final value of current.
36. How does exception handling with try/except/else/finally work?
def divide(a, b):
try:
result = a / b
except ZeroDivisionError as e:
print("caught:", e)
return None
else:
print("no exception, else runs")
return result
finally:
print("finally always runs")
print(divide(10, 2))
# no exception, else runs
# finally always runs
# 5.0
print(divide(10, 0))
# caught: division by zero
# finally always runs
# None
I ran both calls and confirmed this exact sequence. try holds the code that might fail. except catches a specific error type if it happens (you should always catch specific exceptions like ZeroDivisionError rather than a bare except:, so you don't accidentally swallow unrelated bugs). else runs only if the try block completed with no exception at all. finally always runs no matter what whether there was an exception, whether it was caught, even if the function returned early which makes it the right place for cleanup code, like closing a file or a database connection.
37. How do you create and raise a custom exception?
You define a new exception by creating a class that inherits from Exception (or a more specific built-in exception type, if one fits better):
class InsufficientBalanceError(Exception):
pass
def withdraw(balance, amount):
if amount > balance:
raise InsufficientBalanceError(f"Cannot withdraw {amount}, balance is {balance}")
return balance - amount
try:
withdraw(100, 150)
except InsufficientBalanceError as e:
print("custom exception caught:", e)
# custom exception caught: Cannot withdraw 150, balance is 100
I confirmed this runs and prints exactly that message. Custom exceptions matter in real projects because they let calling code catch and react to specific problems (like "insufficient balance" vs. "invalid account") instead of catching a generic Exception and having to inspect an error message string to figure out what actually went wrong.
Section G: Modules & Practical Tooling
38. What are modules and packages in Python?
A module is simply a single Python file (.py) containing functions, classes or variables you can import elsewhere with import module_name or from module_name import some_function.
A package is a folder containing multiple related modules, structured using dot notation (package.module). Creating one is as simple as putting your module files into a folder the folder name becomes the package name.
Splitting code into modules and packages gives you a few real benefits: it's easier to focus on one piece of the problem at a time, changes in one module are less likely to break unrelated ones, functions can be reused across different parts of the project instead of copy-pasted and each module gets its own namespace, which avoids naming collisions between different parts of a large codebase.
39. What does if __name__ == "__main__": actually do?
Every Python module has a built-in __name__ variable. If the file is being run directly (python myfile.py), Python sets __name__ to the string "__main__". If the file is instead being imported by another file, __name__ is set to the module's own name instead.
print(__name__)
# if run directly: __main__
# if imported elsewhere: the module's actual name
Wrapping your "run this script's main logic" code inside if __name__ == "__main__": means that logic only executes when the file is run directly not when someone else imports functions or classes from it. This is extremely common in real Python files, because it lets a file be both a reusable, importable module and a standalone runnable script, without one use case interfering with the other.
40. What is the with statement and what is a context manager?
with is used for managing resources that need explicit setup and cleanup like opening a file, a network connection or a lock and guarantees the cleanup happens even if an error occurs partway through.
class MyResource:
def __enter__(self):
print("acquiring resource")
return self
def __exit__(self, exc_type, exc_val, exc_tb):
print("releasing resource")
return False
with MyResource() as r:
print("using resource")
# acquiring resource
# using resource
# releasing resource
I ran this and confirmed the cleanup (releasing resource) prints even though nothing went wrong and critically, __exit__ is guaranteed to run even if an exception is raised inside the with block. This is exactly the mechanism behind the very common pattern with open("file.txt") as f: the file is automatically closed once the block ends, whether it finished normally or crashed halfway through.
41. How do you make a Python script directly executable on Unix/Linux?
Add a shebang line as the very first line of the script, telling the OS which interpreter to run it with:
#!/usr/bin/env python3
Then make the file executable and run it directly:
chmod +x myscript.py
./myscript.py
Using /usr/bin/env python3 rather than hardcoding a path like /usr/bin/python3 is the more portable choice, since it finds whichever python3 is first on the user's PATH (including inside an active virtual environment) rather than assuming one fixed install location.
Section H: Miscellaneous Fresher Favorites
42. What are unit tests and why do they matter?
A unit test checks one small, isolated piece of your code (a "unit" usually a single function or method) in complete isolation from the rest of the system, to confirm it behaves correctly. Python's standard library includes a built-in framework for this called unittest and pytest is the most popular third-party alternative.
The reason this matters in a real project: imagine your application is built from components A, B and C and something breaks. Without unit tests, you'd have to manually trace through the whole system to figure out which component actually failed. With good unit test coverage, a failing test tells you exactly which component broke and why, right away.
43. What is a docstring?
A docstring is a string literal placed as the first statement inside a function, class or module, used to document what it does:
def add(a, b):
"""Return the sum of a and b."""
return a + b
print(add.__doc__) # Return the sum of a and b.
Unlike a regular # comment, a docstring is accessible at runtime through the object's __doc__ attribute and it's what tools like help(add), IDEs and documentation generators (like Sphinx) pull from automatically to show usage information.
44. What is the GIL (Global Interpreter Lock)?
The GIL is a lock in CPython (the standard, most widely used Python implementation) that allows only one thread to execute Python bytecode at any given moment, even on a machine with multiple CPU cores.
The practical consequence: creating multiple threads in Python doesn't give you true parallel execution for CPU-bound work (like heavy number crunching), because the GIL forces those threads to take turns rather than genuinely run simultaneously. However, threading in Python is still genuinely useful for I/O-bound work (waiting on network requests, file reads, database queries), because the GIL is released while a thread is waiting on I/O, letting other threads run during that wait.
For real CPU-bound parallelism in Python, the usual answers are the multiprocessing module (which uses separate OS processes, each with its own GIL and memory space, sidestepping the problem entirely) or offloading the heavy work to a library like NumPy, which does its number-crunching in optimized C code outside the GIL's reach.
45. Why do we use enumerate() and zip() instead of manual indexing?
fruits = ["apple", "banana", "cherry"]
for i, f in enumerate(fruits, start=1):
print(i, f)
# 1 apple
# 2 banana
# 3 cherry
prices = [100, 40, 60]
for f, p in zip(fruits, prices):
print(f, p)
# apple 100
# banana 40
# cherry 60
I confirmed both outputs. enumerate() gives you both the index and the value while looping, without you having to manually track a counter variable (start=1 lets you begin counting from 1 instead of the default 0). zip() lets you loop over two or more sequences together in lockstep, pairing up corresponding elements much cleaner than looping by index and manually pulling fruits[i] and prices[i] from two separate lists.
46. What's the difference between f-strings, .format() and % formatting?
All three produce the same result, but represent three different eras of Python string formatting:
name, age = "Karan", 22
print(f"{name} is {age} years old") # f-string (Python 3.6+)
print("{} is {} years old".format(name, age)) # .format() (Python 2.7+ / 3.x)
print("%s is %d years old" % (name, age)) # % formatting (oldest style, from Python's early days)
I confirmed all three print the exact same output: Karan is 22 years old. In modern Python, f-strings are the preferred choice they're the most readable (the variable is written right where it's used) and the fastest of the three at runtime. .format() still shows up in older codebases and in cases needing more complex formatting logic. %-style formatting is mostly legacy at this point, though you'll still see it in some older libraries and logging code.
47. What is the walrus operator (:=) and what does it do?
Introduced in Python 3.8, the walrus operator lets you assign a value to a variable as part of a larger expression, instead of needing a separate assignment statement beforehand.
data = [1, 2, 3, 4, 5, 6, 7, 8]
result = [y for x in data if (y := x * 2) > 6]
print(result) # [8, 10, 12, 14, 16]
I confirmed this returns exactly [8, 10, 12, 14, 16] inside the list comprehension, y := x * 2 both computes the doubled value and assigns it to y in one step, so it can be reused in the output expression without recalculating x * 2 a second time. It's a small feature, but it comes up in interviews specifically because it's one of the more recent additions to the language and interviewers sometimes ask about it to see if your Python knowledge is current.
Quick-Fire FAQ
1. Is Python case-sensitive?
Yes. myVar and myvar are treated as two completely different names.
2. Is Python free to use?
Yes, Python is completely free and open-source, released under the OSI-approved PSF License.
3. What is the latest stable version of Python?
As of now, the 3.14.x series (Python 3.14.4) is the latest stable release. Most companies in production still commonly run somewhere in the 3.10–3.12 range, since it takes time for large codebases to upgrade so knowing the concepts matters far more than memorizing the exact newest version number.
4. Does Python support multiple inheritance?
Yes unlike some languages (like Java, which only allows single inheritance for classes), Python lets a class inherit from more than one parent class at once (see question 33).
5. Is Python good for beginners?
Yes, it's one of the most commonly recommended first languages, largely because of its readable, English-like syntax and the fact that you don't need to deal with manual memory management or a compile step just to run a simple script.
6. What's the difference between a script and a module in Python?
There's no technical difference in the file itself a .py file is a "script" when you run it directly and a "module" when another file imports it (see question 39).
Common Mistakes Freshers Make in Python Interviews
A few patterns that come up again and again, worth being aware of before you walk in:
-
Confusing mutable and immutable default arguments (question 13) is one of the single most commonly asked "spot the bug" questions if you understand why it happens, not just that it happens, you'll be able to explain it clearly instead of just reciting that it's "a known gotcha."
-
Saying
isand==are "basically the same thing" is a red flag to most interviewers even if it happens to work for small integers by coincidence (question 9), the correct answer is that you should always default to==for value comparison. -
Not knowing the difference between shallow and deep copies (question 11) shows up constantly, because it's a genuinely common source of real production bugs not just an academic question.
-
Treating list comprehensions as always "better" than a regular loop, without being able to explain why the honest answer is they're often more concise and slightly faster, but readability should still win when the logic gets complex.
-
Being unable to explain what actually happens when you
raisean exception versus just describing whattry/exceptlooks like syntactically interviewers often want to see that you understand exception propagation, not just the block structure.
