Top 50 Python Interview Questions and Answers by IT Trainings Institute
Introduction
Preparing for a Python interview? This Top 50 Python Interview Questions and Answers guide by IT Trainings Institute is your go-to resource for Python interview preparation—featuring commonly asked Python questions and answers to help both beginners and experienced candidates succeed. If you’re looking to strengthen your fundamentals, check out our comprehensive Python course to boost your knowledge and confidence.
So, let’s dive into this comprehensive collection of Python Technical Interview Questions and Answers, carefully categorized by IT Trainings Institute to support your interview preparation journey:
Python Interview Questions and Answers for Freshers
1. What is Python?
Answer:
Python is a high-level, interpreted, object-oriented programming language. It is known for its simple syntax, making it easy to read and write. It supports multiple programming paradigms like procedural, object-oriented, and functional programming.
2. What are Python's key features?
Answer:
Easy to learn and use
Interpreted (runs line by line)
Dynamically typed (no need to declare variable types)
Portable (works on many platforms)
Large standard library
Supports OOP and functional programming
3. What are variables and how are they declared in Python?
Answer:
A variable stores data. In Python, you don’t need to declare its type.
x = 10
name = "Alice"
4. What are lists and tuples?
Answer:
Lists and tuples are both used to store multiple items in a single variable, but they are different in behavior.
🔹 List:
A list is a collection that is changeable (mutable).
You can add, remove, or modify items in a list.
It uses square brackets [ ].
x = 10
name = "Alice"fruits = ["apple", "banana", "mango"]
fruits[1] = "orange" # changes 'banana' to 'orange'
🔹 Tuple:
A tuple is a collection that is unchangeable (immutable).
Once created, you cannot change its items.
It uses parentheses ( ).
colors = ("red", "green", "blue")
# colors[1] = "yellow" → This will give an error
In short:
Use lists when your data might change.
Use tuples when your data should stay the same.
5. What is the difference between is and ==?
Answer:
== checks if values are equal.
is checks if they refer to the same object in memory.
a = [1, 2]
b = [1, 2]
print(a == b) # True
print(a is b) # False

Learn via our Course
Level Up Your Coding Skills with Expert Python Training in Chandigarh & Mohali!
6. What are Python data types?
Answer:
- int: integers → x = 5
- float: decimal numbers → x = 3.14
- str: text → x = “hello”
- bool: true or false → x = True
- list, tuple, set, dict: collections
7. What is a function and how do you define one?
Answer:
A function is a reusable block of code.
def greet(name):
return "Hello, " + name
8. What is the difference between None, False, and 0?
- None: Represents nothing (null).
- False: A Boolean value.
- 0: A number.
Each is different, but all are falsy in conditions.
9. What are loops in Python?
Answer:
For loop: Iterates over a sequence.
While loop: Repeats while a condition is true.
for i in range(3):
print(i) # 0 1 2
10. What is a dictionary in Python?
Answer:
A dictionary stores data in key-value pairs.
student = {"name": "Alice", "age": 20}
print(student["name"]) # Alice
11. What is list comprehension?
Answer:
A shorter way to create lists.
squares = [x*x for x in range(5)]
12. What is the difference between deepcopy() and copy()?
Answer:
copy(): Creates a shallow copy (nested objects still linked).
deepcopy(): Creates a full independent copy.
13. What are *args and **kwargs?
Answer:
- *args: Accepts multiple positional arguments.
- **kwargs: Accepts multiple keyword arguments.
def test(*args, **kwargs):
print(args, kwargs)
test(1, 2, a=3, b=4)
14. What is a lambda function?
Answer:
A lambda is a short anonymous function.
square = lambda x: x * x
print(square(5)) # 25
15. What is exception handling in Python?
Answer:
Used to handle errors and avoid crashes.
try:
x = 5 / 0
except ZeroDivisionError:
print("Cannot divide by zero")
16. What is indentation in Python?
Answer:
Indentation refers to the spaces at the beginning of a code line. In Python, indentation is mandatory as it defines the scope of loops, functions, and conditionals. Unlike other languages that use curly braces {}
, Python uses indentation.
if True:
print("Hello, World!")
17. What is the difference between break, continue, and pass in Python?
Answer:
break: Exits the loop entirely.
continue: Skips the current iteration and continues with the next.
pass: Does nothing; used as a placeholder.
for i in range(5):
if i == 2:
continue
print(i)
18. What are modules and packages in Python?
Answer:
A module is a single .py file that contains Python code.
A package is a collection of modules stored in a directory with an __init__.py file.
import math
from mypackage import mymodule
19. What is the use of self in Python?
Answer:
self refers to the instance of the class. It allows access to the attributes and methods of the object.
class Person:
def __init__(self, name):
self.name = name
20. Difference between append() and extend() in Python lists?
Answer:
- append() adds an item to the end of the list.
- extend() adds elements from another list (or iterable) to the list.
a = [1, 2]
a.append([3, 4]) # [1, 2, [3, 4]]
a.extend([5, 6]) # [1, 2, [3, 4], 5, 6]

Learn via our Course
Level Up Your Coding Skills with Expert Python Training in Chandigarh & Mohali!
21. What is a class and an object in Python?
Answer:
A class is a blueprint for creating objects.
An object is an instance of a class.
class Dog:
def bark(self):
print("Woof")
d = Dog()
d.bark()
22. What is a docstring in Python?
Answer:
A docstring is a documentation string used to describe what a function, class, or module does. It’s placed inside triple quotes.
def greet():
"""This function greets the user."""
print("Hello!")
23. What are sets in Python?
Answer:
A set is an unordered collection of unique elements.
s = {1, 2, 3}
s.add(4)
24. Difference between == and is in Python?
Answer:
== checks if values are equal.
is checks if they are the same object in memory.
a = "hello"
b = "hello"
print(a == b) # True
print(a is b) # True (but not always in all cases)
25. What is recursion in Python?
Answer:
Recursion is when a function calls itself to solve a smaller part of a problem.
def factorial(n):
if n == 1:
return 1
return n * factorial(n - 1)
26. What is the use of init in Python?
Answer:
__init__ is a special method used as a constructor. It is called automatically when an object is created.
class Student:
def __init__(self, name):
self.name = name
27. What is the use of isinstance() function?
Answer:
isinstance() checks whether an object is an instance of a particular class or type.
x = 10
print(isinstance(x, int)) # True
28. What is a generator in Python?
Answer:
A generator is a special type of function that returns an iterator using the yield keyword instead of return.
def my_gen():
yield 1
yield 2
29. What is the difference between remove(), pop(), and del in Python lists?
Answer:
- remove(value): Removes the first matching value.
- pop(index): Removes item at a given index.
- del: Deletes by index or deletes the list entirely.
lst = [1, 2, 3]
lst.remove(2)
lst.pop(0)
del lst[0]
30. What is slicing in Python?
Answer:
Slicing allows you to extract a portion of a list, string, or tuple.
my_list = [0, 1, 2, 3, 4]
print(my_list[1:4]) # [1, 2, 3]
Python Interview Questions and Answers for Experienced
31. Explain the Global Interpreter Lock (GIL) in Python. What are its implications for multithreaded applications, and how can you work around it for CPU-bound tasks?
Answer: The GIL is a mutex that protects access to Python objects, preventing multiple native threads from executing Python bytecodes simultaneously. Even on multi-core processors, only one thread can execute Python bytecode at a time.
Implications:
- CPU-bound tasks: The GIL limits true parallelism for CPU-bound tasks, as only one thread can actively compute at any given moment. This can lead to underutilization of CPU cores.
- I/O-bound tasks: For I/O-bound tasks (like network requests, file operations), the GIL is released during I/O operations, allowing other threads to run. This means multithreading can still provide concurrency benefits for I/O-bound operations.
Working around it for CPU-bound tasks:
- Multiprocessing: Use the multiprocessing module, which spawns separate processes, each with its own Python interpreter and GIL. This allows true parallelism across multiple CPU cores.
- C extensions: Implement performance-critical parts of your code in C/C++ as Python extensions. These extensions can release the GIL, allowing multiple threads to execute C code in parallel.
- Asynchronous programming (asyncio): For I/O-bound concurrency,
asyncio
provides a single-threaded, cooperative multitasking framework that manages concurrency without relying on multiple threads and the GIL.
import multiprocessing
import time
def cpu_bound_task(n):
result = 0
for _ in range(n):
result += sum(i*i for i in range(1000))
return result
if __name__ == "__main__":
start_time = time.time()
num_processes = 4
iterations_per_process = 100
with multiprocessing.Pool(num_processes) as pool:
results = pool.map(cpu_bound_task, [iterations_per_process] * num_processes)
print(f"Results: {results}")
print(f"Time taken with multiprocessing: {time.time() - start_time:.4f} seconds")
# For comparison, single-threaded execution
start_time_single = time.time()
single_result = cpu_bound_task(iterations_per_process * num_processes)
print(f"Time taken single-threaded: {time.time() - start_time_single:.4f} seconds")
32. Describe Python's memory management mechanisms. How does garbage collection work, and what is reference counting?
Answer: Python uses automatic memory management primarily through reference counting and a cyclic garbage collector.
Reference Counting:
- Every Python object has a reference count, which keeps track of how many pointers (references) are pointing to it.
- When an object’s reference count drops to zero, it means no variables or data structures are referencing it, and the object’s memory is immediately deallocated.
- This is efficient and handles most memory cleanup.
Cyclic Garbage Collector:
- Reference counting alone cannot handle reference cycles, where objects refer to each other in a loop, but are no longer accessible from the rest of the program. For example, A refers to B and B refers to A, but nothing else refers to A or B.
- The cyclic garbage collector periodically identifies and reclaims these uncollectable cycles of objects. It typically runs when a certain number of object allocations have occurred.
- You can manually trigger it using gc.collect().
33. What are decorators in Python? Provide an example of how you might use them in a real-world scenario.
Answer:
Decorators are a powerful way to modify or enhance the behavior of functions or methods without directly changing their source code. They are essentially functions that take another function as an argument, add some functionality, and return the modified function.
import functools
def log_function_call(func):
@functools.wraps(func) # Preserves the original function's metadata
def wrapper(*args, **kwargs):
print(f"Calling function: {func.__name__} with args: {args}, kwargs: {kwargs}")
result = func(*args, **kwargs)
print(f"Function {func.__name__} returned: {result}")
return result
return wrapper
@log_function_call
def add(a, b):
return a + b
@log_function_call
def multiply(x, y, z=1):
return x * y * z
print(add(5, 3))
print(multiply(2, 4, z=5))
34. Explain the concept of metaclasses in Python. When would you use a metaclass, and can you provide a simple example?
Answer:
A metaclass is the “class of a class.” Just as an ordinary class defines the behavior of its instances, a metaclass defines the behavior of classes. In Python, type is the default metaclass for all classes.
When to use metaclasses: Metaclasses are advanced features and are rarely needed for typical application development. They are used when you need to:
- Automatically register new classes when they are defined.
- Modify classes after they are created (e.g., adding methods, enforcing interfaces).
- Implement frameworks or ORMs that require dynamic class creation or modification.
35. How do you handle asynchronous programming in Python? Discuss asyncio, await, and async keywords.
Answer:
Asynchronous programming in Python is primarily handled by the asyncio library, which uses coroutines to achieve concurrent execution without true parallelism (unlike multiprocessing).
- async keyword: Declares a function as a coroutine. An async function, when called, returns a coroutine object, not the result directly.
- await keyword: Used inside an async function to pause its execution until an awaitable object (like another coroutine, a Future, or a Task) completes. While waiting, asyncio can switch to run other tasks.
- asyncio library: Provides the infrastructure to run and manage coroutines. Key functions include:
- asyncio.run(coroutine): Runs the top-level coroutine.
- asyncio.create_task(coroutine): Schedules a coroutine to run as a Task.
- asyncio.gather(*awaitables): Runs multiple awaitables concurrently and waits for all of them to complete.
import asyncio
import time
async def fetch_data(delay, data):
print(f"Fetching data '{data}'... (simulating {delay} seconds delay)")
await asyncio.sleep(delay)
print(f"Data '{data}' fetched.")
return f"Processed {data}"
async def main():
start_time = time.time()
# Run tasks concurrently
results = await asyncio.gather(
fetch_data(2, "User A"),
fetch_data(1, "User B"),
fetch_data(3, "User C")
)
end_time = time.time()
print(f"\nAll tasks completed in {end_time - start_time:.2f} seconds.")
print(f"Results: {results}")
if __name__ == "__main__":
asyncio.run(main())
36. Discuss descriptors in Python. How do they work, and when would you use them?
Answer:
Descriptors are objects that implement one or more of the __get__, __set__, or __delete__ methods. They allow you to customize attribute access (getting, setting, or deleting) for an object’s attributes.
How they work:
When an attribute lookup is performed on an object, if the attribute is a descriptor, Python’s lookup mechanism calls the appropriate descriptor method.
- __get__(self, instance, owner): Called to retrieve the attribute. instance is the object the attribute was accessed from, owner is the class of instance.
- __set__(self, instance, value): Called to set the attribute.
- __delete__(self, instance): Called to delete the attribute.
When to use them:
- Validation: Enforcing rules for attribute values (e.g., type checking, range validation).
- Lazy Loading: Loading an attribute’s value only when it’s accessed for the first time.
- Managed Attributes: Implementing properties with custom logic without using the @property decorator directly on the class.
- ORM fields: Database column definitions in ORMs often use descriptors.
37. What is the difference between __init__, __new__, and __call__ methods in Python classes?
Answer:
- __new__(cls, *args, **kwargs):
- This is a static method that is responsible for creating and returning a new instance of the class.
- It is called before __init__.
- If you override __new__, it must return an instance of cls (or a subclass) for __init__ to be called.
Commonly used in metaclasses, singletons, or when creating immutable objects.
- __init__(self, *args, **kwargs):
- This is the constructor method. It’s responsible for initializing the newly created instance.
- It receives the instance (self) as its first argument, along with any arguments passed to the class constructor.
- You typically set initial attributes and perform setup logic here.
- __call__(self, *args, **kwargs):
- This method makes an instance of a class callable like a function.
- If defined, you can call an object of the class directly, e.g., my_object().
class MyClass:
def __new__(cls, *args, **kwargs):
print("__new__ called: Creating instance")
instance = super().__new__(cls)
return instance
def __init__(self, value):
print("__init__ called: Initializing instance with value", value)
self.value = value
def __call__(self, operation):
print(f"__call__ called: Performing operation '{operation}' on value {self.value}")
if operation == "double":
return self.value * 2
return self.value
obj = MyClass(10)
print(obj.value)
print(obj("double"))
38. What is a context manager in Python, and how do you implement one?
Answer:
A context manager is an object that defines the runtime context for a block of code. It ensures that resources are properly acquired and released, even if errors occur. The with statement is used to work with context managers.
How to implement one:
- There are two main ways:
- Using a class: By defining __enter__ and __exit__ methods.
- __enter__(self): Called when the with statement is entered. It should return the resource to be used inside the with block (or self if the object itself is the resource).
- __exit__(self, exc_type, exc_value, traceback): Called when the with block is exited (either normally or due to an exception). It’s responsible for cleaning up the resource. If it returns True, it suppresses the exception.
- Using a class: By defining __enter__ and __exit__ methods.
2. Using contextlib.contextmanager decorator: A more concise way for simple context managers.
39. Explain different ways to manage dependencies in a Python project. What are pip, requirements.txt, and virtual environments?
Answer:
Dependency Management: The process of tracking and installing the external libraries and packages required by a Python project.
- pip: The official package installer for Python. It’s used to install, uninstall, and manage Python packages from the Python Package Index (PyPI) or other sources.
- pip install <package_name>
- pip install -r requirements.txt
- requirements.txt: A text file (conventionally named requirements.txt) that lists all the direct and indirect dependencies of a project, often with specific version numbers. This ensures that everyone working on the project uses the exact same versions of libraries, preventing “works on my machine” issues.
- Virtual Environments (venv, virtualenv):
- Isolated Python environments that allow you to install dependencies for a specific project without affecting other projects or the global Python installation.
- This prevents dependency conflicts between different projects (e.g., Project A needs Library X v1.0, Project B needs Library X v2.0).
40. What are Python's built-in data structures (beyond lists, tuples, dicts, sets)? Discuss collections module.
Answer:
Beyond the basic built-in data types (list, tuple, dict, set, str, int, float, bool), Python’s collections module provides specialized container datatypes that offer alternatives with additional features or different performance characteristics.
Key data structures from the collections module:
- collections.Counter: A dict subclass for counting hashable objects. It’s a convenient way to count the occurrences of items in an iterable.
from collections import Counter
my_list = ['apple', 'banana', 'apple', 'orange', 'banana', 'apple']
counts = Counter(my_list)
print(counts) # Counter({'apple': 3, 'banana': 2, 'orange': 1})
- collections.deque (double-ended queue): A list-like container with fast appends and pops from both ends. Efficient for implementing queues and stacks.
from collections import deque
d = deque([1, 2, 3])
d.appendleft(0) # deque([0, 1, 2, 3])
d.pop() # 3, deque([0, 1, 2])
collections.defaultdict: A dict subclass that calls a factory function to supply missing values. Simplifies initializing dictionary entries when a key is accessed for the first time.
from collections import defaultdict
s = [('yellow', 1), ('blue', 2), ('yellow', 3), ('blue', 4), ('red', 1)]
d = defaultdict(list)
for k, v in s:
d[k].append(v)
print(dict(d))
# Output: {'yellow': [1, 3], 'blue': [2, 4], 'red': [1]}
41. Explain the concept of MRO (Method Resolution Order) in Python, especially in the context of multiple inheritance.
Answer:
MRO (Method Resolution Order) is the order in which Python searches for methods and attributes in a class hierarchy, particularly in the presence of multiple inheritance. Python uses the C3 Linearization algorithm to determine the MRO.
How it works (C3 Linearization):
The C3 algorithm ensures several properties:
- Monotonicity: If a class C precedes D in the MRO of a class X, then C will also precede D in the MRO of any subclass of X.
- Local Precedence: A class always precedes its parents.
- Extended Precedence: If a class C inherits from A and B (e.g., class C(A, B)), then A will always come before B in C’s MRO.
You can inspect the MRO of a class using ClassName.mro() or ClassName.__mro__.
class A:
def method(self):
print("Method from A")
class B(A):
def method(self):
print("Method from B")
class C(A):
def method(self):
print("Method from C")
class D(B, C):
# D inherits from B and C, both of which inherit from A
pass
class E(C, B):
# Order of inheritance is swapped
pass
print("MRO for D:", D.mro())
d = D()
d.method() # Output: Method from B (B is before C in D's MRO)
print("\nMRO for E:", E.mro())
e = E()
e.method()
42. How do you implement unit testing in Python? Which frameworks do you prefer and why?
Answer: Unit testing in Python involves testing individual components (functions, classes, methods) of your code in isolation to ensure they work as expected.
Common Frameworks:
unittest (Built-in):
- Pros: Part of the Python standard library, so no external installation is needed. Familiar for those with Java/JUnit background.
- Cons: Can be verbose due to class-based test cases and method naming conventions (test_something).
import unittest
def add(a, b):
return a + b
class TestAddFunction(unittest.TestCase):
def test_positive_numbers(self):
self.assertEqual(add(2, 3), 5)
def test_negative_numbers(self):
self.assertEqual(add(-1, -5), -6)
if __name__ == '__main__':
unittest.main()
2. pytest (Third-party, highly recommended):
- Pros:
- Simpler Syntax: Tests can be written as plain functions without requiring class inheritance.
- Fixtures: Powerful and flexible mechanism for setting up and tearing down test environments.
- Plugins: Rich ecosystem of plugins for various functionalities (e.g., coverage reports, parallel execution).
- Detailed Test Reports: Provides clear and concise output.
- Parameterization: Easy to run the same test with different inputs.
- Cons: Requires installation (pip install pytest).
- Pros:
# test_calculator.py
def add(a, b):
return a + b
def test_add_positive_numbers():
assert add(2, 3) == 5
def test_add_negative_numbers():
assert add(-1, -5) == -6
Preference: I generally prefer pytest due to its simpler syntax, powerful fixtures, and extensive plugin ecosystem, which significantly improves developer productivity and test readability. It makes writing and maintaining tests much more enjoyable.
43. How do you handle file operations in Python, including binary files and common operations like reading, writing, and appending?
Answer:
File operations in Python are handled using the built-in open() function, which returns a file object. The mode argument specifies how the file will be used.
Common Modes:
- ‘r’: Read (default). Error if file doesn’t exist.
- ‘w’: Write. Creates a new file or truncates an existing one.
- ‘a’: Append. Creates a new file or appends to an existing one.
- ‘x’: Exclusive creation. Creates a new file; error if file already exists.
- ‘b’: Binary mode (e.g., ‘rb’, ‘wb’, ‘ab’). Used for non-text files.
- ‘t’: Text mode (default). Used for text files.
- ‘+’: Open for updating (reading and writing) (e.g., ‘r+’, ‘w+’, ‘a+’).
# my_file.txt:
# Hello world
# Python is great
with open("my_file.txt", "r") as f:
content = f.read() # Reads entire file
print(content)
with open("my_file.txt", "r") as f:
for line in f: # Iterates line by line (memory efficient for large files)
print(line.strip()) # .strip() removes newline characters
with open("new_file.txt", "w") as f:
f.write("This is the first line.\n")
f.write("This is the second line.\n")
with open("new_file.txt", "a") as f:
f.write("This line was appended.\n")
# Simulating a small binary file (e.g., an image)
with open("my_image.bin", "wb") as f_out:
f_out.write(b'\x89PNG\r\n\x1a\n') # Example PNG signature bytes
with open("my_image.bin", "rb") as f_in:
binary_data = f_in.read()
print(f"Read binary data: {binary_data}")
44. What are __slots__ in Python classes? When would you use them, and what are their benefits/drawbacks?
Answer:
__slots__ is a special class attribute that allows you to explicitly declare instance attributes (variables) in a class. When __slots__ is defined, Python will not create a __dict__ for each instance of that class.
When to use them:
- Memory Optimization: Primarily used to save memory, especially when creating a large number of instances of a class.
- Preventing Arbitrary Attribute Assignment: It prevents the creation of new, undeclared attributes on instances, effectively limiting the allowed attributes.
Benefits:
- Reduced Memory Footprint: Significantly reduces the memory usage per instance, as __dict__ (which is a dictionary) is not created for each object. This can be crucial for applications with many small objects.
- Faster Attribute Access: Accessing attributes defined in __slots__ can be slightly faster than accessing attributes in __dict__.
Drawbacks:
- No __dict__ for Instances: Instances cannot have additional attributes beyond those declared in __slots__ (unless __slots__ explicitly includes ‘__dict__’).
- No Dynamic Attribute Creation: You cannot dynamically add new attributes to an instance after it’s created.
- No __weakref__ by Default: If you need weak references to instances, you must explicitly include ‘__weakref__’ in __slots__.
- Inheritance Complexity: Inheriting from a class that uses __slots__ can be tricky. Subclasses also need to define __slots__ to benefit, and their __slots__ should include the parent’s __slots__ or a __dict__.
- Readability (Potentially): Can make the class definition slightly less intuitive for developers unfamiliar with __slots__.
45. How does Python handle the difference between mutable and immutable objects? Give examples of each and explain the implications.
Answer: In Python, objects are classified as either mutable or immutable based on whether their state (value) can be changed after they are created.
Mutable Objects:
- Their state can be changed after creation.
- Operations that modify the object directly do not create a new object; they alter the existing one in memory.
- Examples: list, dict, set, bytearray, custom class instances.
- Implication: When you pass a mutable object to a function, any modifications made to that object within the function will affect the original object outside the function.
my_list = [1, 2, 3]
another_list = my_list # Both refer to the same list object
another_list.append(4)
print(my_list) # Output: [1, 2, 3, 4] - my_list is changed
def modify_list(lst):
lst.append(5)
data = [10, 20]
modify_list(data)
print(data) # Output: [10, 20, 5]
- Immutable Objects:
- Their state cannot be changed after creation.
- Any operation that appears to “modify” an immutable object actually creates a new object with the desired changes, leaving the original object untouched.
- Examples: int, float, str, tuple, frozenset, bytes.
- Implication: When you pass an immutable object to a function, the function receives a copy of the reference. Even if the function “modifies” the object, it’s actually creating a new object within its scope, and the original object outside the function remains unchanged.
my_string = "hello"
another_string = my_string
another_string += " world" # Creates a *new* string "hello world"
print(my_string) # Output: "hello" - my_string remains unchanged
def modify_string(s):
s += " world"
print(f"Inside function: {s}")
message = "Python"
modify_string(message)
print(f"Outside function: {message}") # Output: "Python"
46. What is the difference between super() and direct parent class calling in Python inheritance? When should you use super()?
Answer: Both super() and direct parent class calling are used to invoke methods from a parent class in an inheritance hierarchy. However, super() provides a more robust and flexible mechanism, especially in multiple inheritance scenarios.
Direct Parent Class Calling:
- Explicitly calls the parent class’s method using the parent class name and passing self explicitly.
- Example: ParentClass.method(self, args)
- Drawback: In multiple inheritance, this approach can lead to issues like duplicate calls to the same method (the “diamond problem”) or incorrect method resolution if the hierarchy changes. It tightly couples the child class to a specific parent.
super():
- Provides a way to refer to the parent or sibling class without explicitly naming them. It delegates method calls to the next method in the MRO (Method Resolution Order).
- Syntax: super().method(args) (in Python 3, super() without arguments is common inside methods) or super(CurrentClass, self).method(args) (more explicit, useful in some cases).
- Benefit: super() correctly navigates the MRO, ensuring that methods are called exactly once and in the correct order, even in complex multiple inheritance scenarios. It promotes loose coupling between parent and child classes.
When to use super(): Always use super() when calling methods (especially __init__) from parent classes in an inheritance hierarchy, particularly in cases of multiple inheritance. This ensures that the MRO is respected and that methods are called correctly and without redundancy.
47. Explain what a closure is in Python.
Answer: A closure in Python is a nested function that “remembers” and has access to variables from its enclosing (outer) function’s scope, even after the outer function has finished execution and its local scope is no longer active.
Key characteristics of a closure:
- It is a nested function.
- It references at least one variable from its enclosing scope.
- The enclosing function returns the nested function.
How it works: When the outer function completes, its local variables are usually destroyed. However, if a nested function (the closure) references these variables and is returned, Python’s garbage collector ensures those referenced variables persist in memory, bound to the closure.
48. Discuss the concept of Polymorphism in Python and how it's achieved.
Answer: Polymorphism (meaning “many forms”) is a fundamental concept in object-oriented programming that allows objects of different classes to be treated as objects of a common type. In Python, polymorphism is primarily achieved through Duck Typing.
- Duck Typing: Python doesn’t rely on explicit interface declarations or class hierarchies to achieve polymorphism. Instead, it follows the “If it walks like a duck and quacks like a duck, then it’s a duck” principle. If an object has the required methods or attributes, it can be used in a particular context, regardless of its actual class.
How it’s achieved in Python:
- Method Overriding: Subclasses can provide their own implementation of methods inherited from their parent class.
- Abstract Base Classes (ABCs): While not strictly necessary for duck typing,
abc
module allows defining abstract methods that subclasses must implement, providing a more formal way to define interfaces. - Inheritance: Objects of derived classes can be treated as objects of their base class.
import math
from mypackage import mymodule
49. Explain the differences between threading and multiprocessing in Python.
Answer: Both threading and multiprocessing are used for concurrent execution in Python, but they differ significantly in how they achieve it and their implications for performance, especially due to Python’s Global Interpreter Lock (GIL).
Threading (threading module):
- Mechanism: Threads run within the same process and share the same memory space.
- GIL Impact: Due to the GIL, only one thread can execute Python bytecode at a time, even on multi-core processors. The GIL is released during I/O operations (e.g., waiting for network response, disk read/write).
- True Parallelism: Not achieved for CPU-bound tasks.
- Memory Usage: Generally lower as threads share memory.
- Communication: Easier, as threads share memory space (though requires careful synchronization to avoid race conditions).
- Use Cases:
- I/O-bound tasks: Ideal for tasks that spend a lot of time waiting for external resources (network requests, file I/O, database queries), as the GIL is released during these waits, allowing other threads to run concurrently.
- Simple concurrency: When the overhead of creating new processes is too high, and the task is primarily I/O-bound.
Multiprocessing (multiprocessing module):
- Mechanism: Creates separate processes, each with its own Python interpreter and its own memory space.
- GIL Impact: Each process has its own GIL. Therefore, the GIL is not an issue for multiprocessing, and true parallelism can be achieved across multiple CPU cores.
- True Parallelism: Achieved for CPU-bound tasks.
- Memory Usage: Higher, as each process has its own memory space.
- Communication: More complex, requiring inter-process communication (IPC) mechanisms like pipes, queues, or shared memory (often handled by the multiprocessing module).
50. What are Type Hints (or Type Annotations) in Python?Why are they used?
Answer:
Type Hints (introduced in PEP 484) are a way to indicate the expected types of variables, function arguments, and return values in Python code. They are not enforced at runtime by the Python interpreter but are used by static analysis tools (like MyPy, Pyright), IDEs, and other developers for better code understanding and validation.
Why they are used:
- Improved Readability and Understanding: Makes the code’s intent clearer, especially in large codebases.
- Static Analysis and Error Detection: Tools can catch type-related bugs before runtime, reducing errors.
- Better IDE Support: IDEs can provide more accurate autocomplete, refactoring, and error checking.
- Easier Refactoring: Helps in confidently refactoring code by highlighting potential type mismatches.
- Documentation: Serves as executable documentation for function signatures.