Skip to main content

Python Interview Questions

35 Master Questions
By Think IT TeluguTheory, Classic Logic & Debugging
Open Python Lab

Python Technical Interview & Coding Master Guide

Designed with line-by-line pedagogical clarity for college freshers, self-taught developers, and career switchers. Covers core theory, algorithmic problem solving, star patterns, and beginner code debugging diagnostics.

1. Python Fundamentals & Freshers (Must Know)

Core language mechanics, dynamic typing, memory references, and basic data structures.

Fresher
#Basics#Features

1.What is Python and what are its key features?

Why Interviewers Ask This

Interviewers ask this as an ice-breaker to test if you understand the high-level architecture of Python compared to compiled languages like C++ or Java.

How to Explain to Interviewer (Your Spoken Answer)

Python is a high-level, dynamically typed, interpreted language created by Guido van Rossum. Its biggest strength is its simple, English-like syntax which emphasizes developer productivity, paired with a massive standard library for everything from web development to AI.

Python is an interpreted, high-level, general-purpose programming language known for readability, rapid prototyping, and a vast ecosystem.

  • Readable & Expressive: Uses indentation instead of curly braces, making code clean and concise.
  • Interpreted & Interactive: Code executes line-by-line via the Python Virtual Machine (PVM) without manual compilation.
  • Dynamically Typed: Variable types are inferred at runtime based on the assigned object.
  • Extensive Ecosystem: Built-in packages plus millions of third-party libraries on PyPI (FastAPI, NumPy, PyTorch).
# Clean and expressive syntax in Python:
name = "Think IT Telugu"
role = "AI Engineer"
print(f"Welcome to {name}! Role: {role}")
# Output: Welcome to Think IT Telugu! Role: AI Engineer
🔍 Line-by-Line Breakdown & Dry Run
Line 1 (name = "Think IT Telugu")Allocates a string object in memory and binds the identifier name to its memory address.
Line 2 (role = "AI Engineer")Allocates another string object and binds role.
Line 3 (print(...))Evaluates the f-string expressions and sends the formatted output to stdout.
Fresher
#Data Types#Variables

2.What is a Dynamically Typed language and how does Python handle types?

Why Interviewers Ask This

Tests if you understand runtime type binding vs compile-time declaration and how variables act as memory references.

How to Explain to Interviewer (Your Spoken Answer)

In Python, types belong to objects, not variable names. Variables are simply named tags pointing to objects in heap memory. Because type checking occurs at runtime, a variable can reference an integer at one moment and a string the next without explicit type declarations.

Variables do not have fixed types; they reference objects that carry type information in memory.

  • No Variable Declarations: You write score = 95 instead of int score = 95.
  • Dynamic Rebinding: A variable can reference different types across its lifecycle.
  • Strong Typing: Even though dynamically typed, Python is strongly typed — it never silently converts "10" + 5.
x = 100 # x references an integer object
print(type(x)) # Output: <class 'int'>

x = "Telugu" # x is rebound to a string object
print(type(x)) # Output: <class 'str'>

# Strong typing prevents invalid operations:
try:
result = "Score: " + 100
except TypeError as e:
print(f"Error caught: {e}")
# Output: Error caught: can only concatenate str (not "int") to str
🔍 Line-by-Line Breakdown & Dry Run
Line 1 (x = 100)Python creates an integer object 100 and points x to it.
Line 4 (x = "Telugu")Python creates a string object "Telugu" and repoints x to the string. The integer 100 is freed if no other reference exists.
Fresher
#Architecture#CPython

3.What does it mean that Python is an "Interpreted" language?

Why Interviewers Ask This

Evaluates your understanding of CPython execution internals, bytecode compilation, and the Python Virtual Machine (PVM).

How to Explain to Interviewer (Your Spoken Answer)

When you run a Python script, CPython first parses and compiles the code into bytecode (.pyc). The Python Virtual Machine then reads this bytecode instruction by instruction and translates it into machine code at runtime, making Python platform-independent.

Python executes code through a two-stage process: compilation to bytecode and interpretation by the PVM.

  • Bytecode Compilation: Source .py is compiled into bytecode stored in __pycache__.
  • PVM Execution: The virtual machine loops over bytecode instructions and executes them.
  • Immediate Error Halting: If line 5 has a runtime error, lines 1–4 execute successfully before execution terminates.
print("Step 1: Processing data...") # Executes immediately
print("Step 2: Connecting...") # Executes immediately

# Uncommenting the next line would stop execution at Step 3:
# print(10 / 0) # ZeroDivisionError occurs here

print("Step 3: Complete!")
# Output:
# Step 1: Processing data...
# Step 2: Connecting...
# Step 3: Complete!
🔍 Line-by-Line Breakdown & Dry Run
Phase 1 (Bytecode)CPython compiles script into bytecode instructions (LOAD_GLOBAL, CALL_FUNCTION).
Phase 2 (PVM)PVM processes instructions sequentially from top to bottom.
Fresher
#Memory#Data Types

4.What is the difference between Mutable and Immutable data types in Python?

Why Interviewers Ask This

Crucial for understanding how Python handles data modifications in memory, dictionary keys, and function parameter passing.

How to Explain to Interviewer (Your Spoken Answer)

Mutable objects can have their contents modified in-place without changing their memory address (id). Immutable objects cannot be altered once created; any modification produces a brand-new object in memory with a new id.

Mutable types (Lists, Dicts, Sets) allow in-place edits. Immutable types (Integers, Floats, Strings, Tuples) create new objects on modification.

  • Mutable: list, dict, set, bytearray — change values without changing memory ID.
  • Immutable: int, float, str, tuple, frozenset, bool — cannot be modified in place.
  • Dict Key Requirement: Only immutable (hashable) objects can be used as dictionary keys.
# Immutable Example (String):
s = "hello"
print(f"Original ID: {id(s)}")
s = s + " world"
print(f"New ID: {id(s)}") # Different ID! New object created

# Mutable Example (List):
nums = [1, 2, 3]
print(f"List ID before: {id(nums)}")
nums.append(4)
print(f"List ID after: {id(nums)}") # Same ID! Modified in-place
# Output shows List ID remains identical, while String ID changes
🔍 Line-by-Line Breakdown & Dry Run
String Concatenations + " world" allocates a new string in memory and rebinds s.
List Appendnums.append(4) mutates the existing buffer in memory without allocating a new list header.
Fresher
#Data Structures#Lists#Tuples

5.What is the difference between a List and a Tuple in Python?

Why Interviewers Ask This

Tests when to choose lists vs tuples for performance, data integrity, and dictionary indexing.

How to Explain to Interviewer (Your Spoken Answer)

Lists are mutable collections defined with square brackets [] that allow dynamic resizing and element modification. Tuples are immutable collections defined with parentheses () that are faster, use less memory, and can be used as dictionary keys.

Lists are mutable sequences for dynamic data; Tuples are immutable fixed records with lower memory overhead.

  • Mutability: Lists can be changed (append, pop); Tuples cannot be modified once defined.
  • Syntax: Lists use [1, 2, 3], Tuples use (1, 2, 3).
  • Memory & Speed: Tuples are allocated in a single fixed memory block, making iteration and creation faster.
  • Hashing: Tuples can be dictionary keys or set elements; Lists cannot because they are unhashable.
import sys

my_list = [1, 2, 3, 4, 5]
my_tuple = (1, 2, 3, 4, 5)

print("List Size (bytes): ", sys.getsizeof(my_list)) # e.g. 104 bytes
print("Tuple Size (bytes):", sys.getsizeof(my_tuple)) # e.g. 80 bytes (more lightweight)

# Lists can be modified:
my_list[0] = 99
print("Updated List:", my_list) # Output: [99, 2, 3, 4, 5]

# Tuples throw TypeError if modified:
try:
my_tuple[0] = 99
except TypeError as e:
print("Tuple Error:", e)
# Output: Tuple Error: 'tuple' object does not support item assignment
🔍 Line-by-Line Breakdown & Dry Run
Memory AllocationLists allocate extra over-provisioned memory slots for fast appends; Tuples allocate exact memory size.
Fresher
#Operators#Memory

6.What is the difference between "==" and "is" in Python?

Why Interviewers Ask This

Fundamental check on value equality vs memory identity and Python object interning.

How to Explain to Interviewer (Your Spoken Answer)

The equality operator == compares the values or contents of two objects (via their __eq__ method). The identity operator is checks whether two variables point to the exact same memory location (id(a) == id(b)).

== checks value equality; is checks identity (same memory address).

  • ==: Compares object values ([1, 2] == [1, 2] is True).
  • is: Compares memory addresses ([1, 2] is [1, 2] is False).
  • Singleton Checking: Always use is when comparing against None (if val is None:).
a = [1, 2, 3]
b = [1, 2, 3]
c = a # c points to the exact same memory address as a

print("a == b:", a == b) # Output: True (Values are identical)
print("a is b:", a is b) # Output: False (Distinct objects in memory)
print("a is c:", a is c) # Output: True (Same memory address)

# Comparing with None:
result = None
print("is None check:", result is None) # Output: True
🔍 Line-by-Line Breakdown & Dry Run
a == bIterates elements: 1==1, 2==2, 3==3 -> True.
a is bCompares id(a) == id(b). Since two separate lists were initialized, id(a) != id(b) -> False.
Fresher
#Loops#Control Flow

7.What is the difference between break, continue, and pass?

Why Interviewers Ask This

Tests basic loop flow control and how empty syntax blocks are handled.

How to Explain to Interviewer (Your Spoken Answer)

break immediately terminates the entire loop. continue skips the rest of the current iteration and jumps to the next cycle. pass is a null statement placeholder that does nothing, used when syntax requires a statement block.

break exits the loop entirely; continue skips to the next iteration; pass is a placeholder that does nothing.

  • break: Immediately halts the loop and jumps to code following the loop.
  • continue: Skips remainder of loop body for current element and moves to next item.
  • pass: Syntax placeholder for empty functions, classes, or conditional branches.
# 1. Break Example: Stops at 3
for i in range(1, 6):
if i == 3:
break
print(f"Break loop: {i}")
# Output: Break loop: 1, Break loop: 2

# 2. Continue Example: Skips 3
for i in range(1, 5):
if i == 3:
continue
print(f"Continue loop: {i}")
# Output: Continue loop: 1, Continue loop: 2, Continue loop: 4

# 3. Pass Example: Placeholder
def future_feature():
pass # Syntactically valid without error
🔍 Line-by-Line Breakdown & Dry Run
Break executionWhen i == 3, loop terminates immediately; 4 and 5 are never processed.
Continue executionWhen i == 3, continue jumps back to loop header for i = 4.
Fresher
#Scope#Functions

8.What is the LEGB Rule for variable scope in Python?

Why Interviewers Ask This

Tests if you understand how Python resolves variable names across local, enclosing, global, and built-in scopes.

How to Explain to Interviewer (Your Spoken Answer)

LEGB stands for Local, Enclosing, Global, and Built-in. When a variable name is referenced, Python searches these four namespaces in order. If the name is not found in any of them, a NameError is raised.

Python searches variable scopes in order: Local -> Enclosing (nested functions) -> Global (module level) -> Built-in.

  • L (Local): Names defined inside current function.
  • E (Enclosing): Names defined in enclosing/outer functions (closures).
  • G (Global): Names defined at top module level.
  • B (Built-in): Pre-loaded names in Python (len, print, range).
x = "Global X" # Global Scope

def outer():
x = "Enclosing X" # Enclosing Scope

def inner():
x = "Local X" # Local Scope
print("Inner sees:", x)

inner()
print("Outer sees:", x)

outer()
print("Module sees:", x)
# Output:
# Inner sees: Local X
# Outer sees: Enclosing X
# Module sees: Global X
🔍 Line-by-Line Breakdown & Dry Run
inner() lookupFinds x immediately in its Local frame -> prints "Local X".
outer() lookupFinds x in its own frame -> prints "Enclosing X".
Module lookupFinds x in module dictionary -> prints "Global X".

2. Classic Logic & Problem Solving (Prime, Fibonacci, Palindrome, Anagram)

Most requested coding problems asked in technical rounds for freshers and junior developers.

Coding
#Math#Algorithms#Prime#Optimization

9.How do you check if a number is Prime and find all primes in a range?

Why Interviewers Ask This

High-frequency screening problem testing loop efficiency, mathematical optimization from O(N) to O(√N), and boundary edge-case handling.

How to Explain to Interviewer (Your Spoken Answer)

A prime number is a natural number greater than 1 with exactly two factors: 1 and itself. Instead of checking all numbers up to N (O(N)), we only test divisors up to √N. If no divisor is found in [2, int(√N)], the number must be prime.

Check if N > 1, then test divisibility from 2 up to int(n**0.5) + 1. If any divide evenly, return False.

  • Edge Cases: Numbers <= 1 are not prime. 2 is the only even prime.
  • Optimization: Any composite factor > √N must pair with a factor < √N.
  • Range Finding: Apply is_prime() across range(start, end + 1).
def is_prime(n):
if n <= 1:
return False
# Check divisors up to square root of n:
for i in range(2, int(n ** 0.5) + 1):
if n % i == 0:
return False
return True

# Test single numbers:
print("is_prime(29):", is_prime(29)) # Output: True
print("is_prime(10):", is_prime(10)) # Output: False

# Find all primes in range [1, 20]:
primes = [num for num in range(1, 21) if is_prime(num)]
print("Primes (1-20):", primes)
# Output: Primes (1-20): [2, 3, 5, 7, 11, 13, 17, 19]
🔍 Line-by-Line Breakdown & Dry Run
Input n = 29n <= 1 is False. √29 ≈ 5.38 -> range(2, 6) tests i = 2, 3, 4, 5.
Divisibility checks29%2 != 0, 29%3 != 0, 29%4 != 0, 29%5 != 0 -> Loop completes -> returns True.
Coding
#Algorithms#Fibonacci#Generators

10.How do you generate the Fibonacci series (Iterative & Generator approaches)?

Why Interviewers Ask This

Tests state tracking in loops, recursion stack dangers, and memory-efficient streaming with generators (yield).

How to Explain to Interviewer (Your Spoken Answer)

The Fibonacci series starts with 0 and 1, where each next term is the sum of the previous two (F(n) = F(n-1) + F(n-2)). An iterative approach with two pointers runs in O(N) time and O(1) space, avoiding exponential recursion call overhead.

Maintain two variables a, b = 0, 1 and update them in a loop using tuple assignment a, b = b, a + b.

  • Iterative Approach: O(N) time with O(1) space — standard and efficient.
  • Generator (yield): Generates terms lazily on-demand without storing millions of numbers in RAM.
  • Avoid Naive Recursion: Naive recursion takes O(2^N) time due to duplicate sub-problem computations.
# 1. Iterative List Generation:
def fibonacci_list(n):
if n <= 0:
return []
series = []
a, b = 0, 1
for _ in range(n):
series.append(a)
a, b = b, a + b
return series

print("First 8 Fibonacci terms:", fibonacci_list(8))
# Output: First 8 Fibonacci terms: [0, 1, 1, 2, 3, 5, 8, 13]

# 2. Memory-Efficient Generator:
def fibonacci_gen(limit):
a, b = 0, 1
while a <= limit:
yield a
a, b = b, a + b

print("Fibonacci numbers <= 20:", list(fibonacci_gen(20)))
# Output: Fibonacci numbers <= 20: [0, 1, 1, 2, 3, 5, 8, 13]
🔍 Line-by-Line Breakdown & Dry Run
Inita = 0, b = 1
Iter 1Append 0. Rebind a, b = 1, 0 + 1 = 1
Iter 2Append 1. Rebind a, b = 1, 1 + 1 = 2
Iter 3Append 1. Rebind a, b = 2, 1 + 2 = 3
Iter 4Append 2. Rebind a, b = 3, 2 + 3 = 5
Coding
#Strings#Palindrome#Math

11.How do you check if a String or Number is a Palindrome (with & without slicing)?

Why Interviewers Ask This

Tests string indexing, two-pointer techniques, and mathematical digit reversal using modulo % 10 and integer division // 10.

How to Explain to Interviewer (Your Spoken Answer)

A palindrome reads the same forwards and backwards. For strings, Python allows instant slicing s == s[::-1]. If the interviewer forbids slicing or string conversion on numbers, we reverse digits mathematically using % 10 and // 10.

Strings: s == s[::-1] or two pointers. Numbers: Extract last digit with num % 10 and build reversed integer with rev * 10 + digit.

  • String Slicing: s == s[::-1] is fastest and idiomatic in Python.
  • Two-Pointer Approach: Compare characters from left and right moving inward in O(1) extra space.
  • Number Without String Conversion: Reverse digits mathematically with % 10 and // 10.
# Method 1: String Two-Pointer Check
def is_palindrome_str(s):
cleaned = "".join(c.lower() for c in s if c.isalnum())
return cleaned == cleaned[::-1]

print(is_palindrome_str("Race Car")) # Output: True
print(is_palindrome_str("Python")) # Output: False

# Method 2: Number Palindrome WITHOUT str() conversion
def is_palindrome_number(num):
if num < 0:
return False # Negative numbers like -121 are not palindromes
original = num
reversed_num = 0
while num > 0:
digit = num % 10
reversed_num = (reversed_num * 10) + digit
num = num // 10
return original == reversed_num

print("121 is palindrome:", is_palindrome_number(121)) # Output: True
print("123 is palindrome:", is_palindrome_number(123)) # Output: False
🔍 Line-by-Line Breakdown & Dry Run
Number num = 121orig = 121, rev = 0
Loop 1digit = 121 % 10 = 1. rev = 0*10 + 1 = 1. num = 121 // 10 = 12
Loop 2digit = 12 % 10 = 2. rev = 1*10 + 2 = 12. num = 12 // 10 = 1
Loop 3digit = 1 % 10 = 1. rev = 12*10 + 1 = 121. num = 1 // 10 = 0
Result121 == 121 -> True
Coding
#Strings#Dictionaries#Anagram

12.How do you check if two strings are Anagrams?

Why Interviewers Ask This

Evaluates character frequency counting, hash map logic, and time complexity trade-offs between sorting and hash tables.

How to Explain to Interviewer (Your Spoken Answer)

Two strings are anagrams if they contain the exact same characters with identical frequencies, just arranged in a different order. We can check this in O(N) time using a frequency dictionary or collections.Counter, or in O(N log N) by comparing sorted strings.

Compare character frequency counts using collections.Counter(s1) == collections.Counter(s2) or a standard dictionary.

  • Hash Map / Counter: O(N) time and O(K) space — optimal.
  • Sorting: sorted(s1) == sorted(s2) takes O(N log N) time and O(N) space.
  • Case & Spaces: Standardize inputs with .lower() and .replace(" ", "").
from collections import Counter

# Method 1: Optimal O(N) using Counter / Dictionary
def is_anagram(str1, str2):
# Clean spaces and convert to lowercase
s1 = str1.replace(" ", "").lower()
s2 = str2.replace(" ", "").lower()
return Counter(s1) == Counter(s2)

# Method 2: Using standard dictionary
def is_anagram_manual(str1, str2):
s1 = str1.replace(" ", "").lower()
s2 = str2.replace(" ", "").lower()
if len(s1) != len(s2):
return False
counts = {}
for char in s1:
counts[char] = counts.get(char, 0) + 1
for char in s2:
if char not in counts or counts[char] == 0:
return False
counts[char] -= 1
return True

print(is_anagram("listen", "silent")) # Output: True
print(is_anagram("Triangle", "Integral")) # Output: True
print(is_anagram("hello", "world")) # Output: False
🔍 Line-by-Line Breakdown & Dry Run
String 1 ("listen")Counts: {"l":1, "i":1, "s":1, "t":1, "e":1, "n":1}
String 2 ("silent")Counts: {"s":1, "i":1, "l":1, "e":1, "n":1, "t":1}
ComparisonDictionaries match identically -> returns True.
Coding
#Math#Numbers#Algorithms

13.How do you check if a number is an Armstrong (Narcissistic) number?

Why Interviewers Ask This

Tests digit extraction, power calculations, and problem breakdown for arithmetic algorithms.

How to Explain to Interviewer (Your Spoken Answer)

An Armstrong number of order N is a number equal to the sum of its digits each raised to the power of N (the number of digits). For example, 153 has 3 digits, and 1³ + 5³ + 3³ = 1 + 125 + 27 = 153.

Count total digits n, then sum digit ** n for each digit and compare against the original number.

  • Determine Power: num_digits = len(str(number))
  • Compute Sum: Add int(digit) ** num_digits for each digit.
  • Examples: 153 (3-digit), 9474 (4-digit), 370, 371, 407.
def is_armstrong(num):
num_str = str(num)
num_digits = len(num_str)

total = sum(int(digit) ** num_digits for digit in num_str)
return total == num

print("153 is Armstrong:", is_armstrong(153)) # Output: True (1^3 + 5^3 + 3^3 = 153)
print("9474 is Armstrong:", is_armstrong(9474)) # Output: True (9^4 + 4^4 + 7^4 + 4^4 = 9474)
print("123 is Armstrong:", is_armstrong(123)) # Output: False (1^3 + 2^3 + 3^3 = 36 != 123)

# Find Armstrong numbers up to 1000:
armstrongs = [n for n in range(1, 1000) if is_armstrong(n)]
print("Armstrong numbers (1-1000):", armstrongs)
# Output: Armstrong numbers (1-1000): [1, 2, 3, 4, 5, 6, 7, 8, 9, 153, 370, 371, 407]
🔍 Line-by-Line Breakdown & Dry Run
Input 153Digits = 3. 1^3 = 1, 5^3 = 125, 3^3 = 27
Summation1 + 125 + 27 = 153 == original 153 -> returns True
Coding
#Lists#Algorithms#Array

14.How do you find the Second Largest element in a list without sorting?

Why Interviewers Ask This

Tests single-pass O(N) array traversal, duplicate handling, and handling edge cases without relying on expensive O(N log N) sorting.

How to Explain to Interviewer (Your Spoken Answer)

Instead of sorting which takes O(N log N), we track two variables first and second initialized to negative infinity. In a single pass through the list, if a number is greater than first, we update second = first and first = num. If it is between first and second, we update second.

Traverse list in O(N) keeping track of first and second maximums, ignoring duplicates.

  • Single Pass: O(N) time with O(1) space.
  • Handles Duplicates: If list is [10, 10, 9], second largest is 9, not 10.
  • Handles Edge Cases: Returns None if list has fewer than 2 unique numbers.
def find_second_largest(numbers):
if len(numbers) < 2:
return None

first = second = float('-inf')
for num in numbers:
if num > first:
second = first
first = num
elif num > second and num != first:
second = num

return second if second != float('-inf') else None

print(find_second_largest([12, 35, 1, 10, 34, 1])) # Output: 34
print(find_second_largest([10, 10, 10])) # Output: None (No unique 2nd largest)
print(find_second_largest([-5, -1, -10, -2])) # Output: -2 (Works with negative numbers)
🔍 Line-by-Line Breakdown & Dry Run
Initfirst = -inf, second = -inf
Process 12second = -inf, first = 12
Process 35second = 12, first = 35
Process 3434 < 35 and 34 > 12 -> second = 34
Final ResultReturns 34
Coding
#Lists#Data Structures#Pythonic

15.How do you remove duplicates from a list while preserving original order?

Why Interviewers Ask This

Tests Pythonic idioms (dict.fromkeys), set lookups, and why list(set(lst)) fails order preservation.

How to Explain to Interviewer (Your Spoken Answer)

Using list(set(lst)) removes duplicates but destroys the original order because sets are unordered. The most Pythonic and fastest way in Python 3.7+ is list(dict.fromkeys(lst)), which runs in O(N) time and guarantees insertion order.

Use list(dict.fromkeys(lst)) for fast O(N) deduplication preserving original order.

  • list(set(lst)): Removes duplicates but LOSES original order.
  • list(dict.fromkeys(lst)): Fast O(N) and PRESERVES insertion order.
  • Manual Set Loop: Maintain a seen set for tracking while appending to a new list.
raw_list = [3, 1, 2, 3, 4, 1, 5, 2]

# Method 1: Pythonic O(N) preserving order (Python 3.7+)
unique_ordered = list(dict.fromkeys(raw_list))
print("Preserved Order:", unique_ordered)
# Output: Preserved Order: [3, 1, 2, 4, 5]

# Method 2: Explicit seen set loop (Clean and universal)
def remove_duplicates(lst):
seen = set()
result = []
for item in lst:
if item not in seen:
seen.add(item)
result.append(item)
return result

print("Seen Set Method:", remove_duplicates(raw_list))
# Output: Seen Set Method: [3, 1, 2, 4, 5]
🔍 Line-by-Line Breakdown & Dry Run
dict.fromkeysDictionary keys in Python 3.7+ preserve insertion order. Duplicate keys are silently overwritten, keeping the first occurrence.
Coding
#Strings#Dictionaries#Hashing

16.How do you count the frequency of characters or words in a string?

Why Interviewers Ask This

Core hash table question for text processing, NLP preprocessing, and interview data manipulation.

How to Explain to Interviewer (Your Spoken Answer)

We can count word or character frequencies using a standard dictionary with the .get(key, default) idiom, or via collections.Counter which produces a frequency hash map in a single readable line.

Use counts[char] = counts.get(char, 0) + 1 in a loop, or collections.Counter(text).

  • Standard Dictionary: freq[w] = freq.get(w, 0) + 1 handles missing keys without KeyError.
  • collections.Counter: High-performance C-optimized class for counting elements.
  • Counter.most_common(k): Easily retrieves top K most frequent words.
from collections import Counter

sentence = "apple banana apple orange banana apple"
words = sentence.split()

# Method 1: Standard Dictionary with .get()
word_counts = {}
for word in words:
word_counts[word] = word_counts.get(word, 0) + 1

print("Word Counts:", word_counts)
# Output: Word Counts: {'apple': 3, 'banana': 2, 'orange': 1}

# Method 2: Using Counter & most_common()
counter = Counter(words)
print("Most Common 2 words:", counter.most_common(2))
# Output: Most Common 2 words: [('apple', 3), ('banana', 2)]
🔍 Line-by-Line Breakdown & Dry Run
Word 1 ("apple")word_counts.get("apple", 0) returns 0 -> sets word_counts["apple"] = 1
Word 3 ("apple")word_counts.get("apple", 0) returns 1 -> sets word_counts["apple"] = 2

3. Star & Number Pattern Programs (Loops Mastery)

Right-angled triangles, Pyramids, Hollow shapes, Diamonds, and Floyd numbers.

Pattern
#Patterns#Loops#Stars

17.How do you print a Right-Angled Star Triangle Pattern?

Why Interviewers Ask This

Tests nested loop row-column coordination and Python string multiplication *.

How to Explain to Interviewer (Your Spoken Answer)

For a right-angled triangle of height N, row i (from 1 to N) contains exactly i stars. In Python, we can either use nested loops or the string repetition operator * to multiply stars.

Row i prints i stars. Loop from i = 1 to n + 1 and print * * i.

  • Nested Loop Logic: Outer loop runs for rows 1..N, inner loop prints i stars.
  • Pythonic Shortcut: print("* " * i) multiplies strings cleanly.
n = 5
for i in range(1, n + 1):
print("* " * i)

# Output:
# *
# * *
# * * *
# * * * *
# * * * * *
🔍 Line-by-Line Breakdown & Dry Run
Row 1 (i = 1)Prints 1 star (* )
Row 2 (i = 2)Prints 2 stars (* * )
Row 5 (i = 5)Prints 5 stars (* * * * * )
Pattern
#Patterns#Loops#Stars

18.How do you print an Inverted Right-Angled Star Triangle?

Why Interviewers Ask This

Tests reverse looping with range(start, stop, step) where step is -1.

How to Explain to Interviewer (Your Spoken Answer)

An inverted triangle starts with N stars on row 1 and decreases by 1 star each row until row N has 1 star. We iterate range(n, 0, -1) and print * * i.

Loop backwards from n down to 1 using range(n, 0, -1) and print * * i.

  • Reverse Range: range(n, 0, -1) starts at n and stops before 0.
  • Decreasing Output: Each row has 1 less star than the previous row.
n = 5
for i in range(n, 0, -1):
print("* " * i)

# Output:
# * * * * *
# * * * *
# * * *
# * *
# *
🔍 Line-by-Line Breakdown & Dry Run
Row 1 (i = 5)Prints 5 stars
Row 2 (i = 4)Prints 4 stars
Row 5 (i = 1)Prints 1 star
Pattern
#Patterns#Loops#Pyramid

19.How do you print a Centered Pyramid Star Pattern?

Why Interviewers Ask This

Tests leading whitespace alignment and calculating odd numbers of stars (2*i - 1).

How to Explain to Interviewer (Your Spoken Answer)

A centered pyramid requires balancing leading spaces and stars on each row. For height N, row i (from 1 to N) requires n - i leading spaces followed by 2*i - 1 stars.

Row i prints n - i spaces followed by 2*i - 1 stars.

  • Leading Spaces: n - i spaces push stars to the center.
  • Star Formula: 2*i - 1 generates odd counts: 1, 3, 5, 7, 9.
n = 5
for i in range(1, n + 1):
spaces = " " * (n - i)
stars = "*" * (2 * i - 1)
print(spaces + stars)

# Output:
# *
# ***
# *****
# *******
# *********
🔍 Line-by-Line Breakdown & Dry Run
Row 1 (i = 1)4 spaces + 1 star ( *)
Row 2 (i = 2)3 spaces + 3 stars ( ***)
Row 5 (i = 5)0 spaces + 9 stars (*********)
Pattern
#Patterns#Loops#Diamond

20.How do you print a Full Diamond Star Pattern?

Why Interviewers Ask This

Tests combining upper pyramid and lower inverted pyramid logic seamlessly.

How to Explain to Interviewer (Your Spoken Answer)

A diamond combines an upper pyramid of height N with a lower inverted pyramid of height N-1. The top half increases stars from 1 to 2N-1, while the bottom half decreases from 2N-3 down to 1.

Print top pyramid (1..n), then print bottom inverted pyramid (n-1..1).

  • Upper Half: 1 to n with n - i spaces and 2*i - 1 stars.
  • Lower Half: n - 1 down to 1 with n - i spaces and 2*i - 1 stars.
n = 5

# Upper Pyramid
for i in range(1, n + 1):
print(" " * (n - i) + "*" * (2 * i - 1))

# Lower Inverted Pyramid
for i in range(n - 1, 0, -1):
print(" " * (n - i) + "*" * (2 * i - 1))

# Output:
# *
# ***
# *****
# *******
# *********
# *******
# *****
# ***
# *
🔍 Line-by-Line Breakdown & Dry Run
Upper peakRow 1 has 1 star, expands to 9 stars at row 5
Lower taperingRow 6 has 7 stars, tapers back down to 1 star at row 9
Pattern
#Patterns#Loops#Hollow

21.How do you print a Hollow Square and Hollow Triangle Pattern?

Why Interviewers Ask This

Tests multi-condition boundary checks (if i == 1 or i == n or j == 1 or j == n).

How to Explain to Interviewer (Your Spoken Answer)

Hollow patterns print stars only on the boundaries (first row, last row, first column, last column) and print empty spaces in the interior.

Check if current position is on a boundary; if yes, print *, otherwise print space.

  • Boundary Condition: i == 1 or i == n or j == 1 or j == n prints stars.
  • Interior Condition: Any non-boundary index prints whitespace.
# 1. Hollow Square:
n = 5
print("Hollow Square:")
for i in range(1, n + 1):
for j in range(1, n + 1):
if i == 1 or i == n or j == 1 or j == n:
print("*", end=" ")
else:
print(" ", end=" ")
print()

# Output:
# * * * * *
# * *
# * *
# * *
# * * * * *

# 2. Hollow Right Triangle:
print("\nHollow Triangle:")
for i in range(1, n + 1):
for j in range(1, i + 1):
if j == 1 or j == i or i == n:
print("*", end=" ")
else:
print(" ", end=" ")
print()

# Output:
# *
# * *
# * *
# * *
# * * * * *
🔍 Line-by-Line Breakdown & Dry Run
Row 1i == 1 -> prints all 5 stars (* * * * *)
Row 2-4j == 1 and j == 5 print *, j = 2,3,4 print spaces (* *)
Row 5i == 5 -> prints all 5 stars (* * * * *)
Pattern
#Patterns#Loops#Numbers

22.How do you print Floyd’s Triangle (Continuous Number Pattern)?

Why Interviewers Ask This

Tests continuous state tracking across nested loops instead of resetting each row.

How to Explain to Interviewer (Your Spoken Answer)

Floyd’s triangle is a right-angled triangular array of natural numbers where numbers increment continuously starting from 1 across rows.

Maintain a running counter variable num = 1 outside the loops and increment num += 1 on every print.

  • State Preservation: Counter num is initialized before the outer loop and NOT reset.
  • Row i: Contains i consecutive numbers.
n = 4
num = 1

for i in range(1, n + 1):
for j in range(1, i + 1):
print(num, end=" ")
num += 1
print()

# Output:
# 1
# 2 3
# 4 5 6
# 7 8 9 10
🔍 Line-by-Line Breakdown & Dry Run
Row 1 (i = 1)Prints 1, num becomes 2
Row 2 (i = 2)Prints 2 3, num becomes 4
Row 3 (i = 3)Prints 4 5 6, num becomes 7
Row 4 (i = 4)Prints 7 8 9 10, num becomes 11

4. Code Debugging & Beginner Error Diagnosis

How to read tracebacks, debug with print and pdb, and fix common Python runtime traps.

Debugging
#Debugging#Traceback#Errors

23.How do you read and understand a Python Traceback when an error occurs?

Why Interviewers Ask This

Tests real-world debugging maturity — whether a developer panics at errors or methodically inspects call stacks.

How to Explain to Interviewer (Your Spoken Answer)

A Python traceback is read from bottom to top. The very last line gives the exact exception type and error message (e.g., IndexError: list index out of range). The lines above show the call stack, pointing to the exact file and line number where the failure originated.

Read tracebacks bottom-up: last line is the exception and message; lines above show the file path, function, and line number.

  • Bottom Line: Exception name and cause (TypeError: unsupported operand...).
  • Location Line: File "app.py", line 42, in process_data tells you where to inspect.
  • Common Types: IndexError (invalid index), KeyError (missing dict key), AttributeError (calling nonexistent method), ValueError (invalid cast).
# Sample code that triggers a traceback:
def calculate_average(scores):
total = sum(scores)
return total / len(scores)

# Passing an empty list triggers ZeroDivisionError:
try:
calculate_average([])
except ZeroDivisionError as e:
import traceback
print("--- TRACEBACK OUTPUT ---")
traceback.print_exc()

# Simulated Output:
# Traceback (most recent call last):
# File "script.py", line 4, in calculate_average
# return total / len(scores)
# ZeroDivisionError: division by zero
🔍 Line-by-Line Breakdown & Dry Run
Bottom lineIdentifies the error type (ZeroDivisionError: division by zero).
Line abovePoints to line 4 inside calculate_average: len(scores) evaluated to 0.
The FixAdd guard clause: if not scores: return 0.
Debugging
#Debugging#pdb#Tools

24.What are the best methods to debug Python code (print vs breakpoint/pdb)?

Why Interviewers Ask This

Distinguishes junior "print-only" coders from engineers who know Python 3.7+ interactive debugging.

How to Explain to Interviewer (Your Spoken Answer)

For quick checks, print(f"{var=}") outputs variable names and values. For complex logic, Python 3.7+ provides built-in breakpoint(), which pauses execution and opens the interactive pdb debugger to inspect variables, step line-by-line (n), step into functions (s), and continue (c).

Use print(f"{x=}") for quick logging; use breakpoint() for interactive stepping with pdb.

  • Self-Documenting Print: print(f"{user_id=}") prints user_id=101.
  • Built-in breakpoint(): Native in Python 3.7+ (calls pdb.set_trace()).
  • PDB Commands: n (next line), s (step into), c (continue), p var (print value), q (quit).
# 1. Python 3.8+ Self-documenting f-strings:
total = 150
discount = 0.2
final_price = total * (1 - discount)
print(f"{total=}, {discount=}, {final_price=}")
# Output: total=150, discount=0.2, final_price=120.0

# 2. Interactive Breakpoint (Uncomment in local terminal):
def process_order(price, tax):
# breakpoint() # Pauses execution and opens interactive PDB shell!
return price + (price * tax)

print("Order Total:", process_order(100, 0.05))
# Output: Order Total: 105.0
🔍 Line-by-Line Breakdown & Dry Run
breakpoint() triggerHalts runtime and gives terminal control to evaluate expressions live.
Debugging
#Traps#Functions#Memory

25.Why is using a Mutable Default Argument (e.g. def func(x=[])) a dangerous trap?

Why Interviewers Ask This

One of the most famous Python interview gotchas testing function definition evaluation timing.

How to Explain to Interviewer (Your Spoken Answer)

In Python, default parameter expressions are evaluated once when the function is defined, NOT each time the function is called. If you use a mutable object like [] or {} as a default, that single object is shared across all subsequent invocations, causing unexpected state pollution.

Default arguments evaluate once at function definition time. Use None as default and initialize inside the body.

  • Root Cause: The list is created once when the def statement executes in memory.
  • Consequence: Repeated calls without arguments append to the same shared list.
  • The Solution: Use arg=None as default, then initialize if arg is None: arg = [].
# THE BUG:
def add_item_buggy(item, cart=[]):
cart.append(item)
return cart

print(add_item_buggy("Apple")) # Output: ['Apple']
print(add_item_buggy("Banana")) # Output: ['Apple', 'Banana'] (Oops! Cart was shared!)

# THE FIX:
def add_item_fixed(item, cart=None):
if cart is None:
cart = [] # Creates a fresh list on EVERY call
cart.append(item)
return cart

print(add_item_fixed("Apple")) # Output: ['Apple']
print(add_item_fixed("Banana")) # Output: ['Banana'] (Clean and independent!)
🔍 Line-by-Line Breakdown & Dry Run
Buggy call 1Appends "Apple" to shared list object at memory address 0x100.
Buggy call 2Uses same list at 0x100 -> now contains ["Apple", "Banana"].
Fixed callcart=None triggers cart = [], creating a new list on every call.
Debugging
#Traps#Lists#Loops

26.Why shouldn’t you modify a List while iterating over it, and how to fix it?

Why Interviewers Ask This

Tests internal iterator index mechanics and how to safely filter collections.

How to Explain to Interviewer (Your Spoken Answer)

When you remove items from a list while iterating forward, the list shrinks and all subsequent elements shift left by one index. However, the internal loop counter continues advancing by +1, silently skipping adjacent elements.

Modifying a list in-place shifts indices and skips items. Fix by iterating over a slice copy lst[:] or using a list comprehension.

  • The Bug: Removing items causes elements to shift left, skipping elements right after removals.
  • Fix 1 (Best): Use a list comprehension [x for x in nums if condition].
  • Fix 2: Iterate over a shallow copy for x in nums[:]:.
# THE BUG:
nums = [1, 2, 2, 3, 4]
for num in nums:
if num == 2:
nums.remove(num)
print("Buggy result:", nums)
# Output: Buggy result: [1, 2, 3, 4] (One '2' was SKIPPED!)

# THE FIX (Method 1: List Comprehension):
nums2 = [1, 2, 2, 3, 4]
filtered_nums = [x for x in nums2 if x != 2]
print("Fixed result:", filtered_nums)
# Output: Fixed result: [1, 3, 4]

# THE FIX (Method 2: Iterate over copy):
nums3 = [1, 2, 2, 3, 4]
for num in nums3[:]: # Slicing creates a snapshot copy
if num == 2:
nums3.remove(num)
print("Copy loop result:", nums3)
# Output: Copy loop result: [1, 3, 4]
🔍 Line-by-Line Breakdown & Dry Run
Iter index 1 (num = 2)Removes first 2. Elements shift left: index 1 now holds the second 2.
Iter index 2Loop advances to index 2 (which is now 3), completely skipping the second 2!
Debugging
#Scope#Errors#Variables

27.What causes UnboundLocalError and how do you fix it?

Why Interviewers Ask This

Tests understanding of Python compile-time variable scope assignment vs runtime resolution.

How to Explain to Interviewer (Your Spoken Answer)

When Python compiles a function, any variable assigned to (=) inside the function body is marked as a Local variable. If you try to read that variable before the assignment line executes, Python raises an UnboundLocalError instead of falling back to the global variable.

Assigning to a variable inside a function marks it as local. Use global or nonlocal if modifying outer variables.

  • Cause: Python sees count = count + 1 and flags count as local, but reading count on the right side fails because local count has not been assigned yet.
  • Fix: Declare global count or pass and return values explicitly.
counter = 0

# THE BUG:
def increment_buggy():
# counter += 1 # UnboundLocalError: local variable 'counter' referenced before assignment
pass

# THE FIX (Method 1: Global Keyword):
def increment_fixed():
global counter
counter += 1

increment_fixed()
print("Counter after global fix:", counter) # Output: Counter after global fix: 1

# THE FIX (Method 2: Pure Function - Recommended):
def increment_pure(val):
return val + 1

score = increment_pure(10)
print("Pure function score:", score) # Output: Pure function score: 11
🔍 Line-by-Line Breakdown & Dry Run
Compiler PhaseCPython scans function, detects assignment counter = ..., and marks counter in local symbol table.
Execution PhaseTries to read counter before local binding exists -> raises UnboundLocalError.

5. OOP, Advanced Internals & Memory Architecture

Object-Oriented Programming, Decorators, Generators, Shallow vs Deep Copy, and the GIL.

Advanced
#OOP#Classes#Architecture

28.What are the 4 Pillars of Object-Oriented Programming (OOP) in Python?

Why Interviewers Ask This

Core architecture question for software engineering, modular design, and API building.

How to Explain to Interviewer (Your Spoken Answer)

The 4 pillars of OOP are: Encapsulation (bundling data and methods with access restrictions), Abstraction (hiding internal implementation details using ABCs), Inheritance (reusing parent class behaviors in child classes), and Polymorphism (providing a unified interface for different underlying types).

Encapsulation, Abstraction, Inheritance, and Polymorphism structure modular, scalable software in Python.

  • Encapsulation: Restricting direct access to internal state using private _ and __ prefixes.
  • Abstraction: Exposing only high-level interfaces using abc.ABC and @abstractmethod.
  • Inheritance: Subclasses inherit attributes and methods from base classes (class Dog(Animal):).
  • Polymorphism: Different classes implementing the same method signature (duck typing).
class Animal:
def speak(self):
return "Some sound"

class Dog(Animal): # Inheritance
def speak(self): # Polymorphism (Method Overriding)
return "Woof!"

class Cat(Animal):
def speak(self):
return "Meow!"

# Polymorphic iteration:
animals = [Dog(), Cat()]
for a in animals:
print(f"{a.__class__.__name__}: {a.speak()}")
# Output:
# Dog: Woof!
# Cat: Meow!
🔍 Line-by-Line Breakdown & Dry Run
Dynamic DispatchAt runtime, Python inspects the object type and invokes the correct overridden speak() method.
Advanced
#OOP#Classes#Constructors

29.What are __init__ and self in Python classes?

Why Interviewers Ask This

Fundamental check on class instantiation and instance memory references.

How to Explain to Interviewer (Your Spoken Answer)

__init__ is Python’s instance initializer (constructor) that runs automatically when a new object is instantiated to configure its initial attributes. self is an explicit reference to the current instance object being created or manipulated, allowing access to instance variables and methods.

__init__ initializes instance attributes; self represents the specific object instance.

  • __init__: Dunder method invoked immediately after object creation to set initial state.
  • self: First parameter of every instance method pointing to the caller object in memory.
  • Automatic Passing: When you call car.drive(), Python translates it to Car.drive(car).
class Student:
def __init__(self, name, marks):
self.name = name # Instance variable bound to self
self.marks = marks

def get_result(self):
return "Pass" if self.marks >= 40 else "Fail"

s1 = Student("Sai", 85)
s2 = Student("Rahul", 32)

print(f"{s1.name}: {s1.get_result()}") # Output: Sai: Pass
print(f"{s2.name}: {s2.get_result()}") # Output: Rahul: Fail
🔍 Line-by-Line Breakdown & Dry Run
s1 = Student("Sai", 85)Allocates instance in memory, passes instance as self, sets s1.name = "Sai" and s1.marks = 85.
Advanced
#Memory#Copy#Data Structures

30.What is the difference between Shallow Copy and Deep Copy?

Why Interviewers Ask This

Critical for avoiding accidental shared mutations when duplicating nested lists or dictionaries.

How to Explain to Interviewer (Your Spoken Answer)

A Shallow Copy (copy.copy() or lst.copy()) creates a new outer collection, but inserts references to the original nested objects. A Deep Copy (copy.deepcopy()) recursively copies both the outer container and all nested objects, creating a completely independent clone in memory.

Shallow copy duplicates only the outer container; Deep copy duplicates the outer container AND all nested child objects.

  • Shallow Copy (copy.copy()): Modifying nested lists in the copy affects the original!
  • Deep Copy (copy.deepcopy()): Fully independent memory clone; changes never affect original.
  • Assignment (b = a): Does NOT copy anything; creates another pointer to the exact same object.
import copy

original = [[1, 2, 3], [4, 5, 6]]

# 1. Shallow Copy
shallow = copy.copy(original)
shallow[0][0] = 999 # Modifies nested list!
print("Original after shallow edit:", original)
# Output: Original after shallow edit: [[999, 2, 3], [4, 5, 6]] (Changed!)

# 2. Deep Copy
original2 = [[1, 2, 3], [4, 5, 6]]
deep = copy.deepcopy(original2)
deep[0][0] = 999
print("Original after deep edit: ", original2)
# Output: Original after deep edit: [[1, 2, 3], [4, 5, 6]] (Protected!)
🔍 Line-by-Line Breakdown & Dry Run
Shallow copy memoryOuter list has new memory address, but shallow[0] points to original original[0] address.
Deep copy memoryRecursively allocates new memory addresses for outer list and every child list.
Advanced
#Advanced#Decorators#Functions

31.What are Decorators in Python and how do they work?

Why Interviewers Ask This

Evaluates first-class functions, closures, and metaprogramming used extensively in FastAPI, Flask, and Django.

How to Explain to Interviewer (Your Spoken Answer)

A decorator is a design pattern that allows you to dynamically extend or modify the behavior of a function without changing its source code. In Python, functions are first-class objects, so a decorator takes a function as input, wraps it in an inner closure, and returns the modified wrapper.

A decorator wraps a function to add logging, timing, authentication, or caching without altering the original function.

  • First-Class Functions: Functions can be passed as arguments, assigned to variables, and returned from other functions.
  • Syntax: @my_decorator above a function is syntactic sugar for func = my_decorator(func).
  • Use Cases: Logging, timing execution, authentication checks, rate limiting.
import time

# Decorator to measure execution time:
def timer_decorator(func):
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
duration = time.time() - start
print(f"[{func.__name__}] Execution Time: {duration:.5f}s")
return result
return wrapper

@timer_decorator
def compute_squares():
return sum(i * i for i in range(100_000))

total = compute_squares()
# Output:
# [compute_squares] Execution Time: 0.00612s
🔍 Line-by-Line Breakdown & Dry Run
@timer_decorator applicationReplaces compute_squares with wrapper.
InvocationCalls wrapper(), records start time, executes func(), logs duration, returns result.
Advanced
#Advanced#Generators#Memory

32.What are Generators and how do they differ from Lists (yield vs return)?

Why Interviewers Ask This

Tests memory optimization for big data processing, streaming, and lazy evaluation.

How to Explain to Interviewer (Your Spoken Answer)

A regular function with return computes all values at once and stores the complete dataset in RAM. A generator function uses yield to return items lazily one by one on-demand, maintaining state between iterations and consuming virtually O(1) memory even for billions of records.

Lists store all elements in memory eagerly; Generators produce values lazily on-demand using yield.

  • yield: Pauses function execution, returns current value, and saves execution state for the next call.
  • Memory Efficiency: Streaming a 10 GB file line-by-line uses mere kilobytes of RAM with generators.
  • next() Protocol: Generators implement the iterator protocol (__iter__ and __next__).
import sys

# 1. List (Eager Evaluation - Uses Memory):
num_list = [i for i in range(1_000_000)]
print("List Memory (bytes):", sys.getsizeof(num_list)) # ~8.4 MB in RAM

# 2. Generator Expression (Lazy Evaluation - O(1) Memory):
num_gen = (i for i in range(1_000_000))
print("Gen Memory (bytes): ", sys.getsizeof(num_gen)) # ~104 bytes in RAM!

# Fetch first 3 values from generator:
print("Next 1:", next(num_gen)) # Output: 0
print("Next 2:", next(num_gen)) # Output: 1
print("Next 3:", next(num_gen)) # Output: 2
🔍 Line-by-Line Breakdown & Dry Run
List allocationEagerly allocates 1,000,000 integer pointers in memory immediately.
Generator evaluationAllocates small generator state object. Computes values only when next() is called.
Advanced
#Architecture#Concurrency#GIL

33.What is the Global Interpreter Lock (GIL) in CPython and why does it exist?

Why Interviewers Ask This

Senior architecture check on multithreading limitations vs multiprocessing for CPU-bound workloads.

How to Explain to Interviewer (Your Spoken Answer)

The GIL is a mutex in CPython that prevents multiple native threads from executing Python bytecode simultaneously. It exists because CPython’s memory management is not thread-safe. For CPU-heavy tasks, threading does not offer speedups; you must use multiprocessing. For I/O-heavy tasks (APIs, network), threading works great.

The GIL allows only one thread to execute Python bytecode at a time in CPython to protect reference count memory management.

  • Why GIL Exists: CPython uses reference counting for garbage collection, which is not thread-safe without locks.
  • CPU-Bound Tasks (Math/ML): Use multiprocessing to bypass GIL and utilize all CPU cores.
  • I/O-Bound Tasks (Web/DB/Files): Standard threading or asyncio works effectively because threads release the GIL during I/O wait.
# CPU-bound tasks should use multiprocessing, not threading:
from multiprocessing import Process

def heavy_calculation(n):
return sum(i * i for i in range(n))

if __name__ == "__main__":
p1 = Process(target=heavy_calculation, args=(10_000_000,))
p2 = Process(target=heavy_calculation, args=(10_000_000,))
p1.start()
p2.start()
p1.join()
p2.join()
print("Multiprocessing finished utilizing multi-core CPU!")
# Output: Multiprocessing finished utilizing multi-core CPU!
🔍 Line-by-Line Breakdown & Dry Run
Thread executionThread 1 acquires GIL -> executes bytecode -> releases GIL -> Thread 2 acquires GIL (serialized).
Process executionProcess 1 and Process 2 run in separate OS memory spaces, each with its own GIL on separate CPU cores.
Advanced
#Memory#Garbage Collection#Internals

34.How does Python manage memory (Reference Counting & Cyclic Garbage Collection)?

Why Interviewers Ask This

Tests deep knowledge of Python internal memory allocation, sys.getrefcount, and circular reference resolution.

How to Explain to Interviewer (Your Spoken Answer)

Python uses two primary memory management mechanisms: Reference Counting and Generational Garbage Collection. When an object’s reference count drops to 0, its memory is deallocated immediately. To resolve circular references (e.g. object A referencing B and B referencing A), Python runs a cyclic generational GC (Generations 0, 1, and 2).

Memory is freed instantly when reference count hits 0; a 3-generation cyclic GC cleans up circular references.

  • Reference Counting: Every object maintains a count of pointers pointing to it (sys.getrefcount()).
  • Immediate Deallocation: Count == 0 -> memory returned to Python memory pool immediately.
  • Cyclic GC: Detects unreferenced islands of circular references using 3 generational buckets.
import sys
import gc

# 1. Reference Counting:
a = [1, 2, 3]
print("Initial refcount:", sys.getrefcount(a) - 1) # 1 reference

b = a # 2nd reference
print("Refcount after b=a:", sys.getrefcount(a) - 1) # 2 references

del b # Decrement count
print("Refcount after del b:", sys.getrefcount(a) - 1) # 1 reference

# 2. Circular Reference Example:
node1 = {}
node2 = {}
node1["next"] = node2
node2["prev"] = node1 # Circular loop!

del node1
del node2 # Reference count is 1, but objects are unreachable!
# Python's cyclic GC runs in background to collect them:
collected = gc.collect()
print(f"Cyclic GC collected {collected} unreachable objects.")
# Output: Cyclic GC collected unreachable objects
🔍 Line-by-Line Breakdown & Dry Run
del bDecrements reference count of list [1, 2, 3] from 2 to 1.
gc.collect()Scans Generation 0/1/2 heuristic graphs to detect unreachable circular loops and deallocates them.
Advanced
#Functions#Args#Pythonic

35.What are *args and **kwargs in Python functions?

Why Interviewers Ask This

Tests flexible function signatures, decorator argument forwarding, and tuple/dictionary unpacking.

How to Explain to Interviewer (Your Spoken Answer)

*args allows a function to accept any number of positional arguments, which are packed into a Tuple. **kwargs allows accepting any number of keyword (named) arguments, which are packed into a Dictionary. They are essential for writing flexible wrappers and decorators.

*args collects extra positional arguments into a Tuple; **kwargs collects keyword arguments into a Dictionary.

  • *args: Packed as a tuple (e.g. (1, 2, 3)).
  • **kwargs: Packed as a dict (e.g. {"name": "Sai", "role": "Dev"}).
  • Argument Forwarding: Used in decorators to forward arbitrary arguments: func(*args, **kwargs).
  • Order: Standard parameters -> *args -> Keyword-only parameters -> **kwargs.
def introduce_team(team_name, *members, **details):
print(f"Team: {team_name}")
print("Members (Tuple):", members)
print("Details (Dict): ", details)

introduce_team(
"Think IT AI",
"Sai", "Kiran", "Divya",
project="Docs Platform",
version=2.0
)

# Output:
# Team: Think IT AI
# Members (Tuple): ('Sai', 'Kiran', 'Divya')
# Details (Dict): {'project': 'Docs Platform', 'version': 2.0}
🔍 Line-by-Line Breakdown & Dry Run
Positional bindingteam_name = "Think IT AI"
*members packingPacks remaining positional args into ("Sai", "Kiran", "Divya").
**details packingPacks keyword arguments into {"project": "Docs Platform", "version": 2.0}.

Want to Practice Live Python Code?

Execute and test all these interview questions live in our interactive Python Lab compiler with real-time feedback.