A focused study guide with three sections — Google, Microsoft, and Infosys — 10 questions each. The questions are grouped to reflect what each company typically emphasises: Google leans on internals, complexity, and algorithms; Microsoft on OOP, design, and language mechanics; Infosys on fundamentals and clear conceptual understanding.
Tip: In real interviews, say the trade-off out loud. Stating complexity, edge cases, and "why this approach" matters as much as the code itself.
The Global Interpreter Lock is a mutex in CPython that allows only one thread to execute Python bytecode at a time. Consequences:
- CPU-bound work sees no speed-up from threads — they can't run bytecode in parallel. Use
multiprocessing(each process has its own interpreter and GIL) or a C extension like NumPy that releases the GIL. - I/O-bound work does benefit from threads, because the GIL is released during blocking I/O (network, disk).
Mention that the free-threaded build (PEP 703, experimental from Python 3.13) makes the GIL optional — a strong signal you follow the language's evolution.
| Operation | list | dict | set |
|---|---|---|---|
| Index / key access | O(1) | O(1) avg | — |
append |
O(1) amortised | — | — |
| Insert/delete at position | O(n) | O(1) avg | O(1) avg |
Membership (in) |
O(n) | O(1) avg | O(1) avg |
Be ready to explain amortised O(1) for append: the underlying dynamic array doubles capacity occasionally, so the cost averages out to constant time. dict/set worst case is O(n) under pathological hash collisions.
Use a min-heap of size k (heapq):
import heapq
def k_largest(stream, k):
heap = []
for x in stream:
heapq.heappush(heap, x)
if len(heap) > k:
heapq.heappop(heap) # drop the smallest
return sorted(heap, reverse=True)O(n log k) time, O(k) space — far better than sorting everything (O(n log n)) when k is small. In practice you'd just call heapq.nlargest(k, stream).
A generator produces values lazily, pausing at each yield and resuming on the next request, keeping its state in between. yield suspends and yields a value; return ends the generator (raising StopIteration). Generators shine for streaming large files, building pipelines, and infinite sequences because they hold only one item in memory at a time.
def read_lines(path):
with open(path) as f:
for line in f: # one line in memory at a time
yield line.strip()== compares values; is compares identity (same object in memory). CPython caches small integers (−5 to 256) and interns some strings, so:
a = 256; b = 256
a is b # True (cached)
a = 257; b = 257
a is b # False (separate objects) — but a == b is TrueRule: use == for value comparison; reserve is for singletons like None, True, False.
Production answer: functools.lru_cache. But interviewers want the mechanics — OrderedDict gives O(1) get/put:
from collections import OrderedDict
class LRUCache:
def __init__(self, capacity):
self.cache = OrderedDict()
self.capacity = capacity
def get(self, key):
if key not in self.cache:
return -1
self.cache.move_to_end(key) # mark as recently used
return self.cache[key]
def put(self, key, value):
if key in self.cache:
self.cache.move_to_end(key)
self.cache[key] = value
if len(self.cache) > self.capacity:
self.cache.popitem(last=False) # evict least recently usedA decorator wraps a function to extend its behaviour without changing its body — built on closures. Use functools.wraps to preserve the wrapped function's name/docstring.
import time, functools
def timed(fn):
@functools.wraps(fn)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = fn(*args, **kwargs)
print(f"{fn.__name__} took {time.perf_counter() - start:.4f}s")
return result
return wrapperTwo mechanisms working together:
- Reference counting (primary): every object tracks how many references point to it; it's freed when the count hits zero.
- Cyclic garbage collector (
gcmodule): reference counting can't free reference cycles (A → B → A), so a generational GC periodically detects and collects them.
Mention that __del__ finalisers and cycles interact subtly, and that memory for small objects is managed via CPython's pymalloc allocator.
Immutable: int, float, str, tuple, frozenset, bytes. Mutable: list, dict, set. The classic bug: default arguments are evaluated once, at function definition, so a mutable default is shared across calls.
def bad(item, bucket=[]): # bucket created once!
bucket.append(item)
return bucket
bad(1); bad(2) # -> [1, 2], not [2]
def good(item, bucket=None): # the fix
bucket = [] if bucket is None else bucket
bucket.append(item)
return bucketFloyd's tortoise-and-hare: advance one pointer by 1 and another by 2; if they ever meet, there's a cycle. O(n) time, O(1) space.
def has_cycle(head):
slow = fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
if slow is fast:
return True
return False- Encapsulation — bundling data with methods; Python signals "private" with a leading underscore and uses name-mangling (
__x→_Class__x), though nothing is truly private ("we're all adults"). - Abstraction — hiding implementation behind a clean interface (e.g. abstract base classes).
- Inheritance — a subclass reuses/extends a base class.
- Polymorphism — the same call works on different types (e.g.
len()on a list or a string; overriding methods).
- Instance method — takes
self; operates on an instance. - Classmethod — takes
cls; great for alternative constructors / factories. - Staticmethod — takes neither; a utility logically grouped under the class.
class Date:
def __init__(self, y, m, d):
self.y, self.m, self.d = y, m, d
@classmethod
def from_string(cls, s): # factory
y, m, d = map(int, s.split("-"))
return cls(y, m, d)
@staticmethod
def is_leap(year): # utility
return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)Python resolves method lookup using C3 linearisation, exposed via Cls.__mro__. super() follows this order, enabling cooperative multiple inheritance and resolving the "diamond" (D inherits from B and C, both from A) deterministically — A's method runs once, in a well-defined order.
class A: ...
class B(A): ...
class C(A): ...
class D(B, C): ...
print(D.__mro__) # D -> B -> C -> A -> objectBy default each instance stores attributes in a per-instance __dict__. Declaring __slots__ replaces that dict with a fixed, array-like layout — less memory and faster attribute access — at the cost of dynamic attribute assignment. Worthwhile when you create millions of small objects.
class Point:
__slots__ = ("x", "y")
def __init__(self, x, y):
self.x, self.y = x, yA context manager guarantees setup/teardown via the __enter__/__exit__ protocol — ideal for files, locks, and connections. Two ways to write one:
# Class-based
class Resource:
def __enter__(self):
print("open"); return self
def __exit__(self, exc_type, exc, tb):
print("close") # runs even if an exception is raised
# Generator-based
from contextlib import contextmanager
@contextmanager
def resource():
print("open")
try:
yield "handle"
finally:
print("close")A shallow copy duplicates the outer container but shares the nested objects; a deep copy recursively duplicates everything. The gotcha:
import copy
original = [[1, 2], [3, 4]]
shallow = copy.copy(original)
shallow[0].append(99) # also mutates original[0]!
deep = copy.deepcopy(original) # fully independent*args collects extra positional arguments into a tuple; **kwargs collects extra keyword arguments into a dict. They also work for unpacking when calling. Argument order is: positional, *args, keyword-only, **kwargs.
def f(a, *args, **kwargs):
print(a, args, kwargs)
f(1, 2, 3, x=10) # 1 (2, 3) {'x': 10}
nums = [1, 2, 3]
f(*nums, x=10) # unpacking@dataclassauto-generates__init__,__repr__,__eq__, etc.; mutable by default, can befrozen=True; supports defaults and type hints.namedtupleis an immutable, tuple-based, lightweight record.
Use a dataclass when you need mutability, methods, or many fields; a namedtuple when you want a tiny immutable record that behaves like a tuple. (Mention pydantic/attrs for validation-heavy cases.)
from dataclasses import dataclass
@dataclass(frozen=True)
class Point:
x: int
y: int = 0tryruns the risky code;excepthandles specific errors;elseruns only if no exception occurred;finallyalways runs (cleanup).- Custom exceptions subclass
Exception. Useraise ... from ...to preserve the original cause (exception chaining).
class ValidationError(Exception):
pass
try:
risky()
except ValueError as e:
raise ValidationError("bad input") from e
else:
print("succeeded")
finally:
cleanup()Duck typing — "if it walks like a duck and quacks like a duck, it's a duck": Python cares about an object's behaviour, not its declared type. Abstract Base Classes (abc) let you enforce an interface at runtime, and Protocols (PEP 544, typing.Protocol) give static structural typing — type checkers verify an object has the right methods without explicit inheritance.
| list | tuple | |
|---|---|---|
| Mutable | Yes | No |
| Syntax | [1, 2] |
(1, 2) |
| Hashable (usable as dict key) | No | Yes (if elements are) |
| Performance | Slightly slower | Slightly faster, less memory |
Use a list when contents change; a tuple for fixed records or when you need a hashable key.
- Immutable (can't change after creation):
int,float,bool,str,tuple,frozenset,bytes. - Mutable (can change in place):
list,dict,set,bytearray.
Reassigning a variable creates a new object; mutating changes the existing one. This matters when objects are passed to functions or shared.
Python is high-level, dynamically typed, interpreted (compiled to bytecode, then run by the CPython virtual machine), and multi-paradigm (procedural, OOP, functional). It's prized for readability, a huge standard library + ecosystem, automatic memory management, and portability across platforms.
PEP 8 is Python's official style guide: 4-space indentation, snake_case for functions/variables, CapWords for classes, ~79-char line length, and import ordering. Tools that enforce it: black (auto-formatter), flake8/pylint (linters). Following it makes code consistent and reviewable.
Python resolves names using LEGB: Local → Enclosing → Global → Built-in. global x lets a function rebind a module-level variable; nonlocal x lets a nested function rebind a variable in the enclosing (non-global) scope.
def counter():
count = 0
def increment():
nonlocal count
count += 1
return count
return incrementConcise, readable ways to build collections in one expression:
squares = [n * n for n in range(5)] # [0, 1, 4, 9, 16]
evens = [n for n in range(10) if n % 2 == 0] # with a condition
squares_map = {n: n * n for n in range(5)} # dict comprehensionThey're usually faster and clearer than an equivalent for loop — but keep them readable (avoid deeply nested ones).
append(x)addsxas a single element (ifxis a list, it's nested).extend(iterable)iterates over the argument and adds each element.
a = [1, 2]
a.append([3, 4]) # [1, 2, [3, 4]]
b = [1, 2]
b.extend([3, 4]) # [1, 2, 3, 4]Wrap risky code in try/except, catching specific exceptions; add finally for cleanup that must always run. Catch precise exception types rather than a bare except:.
try:
value = int(user_input)
except ValueError:
print("Please enter a valid number")
except ZeroDivisionError:
print("Cannot divide by zero")
finally:
print("Done")- A module is a single
.pyfile you canimport. - A package is a directory of modules (traditionally containing an
__init__.py).
if __name__ == "__main__": ensures the block runs only when the file is executed directly, not when it's imported — useful for putting test/CLI code in a module without it firing on import.
A lambda is a small anonymous function written inline, limited to a single expression.
square = lambda x: x * x
nums = [1, 2, 3, 4]
list(map(lambda x: x * 2, nums)) # [2, 4, 6, 8]
list(filter(lambda x: x % 2 == 0, nums)) # [2, 4]
from functools import reduce
reduce(lambda a, b: a + b, nums) # 10 (sum)Use lambdas for short throwaway functions (e.g. a key= argument in sorted); prefer a named def when logic gets complex.
- Practise saying answers out loud — clarity of explanation is scored heavily.
- For coding questions, always state time and space complexity and discuss edge cases.
- Tie answers to real experience where you can ("I used a generator to stream a 10 GB log file…").
- Know why, not just what — interviewers probe follow-ups.
Good luck!