Python Technical Interview Masterclass
Curated by Vinod Kumar Kayartaya (30+ Years Experience as Software Trainer, Senior Developer & AI Architect). Complete preparation suite spanning language syntax, CPython internals, async architectures, and system design.
Members-Only Access
This comprehensive 17-module Python interview masterclass is exclusively available to authenticated members. Please sign in with your Google account to unlock full questions, runnable code examples, and architectural explanations.
Language Fundamentals & Syntax
01. Is Python an interpreted or compiled language? Explain the CPython execution model.
- Difficulty: Beginner
- Frequency: ⭐⭐⭐⭐⭐ (Universal fundamental)
- Concept / Summary: Python is both. Source code (
.py) is compiled into intermediate bytecode (.pyc), which is subsequently interpreted and executed by a virtual machine (the Python Virtual Machine, or PVM). - Detailed Explanation:
- Source Code (
.py): You write high-level human-readable code. - Parsing & AST: CPython's parser turns the source text into a parse tree, then builds an Abstract Syntax Tree (AST).
- Compilation to Bytecode: The compiler transforms the AST into low-level, platform-independent instructions known as bytecode (stored in
__pycache__/*.pycfiles). Bytecode instructions are stack-based machine codes (e.g.,LOAD_NAME,BINARY_OP,RETURN_VALUE). - PVM Execution: The Python Virtual Machine (an evaluation loop written in C, historically
ceval.c) iterates through the bytecode instructions and executes native machine operations on the host CPU.
- Source Code (
- Code Example:
PYTHON
import dis def add(a, b): return a + b # Inspect bytecode generated by CPython dis.dis(add) # Output: # RESUME 0 # LOAD_FAST 0 (a) # LOAD_FAST 1 (b) # BINARY_OP 0 (+) # RETURN_VALUE - Common Pitfalls & Edge Cases:
- Mistakenly claiming Python is purely interpreted with no compilation stage.
- Assuming
.pycfiles contain raw CPU machine code (they contain bytecode, not machine assembly).
- Follow-up Questions:
- What are alternative Python runtimes (PyPy, Jython, IronPython)?
- How does PyPy achieve higher speeds than CPython? (Answer: JIT compiler vs. standard bytecode interpreter).
02. What does "everything is an object" mean in Python?
Difficulty: Intermediate
Frequency: ⭐⭐⭐⭐⭐
Concept / Summary: In Python, all values—primitive types (integers, floats, booleans), collections, functions, classes, instances, and even imported modules—are first-class objects instantiated from a
class(ortype) and managed via a C-levelPyObjectstructure.Detailed Explanation:
In languages like C or Java, primitive types (e.g.,int,char,boolean) are raw memory values distinct from reference objects. In Python, even1is an instance of<class 'int'>.Every object has:
- An Identity: Unique integer memory address (
id(obj)). - A Type: Defines supported methods and behavior (
type(obj)). - A Value: The data payload stored in memory.
- A Reference Count: For CPython's memory management.
Because functions and classes are objects, you can pass them as arguments, return them from functions, attach attributes dynamically, and store them in collections.
- An Identity: Unique integer memory address (
Code Example:
PYTHONdef greet(): return "Hello, World!" # Functions are objects: can assign attributes, pass as data greet.language = "English" greet.version = 1.0 print(isinstance(greet, object)) # True print(isinstance(5, object)) # True print(greet.language) # English print(type(int)) # <class 'type'>Common Pitfalls & Edge Cases:
- Thinking primitive types in Python are unboxed raw primitives.
- Adding attributes to built-in objects directly (e.g.,
(1).custom = 'fail'), which raisesAttributeErrorbecause built-in types have no__dict__unless subclassed.
Follow-up Questions:
- How is
PyObjectdefined at the C level incpython/Include/object.h? - What is the relationship between
typeandobject? (isinstance(object, type)andisinstance(type, object)are bothTrue).
- How is
03. How is Python typed? Differentiate between Strongly vs. Weakly typed, and Statically vs. Dynamically typed.
- Difficulty: Beginner / Intermediate
- Frequency: ⭐⭐⭐⭐⭐
- Concept / Summary: Python is dynamically typed (types are associated with runtime values, not variable names) and strongly typed (types are strictly enforced without implicit incompatible coercions).
- Detailed Explanation:
- Dynamic vs. Static:
- Static Typing (C++, Java, Go): Variable types are declared and verified at compile time.
- Dynamic Typing (Python, Ruby, JavaScript): Variables are mere labels bound to objects. The type is determined when the code executes. A variable name can bind to an
intnow and astrlater.
- Strong vs. Weak:
- Strong Typing (Python, Rust): Implicit conversion between incompatible types is disallowed.
"5" + 5raisesTypeError. - Weak Typing (JavaScript, C, PHP): The runtime automatically coerces types in operations (e.g., in JavaScript
"5" + 5evaluates to"55","5" - 2evaluates to3).
- Strong Typing (Python, Rust): Implicit conversion between incompatible types is disallowed.
- Duck Typing: "If it walks like a duck and quacks like a duck, it's a duck." Python cares about whether an object implements a particular interface or method (
__len__,__iter__), not its explicit inheritance hierarchy.
- Dynamic vs. Static:
- Code Example:
PYTHON
x = 42 # x references an integer x = "hello" # Dynamic: x now references a string (valid) # Strongly typed: No implicit conversion try: result = "Price: $" + 100 except TypeError as e: print(f"Caught expected error: {e}") # Fix: Explicit type casting result = "Price: $" + str(100) - Common Pitfalls & Edge Cases:
- Confusing Python's
typingmodule (type hints) with static enforcement. Type hints in Python do not enforce types at runtime; they are metadata for linters (mypy,pyright).
- Confusing Python's
- Follow-up Questions:
- What is gradual typing?
- What is the runtime performance cost of dynamic type dispatch?
04. What is the difference between Mutable and Immutable objects? Name common examples of each.
Difficulty: Beginner
Frequency: ⭐⭐⭐⭐⭐
Concept / Summary: Immutable objects cannot be modified after creation; any modification yields a new object. Mutable objects allow altering their internal state in-place without changing their memory identity (
id()).Detailed Explanation:
- Immutable Types:
int,float,complex,bool,str,tuple,frozenset,bytes. - Mutable Types:
list,dict,set,bytearray, custom user classes (by default).
Immutability guarantees thread safety against unexpected side-effects, enables safe caching, and makes objects eligible to be hashable (usable as dictionary keys or set elements), provided all nested elements are also immutable.
- Immutable Types:
Code Example:
PYTHON# Immutable demonstration (str) s = "hello" old_id = id(s) s += " world" print(id(s) == old_id) # False: A new string object was created # Mutable demonstration (list) lst = [1, 2, 3] old_id = id(lst) lst.append(4) print(id(lst) == old_id) # True: Mutated in-place at the same memory addressCommon Pitfalls & Edge Cases:
- A
tupleis immutable, but if it contains a mutable element (like alist), the nested list can be modified:PYTHONt = (1, [2, 3]) t[1].append(4) # Works: t is now (1, [2, 3, 4]) # However, hash(t) raises TypeError because its content is not fully immutable!
- A
Follow-up Questions:
- What makes an object hashable in Python?
- Why can't a
listbe used as a key in a dictionary?
05. Explain Python's parameter passing mechanism ("Pass by Object Reference" / "Pass by Assignment").
- Difficulty: Intermediate
- Frequency: ⭐⭐⭐⭐⭐
- Concept / Summary: Python uses pass-by-assignment (also called call-by-object-reference). When an argument is passed to a function, the function parameter receives a copy of the reference (pointer) to the object, never a direct copy of the object itself.
- Detailed Explanation:
- If you pass an immutable object (
int,str,tuple) and reassign the parameter inside the function, the parameter simply points to a new object. The caller's variable remains unchanged. - If you pass a mutable object (
list,dict,set) and modify it in-place (e.g.lst.append()), the modification reflects in the caller's scope because both point to the same memory location. - If you reassign a mutable parameter inside the function (
lst = [10, 20]), you break the binding; subsequent operations do not affect the caller.
- If you pass an immutable object (
- Code Example:
PYTHON
def modify_in_place(items): items.append("new_item") # Modifies caller's object def reassign_parameter(items): items = ["brand_new"] # Local rebinding only data = ["initial"] modify_in_place(data) print(data) # ['initial', 'new_item'] reassign_parameter(data) print(data) # Still ['initial', 'new_item'] - Common Pitfalls & Edge Cases:
- Believing Python is pass-by-value or pass-by-reference. It behaves like pass-by-value where the "value" being copied is the memory reference.
- Follow-up Questions:
- How do you guarantee a function cannot mutate an incoming list? (Pass
tuple(data)ordata.copy()).
- How do you guarantee a function cannot mutate an incoming list? (Pass
06. What is the difference between `==` and `is`? When should each be used?
- Difficulty: Beginner / Intermediate
- Frequency: ⭐⭐⭐⭐⭐
- Concept / Summary:
==checks for value equality (invoking__eq__), verifying if two objects have equivalent content.ischecks for reference identity, verifying if two variables point to the exact same location in memory (id(a) == id(b)). - Detailed Explanation:
- Use
==when comparing values (numbers, strings, lists, custom objects). - Use
iswhen comparing against singletons:x is None,x is not None, or checking sentinel objects. - Integer Caching / Small Integer Interning: CPython pre-allocates an array of integer objects for values between
-5and256. Therefore,a = 250; b = 250; a is bevaluates toTrue, buta = 1000; b = 1000; a is bmay evaluate toFalse(depending on code-block compilation scope). Never useisto compare numbers!
- Use
- Code Example:
PYTHON
list_a = [1, 2, 3] list_b = [1, 2, 3] list_c = list_a print(list_a == list_b) # True: identical content print(list_a is list_b) # False: distinct objects in memory print(list_a is list_c) # True: both reference the same object # Recommended usage: result = None if result is None: print("Safe singleton check") - Common Pitfalls & Edge Cases:
- Writing
if x is True:instead ofif x:orif bool(x):. - Expecting
1000 is 1000to be guaranteed across all contexts and Python implementations.
- Writing
- Follow-up Questions:
- What is string interning and when does CPython intern strings automatically?
07. Explain Variable Scope and the LEGB Rule in Python. How do `global` and `nonlocal` differ?
Difficulty: Intermediate
Frequency: ⭐⭐⭐⭐⭐
Concept / Summary: Python resolves variable names by searching scopes in a strict hierarchical order known as LEGB: Local $\rightarrow$ Enclosing $\rightarrow$ Global $\rightarrow$ Built-in.
Detailed Explanation:
- L (Local): Names assigned inside a function or lambda (not marked
globalornonlocal). - E (Enclosing): Names in the local scope of enclosing (outer) functions, from innermost to outermost (closures).
- G (Global): Names defined at the top level of the current module file.
- B (Built-in): Built-in namespace containing functions and exceptions (
len,range,Exception).
Modifying Outer Variables:
- By default, referencing an outer variable is allowed, but assigning to a variable creates a new local variable.
global <var>: Instructs Python that assignments to<var>target the module-level global scope.nonlocal <var>: (Python 3+) Targets the nearest enclosing non-global scope (used in nested functions / closures).
- L (Local): Names assigned inside a function or lambda (not marked
Code Example:
PYTHONdef outer(): count = 0 def inner(): nonlocal count # Binds to outer's count count += 1 return count return inner counter = outer() print(counter()) # 1 print(counter()) # 2 # UnboundLocalError trap x = 10 def faulty_read_and_write(): try: # Python sees assignment below and marks 'x' as local for entire function! print(x) x = 20 except UnboundLocalError as e: print(f"Error: {e}") faulty_read_and_write()Common Pitfalls & Edge Cases:
- The
UnboundLocalError: Any assignment to a name inside a function makes that name local across the entire body, even before the line of assignment.
- The
Follow-up Questions:
- Do
ifstatements,forloops, ortryblocks create their own scope in Python? (Answer: No, only functions, classes, and comprehensions/generator expressions create scope).
- Do
08. What is the "Mutable Default Argument" trap in Python functions? How do you prevent it?
Difficulty: Beginner / Intermediate
Frequency: ⭐⭐⭐⭐⭐
Concept / Summary: In Python, default arguments are evaluated once at the moment the function is defined (
def), not each time the function is called. Using a mutable object (list, dictionary, set) as a default creates a shared state across all invocations.Detailed Explanation:
When Python executes adefstatement, it compiles the code object and binds default arguments into the function's__defaults__attribute tuple. If that object is mutable, modifications persist across subsequent calls where the argument is omitted.The Idiomatic Solution: Use
Noneas the sentinel default value, and instantiate the mutable object inside the function body.Code Example:
PYTHON# BUGGY CODE: def bad_append(val, lst=[]): lst.append(val) return lst print(bad_append(1)) # [1] print(bad_append(2)) # [1, 2] -- Unexpected retention! # IDIOMATIC SOLUTION: def safe_append(val, lst=None): if lst is None: lst = [] lst.append(val) return lst print(safe_append(1)) # [1] print(safe_append(2)) # [2]Common Pitfalls & Edge Cases:
- Intentionally using mutable defaults for caching / memoization (discouraged; prefer
@functools.lru_cache).
- Intentionally using mutable defaults for caching / memoization (discouraged; prefer
Follow-up Questions:
- Where are default parameter values stored on a function object? (
func.__defaults__for positional,func.__kwdefaults__for keyword-only).
- Where are default parameter values stored on a function object? (
09. What constitutes Truthy and Falsy values in Python? How do `bool()` and `__bool__` / `__len__` work?
Difficulty: Beginner
Frequency: ⭐⭐⭐⭐
Concept / Summary: Every object in Python has an inherent boolean truth value. An object is evaluated as
Falseif it meets defined "falsy" conditions; otherwise, it evaluates toTrue.Detailed Explanation:
Exhaustive List of Built-in Falsy Values:- Constants:
None,False. - Numeric zeroes:
0,0.0,0j,Decimal(0),Fraction(0, 1). - Empty sequences & collections:
"",(),[],{},set(),range(0).
Custom Object Evaluation Protocol:
Whenbool(obj)is called:- Python checks for the
__bool__()method. If defined, it must returnTrueorFalse. - If
__bool__()is not defined, Python falls back to__len__(). Iflen(obj) == 0, it isFalse; otherwiseTrue. - If neither method is defined, the object is considered
Trueby default.
- Constants:
Code Example:
PYTHONclass CustomContainer: def __init__(self, data): self.data = data def __len__(self): return len(self.data) empty_box = CustomContainer([]) full_box = CustomContainer([1, 2]) print(bool(empty_box)) # False (via __len__) print(bool(full_box)) # True (via __len__)Common Pitfalls & Edge Cases:
- Defining
__bool__to return a non-boolean (raisesTypeError). - Checking
if len(items) > 0:instead of idiomaticif items:. - Checking
if x:whenx = 0is a valid meaningful number (checkif x is not None:instead).
- Defining
Follow-up Questions:
- What happens if both
__bool__and__len__are implemented? (__bool__takes priority).
- What happens if both
10. How does Short-Circuit Evaluation work with `and` and `or`? What values do they actually return?
- Difficulty: Intermediate
- Frequency: ⭐⭐⭐⭐
- Concept / Summary: Python's logical operators
andandorevaluate from left to right and stop evaluation immediately once the outcome is guaranteed. Crucially, they do not return boolean literals (True/False); they return the actual operand object that determined the outcome. - Detailed Explanation:
a and b: Evaluatesa. Ifais falsy, returnsaimmediately (short-circuits,bis never evaluated). Ifais truthy, evaluates and returnsb.a or b: Evaluatesa. Ifais truthy, returnsaimmediately (short-circuits,bis never evaluated). Ifais falsy, evaluates and returnsb.
- Code Example:
PYTHON
# Returning actual operand values print([] and "hello") # [] (first operand is falsy) print([1, 2] and "hi") # "hi" (first operand is truthy, returns second) print("default" or 42) # "default" (first operand is truthy) print("" or 42) # 42 (first operand is falsy) # Preventing exceptions via short-circuiting: user_input = None # Safe: len() is never executed if user_input is None if user_input is not None and len(user_input) > 0: print("Valid input") # CLASSIC INTERVIEW GOTCHA: num = 10 # BUG: '10 == 1' is False, then evaluates truthiness of 2 -> 2 is Truthy! if num == 1 or 2 or 3: print("Always triggers!") # Triggers even though num is 10! # CORRECT WAY: if num in (1, 2, 3): print("Won't trigger") - Common Pitfalls & Edge Cases:
- Writing
if val == 'a' or 'b':expecting it to check ifvalequals'a'or'b'. Because'b'is a non-empty string, it evaluates to truthy, making the condition unconditionally true.
- Writing
- Follow-up Questions:
- How does Python's
any()andall()relate to short-circuiting? (Both short-circuit:any()on first truthy,all()on first falsy).
- How does Python's
11. Why does `t = ([],); t[0] += [1, 2]` raise a `TypeError` yet still mutate the list inside the tuple?
Difficulty: Advanced
Frequency: ⭐⭐⭐⭐⭐ (Famous Python brain-teaser)
Concept / Summary: The
+=operator on a list calls__iadd__, which mutates the list in-place (succeeds). Python then attempts to store the returned reference back into the tuple element (t[0] = ...), which raises aTypeErrorbecause tuples are immutable.Detailed Explanation:
Under the hood, the bytecode fortarget[index] += valueperforms:ROT_TWO/ fetch element: retrievest[0](the list).- In-place addition: executes
list.__iadd__([1, 2]), which appends[1, 2]in-place and returnsself(reference to the same list). STORE_SUBSCR: executest[0] = <result of __iadd__>.
Becausetis atuple, itstp_as_mapping->mp_ass_subscriptslot isNULL. This immediately raisesTypeError: 'tuple' object does not support item assignment.
The mutation happened before the assignment was attempted!
Code Example:
PYTHONt = ([1, 2],) try: t[0] += [3, 4] except TypeError as e: print(f"Exception raised: {e}") print(t) # Output: ([1, 2, 3, 4],) -- The list WAS modified!Common Pitfalls & Edge Cases:
- Storing mutable objects inside tuples defeats the conceptual integrity of immutability.
Follow-up Questions:
- What happens if you do
t[0].extend([3, 4])instead? (No error is raised because no reassignmentt[0] = ...is attempted).
- What happens if you do
12. How do Chained Comparisons work in Python? What makes `False == False in [False]` tricky?
Difficulty: Intermediate / Advanced
Frequency: ⭐⭐⭐⭐
Concept / Summary: In Python, comparison operators can be chained arbitrarily:
a < b < ctranslates to(a < b) and (b < c), with the middle operandbevaluated only once. All comparison operators (including==,<,>,in,is) have the same precedence and chain together.Detailed Explanation:
Consider:False == False in [False]
Many candidates assume it evaluates as(False == False) in [False]$\rightarrow$True in [False]$\rightarrow$False.However, in Python,
==andinare both comparison operators of equal precedence.
Thus, Python chains them:(False == False) and (False in [False])False == FalseisTrue.False in [False]isTrue.True and Trueevaluates toTrue!
Code Example:
PYTHON# Chained comparisons evaluation: print(False == False in [False]) # Evaluates to True! # Proof of equivalence: step1 = (False == False) step2 = (False in [False]) print(step1 and step2) # True # Single evaluation guarantee: def get_value(): print("Called once!") return 5 print(1 < get_value() < 10) # Output: # Called once! # TrueCommon Pitfalls & Edge Cases:
- Writing
1 < x < 10in languages like C/Java evaluates as(1 < x) < 10(comparing boolean0or1with10). Python explicitly avoids this trap via semantic chaining.
- Writing
Follow-up Questions:
- What does
1 == 1 == 1evaluate to? ((1 == 1) and (1 == 1)$\rightarrow$True). What about(1 == 1) == 1? (True == 1$\rightarrow$True, becauseboolsubclassesintandTrue == 1).
- What does
13. What is the difference between Shallow Copy and Deep Copy?
- Difficulty: Intermediate
- Frequency: ⭐⭐⭐⭐⭐
- Concept / Summary: A shallow copy constructs a new compound collection object, but inserts references to the child objects found in the original. A deep copy constructs a new compound object and recursively duplicates every nested object.
- Detailed Explanation:
- Assignment (
b = a): No copy. Both variables share the exact same reference (b is a). - Shallow Copy (
copy.copy(a),list.copy(),a[:]): Top-level container is cloned. If the container holds nested mutable structures (e.g. lists within a list), modifying a nested list affects both copies. - Deep Copy (
copy.deepcopy(a)): Complete recursive duplication. Also handles circular references safely via an internal memoization dictionary.
- Assignment (
- Code Example:
PYTHON
import copy original = [[1, 2, 3], [4, 5, 6]] # Shallow Copy shallow = copy.copy(original) # Deep Copy deep = copy.deepcopy(original) original[0].append(999) print("Original:", original) # [[1, 2, 3, 999], [4, 5, 6]] print("Shallow: ", shallow) # [[1, 2, 3, 999], [4, 5, 6]] -> affected! print("Deep: ", deep) # [[1, 2, 3], [4, 5, 6]] -> isolated! - Common Pitfalls & Edge Cases:
- Deep copying objects with open sockets, file descriptors, or database connections will raise
TypeErroror unexpected state errors. - Performance:
deepcopyhas significant CPU and memory overhead on complex data graphs.
- Deep copying objects with open sockets, file descriptors, or database connections will raise
- Follow-up Questions:
- How can a custom class control its copy behavior? (Implement
__copy__()and__deepcopy__(memo)).
- How can a custom class control its copy behavior? (Implement
14. What is the Walrus Operator (`:=`), and what are its best use cases?
Difficulty: Intermediate
Frequency: ⭐⭐⭐⭐ (Introduced in Python 3.8 / PEP 572)
Concept / Summary: The walrus operator (
:=) is the assignment expression operator. It enables assigning values to variables inside expressions where statements were previously disallowed (such asifstatements,whileloops, and list comprehensions).Detailed Explanation:
Before Python 3.8, assignment was exclusively a statement (x = 10), unable to return a value. The walrus operator evaluates an expression, binds the result to a variable name, and returns that value simultaneously.Primary Benefits:
- Reduces redundant computations (e.g., computing
len(), regex matching, or querying an API). - Keeps variable scopes tighter and code cleaner without awkward pre-loop initialization.
- Reduces redundant computations (e.g., computing
Code Example:
PYTHONimport re # Use Case 1: Regex matching pattern = re.compile(r"user_(\d+)") text = "Found record: user_4821 in database" if match := pattern.search(text): user_id = match.group(1) print(f"User ID found: {user_id}") # Use Case 2: Reading streams / chunks # Instead of while True: chunk = f.read(1024); if not chunk: break # with open("data.txt") as f: # while chunk := f.read(1024): # process(chunk) # Use Case 3: List comprehension filtering + transformation raw_data = [" 12 ", " ", "45", "invalid", "100"] # Calculate once, test, and store clean_ints = [val for item in raw_data if (val := item.strip()).isdigit()] print(clean_ints) # ['12', '45', '100']Common Pitfalls & Edge Cases:
- Overusing it leads to cryptic, hard-to-read one-liners.
- Walrus expressions inside comprehensions leak their target variable into the enclosing function scope (unlike regular comprehension loop variables).
Follow-up Questions:
- Why was PEP 572 so controversial in the Python community? (Led to Guido van Rossum stepping down as BDFL).
15. Explain Positional-Only (`/`) and Keyword-Only (`*`) parameters in function definitions.
- Difficulty: Intermediate
- Frequency: ⭐⭐⭐⭐
- Concept / Summary:
- Parameters before
/are positional-only (cannot be passed by name). - Parameters after
*are keyword-only (must be passed by name). - Parameters between
/and*can be passed either way.
- Parameters before
- Detailed Explanation:
- Syntax:
def func(pos_only, /, standard, *, kw_only): pass - Why Positional-Only (
/)? Introduced in Python 3.8 (PEP 570).- Allows library authors to rename internal parameter names in future versions without breaking callers.
- Mimics C-accelerated built-ins like
len(obj)(you cannot calllen(obj=x)).
- Why Keyword-Only (
*)? Introduced in Python 3.0 (PEP 3102).- Enforces readability at the call site for flags and configuration options (e.g.
open("file", mode="r", encoding="utf-8")).
- Enforces readability at the call site for flags and configuration options (e.g.
- Syntax:
- Code Example:
PYTHON
def configure_server(host, port, /, timeout=30, *, secure=False, cert=None): return f"Server({host}:{port}, timeout={timeout}, secure={secure})" # Valid calls: print(configure_server("localhost", 8080)) print(configure_server("localhost", 8080, 60, secure=True)) # INVALID CALLS: # configure_server(host="localhost", port=8080) # -> TypeError: configure_server() got some positional-only arguments passed as keyword arguments: 'host, port' # configure_server("localhost", 8080, 30, True) # -> TypeError: configure_server() takes from 2 to 3 positional arguments but 4 were given - Common Pitfalls & Edge Cases:
- Forgetting that bare
*takes no arguments itself—it is simply a delimiter separating standard parameters from keyword-only parameters.
- Forgetting that bare
- Follow-up Questions:
- How do
*argsand**kwargsinteract with/and*?
- How do
16. How does Structural Pattern Matching (`match-case`) work in Python 3.10+?
- Difficulty: Advanced
- Frequency: ⭐⭐⭐⭐
- Concept / Summary: Introduced in Python 3.10 (PEP 634),
match-caseis not merely aswitch-casestatement. It performs structural pattern matching, validating shapes, destructuring sequences, dictionaries, and objects, and binding variables conditionally with guard expressions. - Detailed Explanation:
Unlike traditional C-style switch statements that only compare scalar values:- Supports sequence matching (
[first, *rest]). - Supports mapping matching (
{"status": 200, "data": data}). - Supports class/type pattern matching (
Point(x, y)). - Supports guards via
ifclauses. - The wildcard
_acts as thedefault/ fall-through arm.
- Supports sequence matching (
- Code Example:
PYTHON
def handle_command(command): match command.split(): case ["quit"]: return "Exiting application" case ["go", ("north" | "south" | "east" | "west") as direction]: return f"Moving {direction}" case ["get", item]: return f"Picked up {item}" case ["drop", *items] if len(items) > 1: return f"Dropping multiple items: {', '.join(items)}" case _: return "Invalid command" print(handle_command("go north")) # Moving north print(handle_command("drop sword key")) # Dropping multiple items: sword, key - Common Pitfalls & Edge Cases:
- Using an unqualified variable name in a case (
case status:), which captures the value and binds it rather than comparing against an existing variable. To match a constant, use dotted attributes (case HttpStatus.OK:).
- Using an unqualified variable name in a case (
- Follow-up Questions:
- How does pattern matching handle custom classes? (Using
__match_args__).
- How does pattern matching handle custom classes? (Using
17. What are the differences between `None`, `...` (Ellipsis), `NotImplemented`, and `pass`?
- Difficulty: Intermediate
- Frequency: ⭐⭐⭐⭐
- Concept / Summary:
None: A singleton object representing the absence of a value or a default return value....(Ellipsis): A singleton object commonly used for multidimensional array slicing (NumPy) or type hints (callable argumentsCallable[..., int]).NotImplemented: A special singleton returned by rich comparison dunders (__eq__,__lt__) to signal to Python that it should attempt the reflected operation on the other operand.pass: A syntax keyword (statement) representing a no-op placeholder.
- Detailed Explanation:
NotImplementedvsNotImplementedError:NotImplementedis a value (return NotImplemented). It tells CPython: "I don't know how to compare with this type; ask the other object via its reflected method (e.g.__gt__for__lt__). If both fail, raiseTypeError."NotImplementedErroris an exception subclass ofRuntimeError(raise NotImplementedError), used in abstract methods.
- Code Example:
PYTHON
class Vector: def __init__(self, x): self.x = x def __eq__(self, other): if not isinstance(other, Vector): return NotImplemented # Allows other to attempt its own __eq__ return self.x == other.x # Ellipsis in type hinting and slicing: from typing import Callable Handler = Callable[..., str] # Accepts any arguments, returns str # pass vs ... as body def stub_pass(): pass def stub_ellipsis(): ... - Common Pitfalls & Edge Cases:
- Raising
NotImplementedinstead ofNotImplementedError(raise NotImplementedraises aTypeErroron modern Python becauseNotImplementedis not anException).
- Raising
- Follow-up Questions:
- How does NumPy utilize
...for tensor indexing? (tensor[..., 0]).
- How does NumPy utilize
18. How does the `del` keyword work? Does `del obj` immediately free memory?
- Difficulty: Intermediate / Advanced
- Frequency: ⭐⭐⭐⭐
- Concept / Summary:
deldoes not directly deallocate memory or destroy objects. It unbinds a name from the current namespace and decrements the object's reference count by 1. The object is only destroyed when its reference count drops to zero (or during cyclic garbage collection). - Detailed Explanation:
del x: Removes the symbolxfrom the local or global namespace dictionary (locals()orglobals()).- The target object's reference counter
ob_refcntis decremented. - If
ob_refcnt == 0, CPython triggerstp_dealloc, invoking__del__()(if implemented) and immediately reclaiming the memory viaPyObject_Free. - If other variables, lists, or cycles still reference the object, it stays alive in memory.
- Code Example:
PYTHON
class Resource: def __del__(self): print("Memory deallocated!") a = Resource() b = a # Two references (a and b) del a # Reference count drops to 1; NOT destroyed yet print("After del a") del b # Reference count drops to 0; triggers __del__ immediately print("After del b") # Output: # After del a # Memory deallocated! # After del b - Common Pitfalls & Edge Cases:
- Relying on
__del__()for critical resource cleanups (e.g. database connections, file locks). If cyclic references exist or the interpreter exits abruptly,__del__might be delayed or skipped. Always prefer Context Managers (with).
- Relying on
- Follow-up Questions:
- What happens when two objects hold references to each other (cyclical reference) and you
delboth variables? (The cyclic garbage collector must detect and collect them).
- What happens when two objects hold references to each other (cyclical reference) and you
19. How are strings represented in Python 3, and how does string interning work?
- Difficulty: Advanced
- Frequency: ⭐⭐⭐⭐
- Concept / Summary: Since Python 3.3 (PEP 393 - Flexible String Representation), strings are stored in memory using the most compact representation necessary: Latin-1 (1 byte per char), UCS-2 (2 bytes per char), or UCS-4 (4 bytes per char), depending on the maximum code point in the string.
- Detailed Explanation:
- Memory Efficiency: If a string contains only ASCII/Latin-1 characters, Python uses 1 byte per character instead of wasting 4 bytes per character.
- Immutability: Python strings are immutable. Any modification (concatenation, slicing,
replace()) allocates a new string. - String Interning: CPython automatically interns string constants that look like valid identifiers (alphanumeric + underscores). Interned strings point to the same memory address, allowing the interpreter to replace costly character-by-character string comparisons (
strcmp) with simple pointer comparisons (is). - You can manually intern strings using
sys.intern(s).
- Code Example:
PYTHON
import sys s1 = "hello_world" s2 = "hello_world" print(s1 is s2) # True: automatically interned identifier-like string # Non-identifier strings are generally not interned automatically at runtime: str_a = "".join(["hello", "!", "!"]) str_b = "".join(["hello", "!", "!"]) print(str_a == str_b) # True (equal values) print(str_a is str_b) # False (different memory addresses) # Explicit interning: str_a = sys.intern(str_a) str_b = sys.intern(str_b) print(str_a is str_b) # True: now identical pointers - Common Pitfalls & Edge Cases:
- Concatenating strings in a loop via
+=causes $O(N^2)$ memory copying. The idiomatic high-performance pattern is''.join(list_of_strings).
- Concatenating strings in a loop via
- Follow-up Questions:
- What is the difference between
strandbytesin Python 3?
- What is the difference between
20. What are Augmented Assignment Operators (`+=`, `*=`) and how do they differ from binary operations?
- Difficulty: Intermediate
- Frequency: ⭐⭐⭐⭐
- Concept / Summary: Augmented assignment operators (
+=,-=,*=, etc.) attempt to modify the target operand in-place via in-place dunder methods (__iadd__,__imul__). If in-place modification is not supported (e.g., on immutable types), they fall back to standard binary operators (__add__,__mul__), reassigning the variable to a newly created object. - Detailed Explanation:
- For Mutable Objects (
list):lst += [4]invokeslst.__iadd__([4]), mutating the existing list in-place (equivalent tolst.extend([4])).lst = lst + [4]invokeslist.__add__, allocating a brand-new list and copying elements from both.
- For Immutable Objects (
int,str,tuple):x += 1cannot mutate the integer. It evaluatesx + 1(a new integer) and rebindsxto it.
- For Mutable Objects (
- Code Example:
PYTHON
# Mutable: += modifies in-place list1 = [1, 2] list2 = list1 list1 += [3] print(list1 is list2) # True: list2 sees the change! # Mutable: + creates a new object list1 = [1, 2] list2 = list1 list1 = list1 + [3] print(list1 is list2) # False: list1 was rebound to a new list - Common Pitfalls & Edge Cases:
- Assuming
a += bis always syntactically identical toa = a + b.
- Assuming
- Follow-up Questions:
- What happens when a class defines
__add__but not__iadd__? (Python falls back to__add__).
- What happens when a class defines
21. How does Python prevent integer overflow? What is arbitrary-precision arithmetic?
- Difficulty: Intermediate / Advanced
- Frequency: ⭐⭐⭐⭐
- Concept / Summary: In Python 3, integers have arbitrary precision (unlimited size). Python integers are not restricted by hardware registers (like 32-bit or 64-bit limits in C/Java); their maximum size is limited solely by available system memory.
- Detailed Explanation:
- In CPython, an integer is represented internally as a C struct:
PyLongObject. - The actual integer magnitude is stored as an array of "digits" (typically 30 bits per digit on 64-bit platforms).
- When an arithmetic calculation exceeds a single digit, CPython allocates additional digits dynamically and carries over the arithmetic using multi-precision algorithms.
- In Python 2, there were two distinct integer types:
int(32/64-bit) andlong(arbitrary precision). Python 3 unified them underint.
- In CPython, an integer is represented internally as a C struct:
- Code Example:
PYTHON
import sys # Extremely large integer calculation (would overflow 64-bit integers) big_num = 2 ** 200 print(big_num) # 1606938044258990275541962092341162602522202993782792835301376 # Inspecting memory size scaling print(sys.getsizeof(0)) # 28 bytes (base PyLongObject overhead) print(sys.getsizeof(1)) # 28 bytes print(sys.getsizeof(2**30)) # 32 bytes (allocated extra digit) print(sys.getsizeof(2**60)) # 36 bytes - Common Pitfalls & Edge Cases:
- Floats in Python do not have arbitrary precision! Floats follow IEEE 754 double precision (64-bit), so
1e308 * 10results inOverflowError: float overfloworinf.
- Floats in Python do not have arbitrary precision! Floats follow IEEE 754 double precision (64-bit), so
- Follow-up Questions:
- If Python integers cannot overflow, why do libraries like NumPy have fixed 32-bit and 64-bit integer types that can overflow? (For raw CPU SIMD optimization and C-speed vectorization).
22. Why should `assert` never be used for security checks or user input validation?
Difficulty: Intermediate
Frequency: ⭐⭐⭐⭐⭐
Concept / Summary: When Python is run in optimized mode (e.g.
python -Oorpython -OO), the internal constant__debug__is set toFalse, and the interpreter completely strips out allassertstatements from the generated bytecode.Detailed Explanation:
assert <condition>, <message>compiles to bytecode that is completely bypassed in optimized production environments.If you use
assertto verify user permissions, authenticate passwords, or validate payload schemas, running with-Ocompletely disables those checks, opening severe security vulnerabilities.Proper Use of
assert:- Internal invariant checks (sanity testing code paths that should logically be impossible).
- Unit tests (
pytestleverages AST rewriting onassert). - For validation, always raise explicit exceptions (
ValueError,PermissionError).
Code Example:
PYTHON# DANGEROUS CODE: def delete_user(admin_user, target_user_id): # IF RUN WITH python -O, THIS CHECK DISAPPEARS! assert admin_user.is_admin, "Unauthorized access!" database.delete(target_user_id) # SECURE, PRODUCTION-GRADE CODE: def delete_user_secure(admin_user, target_user_id): if not admin_user.is_admin: raise PermissionError("Unauthorized access!") database.delete(target_user_id)Common Pitfalls & Edge Cases:
- Writing
assert(condition, message)with parentheses. In Python,(condition, message)is parsed as a 2-element tuple. A non-empty tuple is always truthy, soassert (False, "Error")will never trigger an AssertionError!
- Writing
Follow-up Questions:
- What is the difference between
-Oand-OOflags in Python? (-OOstrips both asserts and docstrings).
- What is the difference between
23. What are the key architectural differences between Python 2 and Python 3?
- Difficulty: Beginner / Intermediate
- Frequency: ⭐⭐⭐⭐
- Concept / Summary: Python 3 (released in 2008) intentionally introduced backward-incompatible changes to fix core design flaws of Python 2. Key changes include Unicode by default,
printbecoming a function, true integer division, iterator-based built-ins, and unified integer types. - Detailed Explanation:
- Text vs. Binary Data:
- Python 2:
strwas ASCII/bytes;unicodewas a distinct type. Mixing them caused silent conversions and notoriousUnicodeDecodeErrorbugs. - Python 3:
stris strictly Unicode;bytesis raw 8-bit binary data. Explicit.encode()and.decode()are required.
- Python 2:
- Print Statement vs Function:
- Python 2:
print "Hello"(statement). - Python 3:
print("Hello", end="\n", file=...)(function).
- Python 2:
- Division Operator (
/):- Python 2:
5 / 2performed floor integer division (2). - Python 3:
5 / 2performs true float division (2.5). Floor division requires5 // 2.
- Python 2:
- Iterators vs Lists:
- Python 2:
range(),zip(),map(),dict.keys()returned concretelistobjects (costly memory consumption). - Python 3: Return memory-efficient lazy iterators or view objects.
- Python 2:
- Exception Handling:
- Python 2:
raise Exception, "message",except Exception, e. - Python 3:
raise Exception("message"),except Exception as e.
- Python 2:
- Text vs. Binary Data:
- Code Example:
PYTHON
# Python 3 division & Unicode guarantees print(5 / 2) # 2.5 (True division) print(5 // 2) # 2 (Floor division) # Explicit bytes vs string boundary raw_bytes = b"hello" text = raw_bytes.decode("utf-8") # Explicit decoding required print(type(text)) # <class 'str'> - Common Pitfalls & Edge Cases:
- Assuming Python 2 code will run in Python 3 with simple formatting tools without auditing binary/text data handling.
- Follow-up Questions:
- How did Python 3 handle dictionary ordering? (Python 3.6+ guarantees insertion order retention as a language specification in 3.7+).
24. How does the Ternary Operator work in Python, and what are its common traps?
Difficulty: Beginner
Frequency: ⭐⭐⭐⭐
Concept / Summary: Python's conditional expression (ternary operator) uses the syntax:
result = <value_if_true> if <condition> else <value_if_false>.
It evaluates lazily—only the branch matching the condition is evaluated.Detailed Explanation:
Unlike C/Java (condition ? true_val : false_val), Python places the true value first, followed by the condition, followed by theelseclause.Short-Circuit Execution:
If the condition isTrue,<value_if_false>is never evaluated (and vice versa), which prevents unintended errors.Code Example:
PYTHONstatus_code = 200 message = "Success" if status_code == 200 else "Failure" # Lazy evaluation prevents ZeroDivisionError divisor = 0 result = 100 / divisor if divisor != 0 else 0 print(result) # 0 # ANTI-PATTERN / TRAP (Tuple Indexing Trick): # (false_val, true_val)[condition] # TRAP: This evaluates BOTH branches eagerly! try: bad_res = (0, 100 / divisor)[divisor != 0] # Raises ZeroDivisionError! except ZeroDivisionError: print("Tuple trick failed because both branches are evaluated!")Common Pitfalls & Edge Cases:
- Nested ternary expressions become difficult to read:
a if cond1 else b if cond2 else c(Use standardif-elif-elsewhen logic gets complex).
- Nested ternary expressions become difficult to read:
Follow-up Questions:
- What is the precedence of conditional expressions compared to arithmetic and logical operators?
25. What are the major syntax additions introduced in Python 3.12 and 3.13?
- Difficulty: Advanced / Modern
- Frequency: ⭐⭐⭐⭐ (High value for demonstrating up-to-date modern Python knowledge)
- Concept / Summary: Python 3.12 and 3.13 introduced significant syntactic modernizations:
- PEP 701 (Python 3.12): Syntactic formalization of f-strings (nested quotes, backslashes, expressions, and inline comments now allowed).
- PEP 695 (Python 3.12): Native Type Parameter Syntax (
typestatement and generic functions withoutTypeVar). - PEP 703 (Python 3.13): Experimental Free-threaded Python (no-GIL build).
- Improved Error Tracebacks: Precise underline locations highlighting exact failing expressions.
- Detailed Explanation:
- F-string Enhancements (PEP 701):
Previously, f-strings could not reuse the quote characters of the outer f-string, could not contain backslashes (\n), and could not contain#comments. In Python 3.12+, f-strings use the standard PEG parser without arbitrary limitations. - Type Parameter Syntax (PEP 695):
Replaces boilerplateT = TypeVar('T')with clean, native generic syntax:def first[T](items: list[T]) -> T: ...andtype Number = int | float.
- F-string Enhancements (PEP 701):
- Code Example:
PYTHON
# 1. Advanced F-strings (Python 3.12+) items = ["apple", "banana", "cherry"] # Nested quotes and backslashes inside f-string: formatted = f"Items: {', \n'.join([f'{item.upper()}' for item in items])}" print(formatted) # 2. Native Type Parameter Syntax (Python 3.12+) # Instead of: T = TypeVar('T'); def get_first(lst: list[T]) -> T: def get_first[T](lst: list[T]) -> T | None: return lst[0] if lst else None # Type alias statement: type Point2D = tuple[float, float] - Common Pitfalls & Edge Cases:
- Using PEP 695 syntax in projects required to support older runtimes (< 3.12) causes immediate
SyntaxError.
- Using PEP 695 syntax in projects required to support older runtimes (< 3.12) causes immediate
- Follow-up Questions:
- What are the architectural implications of free-threaded Python (PEP 703) on extension modules and thread safety?