Unique
Unique
Answer:
An iterator in Python is an object that allows you to iterate over a sequence of elements,
such as lists, tuples, strings, or even custom objects, one element at a time without needing
to know the underlying structure.
Answer:
An iterable is any Python object capable of returning its elements one at a time. It has
an __iter__() method that returns an iterator. Examples: lists, tuples, dictionaries,
strings.
An iterator is an object with a state so that it remembers where it is during iteration.
It implements both __iter__() and __next__() methods.
You can get an iterator from an iterable using the built-in iter() function:
python
CopyEdit
my_list = [1, 2, 3]
it = iter(my_list) # it is an iterator
print(next(it)) # 1
print(next(it)) # 2
Answer:
Yes. You can create a custom iterator by defining a class that implements __iter__() and
__next__().
Example:
python
CopyEdit
class MyRange:
def __init__(self, start, end):
self.current = start
self.end = end
def __iter__(self):
return self
def __next__(self):
if self.current >= self.end:
raise StopIteration
val = self.current
self.current += 1
return val
# Usage:
for num in MyRange(1, 5):
print(num)
Output:
CopyEdit
1
2
3
4
Answer:
Memory efficiency: They don’t load the entire sequence into memory at once (useful
for large datasets).
Lazy evaluation: Elements are produced only when needed.
Flexibility: Works well with loops and comprehensions.
Answer:
A for loop in Python:
So, this:
python
CopyEdit
for x in [1, 2, 3]:
print(x)
python
CopyEdit
it = iter([1, 2, 3])
while True:
try:
x = next(it)
print(x)
except StopIteration:
break
Answer:
Both are iterators, but generators are created using functions with the yield keyword
or generator expressions, whereas custom iterators require writing a class.
Generators are more compact and easier to write.
Generators are also lazy and memory efficient.
GENARATORS
Answer:
A generator in Python is a special type of iterator that allows you to iterate over a sequence
of values without storing the entire sequence in memory. It’s implemented using a
function with the yield keyword instead of return.
• yield pauses the function, sends a value back, and resumes from that point later.
________________________________________
Example:
python
CopyEdit
def my_generator():
yield 1
yield 2
yield 3
gen = my_generator()
print(next(gen)) # 1
print(next(gen)) # 2
print(next(gen)) # 3
Answer:
Example difference:
python
CopyEdit
def normal_function():
return 1
return 2 # This never executes
def generator_function():
yield 1
yield 2
Answer:
Answer:
Memory Efficiency: They don’t store all values in memory; values are produced on
demand.
Performance: Faster for large datasets because they don’t require full
materialization.
Convenience: Easier to implement than writing a custom iterator class.
✅ Q5: Can you show a real-world example where generators are useful?
Answer:
Example: Reading a large file line by line
python
CopyEdit
def read_large_file(filename):
with open(filename) as f:
for line in f:
yield line.strip()
This way, the entire file isn’t loaded into memory at once.
Answer:
Example:
python
CopyEdit
gen = (x**2 for x in range(5))
print(next(gen)) # 0
print(next(gen)) # 1
Answer:
python
CopyEdit
def coro():
val = yield "start"
yield f"received {val}"
g = coro()
print(next(g)) # "start"
print(g.send("hello")) # "received hello"
Answer:
When all yield statements are done, the generator raises StopIteration.
Answer:
No. Once exhausted, a generator cannot be restarted. You need to create a new generator
instance.
NAME SPACE AND SCCOPE IMP
https://www.youtube.com/watch?v=uAi6ZzsWN0k
✅ a) Built-in Namespace
Contains names of built-in functions and exceptions (print(), len(), int(), etc.).
Created when the Python interpreter starts.
Stays alive until the interpreter terminates.
Example:
python
CopyEdit
print(len("Python")) # 'print' and 'len' are in the built-in namespace
✅ b) Global Namespace
Example:
python
CopyEdit
x = 100 # Global namespace
def func():
print(x)
func() # Output: 100
✅ c) Enclosing Namespace
Example:
python
CopyEdit
def outer():
y = 20 # Enclosing namespace
def inner():
print(y) # Accesses enclosing variable
inner()
outer()
✅ d) Local Namespace
Example:
python
CopyEdit
def func():
z = 30 # Local namespace
print(z)
func()
✅ 3. How does Python resolve names across these
namespaces? (LEGB Rule)
Python follows the LEGB rule:
Example:
python
CopyEdit
x = "global"
def outer():
x = "enclosing"
def inner():
x = "local"
print(x)
inner()
outer()
# Output: local
Example:
python
CopyEdit
x = "global"
def outer():
x = "enclosing"
def inner():
nonlocal x
x = "modified by inner"
inner()
print(x)
counter = outer()
print(counter()) # 1
print(counter()) # 2
✅ 9. Summary
Namespace = Mapping of names to objects.
Scope = Region of code where a name is accessible.
Python uses LEGB rule.
global and nonlocal help modify outer variables.
COMPREHENSIONS
1. List Comprehension
Syntax:
python
CopyEdit
[expression for item in iterable if condition]
Example:
python
CopyEdit
squares = [x**2 for x in range(1, 6)]
# Output: [1, 4, 9, 16, 25]
2. Set Comprehension
Similar to list comprehension but creates a set:
python
CopyEdit
unique_squares = {x**2 for x in [1, 2, 2, 3]}
# Output: {1, 4, 9}
3. Dictionary Comprehension
Syntax:
python
CopyEdit
{key_expr: value_expr for item in iterable if condition}
Example:
python
CopyEdit
squares_dict = {x: x**2 for x in range(1, 6)}
# Output: {1: 1, 2: 4, 3: 9, 4: 16, 5: 25}
python
CopyEdit
matrix = [[1, 2], [3, 4]]
flattened = [num for row in matrix for num in row]
# Output: [1, 2, 3, 4]
python
CopyEdit
even_squares = [x**2 for x in range(10) if x % 2 == 0]
# Output: [0, 4, 16, 36, 64]
DECORATORS
https://www.youtube.com/watch?v=uG4FD0SZ-cQ
def decorator(func):
def wrapper():
print("initial")
func()
print("completed")
return wrapper
def hello():
print("Executing")
First hello() will be called then it will create obj and call the
hello=decorator(hello) soo on…
A:
A decorator in Python is a function that takes another function (or method) as input and
returns a new function with additional functionality, without modifying the original
function's code.
Decorators allow you to add behavior dynamically at runtime.
A:
They use functions as first-class citizens and closures.
A:
python
CopyEdit
def my_decorator(func):
def wrapper():
print("Before function execution")
func()
print("After function execution")
return wrapper
@my_decorator
def say_hello():
print("Hello!")
say_hello()
Output:
pgsql
CopyEdit
Before function execution
Hello!
After function execution
A:
@decorator is just syntactic sugar for:
python
CopyEdit
say_hello = my_decorator(say_hello)
python
CopyEdit
def repeat(n):
def decorator(func):
def wrapper(*args, **kwargs):
for _ in range(n):
func(*args, **kwargs)
return wrapper
return decorator
@repeat(3)
def greet():
print("Hi!")
greet()
Output:
CopyEdit
Hi!
Hi!
Hi!
A:
Example:
python
CopyEdit
class Example:
@staticmethod
def static_method():
print("Static Method")
@classmethod
def class_method(cls):
print("Class Method")
CLOSURES
https://www.programiz.com/python-programming/closure
A:
A closure is a function object that remembers the values from its enclosing lexical scope
even after the outer function has finished executing.
In simple terms:
def calculate():
num = 1
def inner_func():
nonlocal num
num += 2
return num
return inner_func
odd = calculate()
A:
Closures work because of Lexical Scoping:
Example:
python
CopyEdit
print(closure_func.__closure__) # Shows cell objects storing values
A:
Example: Creating a multiplier function
python
CopyEdit
def multiplier(n):
def multiply(x):
return x * n
return multiply
double = multiplier(2)
triple = multiplier(3)
print(double(5)) # Output: 10
print(triple(5)) # Output: 15
A:
A:
Decorators are built using closures because they need to wrap another function and
maintain state.
python
CopyEdit
print(closure_func.__closure__) # Non-None means it's a closure
A:
Yes, but you need the nonlocal keyword (for enclosing scope).
Example:
python
CopyEdit
def outer():
count = 0
def inner():
nonlocal count
count += 1
return count
return inner
counter = outer()
print(counter()) # 1
print(counter()) # 2
A:
Late binding issue: Inner functions capture variables by reference, not by value.
Example:
python
CopyEdit
funcs = []
for i in range(3):
funcs.append(lambda: i)
for f in funcs:
print(f()) # Output: 2, 2, 2 (NOT 0, 1, 2)
Fix:
python
CopyEdit
funcs = []
for i in range(3):
funcs.append(lambda i=i: i)
✅ Python Multithreading Example using threading.Thread
Code:
python
CopyEdit
from threading import Thread
from time import sleep
Explanation:
1. Import Modules
o threading.Thread → Used to create threads.
o time.sleep() → Adds delay to simulate work
and allow thread interleaving.
2. Create Thread Classes
o Task1 and Task2 inherit from Thread class.
o Override run() method to define the work each thread will do.
3. Create Thread Instances
python
CopyEdit
thread1 = Task1()
thread2 = Task2()
4. Start Threads
python
CopyEdit
thread1.start()
thread2.start()
python
CopyEdit
thread1.join()
thread2.join()
css
CopyEdit
Main THREAD executing
Output (Interleaved):
css
CopyEdit
TASK1
TASK2
TASK1
TASK2
TASK1
TASK2
TASK1
TASK2
TASK1
TASK2
Main THREAD executing
✅ Key Points:
Code:
python
CopyEdit
from multiprocessing import Process
from time import sleep
✅ Explanation
1. Import Modules
o multiprocessing.Process → Creates a new process (not just a thread).
o time.sleep() → Simulates work and shows concurrency.
2. Define Functions for Tasks
o Unlike threading, we usually define functions (not classes) for Process.
3. Create Process Instances
python
CopyEdit
process1 = Process(target=task1)
process2 = Process(target=task2)
4. Start Processes
python
CopyEdit
process1.start()
process2.start()
o Each process runs in its own memory space, separate from the main process.
o Can run on multiple CPU cores (true parallelism).
5. Wait for Processes to Complete
python
CopyEdit
process1.join()
process2.join()
css
CopyEdit
Main PROCESS executing
✅ Output (Interleaved):
css
CopyEdit
TASK1
TASK2
TASK1
TASK2
TASK1
TASK2
TASK1
TASK2
TASK1
TASK2
Main PROCESS executing
Q1: What is Multithreading in Python?
Answer:
Multithreading is a technique where multiple threads (smaller units of a process) run
concurrently within the same process. Threads share the same memory space, which makes
communication between them easy and efficient.
Answer:
Multiprocessing is the technique of running multiple processes simultaneously, with each
process having its own Python interpreter and memory space.
Example Use Case: Performing heavy numerical computations on multiple CPU cores.
Multithreading Example
python
CopyEdit
import threading
import time
def print_numbers():
for i in range(5):
print(f"Number: {i}")
time.sleep(1)
thread1 = threading.Thread(target=print_numbers)
thread2 = threading.Thread(target=print_numbers)
thread1.start()
thread2.start()
thread1.join()
thread2.join()
print("Finished Multithreading")
Multiprocessing Example
python
CopyEdit
import multiprocessing
import time
def print_numbers():
for i in range(5):
print(f"Number: {i}")
time.sleep(1)
if __name__ == "__main__":
process1 = multiprocessing.Process(target=print_numbers)
process2 = multiprocessing.Process(target=print_numbers)
process1.start()
process2.start()
process1.join()
process2.join()
print("Finished Multiprocessing")
✅ Key Takeaways:
✅ 1. threading.Thread Methods
In the threading example, we used these methods:
(a) start()
(b) run()
python
CopyEdit
class MyThread(Thread):
def run(self):
print("Running in a new thread")
(c) join()
Purpose: Blocks the calling (main) thread until the thread on which join() is called
terminates.
Why used:
o Ensures that the main program waits for all threads to finish before continuing.
Example:
python
CopyEdit
thread1.join() # Wait for thread1 to finish
(d) sleep()
python
CopyEdit
sleep(0.5) # Pauses for half a second
✅ 2. multiprocessing.Process Methods
In the multiprocessing example, we used:
(a) Process()
python
CopyEdit
process1 = Process(target=task1)
(b) start()
Purpose: Blocks the calling process until the process whose join is called terminates.
Why used:
o Prevents the main process from exiting before child processes complete.
Example:
python
CopyEdit
process1.join()
(d) sleep()
2. Code Overview
python
CopyEdit
from multiprocessing import Pool
import time
def square(n):
time.sleep(0.5) # Simulate heavy computation
return n * n
if __name__ == "__main__":
numbers = [1, 2, 3, 4, 5]
with Pool(processes=3) as pool: # Create 3 worker processes
results = pool.map(square, numbers)
print("Results:", results)
3. Key Components
4. Execution Flow
5. Output
makefile
CopyEdit
Results: [1, 4, 9, 16, 25]
7. Advantages
True parallelism using multiple CPU cores (unlike threads, which are limited by GIL
in CPython).
Ideal for CPU-bound tasks.
8. Alternatives
ThreadPool (for I/O-bound tasks).
apply() or apply_async() for single-task submission.
concurrent.futures.ProcessPoolExecutor.
Asyncio
Imagine you’re a single chef (one thread) cooking 10 dishes. Instead of waiting for
each dish to finish cooking before starting the next, you chop vegetables for one dish,
then while it’s simmering, you mix ingredients for another. You keep switching
between tasks to keep everything moving.
How it works:
o Uses an event loop, which is like a scheduler that decides which task runs
next.
o Tasks are written as coroutines (special functions using async def and await).
o When a task is waiting (e.g., for a network response), it “pauses” and lets other
tasks run.
o Everything happens in one thread, so it’s lightweight and efficient.
Example:
python
CollapseWrapRun
Copy
import asyncio
print(f"Starting {name}")
print(f"Finished {name}")
async def main():
asyncio.run(main())
Output:
text
CollapseWrap
Copy
Starting Pizza
Starting Pasta
Finished Pizza
Finished Pasta
Here, the chef (event loop) starts Pizza, waits (but works on Pasta during the wait),
then finishes both.
Threading
Imagine you hire two chefs (two threads) to cook Pizza and Pasta. Each chef works on
their dish independently, but they share the same kitchen (memory). The kitchen has a
rule (the GIL) that only one chef can use the stove at a time for certain tasks.
How it works:
o Each thread runs its own task, and the operating system decides when to
switch between them.
o Threads are preemptive, meaning the system can interrupt one chef to let
another work.
o Good for tasks where waiting happens (e.g., downloading files), but the GIL
limits performance for tasks needing heavy computation.
Example:
python
CollapseWrapRun
Copy
import threading
import time
def cook_dish(name):
print(f"Starting {name}")
print(f"Finished {name}")
threading.Thread(target=cook_dish, args=("Pasta",))]
for t in threads:
t.start()
for t in threads:
t.join()
Output:
text
CollapseWrap
Copy
Starting Pizza
Starting Pasta
Finished Pizza
Finished Pasta
Here, two chefs (threads) work on their dishes, but the GIL might slow things down if
they need to share resources.
3. Key Differences
Imp
✅ 1. Yield
Yield = temporarily give up control so something else can run.
In asyncio, you don’t use yield (except in generator-based coroutines); you use
await.
await is the modern way to say:
“Pause me here until the awaited result is ready, and let the event loop run other
tasks in the meantime.”
Without yield/await, you block the entire loop.
✅ 2. Coroutine
A coroutine is a special function that can be suspended and resumed.
Declared with async def, returns a coroutine object when called.
Unlike normal functions, coroutines can stop mid-way at await points and come back
later.
Example:
python
CopyEdit
async def say_hello():
print("Hello")
await asyncio.sleep(1) # yield to event loop
print("World")
✅ 3. Event Loop
The event loop is the scheduler that runs your coroutines.
Single-threaded in asyncio (usually).
It keeps a list of:
o Ready tasks (can run immediately)
o Waiting tasks (blocked on I/O, timers)
Runs in a loop:
arduino
CopyEdit
while tasks remain:
pick ready tasks -> run until next await
check I/O events
wake tasks that are ready
✅ 4. Preemption
Preemptive multitasking = The OS can interrupt any thread at any time, without its
consent.
Threads use this: The OS scheduler forces context switches even if your code never
yields.
Asyncio is NOT preemptive:
o The loop never interrupts a coroutine in the middle of Python code.
o Coroutines must cooperate by calling await to give control back.
⚖️Key Difference
If a coroutine runs CPU-heavy work without await, the event loop cannot schedule
anything else.
→ Other tasks freeze.
Want me to draw a diagram showing:
Or do you want a low-level explanation of coroutine internals (send(), yield from, etc.)
next?
✅ Definition
Every time a coroutine hits an await on something that’s not immediately ready (I/O,
timer), it yields control back to the event loop.
The event loop then schedules other tasks that are ready.
When the awaited operation completes, the event loop resumes the original
coroutine.
Example:
python
CopyEdit
async def task1():
print("Start T1")
await asyncio.sleep(2) # yields control to loop
print("End T1")
asyncio.run(main())
Output:
sql
CopyEdit
Start T1
Start T2
End T2
End T1
Notice:
For interview
Answer:
asyncio is a Python library used to write concurrent code using the async/await syntax. It
provides an event loop that runs asynchronous tasks and callbacks, performs network I/O,
and manages subprocesses. Unlike threads, asyncio uses single-threaded cooperative
multitasking, which means tasks voluntarily yield control when they perform I/O or await
something, making it very efficient for I/O-bound operations like web scraping, database
calls, or network requests.
Key points:
Introduced in Python 3.4, modern usage uses async and await (from Python 3.5+).
Uses coroutines for concurrency, not threads.
Great for I/O-bound tasks but not suitable for CPU-bound tasks because the event
loop runs in a single thread.
Example:
python
CopyEdit
import asyncio
asyncio.run(main())
Q2. What is multithreading in Python, and how is it different from asyncio?
Answer:
Multithreading in Python refers to using multiple threads within a single process to execute
code concurrently. It is managed by the threading module. However, due to Python's
Global Interpreter Lock (GIL), only one thread executes Python bytecode at a time. This
means multithreading is not truly parallel for CPU-bound tasks, but it works well for I/O-
bound tasks like reading files or making network requests.
Key points:
Example:
python
CopyEdit
import threading
import time
def task():
time.sleep(1)
print("Task complete")
threads = []
for _ in range(5):
t = threading.Thread(target=task)
t.start()
threads.append(t)
for t in threads:
t.join()
✅ Pro tip for interview: If asked "When to use asyncio vs threads?", say:
Use asyncio when your tasks involve lots of network or I/O and you want minimal
overhead.
Use multithreading when you have blocking I/O libraries that are not async-
compatible.
1. Memory Management in Python: Overview
Python uses automatic memory management, meaning the developer does not need
to manually allocate or free memory.
It is built on top of the private heap space where all Python objects and data
structures are stored.
Python’s memory management system is mainly handled by:
1. Python Memory Manager
2. Garbage Collector
3. Reference Counting
4. Dynamic Typing & Object Reuse
All objects (e.g., integers, lists, dictionaries) are stored in a private heap reserved for
the Python interpreter.
This heap is not accessible directly by the programmer.
The Python memory manager handles the allocation of this heap for various objects.
3. Reference Counting
Python uses reference counting as the primary memory management technique.
Every object in Python has an internal counter that tracks the number of references
pointing to it.
When you assign an object to a variable, the reference count increases:
python
CopyEdit
a = [1, 2, 3]
b = a # reference count of the list increases
python
CopyEdit
import sys
a = []
print(sys.getrefcount(a)) # Usually returns 2 (one for 'a', one for the
function argument)
python
CopyEdit
a = []
b = []
a.append(b)
b.append(a)
Both a and b have references > 0, even though they are unreachable.
Python’s garbage collector (in the gc module) detects these cycles and frees
memory.
GC Mechanism
python
CopyEdit
del a
✅ Importing re Module
python
CopyEdit
import re
pattern = r"Hello"
text = "Hello World"
✔ Explanation:
✔ Explanation:
✔ Explanation:
✔ Explanation:
✔ Explanation:
✔ Explanation:
for t in texts:
print(re.match(pattern, t))
✔ Explanation:
u?means u is optional.
Matches both "color" and "colour".
✔ Explanation:
4. Validate IP Address
LeetCode #468
Difficulty: Medium
Problem:
Write a function to check if a given string is a valid IPv4 or IPv6 address.
IPv4: ^(25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(...)){3}$
IPv6: ^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$
LeetCode #420
Difficulty: Hard
Problem:
Check if a string meets strong password criteria: