104 Python Interview Questions and Answers (2026)

Blog / 104 Python Interview Questions and Answers (2026)
Python interview questions

Python is now the default language for shipping APIs and AI/ML backends, which means more companies are hiring for it and more interviewers expect real fluency. Vague, memorized answers don't cut it anymore. Walk in shaky on scopes, the GIL, or generators and you'll lose the offer to someone who isn't.

This guide gives you 104 questions with concise, interview-ready answers and code where it actually helps. It's structured Junior to Mid to Senior, so you build from fundamentals up to metaclasses, memory management, concurrency, and asyncio. Work through it and you'll know your stuff cold.

Q1.
What does the if __name__ == '__main__' idiom do, and why is it used?

Junior

It checks whether the file is being run directly or imported. __name__ equals "__main__" only when the file is executed as the main program, so the guarded block runs on direct execution but not on import.

  • How __name__ is set:

    • Run directly: __name__ == "__main__".

    • Imported as a module: __name__ is the module's name instead.

  • Why it's useful:

    • Lets a file double as both an importable library and a runnable script.

    • Prevents side effects (running a demo, starting a server) from firing on import.

    • Required for correct behavior with multiprocessing on spawn-based platforms.

python

def main(): print("running") if __name__ == "__main__": main() # runs only on direct execution

Q2.
What is the fundamental difference between the is operator and the == operator in terms of memory and the data model?

Junior

== tests value equality (do these objects represent the same value), while is tests identity (are these the exact same object in memory, same id()).

  • == calls the data model: It invokes __eq__, so classes can define what "equal" means.

  • is compares identity: Compares memory identity directly, cannot be overridden, and is very fast.

  • Correct usage:

    • Use is only for singletons: is None, is True, is False.

    • Use == for value comparison of numbers, strings, containers.

  • The caching gotcha: Small ints (-5..256) and some strings are interned, so is may accidentally be True; never rely on this for equality.

python

a = [1, 2]; b = [1, 2] a == b # True (same value) a is b # False (different objects) x = 256; y = 256 x is y # True (cached) -> don't depend on this

Q3.
What is the difference between a list, a tuple, a set, and a dictionary, and when would you choose each?

Junior

All four are built-in collections, but they differ in ordering, mutability, uniqueness, and lookup model: pick by what guarantees you need. Lists are ordered mutable sequences, tuples are immutable fixed records, sets are unordered collections of unique items, and dicts are key-value maps.

  • list:

    • Ordered, mutable, allows duplicates, indexed by position.

    • Use for an ordered, growable sequence you'll iterate or modify.

  • tuple:

    • Ordered, immutable, allows duplicates; hashable if its contents are.

    • Use for fixed records, function returns of multiple values, or dict keys.

  • set:

    • Unordered, mutable, unique elements with O(1) membership tests.

    • Use for deduplication and fast in checks or set algebra (union, intersection).

  • dict:

    • Maps hashable keys to values, insertion-ordered (3.7+), O(1) lookup by key.

    • Use when you need to associate and look up data by an identifier.

Q4.
What is a namedtuple, and how does it compare to a regular tuple or a class?

Junior

A namedtuple is a tuple subclass whose fields are accessible by name as well as index: it gives you the immutability and lightness of a tuple plus the readability of a class.

  • vs a regular tuple: Same memory and immutability, but you access p.x instead of p[0], which is self-documenting.

  • vs a class: Lighter and immutable, with free __repr__, __eq__, and tuple unpacking; but no easy mutability or rich behavior.

  • Useful helpers: _replace() returns a modified copy, _asdict() converts to a dict, _fields lists field names.

  • Modern alternatives: typing.NamedTuple adds type hints; a @dataclass is better when you need mutability or methods.

python

from collections import namedtuple Point = namedtuple("Point", "x y") p = Point(1, 2) p.x # 1 p._replace(y=9) # Point(x=1, y=9)

Q5.
What is the purpose of the else and finally clauses in a try/except block?

Junior

In a try statement, else runs only if no exception was raised, and finally always runs regardless of outcome: together they let you separate the success path from cleanup.

  • else:

    • Runs when the try block completes with no exception.

    • Keeps the try block minimal so you don't accidentally catch exceptions from follow-up code that depends on success.

  • finally:

    • Always executes, whether the block succeeded, raised, or even returned, ideal for releasing resources (files, locks, connections).

    • Runs even if an exception propagates uncaught or a return is hit.

  • Order of execution: try then else (if clean) or except (if error), and finally last in every case.

python

try: f = open("data.txt") data = f.read() except IOError: print("read failed") else: process(data) # only if no exception finally: f.close() # always runs

Q6.
What is the difference between a classmethod, a staticmethod, and a regular instance method?

Junior

They differ in what implicit first argument they receive: an instance method gets the instance (self), a classmethod gets the class (cls), and a staticmethod gets nothing automatic.

  • Instance method: Default form; takes self and operates on per-instance state.

  • classmethod: Decorated with @classmethod, takes cls; commonly used for alternative constructors / factory methods that respect subclassing.

  • staticmethod: Decorated with @staticmethod, takes no implicit arg; just a plain function grouped in the class namespace for organization.

python

class Date: def __init__(self, y, m, d): self.y, self.m, self.d = y, m, d @classmethod def from_string(cls, s): # alternative constructor y, m, d = map(int, s.split('-')) return cls(y, m, d) @staticmethod def is_leap(year): # pure helper, no self/cls return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)

Q7.
What is the difference between __str__ and __repr__, and when is each used?

Junior

__repr__ is the unambiguous, developer-facing representation (ideally how to recreate the object), while __str__ is the readable, user-facing representation. If __str__ is absent, str() falls back to __repr__.

  • __repr__:

    • Called by repr(), the interactive shell, and containers (a list prints each element's repr).

    • Aim for unambiguity: Date(2024, 1, 1) is better than 2024-01-01.

  • __str__: Called by str(), print(), and f-strings; optimized for human readability.

  • Practical rule: Always define __repr__ (covers debugging and the fallback); add __str__ only when a distinct user-friendly form is needed.

python

class Temp: def __init__(self, c): self.c = c def __repr__(self): return f"Temp({self.c})" # for developers def __str__(self): return f"{self.c}\u00b0C" # for users print(repr(Temp(20))) # Temp(20) print(str(Temp(20))) # 20\u00b0C

Q8.
What is an Enum in Python, and why would you use one instead of plain constants?

Junior

An Enum is a class of named, symbolic constants with fixed values, giving you readable, type-safe, self-documenting alternatives to loose constants.

  • What it is:

    • Subclass enum.Enum; each member is a singleton with a .name and .value.

    • Members are unique and hashable, usable in sets, dicts, and comparisons.

  • Why over plain constants:

    • Grouping and namespacing: related values live under one type instead of scattered globals.

    • Safety: Color.RED is distinct, while a bare 1 could be anything.

    • Readability in debugging: prints as Color.RED rather than a magic number.

    • Iteration and membership checks come for free.

  • Common variants: IntEnum compares equal to ints; auto() assigns values automatically; Flag supports bitwise combinations.

python

from enum import Enum, auto class Color(Enum): RED = auto() GREEN = auto() Color.RED.name # 'RED' Color(1) is Color.RED # True

Q9.
What is the difference between a generator and a list? Why are generators considered 'lazy'?

Junior

A list stores all its elements in memory at once; a generator produces values one at a time on demand. Generators are 'lazy' because they compute each value only when requested, never holding the whole sequence.

  • List: eager and fully materialized: All items exist in memory, so you can index, slice, and iterate repeatedly.

  • Generator: lazy and single-pass:

    • Values are produced via next() only when needed; once exhausted it can't be reused.

    • No indexing or len(); you iterate forward only.

  • Why 'lazy' matters:

    • Constant memory regardless of length, and it can represent infinite sequences.

    • Work is deferred, so unused values are never computed.

Q10.
What are the memory benefits of using a generator expression over a list comprehension when processing a very large file?

Junior

A list comprehension builds the entire result in memory at once, while a generator expression yields one item at a time, so memory stays roughly constant no matter how big the file is.

  • List comprehension: O(n) memory: Reading a multi-GB file into a list can exhaust RAM because every line is held simultaneously.

  • Generator expression: O(1) memory:

    • Produces each transformed line on demand, so only one item is in memory at a time.

    • Pipelines compose lazily: chained generators still process one item end-to-end.

  • Tradeoff: single-pass and no indexing, but ideal for streaming large data.

python

# Constant memory: processes one line at a time total = sum(len(line) for line in open("huge.log")) # vs. list comp: loads every line into memory first # total = sum([len(line) for line in open("huge.log")])

Q11.
What is the difference between *args and **kwargs, and how are they unpacked in a function call?

Junior

Both let a function accept a variable number of arguments: *args collects extra positional arguments into a tuple, and **kwargs collects extra keyword arguments into a dict.

  • In a definition they collect:

    • *args gathers positional args into a tuple; **kwargs gathers keyword args into a dict.

    • Order in the signature is fixed: positional, *args, keyword-only, then **kwargs.

  • In a call they unpack: *seq spreads an iterable into positional arguments; **dict spreads a mapping into keyword arguments.

  • The names are convention only: Only the * and ** matter; args and kwargs are just customary names.

  • Common use: transparent pass-through wrappers (e.g. decorators forwarding any signature).

python

def f(*args, **kwargs): print(args, kwargs) f(1, 2, x=3) # (1, 2) {'x': 3} nums = [1, 2]; opts = {'x': 3} f(*nums, **opts) # (1, 2) {'x': 3}

Q12.
What are the limitations of lambda functions compared to named functions, and when should they be avoided in production code?

Junior

A lambda is a single-expression anonymous function: convenient inline, but limited because it can't contain statements, has no name for debugging, and hurts readability when overused.

  • Single expression only: No statements, assignments, loops, try/except, or annotations; logic must fit one expression.

  • Poor debuggability: Its __name__ is <lambda>, so tracebacks are uninformative; it can't carry a docstring.

  • Readability cost: Complex or nested lambdas are hard to read; PEP 8 advises against assigning a lambda to a name (use def).

  • When to avoid in production:

    • Anything reused, tested, or non-trivial deserves a named def.

    • Good lambda use stays small and inline: a key= function for sorted() or a short callback.

Q13.
What is a list comprehension, and what are its advantages over using a for loop with append?

Junior

A list comprehension builds a list in a single expression by iterating and optionally filtering: [expr for x in iterable if cond]. It's more concise and usually faster than an empty list plus a for loop with .append().

  • Conciseness and intent: One line states "build a list from these items"; less boilerplate than initializing and appending.

  • Performance: Faster: the append is done in optimized C internally, avoiding repeated attribute lookups of .append.

  • Scoping: The loop variable doesn't leak into the enclosing scope (unlike a plain for loop).

  • When to avoid: Deeply nested or side-effect-heavy logic: a readable loop is better. For huge data, prefer a generator expression to avoid building the whole list.

python

squares = [x*x for x in range(10) if x % 2 == 0]

Q14.
How does the sorted() function differ from the list.sort() method, and what is the role of the key parameter?

Junior

sorted() is a built-in function that returns a new sorted list from any iterable, while list.sort() is a method that sorts a list in place and returns None. The key parameter customizes what each element is sorted by.

  • sorted(): Works on any iterable (lists, tuples, dict keys, generators) and always returns a new list, leaving the original untouched.

  • list.sort(): Lists only; mutates in place and returns None (so don't write x = mylist.sort()). Slightly more memory-efficient since it avoids a copy.

  • The key parameter:

    • A one-argument function applied to each element to derive its comparison value; called once per element.

    • Example: key=len or key=lambda p: p.age.

  • Shared options: Both accept reverse=True and use Timsort, which is stable (equal elements keep their original order).

python

words = ["banana", "kiwi", "apple"] sorted(words, key=len) # ['kiwi', 'apple', 'banana'] (new list) words.sort(reverse=True) # mutates words, returns None

Q15.
What is the difference between f-strings, str.format(), and %-formatting?

Junior

All three interpolate values into strings, but f-strings are the newest, fastest, and most readable; str.format() is more verbose but works with externally stored templates; %-formatting is the legacy C-style syntax.

  • f-strings (f"...", Python 3.6+):

    • Expressions are embedded inline and evaluated at runtime; fastest because parsing happens at compile time.

    • Support inline format specs and the = debug syntax (f"{x=}").

  • str.format():

    • Uses {} placeholders filled from arguments; good when the template is stored separately (config, i18n).

    • More verbose since values are passed as arguments rather than inlined.

  • %-formatting:

    • Old C-style %s/%d; concise but error-prone (tuple vs single value) and limited.

    • Still common in logging, where % args defer formatting until needed.

python

name, n = "Ada", 3 f"{name} has {n} items" # f-string "{} has {} items".format(name, n) # str.format "%s has %d items" % (name, n) # %-formatting

Q16.
What is the difference between a module and a package, and what role does __init__.py play?

Junior

A module is a single .py file, while a package is a directory that groups related modules; __init__.py marks a directory as a (regular) package and runs as its initialization code.

  • Module: A single file of Python code imported as a namespace (import math).

  • Package: A directory of modules (and possibly subpackages) enabling dotted imports like import pkg.sub.mod.

  • Role of __init__.py:

    • Its code runs when the package is first imported; use it to expose a curated public API or set __all__.

    • Can be empty just to mark the directory as a regular package.

    • Since Python 3.3, a directory without it can still be a namespace package (used for splitting one package across paths), but regular packages still use it.

Q17.
Is Python a compiled or interpreted language? Explain the role of .pyc files and the Python Virtual Machine (PVM).

Mid

Python is both: source is compiled to bytecode, then that bytecode is interpreted by the Python Virtual Machine. So it's an interpreted language with an implicit compilation step.

  • Compilation step: CPython compiles .py source into platform-independent bytecode (a sequence of opcodes).

  • .pyc files cache the bytecode:

    • Stored under __pycache__/ so imports skip recompilation if source is unchanged (checked via timestamp/hash).

    • It's an optimization, not machine code: a .pyc still needs the PVM to run.

  • The PVM executes bytecode:

    • A stack-based interpreter loop that reads each opcode and performs it.

    • This is why Python is portable: the same bytecode runs anywhere the PVM exists.

  • Note: this describes CPython; other implementations (PyPy JIT) compile differently.

Q18.
What is your favorite feature introduced in a recent Python version (3.12 or 3.13), and how does it solve a problem that existed in older versions?

Mid

A strong pick is the new type-parameter syntax in Python 3.12 (PEP 695), which makes generics readable and removes boilerplate that older versions required with TypeVar.

  • The old problem: You had to declare a TypeVar separately, import it, and wire it into Generic[T], which was verbose and easy to get wrong.

  • The 3.12 fix: Declare type parameters inline on the class or function with bracket syntax: no separate TypeVar needed.

  • Honorable mentions: 3.12 improved f-string parsing (PEP 701) and clearer error messages; 3.13 added an experimental free-threaded (no-GIL) build.

python

# Old way from typing import TypeVar, Generic T = TypeVar("T") class Stack(Generic[T]): ... # 3.12 (PEP 695) class Stack[T]: ... def first[T](items: list[T]) -> T: ...

Q19.
What are the major differences between Python 2 and Python 3 that you should be aware of?

Mid

Python 3 is a backward-incompatible cleanup of Python 2 (which is end-of-life): the headline changes are how text vs bytes, division, and printing work, plus many APIs returning iterators instead of lists.

  • print is a function: print("x") in 3, vs the print statement in 2.

  • Strings and bytes: In 3, str is Unicode and bytes is separate; in 2 str was bytes, causing encoding confusion.

  • Division: 5 / 2 is 2.5 in 3 (true division); was 2 in 2. Use // for floor division.

  • Iterators by default: range, dict.keys(), map return lazy views/iterators, not lists.

  • Other changes: Integers unified (no long), exceptions use as syntax, and 3 added type hints, f-strings, and async.

Q20.
Explain the LEGB rule. What happens to the scope when you use the global or nonlocal keywords inside a nested function?

Mid

LEGB is the order Python resolves a name: Local, then Enclosing, then Global, then Built-in. global and nonlocal change where an assignment binds, letting you rebind a name in an outer scope instead of creating a new local.

  • The LEGB search order:

    1. Local: names assigned inside the current function.

    2. Enclosing: locals of any enclosing function (closures).

    3. Global: module-level names.

    4. Built-in: names like len, print.

  • global: Assignments bind to the module-level name instead of creating a local one.

  • nonlocal: Binds to the nearest enclosing function's variable: used in closures to mutate captured state.

  • Key rule: Reading an outer name works without keywords; only assignment makes a name local, which is what these keywords override.

python

def counter(): n = 0 def inc(): nonlocal n # rebind enclosing n, not a new local n += 1 return n return inc

Q21.
What happens if you use a mutable object like a list or dictionary as a default argument in a function, and why is this considered a gotcha?

Mid

A mutable default argument is evaluated once, when the function is defined, and the same object is reused across every call. If a call mutates it, the change persists into later calls, producing surprising shared state.

  • Default is bound at definition time: The default object is created once and stored on the function object (func.__defaults__), not freshly per call.

  • Why it's a gotcha: Appending to a default list or updating a default dict leaks data between independent calls, causing subtle bugs.

  • The fix: use None as a sentinel: Default to None and build a fresh mutable object inside the function body.

python

# Buggy: shared list def add(item, bucket=[]): bucket.append(item) return bucket add(1) # [1] add(2) # [1, 2] <- leaked! # Correct def add(item, bucket=None): if bucket is None: bucket = [] bucket.append(item) return bucket

Q22.
What is a closure in Python, and how does the interpreter 'remember' the values of the enclosing scope even after the outer function has finished executing?

Mid

A closure is a nested function that captures and remembers variables from its enclosing scope, so it can use them even after the outer function has returned. Python keeps those variables alive by storing them in cell objects referenced by the inner function, rather than copying their values.

  • What makes a closure: A nested function references a free variable from the enclosing (non-global) scope and is returned or passed out.

  • How it remembers:

    • Captured variables are stored in cell objects; the inner function holds them in __closure__.

    • It captures the variable itself by reference, not a snapshot of its value, so later changes are visible.

  • Writing to captured variables: Use nonlocal to rebind an enclosing variable from inside the inner function.

  • Common trap: Closures in a loop all share the same loop variable; bind it with a default argument to capture per-iteration values.

python

def make_counter(): count = 0 def inc(): nonlocal count count += 1 return count return inc c = make_counter() c() # 1 c() # 2 (count survives after make_counter returned)

Q23.
Is Python 'call-by-value' or 'call-by-reference'? Explain the concept of 'call-by-object-reference' (or 'assignment').

Mid

Python is neither strictly call-by-value nor call-by-reference: it's "call-by-object-reference" (also called call-by-assignment). The function receives a reference to the same object the caller passed, but the parameter name is a new binding, so reassigning it doesn't affect the caller while mutating a mutable object does.

  • Arguments are passed as object references: Both caller and callee names point at the same object initially (no copy of the object is made).

  • Rebinding vs mutating:

    • Assigning to the parameter (x = ...) just rebinds the local name; the caller's variable is unchanged.

    • Mutating the object in place (lst.append(...)) is visible to the caller because it's the same object.

  • Practical consequence: Immutable arguments (ints, strings, tuples) feel like call-by-value; mutable ones (lists, dicts) feel like call-by-reference.

python

def f(lst, x): lst.append(99) # mutates caller's list x = x + 1 # rebinds local only my = [1] n = 5 f(my, n) # my == [1, 99], n == 5

Q24.
How do Python's collections.deque and list differ in terms of time complexity for common operations?

Mid

A list is a contiguous dynamic array optimized for indexing and end operations, while a deque is a doubly-linked block structure optimized for fast appends and pops at both ends.

  • Operations at the right end: Both append/pop at the end are amortized O(1).

  • Operations at the left end:

    • list insert(0, x) and pop(0) are O(n) because every element shifts.

    • deque appendleft/popleft are O(1).

  • Random access: list indexing is O(1); deque indexing in the middle is O(n).

  • When to choose: Use deque for queues/stacks needing both ends; use list for index-heavy, mostly-append workloads.

Q25.
What requirements must an object meet to be used as a key in a Python dictionary, and how does Python handle hash collisions?

Mid

A dict key must be hashable: it needs a stable __hash__ and a consistent __eq__. This usually means it must be immutable. Python uses the hash to find a bucket, then resolves collisions (different keys landing in the same slot) by probing for another open slot.

  • Requirements for a key:

    • Hashable: implements __hash__() returning a value stable over the object's lifetime.

    • Comparable: implements __eq__(), and equal objects must have equal hashes.

    • Effectively immutable: lists and dicts are unhashable; tuples of immutables are fine.

  • How collisions are handled:

    • CPython uses open addressing: on a collision it probes other slots in the table rather than chaining lists.

    • Lookup compares hash first, then == to confirm the real key.

    • The table resizes (rehashes) when it gets too full to keep probing cheap.

  • Custom objects: If you override __eq__ you must also define __hash__ consistently, or the object becomes unhashable.

Q26.
What is a frozenset, and how does it differ from a regular set?

Mid

A frozenset is an immutable version of a set: same membership and set operations, but it can't be changed after creation, which makes it hashable.

  • Immutable: No add(), remove(), or discard(); once built it stays fixed.

  • Hashable: Because it's immutable it can be a dict key or an element of another set, which a regular set cannot.

  • Same operations: Supports union, intersection, difference, and membership tests, just not the mutating methods.

  • Created with frozenset(iterable): There is no literal syntax like {} for it.

python

fs = frozenset([1, 2, 3]) lookup = {fs: "a"} # frozenset can be a dict key # fs.add(4) # AttributeError: immutable

Q27.
Are Python dictionaries ordered? Explain the insertion-order guarantee introduced in Python 3.7.

Mid

Yes: since Python 3.7 dictionaries preserve insertion order as a guaranteed language feature, so iteration yields keys in the order they were first added.

  • The guarantee: Iteration, keys(), values(), and items() reflect insertion order.

  • History: It was an implementation detail in CPython 3.6, then made an official language guarantee in 3.7.

  • What ordering means here: Reassigning an existing key keeps its original position; only deleting and re-inserting moves it to the end.

  • Not the same as sorted: It's insertion order, not key order; use sorted() if you need sorted keys.

  • OrderedDict still differs slightly: Its == is order-sensitive and it has move_to_end(), which plain dicts lack.

Q28.
What are defaultdict, Counter, and OrderedDict in the collections module, and when would you use them?

Mid

They are specialized dict subclasses in collections that handle common patterns more cleanly than a plain dict: automatic defaults, counting, and order-aware behavior.

  • defaultdict:

    • Calls a factory to supply a default value for missing keys, avoiding KeyError and setdefault() boilerplate.

    • Great for grouping or accumulating: defaultdict(list), defaultdict(int).

  • Counter:

    • A dict that counts hashable items; Counter(iterable) tallies occurrences.

    • Offers most_common() and supports arithmetic between counters.

  • OrderedDict: Less essential since 3.7, but adds move_to_end() and order-sensitive equality, useful for LRU-style structures.

python

from collections import defaultdict, Counter groups = defaultdict(list) groups["a"].append(1) # no KeyError Counter("banana").most_common(1) # [('a', 3)]

Q29.
Explain the 'Easier to Ask for Forgiveness than Permission' (EAFP) coding style. Why is it often preferred in Python over 'Look Before You Leap' (LBYL)?

Mid

EAFP means you just try the operation and catch the exception if it fails, rather than checking preconditions first (LBYL). It's preferred in Python because it's cleaner, often faster on the happy path, and avoids race conditions.

  • EAFP: try then handle: Wrap the access in try/except and react only when it actually fails.

  • LBYL: check first: Test conditions (key exists, file exists) before acting, which adds noise and can drift out of date.

  • Why EAFP wins in Python:

    • Avoids a check-then-act race: between an LBYL check and the action, state can change (e.g. a file is deleted).

    • Exceptions are cheap when not raised, so the common success path stays fast.

    • Reads more directly and matches idiomatic Python.

  • Caveat: Keep the try block narrow and catch specific exceptions so you don't hide unrelated bugs.

python

# EAFP try: value = d["key"] except KeyError: value = default # LBYL (vulnerable to races, more verbose) if "key" in d: value = d["key"] else: value = default

Q30.
Why is it considered bad practice to use except Exception? Explain the Python exception hierarchy and where BaseException fits in.

Mid

Catching except Exception (or worse, a bare except:) is risky because it swallows nearly every error, including bugs you didn't anticipate, making failures silent and hard to debug. You should catch the specific exceptions you can actually handle.

  • The hierarchy:

    • BaseException is the root of all exceptions.

    • Exception is its subclass and the base of ordinary errors (ValueError, KeyError, IOError, etc.).

    • System-control exceptions like SystemExit, KeyboardInterrupt, and GeneratorExit inherit from BaseException directly, not Exception.

  • Why that matters:

    • except Exception correctly does NOT catch KeyboardInterrupt or SystemExit, so Ctrl-C and clean exits still work, that's the small upside over a bare except:.

    • But it still hides programming errors (typos, AttributeError) that should surface loudly.

  • Better practice: Catch the narrowest exception you can handle, and if you must catch broadly, log it and re-raise.

Q31.
What is duck typing, and how does it differ from EAFP?

Mid

Duck typing means an object's suitability is determined by the methods and behavior it provides, not by its declared type: "if it walks like a duck and quacks like a duck, it's a duck." It's about typing philosophy, whereas EAFP is an error-handling style, though they often work together.

  • Duck typing:

    • You care whether an object supports an operation (e.g. is iterable, has read()), not what class it is.

    • Enables polymorphism without inheritance or interfaces.

  • EAFP: A pattern for handling failure: try the operation, catch the exception if it isn't supported.

  • How they relate:

    • EAFP is how you commonly act on a duck-typed assumption: just call the method and catch AttributeError/TypeError if it's absent.

    • Duck typing is the "what" (behavior defines type); EAFP is the "how" (try without pre-checking the type).

Q32.
How do you define a custom exception, and what are the best practices for exception hierarchies?

Mid

Define a custom exception by subclassing Exception (or a more specific built-in), and design a hierarchy so callers can catch broadly or narrowly as needed.

  • Subclass the right base: Inherit from Exception, not BaseException (which is reserved for system-exiting events like KeyboardInterrupt).

  • Give the package one root exception: Define a base like class MyLibError(Exception), then derive specific errors from it, so users can catch the whole library with one except.

  • Make exceptions meaningful, not numerous: Add new classes only when callers need to distinguish them; otherwise reuse built-ins like ValueError or KeyError.

  • Attach context, not just a message: Store useful attributes (the offending value, an error code) so handlers can act on data instead of parsing strings.

  • Name them with the Error suffix by convention.

python

class MyLibError(Exception): """Base for everything this library raises.""" class ConfigError(MyLibError): def __init__(self, key): super().__init__(f"Missing config key: {key}") self.key = key # structured context for handlers

Q33.
What is exception chaining, and what does 'raise ... from ...' do?

Mid

Exception chaining links a new exception to the one that caused it, preserving the original traceback. raise NewError from cause sets that cause explicitly so the full story is visible.

  • Implicit chaining: If an exception is raised inside an except block, Python auto-sets __context__ and prints "During handling of the above exception, another occurred."

  • Explicit chaining with from: raise X from Y sets __cause__ and prints "The above exception was the direct cause...", signaling intentional translation of one error into another.

  • Suppress a chain with from None: raise X from None hides the original cause when it would only add noise.

  • Why it matters: it converts low-level errors into domain errors without losing the root cause during debugging.

python

try: value = data["port"] except KeyError as exc: raise ConfigError("port") from exc # preserves the KeyError as the cause

Q34.
What are the advantages of using @dataclass over a regular class or a namedtuple, and how does it handle default values and mutability?

Mid

@dataclass auto-generates boilerplate (__init__, __repr__, __eq__) from typed field declarations, giving you a regular mutable class with far less code than writing them by hand, and more flexibility than a namedtuple.

  • vs. a regular class: No manual boilerplate; fields are declared once with type hints and the methods are generated.

  • vs. namedtuple: Dataclasses are mutable by default and class-based, so they support methods, inheritance, and defaults more naturally; a namedtuple is an immutable tuple.

  • Default values:

    • Use x: int = 0 for simple defaults.

    • For mutable defaults you MUST use field(default_factory=list); a bare = [] raises an error to prevent the shared-mutable-default bug.

  • Mutability control: @dataclass(frozen=True) makes instances immutable and hashable, useful as dict keys or set members.

python

from dataclasses import dataclass, field @dataclass class Cart: owner: str items: list = field(default_factory=list) # not = []

Q35.
How do Python's 'dunder' (magic) methods allow for operator overloading and protocol implementation?

Mid

Dunder methods are special hooks the Python interpreter calls for built-in operations, so defining them lets your objects respond to operators, built-in functions, and language protocols as if they were native types.

  • Operator overloading: a + b calls a.__add__(b); comparisons map to __eq__, __lt__, etc.

  • Protocols are duck-typed via dunders:

    • Iterable: __iter__ / __next__. Container: __len__, __getitem__, __contains__.

    • Context manager: __enter__ / __exit__. Callable: __call__.

  • The interpreter dispatches to them: Built-ins like len(x) and str(x) simply call x.__len__() and x.__str__() under the hood.

  • Reflected/fallback variants exist: __radd__ handles other + self when the left operand can't.

python

class Vector: def __init__(self, x, y): self.x, self.y = x, y def __add__(self, other): return Vector(self.x + other.x, self.y + other.y) def __repr__(self): return f"Vector({self.x}, {self.y})" Vector(1, 2) + Vector(3, 4) # Vector(4, 6)

Q36.
What is the relationship between __eq__ and __hash__, and what happens if you override one but not the other?

Mid

Objects that compare equal must have the same hash, so __eq__ and __hash__ must stay consistent for the object to behave correctly in sets and dict keys.

  • The contract:

    • If a == b then hash(a) == hash(b) must hold (the reverse is not required: hash collisions are fine).

    • Hashable objects should be immutable in the fields used for equality.

  • Override __eq__ only: Python sets __hash__ to None, making instances unhashable: they can't be used in sets or as dict keys.

  • Override __hash__ only: Equality falls back to identity (is), so two "equal" objects may not be treated as duplicates.

  • Do both together:

    • Base the hash on the same fields used in equality, typically a tuple of them.

    • @dataclass(frozen=True) generates a consistent pair automatically.

python

class Point: def __init__(self, x, y): self.x, self.y = x, y def __eq__(self, other): return (self.x, self.y) == (other.x, other.y) def __hash__(self): return hash((self.x, self.y))

Q37.
What is the __call__ method, and how does it let you make instances of a class callable?

Mid

Defining __call__ on a class makes its instances callable like functions: obj() invokes obj.__call__().

  • How it works:

    • Any object whose type defines __call__ satisfies callable(obj).

    • It can take arguments just like a normal function signature.

  • Why use it:

    • Stateful "functions": the object carries configuration or accumulated state between calls.

    • Function-like objects that also need methods or attributes (e.g. caches, counters).

    • Underlies decorators implemented as classes and many functools utilities.

  • Note: Even ordinary functions are callable because the function type defines __call__; classes themselves are callable because their metaclass does.

python

class Multiplier: def __init__(self, factor): self.factor = factor def __call__(self, x): return x * self.factor double = Multiplier(2) double(10) # 20

Q38.
How does Python determine the truthiness of an object, and what role do __bool__ and __len__ play?

Mid

Python evaluates truthiness by calling __bool__ first, falling back to __len__, and defaulting to True if neither exists.

  • The resolution order:

    1. If __bool__ is defined, its return (must be True/False) decides.

    2. Otherwise, if __len__ is defined, the object is falsy when length is 0.

    3. If neither is defined, the object is always truthy.

  • Built-in falsy values: None, False, 0, 0.0, empty "", [], {}, set().

  • Why it matters:

    • Drives if obj:, while, and boolean operators.

    • Implement __len__ on containers so empties evaluate as falsy for free.

python

class Box: def __init__(self, items): self.items = items def __len__(self): return len(self.items) bool(Box([])) # False (len 0) bool(Box([1])) # True

Q39.
What is the purpose of the abc module, and how does it help enforce an interface compared to standard inheritance?

Mid

The abc (Abstract Base Classes) module lets you define base classes that declare a required interface and refuse to be instantiated until subclasses implement it, turning an informal contract into one the runtime actually enforces.

  • Standard inheritance does not enforce anything: A subclass can simply omit a method, and the failure only surfaces later when that method is called (often a confusing AttributeError).

  • abc enforces the contract at instantiation:

    • Inherit from ABC and decorate required methods with @abstractmethod.

    • Instantiating a subclass that hasn't overridden every abstract method raises TypeError immediately, failing fast.

  • Extra capabilities:

    • Combine with @property or @classmethod to require those too.

    • register() lets unrelated classes be treated as virtual subclasses for isinstance() checks without inheritance.

python

from abc import ABC, abstractmethod class Repository(ABC): @abstractmethod def get(self, id: int): ... class SqlRepo(Repository): pass SqlRepo() # TypeError: Can't instantiate abstract method get

Q40.
What is the purpose of the typing module? Does Python enforce these types at runtime, and if not, why are they used in modern production codebases?

Mid

The typing module provides a vocabulary for annotating the types of variables, parameters, and return values. Python does NOT enforce these at runtime (they're ignored by the interpreter); they exist to power static analysis, tooling, and documentation that catch bugs before code runs.

  • What it provides: Generic and composite types (list[int], dict[str, int], Optional, Union) plus tools like Protocol, TypedDict, and Callable.

  • Not enforced at runtime: Annotations are stored in __annotations__ but never checked: passing a str where an int is hinted won't raise on its own.

  • Why production code uses them anyway:

    • Static checkers (mypy, pyright) catch type errors in CI before deployment.

    • Editor autocomplete, refactoring safety, and self-documenting signatures.

    • Libraries like pydantic and FastAPI opt into reading them at runtime to validate data, but that's the library's choice, not the language's.

Q41.
How does Python's 'Duck Typing' philosophy differ from 'Strong Typing'?

Mid

They answer different questions: duck typing is about WHEN type compatibility is decided (by behavior at runtime, not declared type), while strong typing is about WHETHER the language lets you mix types without explicit conversion. Python is both duck-typed and strongly typed.

  • Duck typing:

    • "If it walks like a duck and quacks like a duck...": an object is usable if it has the needed methods/attributes, regardless of its class.

    • Compatibility is checked implicitly when an operation runs, not by a declared interface.

  • Strong typing:

    • Python doesn't silently coerce unrelated types: "3" + 5 raises TypeError rather than guessing.

    • Contrast with weak typing (e.g. JavaScript) where such mixes are coerced silently.

  • The key distinction:

    • Duck typing = dynamic (type identity checked late, by behavior).

    • Strong typing = strict about conversions (no implicit coercion). They're orthogonal axes.

Q42.
Why would you use typing.Annotated?

Mid

typing.Annotated lets you attach extra metadata to a type without changing the type itself: type checkers see only the underlying type, while frameworks and tools can read the attached metadata at runtime. It's how you keep a hint meaningful AND carry library-specific instructions on the same annotation.

  • Syntax: Annotated[T, meta1, meta2, ...]: the first argument is the real type, the rest is arbitrary metadata.

  • Why it's useful:

    • Adds validation/config without a separate parameter, e.g. FastAPI's Annotated[str, Query(max_length=50)] or Depends().

    • pydantic uses it for constraints like Annotated[int, Field(gt=0)].

  • How it stays safe:

    • Static checkers ignore the metadata and treat the value as the underlying type T.

    • Tools retrieve the metadata via typing.get_type_hints(..., include_extras=True).

  • Benefit over alternatives: Keeps one source of truth on the annotation, allowing reusable aliases like UserId = Annotated[int, ...].

Q43.
What is the difference between Final and ClassVar in type hinting?

Mid

Both come from typing and constrain how a name is used, but they answer different questions: Final says "this name must not be reassigned/overridden," while ClassVar says "this attribute belongs to the class, not to each instance."

  • Final means "constant":

    • A type checker flags any reattempt to bind the name, or any override of a Final attribute/method in a subclass.

    • Use it for module-level constants and for methods/classes that should not be overridden (@final decorator for classes/methods).

  • ClassVar means "shared on the class":

    • Marks an attribute as class-level state; the checker forbids setting it via an instance as if it were an instance field.

    • It is not assignable per-instance in annotations and is excluded from things like @dataclass instance fields.

  • They are orthogonal:

    • You can combine them: x: Final[ClassVar[int]] is not valid, but x: ClassVar[int] = 5 can also be made constant via design.

    • Both are checker-only hints: Python does not enforce them at runtime.

python

from typing import Final, ClassVar class Config: VERSION: Final = "1.0" # must not be reassigned instances: ClassVar[int] = 0 # shared across all instances def __init__(self): Config.instances += 1

Q44.
What is 'Duck Typing', and how does it influence the way you design interfaces in Python compared to a strictly typed language like Java?

Mid

Duck typing means an object's suitability is determined by the methods and attributes it actually has, not by its declared type: "if it walks like a duck and quacks like a duck, it's a duck." In Python you program against behavior (protocols), whereas Java forces you to declare and implement explicit interfaces up front.

  • Behavior over declared type:

    • A function works with any object that supports the operations it calls (e.g. anything with __iter__ is iterable), regardless of class hierarchy.

    • No need to inherit a common base or implement a named interface.

  • Contrast with Java's nominal typing:

    • Java requires a class to formally implements SomeInterface; compatibility is by name/declaration.

    • Python is structural: matching shape is enough, so interfaces stay implicit and flexible.

  • Design impact:

    • You design small, focused capabilities and rely on consistent method names (the data model / dunder protocols).

    • "EAFP" style fits: try the operation and handle failure, rather than checking types first.

  • Making contracts explicit when needed: Use typing.Protocol for static structural typing, or ABCs for runtime isinstance checks, getting documented contracts without sacrificing duck-typing flexibility.

python

def total_length(items): return sum(len(x) for x in items) # works for lists, strings, dicts... # any object with __len__ qualifies; no shared base class required

Q45.
What is the difference between an iterable, an iterator, and a generator? What are the memory implications of using a generator over a list comprehension?

Mid

An iterable is anything you can loop over (it provides __iter__), an iterator is the object that actually produces values one at a time via __next__, and a generator is a convenient way to write an iterator using a function with yield. A generator computes values lazily, so it uses constant memory, while a list comprehension materializes every element at once.

  • Iterable:

    • Implements __iter__ returning a fresh iterator (e.g. list, str, dict).

    • Can usually be iterated many times, each giving a new iterator.

  • Iterator:

    • Implements both __next__ (yield next value or raise StopIteration) and __iter__ (returns self).

    • Stateful and one-shot: once exhausted, it stays exhausted.

  • Generator:

    • A function using yield, or a generator expression (x for x in ...); it is itself an iterator.

    • Suspends and resumes, producing values on demand.

  • Memory implications:

    • A list comprehension builds and holds all N elements in memory at once.

    • A generator holds only the current value plus its state, so memory is O(1) regardless of size, ideal for large or infinite streams.

    • Trade-off: generators are single-pass and have no len() or indexing.

python

squares_list = [x*x for x in range(1_000_000)] # all in memory squares_gen = (x*x for x in range(1_000_000)) # lazy, O(1) memory next(squares_gen) # 0 -> produced on demand

Q46.
How does the yield keyword work, and what is the difference between yield and yield from?

Mid

yield turns a function into a generator: each yield produces a value and suspends the function, preserving its state until resumed. yield from delegates to another iterable/generator, transparently passing through its values (and return value/sends).

  • yield suspends and resumes: Execution pauses at the yield, local state is saved, and the next next() resumes right after it.

  • yield from delegates to a sub-iterator:

    • Yields every item from the inner iterable without a manual loop, and forwards send()/throw() to it.

    • It also captures the inner generator's return value as the result of the yield from expression.

  • Equivalence: yield from gen is essentially for x in gen: yield x but also handles delegation correctly.

python

def inner(): yield 1 yield 2 def outer(): yield from inner() # delegates yield 3 list(outer()) # [1, 2, 3]

Q47.
What is the difference between an Iterator and an Iterable? How do you implement the Iterator protocol?

Mid

An iterable is anything you can loop over (it implements __iter__()), while an iterator is the object that actually produces values one at a time (it implements both __iter__() and __next__()). Every iterator is an iterable, but not every iterable is an iterator.

  • Iterable: Defines __iter__(), which returns a fresh iterator (e.g. list, str, dict).

  • Iterator:

    • Defines __next__() to return the next value and raise StopIteration when done.

    • Its __iter__() returns self, and it carries state (is consumed once).

  • Generators are the easy way to build iterators: they implement the protocol automatically.

python

class Count: def __init__(self, n): self.n = n self.i = 0 def __iter__(self): return self def __next__(self): if self.i >= self.n: raise StopIteration self.i += 1 return self.i

Q48.
How do decorators work, and how can you ensure the original function's metadata is preserved?

Mid

A decorator is a callable that takes a function and returns a replacement (usually a wrapper) function, letting you add behavior without modifying the original. To preserve the original's metadata (name, docstring, signature), wrap the inner function with functools.wraps.

  • How it works:

    • @decorator above def f is just sugar for f = decorator(f).

    • The wrapper typically does work before/after calling the original via *args, **kwargs.

  • The metadata problem: Without help, the returned wrapper replaces the original, so f.__name__ and f.__doc__ show the wrapper's.

  • The fix: functools.wraps: Applied to the wrapper, it copies __name__, __doc__, and __wrapped__ from the original.

python

import functools def log(func): @functools.wraps(func) def wrapper(*args, **kwargs): print(f"calling {func.__name__}") return func(*args, **kwargs) return wrapper @log def greet(name): """Say hello.""" return f"hi {name}" greet.__name__ # 'greet' (preserved)

Q49.
How do decorators work under the hood? Explain the concept of a closure and how it allows a decorator to remember the state of the wrapped function.

Mid

A decorator is a function that takes a function and returns a new function, usually an inner wrapper that adds behavior around the original. The wrapper "remembers" the original via a closure: it captures the enclosing variable (the wrapped function) and keeps it alive after the decorator returns.

  • Decorator syntax is sugar: @dec above def f is just f = dec(f).

  • A closure captures enclosing state:

    • The inner wrapper references func from the outer scope; Python keeps that binding in the function's __closure__ cells.

    • So each decorated function carries its own captured original, even after the decorator call has returned.

  • Preserve metadata: Use @functools.wraps(func) on the wrapper so __name__ and __doc__ survive.

python

import functools def log(func): @functools.wraps(func) def wrapper(*args, **kwargs): # closes over func print(f"calling {func.__name__}") return func(*args, **kwargs) return wrapper @log def add(a, b): return a + b

Q50.
Explain how a decorator works conceptually. How would you write a decorator that accepts its own arguments?

Mid

A decorator wraps a function to add behavior without modifying its body: @dec means f = dec(f). To make a decorator accept its own arguments, you add an extra outer layer: a factory that takes the arguments and returns the actual decorator.

  • Plain decorator: one layer: decorator(func) returns a wrapper that calls func.

  • Parameterized decorator: three layers:

    • Outer factory takes the args; middle decorator(func) takes the function; inner wrapper does the work.

    • @dec(args) first calls dec(args) to get a decorator, which is then applied to the function.

  • Still use @functools.wraps on the innermost wrapper to keep metadata.

python

import functools def repeat(n): # factory: takes the argument def decorator(func): # real decorator @functools.wraps(func) def wrapper(*args, **kwargs): for _ in range(n): result = func(*args, **kwargs) return result return wrapper return decorator @repeat(3) def greet(name): print(f"hi {name}")

Q51.
What are dictionary and set comprehensions, and when would you use them?

Mid

They are the same comprehension syntax applied to dicts and sets: a dict comprehension builds key-value pairs with {k: v for ...}, and a set comprehension builds a deduplicated collection with {expr for ...}.

  • Dict comprehension:

    • Use to transform or filter mappings, or build a lookup from pairs: {k: v for k, v in items}.

    • Great for inverting a dict or indexing a list by a key.

  • Set comprehension: Use when you need unique results: {f(x) for x in data} automatically drops duplicates.

  • Syntax distinction: Curly braces with a : make a dict; without the colon it's a set. {} alone is an empty dict, not a set.

python

prices = {'a': 10, 'b': 20} doubled = {k: v*2 for k, v in prices.items()} # dict lengths = {len(w) for w in ['hi', 'yo', 'hey']} # set -> {2, 3}

Q52.
How does functools.lru_cache work, and how does it help with memoization?

Mid

functools.lru_cache is a decorator that memoizes a function: it stores results keyed by the call arguments so repeated calls with the same args return instantly from a cache instead of recomputing. LRU means it evicts the least-recently-used entries once the cache hits maxsize.

  • How it caches:

    • It builds a key from the positional and keyword arguments, so those args must be hashable.

    • maxsize bounds the cache; maxsize=None makes it unbounded.

  • Why it helps memoization: Turns expensive or repeated computations (e.g. recursive Fibonacci) into O(1) lookups after the first call.

  • Introspection and control: cache_info() reports hits/misses; cache_clear() resets it.

  • Caveats: Don't cache functions with side effects or those depending on external mutable state; the cache holds references and can grow memory.

python

from functools import lru_cache @lru_cache(maxsize=None) def fib(n): return n if n < 2 else fib(n-1) + fib(n-2)

Q53.
What does functools.partial do, and when would you use it?

Mid

functools.partial creates a new callable from an existing one with some arguments pre-filled ("frozen"), so you can call it later with fewer arguments. It's a clean way to specialize a general function.

  • What it does:

    • partial(func, *args, **kwargs) binds those args; calling the result appends any new args to them.

    • Keyword args set this way can still be overridden at call time.

  • When to use:

    • Adapting a function to a callback or key= API that expects fewer parameters.

    • Creating readable specialized versions without a full wrapper function or lambda.

  • Versus a lambda: partial is picklable and introspectable (exposes .func, .args), which a lambda is not.

python

from functools import partial int2 = partial(int, base=2) # binary parser int2('1010') # 10

Q54.
What is functools.reduce, and how does it differ from a simple loop?

Mid

functools.reduce repeatedly applies a two-argument function to an accumulator and each item of an iterable, collapsing it to a single value. A plain loop does the same thing, but reduce expresses the fold declaratively in one call.

  • Signature:

    • reduce(func, iterable[, initializer]): func(accumulator, item) returns the new accumulator.

    • The optional initializer is the starting accumulator and the safe value for an empty iterable.

  • How it differs from a loop:

    • Semantically identical, but more compact and signals intent: "fold this sequence to one value".

    • A loop is often clearer to readers and lets you add side effects or early exits; reduce is purely functional.

  • When to prefer built-ins instead: For sums/products/joins, sum(), math.prod(), or str.join() are faster and more readable. Guido even moved reduce out of builtins into functools.

python

from functools import reduce # 1+2+3+4 with an explicit start of 0 total = reduce(lambda acc, x: acc + x, [1, 2, 3, 4], 0) # 10

Q55.
What is the itertools module, and what kinds of problems does it help solve?

Mid

itertools is a standard-library module of fast, memory-efficient building blocks for working with iterators. It helps you compose lazy data pipelines and handle combinatorics without materializing large lists.

  • Infinite iterators: count(), cycle(), repeat(): generate endless streams you slice or stop later.

  • Terminating / combining iterators: chain() (flatten sequences), islice() (slice lazily), groupby(), accumulate(), takewhile()/dropwhile().

  • Combinatorics: product(), permutations(), combinations(): generate arrangements without nested loops.

  • Why it matters:

    • Everything is lazy: values are produced on demand, so you stream huge or infinite data with constant memory.

    • Implemented in C, so it is typically faster than hand-written equivalents.

python

from itertools import islice, count # first 5 even numbers from an infinite stream evens = islice((n for n in count() if n % 2 == 0), 5) list(evens) # [0, 2, 4, 6, 8]

Q56.
What is the walrus operator (:=), and in what situations is it useful?

Mid

The walrus operator := (assignment expression, added in Python 3.8) assigns a value and returns it in the same expression, so you can bind a name where only an expression is allowed.

  • Avoid repeating work: Compute once and reuse the result inside a condition or comprehension without a separate prior line.

  • Common use cases:

    • Loop-and-read patterns: while (line := f.readline()):.

    • Filtering in comprehensions: reuse an expensive call in both the filter and the output.

    • Guarding on a value you also need: if (n := len(data)) > 10:.

  • Cautions:

    • It is an expression, not a statement: you cannot use it as a top-level assignment (just use =).

    • Overusing it hurts readability; prefer it only when it removes a clear duplication.

python

# without walrus result = expensive(x) if result: use(result) # with walrus if result := expensive(x): use(result)

Q57.
How do context managers work, and what is the difference between the __enter__/__exit__ and @contextmanager approaches?

Mid

A context manager defines setup and teardown logic that runs around a with block, guaranteeing cleanup even on exceptions. You can implement it either as a class with __enter__/__exit__ or as a generator decorated with @contextmanager.

  • How with works:

    • On entry it calls __enter__() and binds its return to the as target.

    • On exit it calls __exit__(exc_type, exc, tb), always: normal flow or exception. Returning True from __exit__ suppresses the exception.

  • Class-based approach: Most flexible: holds state across methods, can implement reusable objects, gives full control over exception handling.

  • @contextmanager approach:

    • Write a generator: code before yield is setup, the yielded value is the as target, code after yield is teardown.

    • Concise for simple cases; wrap the yield in try/finally so cleanup runs on exceptions.

python

from contextlib import contextmanager @contextmanager def open_resource(name): r = acquire(name) # setup try: yield r # value bound by `as` finally: r.release() # teardown, even on error

Q58.
What is Structural Pattern Matching (match/case) and how does it differ from a standard if/elif/else block?

Mid

Structural pattern matching (match/case, Python 3.10+) matches a value against patterns that describe its structure, binding parts of it as it goes. Unlike if/elif, which tests boolean conditions, it destructures and matches shape.

  • What patterns can match:

    • Literals, sequences ([a, b, *rest]), mappings, and class patterns (Point(x=0, y=y)) that check type and capture attributes.

    • Capture names bind sub-values; _ is the wildcard.

  • Extra power vs if/elif:

    • Guards add conditions: case Point(x=x) if x > 0:.

    • Combines matching and unpacking in one step, far cleaner than nested isinstance + index checks.

  • Watch out:

    • A bare name is a capture, not a comparison: use dotted names (Color.RED) to match against constants.

    • For simple value tests, if/elif is still clearer; reach for match when structure matters.

python

match command.split(): case ["go", direction]: move(direction) case ["drop", *items]: drop(items) case _: unknown()

Q59.
What does it mean that Python's sort is stable, and why does that matter?

Mid

A stable sort preserves the original relative order of elements that compare equal. Python's sort() and sorted() are guaranteed stable (they use Timsort), which lets you sort by multiple keys in successive passes.

  • What stability means: If two records have equal keys, the one that appeared first in the input still appears first in the output.

  • Why it matters: multi-level sorting:

    • Sort by the least significant key first, then by the most significant; equal-key ties keep the earlier ordering.

    • Example: sort by name, then by age, and people of the same age stay alphabetized.

  • Guaranteed, not incidental: The language spec promises stability, so you can rely on it across implementations.

python

people = [("Bob", 30), ("Ann", 30), ("Cy", 25)] # sort by age; Bob and Ann (both 30) keep input order by_age = sorted(people, key=lambda p: p[1]) # -> [('Cy', 25), ('Bob', 30), ('Ann', 30)]

Q60.
What is serialization with the pickle module, and what are its security concerns?

Mid

Pickle is Python's built-in serialization (object-to-bytes) protocol that can persist almost any Python object and reconstruct it later. Its key danger: unpickling executes code, so loading untrusted data can run arbitrary commands.

  • What it does:

    • pickle.dumps() / pickle.loads() convert objects to/from a byte stream, handling nested structures and most custom classes.

    • It is Python-specific and version-sensitive, not a cross-language format.

  • The security concern:

    • Unpickling can invoke __reduce__ and call arbitrary callables, so a malicious payload can execute code on load.

    • Rule: never unpickle data from an untrusted or unauthenticated source.

  • Safer alternatives: Use json for interoperable, non-executable data, or sign/verify pickle blobs (e.g. HMAC) when you control both ends.

Q61.
What is the Global Interpreter Lock (GIL), and given its existence, how do you write concurrent code that actually runs in parallel across multiple CPU cores?

Mid

The GIL (Global Interpreter Lock) is a mutex in CPython that allows only one thread to execute Python bytecode at a time, so plain threads can't run CPU-bound Python in parallel. To get real multi-core parallelism you use multiple processes (or release the GIL in native code).

  • What the GIL is:

    • A single lock protecting interpreter state (notably reference counts), simplifying memory management and making single-threaded code fast.

    • It serializes bytecode execution, so threading doesn't speed up CPU-bound work.

  • How to actually run in parallel:

    • multiprocessing or concurrent.futures.ProcessPoolExecutor: each process has its own GIL and runs on its own core.

    • Native extensions (NumPy, Cython) release the GIL during heavy C computation, enabling parallelism within threads.

    • The experimental free-threaded build (PEP 703) removes the GIL entirely.

  • Note on I/O: The GIL is released during blocking I/O, so threads do help I/O-bound workloads even though they don't parallelize CPU work.

Q62.
Are Python threads 'real' native threads?

Mid

Yes: CPython threads are real native OS threads, scheduled by the operating system. The catch is the GIL, which prevents more than one of them from executing Python bytecode simultaneously.

  • They are genuine OS threads: threading.Thread maps to a real pthread/Windows thread, with OS-level scheduling and preemption.

  • But the GIL serializes bytecode:

    • Only the thread holding the GIL runs Python code; others wait, so CPU-bound code doesn't parallelize.

    • The GIL is handed off periodically and around I/O, giving concurrency (not parallelism) for CPU work.

  • Practical takeaway: Real threads are great for overlapping I/O; for parallel CPU you need processes or the free-threaded build.

Q63.
What is the GIL, and why was it originally implemented?

Mid

The GIL (Global Interpreter Lock) is a mutex in CPython that allows only one thread to execute Python bytecode at a time, even on multi-core machines. It was added to make memory management (reference counting) safe and the interpreter simple to implement.

  • What it is:

    • A single lock protecting interpreter internals; a thread must hold it to run bytecode.

    • It is released periodically and during blocking I/O so other threads can run.

  • Why it exists:

    • CPython manages memory via reference counting; concurrent updates to refcounts would race without locking every object.

    • One global lock is far cheaper and simpler than fine-grained per-object locks, and it made C extension integration easy.

  • Consequence:

    • Threads can't run Python bytecode in true parallel, so CPU-bound code sees no speedup from threading; I/O-bound code still benefits.

    • Note: it is a CPython implementation detail (Jython, PyPy STM, and the experimental free-threaded build differ).

Q64.
What is the difference between a thread and a coroutine in the context of the Python runtime?

Mid

A thread is an OS-level execution context the kernel schedules preemptively; a coroutine is a user-space function that voluntarily suspends and resumes at await points, all within a single thread driven by the event loop.

  • Thread:

    • Managed by the OS; switching can happen at any time (preemptive) and has higher memory/context-switch cost.

    • In CPython the GIL still serializes bytecode, so threads give concurrency for I/O but not CPU parallelism.

  • Coroutine:

    • Created with async def; suspends only where you write await (cooperative), so switch points are explicit.

    • Extremely lightweight: thousands can live in one thread with one stack frame each, scheduled by the event loop.

  • Key contrast: Threads need locks for shared state because switches are unpredictable; coroutines have fewer race windows since control only yields at await.

Q65.
Why is multiprocessing often preferred over threading for CPU-bound tasks in CPython?

Mid

Because the GIL prevents threads from running Python bytecode in parallel, threading gives no real speedup for CPU-bound work. Multiprocessing sidesteps the GIL by using separate processes, each with its own interpreter and GIL, achieving true parallelism across cores.

  • Threading limitation: CPU-bound threads contend for the single GIL, so they effectively run one at a time on one core.

  • Multiprocessing advantage:

    • Each process has its own GIL and memory space, so N processes use N cores for genuine parallel computation.

    • Use multiprocessing or concurrent.futures.ProcessPoolExecutor.

  • Trade-offs:

    • Higher memory cost and slower startup; data passed between processes must be pickled (IPC overhead).

    • Best when work is heavy compute per task so parallelism outweighs the IPC cost.

Q66.
Explain the difference between preemptive multitasking (threading) and cooperative multitasking (asyncio). When is asyncio preferred over standard threading?

Mid

In preemptive multitasking (threading), the OS can interrupt and switch threads at any moment; in cooperative multitasking (asyncio), a coroutine keeps control until it explicitly yields at an await. Prefer asyncio when you have very many concurrent I/O operations and async-capable libraries.

  • Preemptive (threading):

    • The scheduler forcibly switches threads, so you can't predict switch points and need locks for shared state.

    • Works with blocking/synchronous libraries without rewriting them.

  • Cooperative (asyncio):

    • Control changes only at await; a task that never yields starves all others.

    • Tasks are cheap, so you can run tens of thousands concurrently in one thread.

  • When asyncio wins:

    • Massive I/O concurrency (many sockets, API calls, web connections) where thread-per-connection overhead is too high.

    • You have async drivers; if only blocking libraries exist, threading is simpler.

Q67.
How does asyncio achieve concurrency without using multiple threads or processes?

Mid

asyncio achieves concurrency by interleaving many tasks on a single thread: while one task waits on I/O, the event loop runs others. It's concurrency through cooperative suspension, not parallelism.

  • Suspend on waiting: At each await on I/O, a coroutine yields control back to the loop instead of blocking.

  • Loop overlaps the waits:

    • The loop starts/resumes other tasks during that idle time, so many I/O operations are in flight simultaneously.

    • OS-level readiness notifications tell the loop exactly when to resume each task.

  • Single-threaded:

    • No threads/processes means no GIL contention and no lock overhead for the task switching itself.

    • Caveat: it only helps I/O-bound work; CPU-bound code still monopolizes the one thread.

Q68.
When should you choose asyncio over threading for a high-concurrency task?

Mid

Choose asyncio over threading when you need very high concurrency of I/O-bound operations and have async-compatible libraries: it scales to far more concurrent tasks at lower memory and switching cost than threads.

  • Favor asyncio when:

    • Workload is I/O-bound with thousands of concurrent connections (web servers, scrapers, proxies).

    • Async drivers exist for your DB/HTTP/socket work so you can actually await them.

    • You want lower overhead: coroutines are far cheaper than OS threads.

  • Favor threading when:

    • You must use blocking/synchronous libraries with no async version.

    • Concurrency is modest and you want simpler code without rewriting to async/await.

  • Neither for CPU-bound: Both are throttled by the GIL for compute; use multiprocessing instead.

Q69.
What is the difference between asyncio.gather() and asyncio.wait()?

Mid

Both run awaitables concurrently, but gather() returns ordered results and is the modern high-level API, while wait() returns two sets of tasks (done/pending) and gives you finer control over when to stop.

  • asyncio.gather():

    • Returns results in the same order as the inputs (not completion order).

    • By default the first exception propagates to the caller; pass return_exceptions=True to collect them as values instead.

  • asyncio.wait():

    • Returns (done, pending) sets of Tasks; you read results off each Task yourself.

    • Supports return_when (e.g. FIRST_COMPLETED) and timeout without cancelling the rest.

    • It does not raise on a failing task; the exception stays inside the Task.

  • Rule of thumb: use gather() when you want all results together; use wait() when you need partial completion or timeouts.

Q70.
What is the difference between a shallow copy and a deep copy, and how does the copy module handle nested objects in each case?

Mid

A shallow copy duplicates the outer container but shares references to the nested objects, while a deep copy recursively duplicates everything so the two structures are fully independent.

  • Shallow copy (copy.copy()):

    • Creates a new top-level object but copies references to the inner elements.

    • Mutating a nested object is visible through both copies; rebinding the outer container is not.

  • Deep copy (copy.deepcopy()):

    • Recursively copies every nested object, producing a fully independent clone.

    • Tracks already-copied objects in a memo dict, so shared references and cycles are handled correctly.

  • Trade-offs:

    • Deep copy is slower and uses more memory; only reach for it when nested mutation must not leak.

    • Customize behavior with __copy__ and __deepcopy__.

python

import copy orig = [[1, 2], [3, 4]] shallow = copy.copy(orig) deep = copy.deepcopy(orig) shallow[0].append(99) # affects orig too -> [[1,2,99],[3,4]] deep[0].append(99) # orig unchanged

Q71.
How does Python handle circular imports, and what are the common strategies to resolve them?

Mid

A circular import happens when module A imports B while B imports A; Python doesn't crash automatically because the half-initialized module is already in sys.modules, but you get an ImportError or AttributeError if you access a name that hasn't been defined yet during that partial execution.

  • Why it breaks: When A is mid-execution and triggers B, and B does from A import x, x may not be defined yet because A hasn't finished running.

  • Common fixes:

    • Refactor: move shared code into a third module both can import (best long-term fix).

    • Import the module, not the name: import b then use b.x at call time, deferring the lookup.

    • Move the import inside the function/method so it runs after both modules are fully loaded.

    • For type hints only, use if TYPE_CHECKING: with string annotations to avoid runtime import.

Q72.
Why are strings immutable in Python, and what implications does that have for performance and memory?

Mid

Strings are immutable so they can be safely hashed, shared, and interned; once created their bytes never change, so any operation that 'modifies' a string actually builds a new one.

  • Why immutable:

    • Hashability: immutable strings have a stable hash, so they can be dict keys and set members.

    • Safe sharing: the interpreter can reuse and intern identical strings without aliasing bugs.

    • Thread safety: no risk of one thread mutating a string another is reading.

  • Memory implications: Interning of small/identifier-like strings lets CPython store one shared copy.

  • Performance implications:

    • Repeated concatenation in a loop creates many throwaway objects (O(n²) cost).

    • Use str.join() on a list, or build with io.StringIO, instead of += in a loop.

python

# Slow: builds a new string each iteration s = "" for part in parts: s += part # Fast: one allocation s = "".join(parts)

Q73.
What happens at the bytecode level when a Python script is executed?

Senior

The interpreter first compiles your source into a code object containing bytecode, then a stack-based evaluation loop executes those opcodes one by one against an in-memory value stack and frames.

  • Parse and compile: Source is tokenized, parsed into an AST, then compiled into a code object (holds bytecode, constants, names).

  • Bytecode instructions: Each opcode (e.g. LOAD_FAST, BINARY_OP, CALL) manipulates a value stack.

  • Execution via frames: Each function call creates a frame object holding its locals, the stack, and instruction pointer.

  • Inspect it yourself: Use the dis module to see the actual bytecode for any function.

python

import dis def add(a, b): return a + b dis.dis(add) # LOAD_FAST a; LOAD_FAST b; BINARY_OP +; RETURN_VALUE

Q74.
How does the 'Specializing Adaptive Interpreter' introduced in 3.11/3.12 improve performance?

Senior

The Specializing Adaptive Interpreter (PEP 659) watches which operations run in a hot loop and rewrites generic bytecode into faster, type-specialized variants at runtime, avoiding repeated type checks and lookups.

  • Adaptive specialization: A generic opcode like BINARY_OP is replaced by a specialized form (e.g. integer-add) once the interpreter observes the operands are always the same type.

  • Quickening: Hot bytecode is swapped in-place for optimized variants after a warm-up threshold of executions.

  • Inline caching: Results of expensive lookups (attributes, globals, method calls) are cached on the instruction so repeats skip the full search.

  • Graceful deoptimization: If an assumption breaks (a different type appears), it falls back to the generic opcode, so behavior is always correct.

  • Net effect: meaningful speedups (the 3.11 "faster CPython" project) with no code changes required.

Q75.
What is string and integer interning in Python, and why does a=256;b=256;a is b return True while a=257;b=257;a is b might return False?

Senior

Interning means Python reuses a single cached object for certain immutable values, so identity (is) comparisons return True. CPython pre-creates small integers from -5 to 256, so two variables holding 256 point to the same object; 257 is created fresh each time, so they may be distinct objects.

  • Integer interning (small int cache):

    • CPython caches integers in the range [-5, 256] at startup; any reference to those values reuses the cached object.

    • Outside that range each literal creates a new object, so a is b can be False even though a == b is True.

  • String interning:

    • String literals that look like identifiers (letters, digits, underscores) are often interned automatically at compile time.

    • You can force it with sys.intern() to speed up repeated equality checks (dict keys, parsers).

  • Key takeaway:

    • Interning is a CPython implementation detail for memory and speed, not a language guarantee.

    • Use == for value equality and reserve is for identity (e.g. is None).

Q76.
Beyond mutability, what are the architectural differences between a list and a tuple, and why is a tuple slightly more memory-efficient?

Senior

Beyond a tuple being immutable and a list mutable, the difference is structural: a list is a variable-length container that over-allocates room to grow, while a tuple is fixed-size and stored more compactly, making it lighter and enabling extra optimizations.

  • Lists over-allocate: A list keeps spare capacity so append is amortized O(1); it stores a pointer array plus length and allocation size.

  • Tuples are fixed-size: Since size never changes, there's no growth buffer and less bookkeeping, so a tuple uses fewer bytes than a list of the same elements.

  • Optimizations tuples enable:

    • CPython can cache (free-list) small tuples for reuse and embed constant tuples directly in compiled bytecode.

    • Immutability lets a tuple be hashable, so it can be a dict key or set member.

  • Intent signaling: Tuples convey a fixed, heterogeneous record; lists convey a homogeneous, growable sequence.

Q77.
When would you use __slots__ in a class definition, and what are the trade-offs regarding memory and flexibility?

Senior

Use __slots__ to declare a fixed set of attributes, which removes each instance's per-object __dict__ and saves significant memory when you create many instances. The trade-off is lost flexibility.

  • How it works: __slots__ = ('x', 'y') stores attributes in a compact fixed layout instead of a dynamic dict, cutting memory and slightly speeding attribute access.

  • When to use it: Classes instantiated in large numbers (millions of small objects) where memory is the bottleneck.

  • Trade-offs / limits:

    • You can't add new attributes not listed in __slots__ (raises AttributeError).

    • No __dict__ means some dynamic patterns and tools that expect it break unless you add '__dict__' to slots.

    • Inheritance is subtle: a subclass without its own __slots__ regains a __dict__, negating savings.

python

class Point: __slots__ = ('x', 'y') def __init__(self, x, y): self.x = x self.y = y p = Point(1, 2) p.z = 3 # AttributeError: no slot for 'z'

Q78.
Explain the difference between __init__ and __new__. When would you specifically need to override __new__?

Senior

__new__ creates and returns the instance; __init__ initializes the already-created instance. __new__ is a static method that runs first and controls object creation, while __init__ just sets attributes on it.

  • __new__:

    • Receives the class cls, allocates the object (usually via super().__new__(cls)), and returns it.

    • If it doesn't return an instance of cls, __init__ is not called.

  • __init__: Receives the new self, returns None, and only configures state.

  • When you actually need __new__:

    • Subclassing immutable types like int, str, or tuple, whose value must be set at creation.

    • Implementing singletons or instance caching/interning.

    • Returning an instance of a different class (factory behavior).

python

class Positive(int): def __new__(cls, value): if value < 0: raise ValueError("must be >= 0") return super().__new__(cls, value) # value fixed at creation

Q79.
Explain the difference between __getattr__ and __getattribute__.

Senior
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem.

Q80.
How does Python handle multiple inheritance? Explain the C3 Linearization algorithm and how super() determines which class to call next.

Senior
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem.

Q81.
Explain how the Descriptor protocol works. How do @property, @classmethod, and @staticmethod use descriptors under the hood?

Senior
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem.

Q82.
What is a metaclass, and how does it differ from a standard class? When would you use a metaclass instead of a class decorator?

Senior
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem.

Q83.
When would you use __init_subclass__ instead of a metaclass?

Senior
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem.

Q84.
Explain the difference between Nominal typing and Structural typing (Protocols) in Python.

Senior
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem.

Q85.
What is the difference between typing.Any and object in a type hint?

Senior
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem.

Q86.
Explain the new Type Parameter Syntax introduced in PEP 695 (Python 3.12).

Senior
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem.

Q87.
How does Python's memory management work? Explain the difference between reference counting and the cyclic garbage collector, and how Python handles two objects that reference each other.

Senior
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem.

Q88.
What is the difference between the stack and the heap in the context of Python's memory management, and who manages the private heap?

Senior
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem.

Q89.
What are 'Immortal Objects' (PEP 683) and how do they help with multi-processing performance?

Senior
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem.

Q90.
Why does Python not immediately release memory back to the operating system when an object is deleted?

Senior
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem.

Q91.
How does generational garbage collection work in Python, and why does the collector move objects between 'Generation 0', '1', and '2'?

Senior
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem.

Q92.
What is the difference between a Coroutine and a standard Generator?

Senior
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem.

Q93.
What do coroutines and generators have in common, and how did generators evolve into async/await?

Senior
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem.

Q94.
What do the send(), throw(), and close() methods do on a generator object?

Senior
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem.

Q95.
What are positional-only and keyword-only parameters, and how do the / and * markers in a function signature work?

Senior
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem.

Q96.
How do you implement an asynchronous context manager (async with), and how do __aenter__ and __aexit__ differ from their synchronous counterparts?

Senior
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem.

Q97.
What is the experimental 'free-threaded' build in Python 3.13 (PEP 703), and what are the trade-offs of removing the GIL for existing C extensions?

Senior
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem.

Q98.
Compare and contrast threading, multiprocessing, and asyncio. When is each appropriate?

Senior
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem.

Q99.
Explain what the GIL is and how it impacts multithreading in Python. If Python has a GIL, when would you still choose to use the threading module over multiprocessing?

Senior
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem.

Q100.
What is the difference between a per-interpreter GIL introduced in 3.12 and the traditional GIL?

Senior
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem.

Q101.
How does the asyncio event loop work, and what happens to the loop when you execute a blocking I/O call inside an async function?

Senior
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem.

Q102.
What makes an object awaitable in Python? Explain the relationship between coroutines, Tasks, and Futures.

Senior
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem.

Q103.
How does the await keyword actually work under the hood?

Senior
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem.

Q104.
How does the Python import system work, and what happens when you import a module for the second time?

Senior
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem.