Basic Syntax
[expression for item in iterable] Example (Without List Comprehension)
squares = [] for i in range(5): squares.append(i * i) Same Using List Comprehension
squares = [i * i for i in range(5)]Key Advantages
-
Shorter and cleaner code
-
Better readability
-
Faster than traditional loops (in many cases)
2. What is dictionary comprehension?
Basic Syntax
{key_expression : value_expression for item in iterable} Example (Without Dictionary Comprehension)
squares = {} for i in range(5): squares[i] = i * i Same Using Dictionary Comprehension
squares = {i: i * i for i in range(5)}Key Advantages
-
Cleaner and more readable code
-
Less boilerplate than loops
-
Efficient and Pythonic
Why Tuple Comprehension Does Not Exist
In Python:
-
List comprehension → creates a list
-
Set comprehension → creates a set
-
Dictionary comprehension → creates a dictionary
-
Tuple comprehension → does not exist
This is because parentheses () are already used for generator expressions.
*args and **kwargs?*args and **kwargs are used in Python functions to accept a variable number of arguments, making functions more flexible and reusable.
-
*args→ Handles variable-length positional arguments -
**kwargs→ Handles variable-length keyword arguments
*args (Non-keyword Arguments)
What it does
-
Allows a function to accept any number of positional arguments
-
Arguments are received as a tuple
**kwargs (Keyword Arguments)
What it does
-
Allows a function to accept any number of keyword arguments
-
Arguments are received as a dictionary
lambda keyword. They are used to write short, single-expression functions without formally defining a function using def.Basic Syntax
lambda arguments : expression -
No function name
-
Expression is evaluated and returned automatically
-
Can have any number of arguments, but only one expression
| Lambda Function | Normal Function |
|---|---|
| Anonymous | Has a name |
| Single expression | Multiple statements |
| One-line | Multi-line |
No return keyword | Uses return |
You should use lambda functions when you need a small, simple, one-time function for a short operation, especially when passing a function as an argument to another function.
Lambda functions are best used where defining a full function using def would be unnecessary or verbose.
Short, Simple Operations
When the logic fits in one expression.
square = lambda x: x * xCommon with map(), filter(), sorted().
nums = [1, 2, 3, 4] evens = list(filter(lambda x: x % 2 == 0, nums))In Python, functions are first-class citizens (first-class functions).
This means functions are treated like any other object, such as integers, strings, or lists.
Because of this, functions in Python can be:
-
Assigned to variables
-
Passed as arguments to other functions
-
Returned from functions
-
Stored in data structures
Key Properties of First-Class Functions
1. Functions Can Be Assigned to Variables
def greet(): return "Hello" say_hello = greet print(say_hello()) 2. Functions Can Be Passed as Arguments
def apply(func): return func() def hello(): return "Hi" print(apply(hello)) 3. Functions Can Be Returned from Other Functions
def outer(): def inner(): return "Inner function" return inner func = outer() print(func()) 4. Functions Can Be Stored in Data Structures
operations = [len, abs, str] print(operations[0]("Python")) Why First-Class Functions Are Important
-
Enable higher-order functions
-
Foundation for decorators
-
Support functional programming
-
Make code more modular and reusable
8. Can a function be passed as an argument?
Yes, in Python a function can be passed as an argument to another function.
This is possible because functions in Python are first-class objects, meaning they can be treated like variables and values.
How It Works
-
A function can be passed by its name (without parentheses)
-
The receiving function can call the passed function inside its body
Simple Example
def greet(): return "Hello" def execute(func): return func() print(execute(greet)) An iterator in Python is an object that allows you to traverse through a sequence of elements one at a time.
It keeps track of the current position and returns the next value on demand.
Iterators follow the iterator protocol, which means they implement:
-
__iter__() -
__next__()
How Iterators Work (Step-by-Step)
-
Convert an iterable into an iterator using
iter() -
Fetch elements using
next() -
When elements are exhausted,
StopIterationis raised
Generators are a special type of function in Python that produce values one at a time using the yield keyword instead of returning them all at once.
They allow lazy evaluation, meaning values are generated only when needed.
How Generators Work
-
Defined like normal functions, but use
yield -
Each
yieldpauses the function’s execution -
The function resumes from where it left off on the next call
-
Automatically follow the iterator protocol
def count_up(n): for i in range(1, n + 1): yield i gen = count_up(3) print(next(gen)) # 1 print(next(gen)) # 2 print(next(gen)) # 3Iterator
What it is
An iterator is an object that:
-
Implements
__iter__()(returns itself) -
Implements
__next__()(returns next value)
Example (Custom Iterator)
class Count: def __init__(self, max): self.num = 0 self.max = max def __iter__(self): return self def __next__(self): if self.num < self.max: self.num += 1 return self.num else: raise StopIteration Characteristics
-
More code required
-
Manual state handling
-
More control over iteration logic
Generator
What it is
A generator is a function that:
-
Uses the
yieldkeyword -
Automatically implements iterator behavior
Example (Generator)
def count(max): for i in range(1, max + 1): yield i Characteristics
-
Less code
-
Automatic state management
-
Cleaner and more readable
Exception handling in Python is a mechanism used to handle runtime errors (exceptions) so that the normal flow of the program is not interrupted.
Instead of crashing the program, Python allows you to catch errors and handle them gracefully.
What is an Exception?
An exception is an error that occurs during program execution.
Examples:
-
Division by zero
-
Accessing an invalid index
-
Converting an invalid string to int
Why Exception Handling Is Needed
-
Prevents program crash
-
Improves program reliability
-
Allows meaningful error messages
-
Helps in debugging
Exception Handling Syntax
try: # Code that may cause an exception except ExceptionType: # Code to handle exception else: # Executes if no exception occurs finally: # Always executes try–except–else–finally block in Python is used for structured exception handling. It allows you to separate normal logic, error handling, success logic, and cleanup code clearly.Execution Flow (Step by Step)
1. try block
-
Python executes this first
-
If an exception occurs → control jumps to
except -
If no exception →
exceptis skipped
2. except block
-
Executes only if an exception occurs in
try -
Handles the error gracefully
-
Prevents program crash
3. else block
-
Executes only if
tryruns successfully -
Runs after
tryand beforefinally -
Used for success-related logic
4. finally block
-
Executes no matter what
- Python
import osos.remove("data.txt")Exception occurs or not
-
-
Used for cleanup operations
14. How do you raise custom exceptions?
In Python, you can create and raise custom exceptions by:
-
Creating a new class that inherits from
Exception -
Using the
raisekeyword to trigger it
This allows you to define application-specific error types for better clarity and control.
Why Use Custom Exceptions?
-
Makes code more readable
-
Separates business logic errors
-
Easier debugging
-
Better error classification
Why File Handling is Important
-
Store application data
-
Log information
-
Read configuration files
-
Process large datasets
Basic Steps in File Handling
-
Open the file
-
Perform operations (read/write)
- Close the file
open() function, appropriate file modes, and usually the with statement for safe handling.Open File in Read Mode ("r")
with open("data.txt", "r") as file: content = file.read() print(content) "r" → Read mode (default)with automatically closes the file
Writing Files
Write Mode ("w")
with open("data.txt", "w") as file: file.write("Hello Python") os module or the pathlib module.import os
os.remove("data.txt")
with statement?The with statement in Python is used to handle resources safely and automatically, such as files, database connections, or network sockets. It ensures that resources are properly acquired and released, even if an error occurs.
Without with | With with |
|---|---|
| Manual cleanup | Automatic cleanup |
| Risk of leaks | Safe handling |
| More code | Cleaner code |
Pickling is the process of converting a Python object into a byte stream (serialization) so that it can be stored in a file or transferred over a network.
Unpickling is the reverse process — it converts the byte stream back into the original Python object (deserialization).
Why Pickling is Used
-
Save Python objects permanently
-
Transfer objects across networks
-
Store machine learning models
-
Cache data
| Pickle | JSON |
|---|---|
| Python-specific | Language-independent |
| Binary format | Text format |
| Can serialize complex objects | Limited to basic types |
| Not human-readable | Human-readable |
A shallow copy creates a new object, but it does not create copies of nested (inner) objects.
Instead, it copies references to the inner objects.
This means:
-
The outer object is new
-
The inner objects are shared between the original and copied object
When to Use Shallow Copy
-
When object does not contain nested mutable objects
-
When shared inner data is acceptable
-
When performance matters
A deep copy creates a completely independent copy of an object, including all nested (inner) objects.
This means:
-
The outer object is new
-
All inner objects are also newly created
-
Changes in the copied object do not affect the original object
When to Use Deep Copy
-
When objects contain nested mutable structures
-
When full independence is required
-
When modifying copied object should not affect original
| Feature | Shallow Copy | Deep Copy |
|---|---|---|
| Outer object copied | Yes | Yes |
| Inner objects copied | No | Yes |
| Shared references | Yes | No |
| Independent copy | Partial | Complete |
| Performance | Faster | Slower |
| Memory usage | Less | More |
| Method | copy.copy() | copy.deepcopy() |
sort() and sorted()?list.sort() and sorted() in Python use the Timsort algorithm.What is Timsort?
Timsort is a hybrid sorting algorithm derived from:
-
Merge Sort
-
Insertion Sort
It was designed by Tim Peters specifically for Python.
Why Python Uses Timsort
-
Very efficient for real-world data
-
Performs well on partially sorted arrays
-
Stable sorting algorithm
-
Optimized for performance
Key Characteristics of Timsort
| Feature | Description |
|---|---|
| Type | Hybrid (Merge + Insertion) |
| Stability | Stable (maintains order of equal elements) |
| Time Complexity (Best) | O(n) |
| Time Complexity (Average) | O(n log n) |
| Time Complexity (Worst) | O(n log n) |
| Space Complexity | O(n) |
print() Statementspdb)help() and dir()?help() and dir() are built-in Python functions used for introspection, meaning they help you explore objects, modules, and functions in Python.
help() Function
Purpose:
Displays the documentation (docstring) of a module, function, class, or object.
Example:
help (len)
Output:
-
Shows description of the
len()function -
Explains parameters and usage
You can also use:
dir() Function
Purpose:
Returns a list of attributes and methods of an object.
Example:
dir(list)
Output:
-
Shows all methods available for lists
(append,pop,sort, etc.)
26. What is a module?
Why Modules Are Important
-
Improve code organization
-
Promote reusability
-
Reduce code duplication
-
Support modular programming
A package in Python is a directory (folder) that contains multiple modules and possibly sub-packages.
It helps in organizing related modules into a structured hierarchy.
Why Packages Are Needed
-
Organize large projects
-
Avoid naming conflicts
-
Improve modularity
-
Support scalable applications
What is a “Unit”?
A unit is the smallest testable part of an application, such as:
-
A function
-
A method
-
A class
Why Unit Testing is Important
-
Detects bugs early
-
Improves code quality
-
Makes refactoring safer
-
Supports continuous integration
-
Provides documentation of behavior
Global Variables
Definition:
A global variable is defined outside all functions and classes and can be accessed throughout the program.
Accessible everywhere
Use global keyword to modify inside functions
Protected Variables
Definition:
A variable with a single underscore _var is considered protected by convention.
Intended for internal use
Can still be accessed outside class
Used mainly in inheritance
Example:
Protected is a convention, not enforced.
Private Variables
Definition:
A variable with double underscore __var is considered private.
Uses name mangling
Cannot be accessed directly outside the class
| Type | Syntax | Accessibility | Enforced? |
|---|---|---|---|
| Global | x = 10 | Entire program | Yes (scope-based) |
| Protected | _var | Inside class & subclasses | No (convention) |
| Private | __var | Inside class only | Partial (name mangling) |
Why Inheritance is Important
-
Promotes code reusability
-
Reduces duplication
-
Supports hierarchical relationships
-
Improves maintainability
Types of Inheritance in Python
Single Inheritance
Multiple Inheritance
Multilevel Inheritance
Hierarchical Inheritance
Hybrid Inheritance
Yes, Python fully supports multiple inheritance.
Multiple inheritance means a class can inherit from more than one parent class.
Why Multiple Inheritance is Useful
-
Code reusability
-
Combines behaviors from multiple classes
-
Supports mixin design pattern
Why Encapsulation is Important
-
Protects sensitive data
-
Prevents accidental modification
-
Improves security
-
Makes code modular and maintainable
How Encapsulation Works in Python
Python uses:
-
Public variables →
var -
Protected variables →
_var -
Private variables →
__var
Why Abstraction is Important
-
Reduces complexity
-
Improves code security
-
Enhances maintainability
-
Encourages modular design
super() function?The super() function in Python is used to call a method from the parent (base) class inside a child (derived) class.
It is commonly used in inheritance to:
-
Access parent class methods
-
Call parent constructors
-
Support method overriding
Why super() is Important
-
Avoids repeating parent class name
-
Supports multiple inheritance properly
-
Works according to Python’s MRO (Method Resolution Order)
-
Keeps code clean and maintainable
In Python, objects can be copied in three main ways:
-
Assignment (Reference Copy)
-
Shallow Copy
-
Deep Copy
The correct method depends on whether the object contains nested mutable elements.
| Method | Copies Outer Object | Copies Inner Objects | Independent |
|---|---|---|---|
| Assignment | No | No | No |
| Shallow Copy | Yes | No | Partial |
| Deep Copy | Yes | Yes | Fully |
Memory management in Python refers to how Python allocates, manages, and releases memory automatically during program execution.
Python handles memory internally using:
-
Private Heap Space
-
Reference Counting
-
Garbage Collection
Private Heap Space
-
All Python objects and data structures are stored in a private heap
-
Managed internally by Python’s memory manager
-
Not directly accessible by the programmer
Reference Counting
Python tracks how many references point to an object.
When the reference count becomes zero, the object is automatically deleted.
Garbage Collection
Reference counting alone cannot handle circular references.
Example:
a = [] b = [] a.append(b) b.append(a) Here:
-
Reference count never becomes zero
-
Python’s Garbage Collector (GC) detects and removes such cycles
Python uses a generational garbage collection system.
Reference counting is the primary memory management technique used by Python to track how many references (variables) are pointing to an object. When the reference count of an object becomes zero, Python automatically deletes the object and frees the memory.
How It Works: The Mechanism :
Each Python object internally maintains a counter (its "reference count") which is automatically managed by the interpreter.
Incrementing the count: The reference count increases when a new reference to the object is created. This happens in several scenarios:
- Variable assignment: x = object sets the count to at least 1.
- Aliasing: y = x makes y an additional reference to the same object, increasing the count.
- Storing in a container: Adding an object to a list, dictionary, or tuple increases its count.
- Function calls: Passing an object as an argument to a function creates a temporary reference, increasing the count while the function is active.
Decrementing the count: The reference count decreases when a reference to the object is removed or goes out of scope. This occurs when:
- del statement: del x explicitly removes the variable name, decreasing the count.
- Reassignment: x = new_object makes x stop referencing the old object, decreasing the old object's count.
- Going out of scope: Local variables are automatically removed when the function they are in finishes executing, decreasing their objects' counts.
- Removing from a container: Removing an object from a list (e.g., using list.remove()) decreases its count.
Deallocation: As soon as an object's reference count reaches zero, it means the object is no longer accessible from any part of the program, and Python's memory manager immediately deallocates its memory.
39. What is garbage collection?Garbage collection (GC) in Python is a memory management mechanism that automatically frees memory occupied by unused objects, especially those involved in circular references.
While Python primarily uses reference counting, garbage collection acts as a secondary mechanism to handle situations where reference counting fails.
Why Garbage Collection is Needed
Reference counting cannot handle circular references.
Python’s garbage collector detects and removes such cycles.
40. What is slicing with step values?When you include a step value, it specifies the interval between elements in the slice.
General Syntax
-
start → Starting index (inclusive)
-
end → Ending index (exclusive)
-
step → Number of positions to move each time
| Method | Preserves Order | Time Complexity | Recommended |
|---|---|---|---|
set() | No | O(n) | Yes (if order not important) |
dict.fromkeys() | Yes | O(n) | Best |
| Loop | Yes | O(n²) | Not for large data |
The most common and Pythonic way is using slicing with a negative step.
| Method | Recommended | Performance | Readability |
|---|---|---|---|
| Slicing | Yes | O(n) | Excellent |
| reversed() | Yes | O(n) | Good |
| Loop | No | O(n²) (in worst case) | Medium |
| Recursion | No | Slower | Complex |
To sort a list of objects by a specific attribute, you use:
-
sorted()function -
list.sort()method -
The
keyparameter -
Usually with
lambdaoroperator.attrgetter
| Feature | sorted() | sort() |
|---|---|---|
| Returns new list | Yes | No |
| Modifies original | No | Yes |
| Works on any iterable | Yes | Only lists |
| Speed | Slightly slower | Slightly faster |
Why Virtual Environments Are Needed
Suppose:
-
Project A requires
Django 3.2 -
Project B requires
Django 4.0
Without virtual environments → Version conflict
With virtual environments → Each project has its own dependencies
How Virtual Environments Work
Each virtual environment contains:
-
Its own Python interpreter
-
Its own
site-packagesdirectory -
Independent installed libraries
It isolates dependencies from the global environment.
45. How do you install third-party libraries?Basic Installation Using pip
Example:
__str__() and __repr__()?__str__() and __repr__() are special (magic/dunder) methods in Python used to define how an object is represented as a string.
They control how objects are displayed when:
-
Printed
-
Logged
-
Inspected in console
__str__() → User-Friendly Representation
-
Used by
print() -
Meant for end-users
-
Should be readable and clean
__repr__() → Developer-Friendly Representation
-
Used in interactive shell
-
Used by
repr() -
Should be unambiguous
-
Ideally, should recreate the object