Python Interview — 50 Questions Short Notes
Condensed revision sheet for exam preparation
1. Python GIL — impact on threading vs multiprocessing vs asyncio
GIL (Global Interpreter Lock) allows only ONE thread to execute Python bytecode at a time, even on multi-core CPUs.
Multithreading: Threads share memory; GIL limits CPU parallelism. Best for I/O-bound (API calls, DB, files).
Multiprocessing: Each process has its own GIL. True parallelism. Best for CPU-bound (ML, image processing).
Asyncio: Single-threaded, cooperative multitasking. Best for massive concurrent I/O (web servers, chat).
KEY: Use Threading for I/O | Multiprocessing for CPU | Asyncio for high-concurrency I/O
2. is vs ==, Mutable vs Immutable
== compares VALUES. is compares IDENTITY (same memory address). Use is only for None checks (if x is None).
Mutable: list, dict, set, bytearray — can be changed after creation.
Immutable: int, float, str, tuple, frozenset — cannot be modified; new object created on change.
Key Trap: def f(items=[]): [Link](1) → default mutable arg shared across calls. Fix: use None default.
KEY: == for value | is for identity/None | Mutable objects passed by reference → side effects!
3. __init__ vs __new__, __str__ vs __repr__, __call__, __slots__
__new__ CREATES the object (returns it). __init__ INITIALIZES it (sets attributes). Python calls __new__ first, then __init__.
__str__: Human-readable: used by print(). __repr__: Developer/debug: used in interactive shell.
__call__: Makes object callable like a function: obj() triggers __call__. Used in ML pipelines, decorators.
__slots__: Replaces __dict__ with fixed attributes → saves memory. Use when creating millions of objects.
KEY: __new__=create | __init__=initialize | __str__=readable | __repr__=debug | __call__=callable | __slots__=memory
4. Generators, Iterators, Iterables + Custom Iterator
Iterable: can be looped (list, str). Iterator: has __iter__() + __next__(), remembers position. Generator: iterator using yield — lazy,
memory efficient.
for loop internally: Calls iter(obj) → returns iterator → repeatedly calls next() → stops at StopIteration.
Generator: def gen(): yield 1; yield 2 → values produced one at a time. Use for large datasets/streams.
Custom Iterator: class Counter: __iter__=return self; __next__=return value or raise StopIteration
KEY: Generator is always an iterator. Iterator is NOT always a generator. yield = lazy evaluation = low memory.
5. Decorators, Context Managers, @contextmanager
Decorator: function that wraps another function to add behavior WITHOUT modifying original code. @logger → greet = logger(greet).
@contextmanager: Use yield instead of __enter__/__exit__. Code before yield = enter. After yield = exit.
Context Manager: Implements __enter__ and __exit__. with statement guarantees cleanup even on exception.
Production use: Decorators: logging, auth, caching, timing. Context managers: DB connections, file handles, locks.
KEY: Decorator = modify behavior | Context Manager = manage resources | @contextmanager = simpler CM via generator
6. List vs Tuple vs Set vs Dict — Time Complexity
Set and Dict use hashing → O(1) average for search/insert/delete. List/Tuple require O(n) scan for membership.
List: Ordered, mutable, duplicates allowed. O(1) access by index. O(n) search. Use: ordered collections.
Tuple: Ordered, IMMUTABLE, faster than list. Use: fixed data, function returns, dict keys, coordinates.
Set: Unordered, unique, O(1) lookup. Use: membership checks, duplicate removal, fast intersection/union.
Dict: Key-value, O(1) lookup. Use: caching, JSON data, lookups by key, grouping.
KEY: 1M in list = O(n) | 1M in set = O(1). Need fastest membership? → Set or Dict
7. MRO — Method Resolution Order + super()
MRO defines search order for methods in inheritance. Python uses C3 Linearization algorithm. Check with [Link]().
Diamond Problem: class D(B, C) where B,C both inherit A → MRO: D, B, C, A, object. A visited only once.
super(): Calls NEXT class in MRO, NOT necessarily the direct parent. This enables cooperative multiple inheritance.
Key point: [Link]() → [Link]() (super) → [Link]() (super) → [Link](). Not D → B → A as many expect.
KEY: super() follows MRO, not just parent class. Use super().__init__() instead of Parent.__init__(self).
8. Shallow Copy vs Deep Copy
Shallow copy ([Link]): creates new object but nested objects are SHARED. Deep copy ([Link]): fully independent
clone.
Shallow: a = [[1,2]]; b = copy(a); b[0].append(3) → a also changes! Nested objects shared.
Deep: [Link](a) → completely independent. Changes to b don't affect a.
Slicing: list[:] creates a shallow copy. So does list(original) and [Link]().
KEY: Shallow = new container, shared contents | Deep = everything new | Only use deepcopy when truly needed (slower)
9. List/Dict/Set Comprehensions — Readability & Performance
Comprehensions are concise and faster than loops for simple transformations. emails = [u['email'] for u in users]
Dict comp: {k: v for k, v in items} | Set comp: {x for x in data}
When to AVOID: Multiple nested loops, complex conditions, business logic → use normal loops for readability.
Performance: Large datasets → comprehension loads ALL into memory. Use generator expression (x for x in data) instead.
KEY: Simple = comprehension. Complex logic = loop. Large data = generator expression (parentheses, not brackets).
10. Optimize 10M Row List Memory
Don't load 10M rows into memory at once. Use generators, chunking, __slots__, NumPy arrays.
Generator: yield rows one at a time instead of building a list. (x for x in data) vs [x for x in data]
Pandas chunks: pd.read_csv('[Link]', chunksize=100000) → process chunk by chunk
__slots__: For millions of objects, __slots__ removes __dict__ → less memory per object
NumPy: [Link]([...]) much more memory-efficient than Python list of integers
KEY: First ask: do I need ALL 10M rows in memory? Usually NO. Push filtering/aggregation to database.
11. Descriptors, Properties, @property
Descriptor: object with __get__, __set__, __delete__ that controls attribute access. @property is built ON TOP of descriptors.
@property: Turns a method into an attribute: [Link] instead of emp.get_salary(). Looks like attr, works like method.
Setter/Deleter: @[Link] for validation. @[Link] for cleanup.
Key point: @property IS a descriptor. Used by Django ORM, SQLAlchemy, Pydantic internally.
KEY: @property = most common. Descriptor = lower-level, used by frameworks. Every property is a descriptor.
12. Metaclasses vs Class Decorators
Metaclass: class that creates classes. Python's default metaclass is type. Metaclass runs BEFORE class is created.
Class Decorator: Modifies class AFTER it is created. Simpler. Use for: adding methods, attributes, registration.
Metaclass: Controls class creation BEFORE class exists. Use for: validation, framework APIs (Django ORM).
Rule: Prefer class decorator. Use metaclass only when you need to intercept class creation itself.
KEY: Object ← created by Class ← created by Metaclass. Class decorator = after. Metaclass = before/during.
13. Dataclasses vs Pydantic vs NamedTuple
All reduce boilerplate for data classes. Key differences: validation, mutability, performance.
NamedTuple: Immutable, fastest, lowest memory. No validation. Use: fixed lightweight records, coordinates.
Dataclass: @dataclass auto-generates __init__, __repr__, __eq__. No runtime type enforcement. Use: internal models, DTOs.
Pydantic: Runtime validation, type coercion, serialization. Slowest. Use: API request/response, external data, FastAPI.
KEY: NamedTuple=fast+immutable | Dataclass=less boilerplate | Pydantic=validation+APIs. Why not always Pydantic?
Performance cost.
14. @staticmethod vs @classmethod vs Instance Methods
Instance method: takes self, accesses instance data. Classmethod: takes cls, works with class. Staticmethod: takes nothing, utility
function.
classmethod use: Alternative constructors: Employee.from_string('Ayesha,24'). Returns cls(...).
staticmethod use: Employee.is_adult(24) → doesn't need self or cls. Pure utility related to the class.
Memory trick: Instance: [Link]() | Class: [Link]() | Static: [Link]() but no self/cls
KEY: Instance→object behavior | Class→factory methods/class-wide logic | Static→utility function grouped in class
15. ABC vs Protocol — Duck Typing vs Static Typing
Duck Typing: Python checks behavior, not type. ABC: explicit inheritance contract enforced at runtime. Protocol: structural typing —
no inheritance needed.
ABC: class Dog(Animal): must inherit. TypeError if @abstractmethod not implemented. Use: strict contracts, plugin systems.
Protocol: class Dog: just needs matching methods. Type checker verifies. Use: loose coupling, modern Python.
Static Typing: Type hints + mypy verify types BEFORE execution. Python itself still dynamically typed.
KEY: Duck Typing=runtime | ABC=inheritance+runtime enforcement | Protocol=structure-only, no inheritance needed
16. __enter__ and __exit__ — Custom DB Context Manager
__enter__ called when entering with block (setup). __exit__ called when leaving — even if exception occurs (cleanup).
Parameters: __exit__(self, exc_type, exc_val, exc_tb). If exception: exc_type has error class.
Pattern: with DatabaseConnection() as conn: → opens in __enter__, closes in __exit__ automatically
vs try-finally: Context manager encapsulates resource logic, making code cleaner and reusable across the codebase.
KEY: Use context managers for: files, DB connections, locks, transactions. Guarantees cleanup on exception.
17. Monkey Patching — Risks and Alternatives
Monkey patching: modifying class/module/object behavior at runtime without changing source. [Link] = new_greet
Risks: Hidden behavior (confusing), hard debugging (source vs runtime mismatch), global side effects, library update breaks.
Safe alternatives: Dependency Injection (pass object in), Inheritance (subclass), Decorators, [Link] for tests.
Test use: with patch('[Link]'): → temporary, auto-restored. Preferred approach for mocking.
KEY: Monkey patching useful for testing/mocking but dangerous in production. Prefer DI/inheritance/decorators.
18. Memory Model — Reference Counting + Garbage Collector
CPython uses Reference Counting (primary) + Cyclic GC (secondary). Ref count = 0 → memory freed immediately.
Circular refs: a.b = b; b.a = a → del a, del b → both still have ref count > 0. GC detects and cleans cycles.
Generations: GC has 3 generations. Young objects checked frequently (die fast). Old objects checked rarely.
gc module: [Link]() force-triggers GC. [Link]() turns it off. Usually not needed.
KEY: Reference counting handles most cleanup. GC handles circular references. GC does NOT run after every allocation.
19. functools.lru_cache, partial, wraps
lru_cache: memoizes function results. partial: pre-fills arguments. wraps: preserves function metadata in decorators.
lru_cache: @lru_cache(maxsize=None) → fib(35) fast. Args must be hashable. Cache hit = no re-execution.
partial: double = partial(multiply, 2) → double(10) calls multiply(2, 10). Creates specialized functions.
wraps: @wraps(func) in decorator → preserves __name__, __doc__, annotations. Without it: add.__name__ = 'wrapper'.
KEY: lru_cache=cache results | partial=pre-fill args | wraps=preserve metadata. Use @wraps in EVERY custom decorator.
20. Circular Import — How to Fix
Circular import: A imports B, B imports A. Python can't finish loading either. Error: 'cannot import name from partially initialized
module'.
Best fix: Move shared code to a THIRD module ([Link]). Both A and B import from common.
Quick fix: Move import INSIDE the function: def foo(): from module import thing. Lazy import at call time.
Design fix: Dependency Inversion — both depend on an abstract interface, not each other.
KEY: Circular imports = design smell. One-direction dependency flow. [Link] > function import > dependency inversion.
21. Threading vs Multiprocessing vs Asyncio
Choose based on work type: CPU-bound → Multiprocessing. I/O-bound → Threading or Asyncio. Massive concurrent I/O → Asyncio.
Threading: Shared memory, lightweight. GIL limits CPU parallelism. Good: API calls, file ops, DB queries.
Multiprocessing: Separate processes, own GIL. True parallel execution. Good: ML, image/video processing.
Asyncio: Single thread, event loop, cooperative. Very low memory. Good: web servers, thousands of connections.
KEY: Threading→I/O | Multiprocessing→CPU | Asyncio→high-concurrency I/O. Asyncio = concurrency NOT parallelism.
22. async/await, Event Loop, Coroutines, Tasks, [Link]()
async def creates a coroutine. await pauses it and yields control. Event loop schedules and runs coroutines.
Coroutine: async def greet(): ... → calling greet() returns coroutine object, doesn't execute. Need await or create_task.
Task: asyncio.create_task(coro()) → schedules coroutine to run concurrently. Without task: sequential.
gather(): await [Link](task1(), task2(), task3()) → all run concurrently → total time = max, not sum.
KEY: await = pause current coroutine | Task = schedule coroutine | gather() = concurrent coroutines. NOT parallel.
23. ThreadPoolExecutor vs ProcessPoolExecutor vs asyncio.to_thread()
Executors run work in parallel. Choose by work type and GIL constraints.
ThreadPoolExecutor: Pool of threads. GIL affected. Best: I/O-bound (API calls, file ops). From [Link].
ProcessPoolExecutor: Pool of processes. Bypasses GIL. Best: CPU-bound (ML, data processing).
asyncio.to_thread(): Runs blocking function in a thread without blocking event loop. Use in FastAPI/async code.
KEY: ThreadPool→I/O | ProcessPool→CPU | to_thread()→blocking code in async apps. to_thread creates a thread, not process.
24. Race Conditions — Lock, RLock, Semaphore, Queue
Race condition: multiple threads modify shared state simultaneously → unpredictable results. counter += 1 is NOT atomic.
Lock: [Link]() → only 1 thread enters critical section. with lock: counter += 1
RLock: Reentrant lock → same thread can acquire multiple times. Use when nested locking needed.
Semaphore: [Link](N) → maximum N threads simultaneously. Use: connection pools, rate limiting.
Queue: Thread-safe communication between threads. No manual locks needed. Preferred for producer-consumer.
KEY: Lock=1 thread | RLock=nested locking | Semaphore=N threads | Queue=thread-safe data exchange. GIL ≠ no race conditions!
25. 100 I/O-Bound API Calls — Speed Without Rate Limit Issues
API calls are I/O-bound → asyncio is best. Don't fire all 100 at once → use Semaphore to limit concurrency.
Solution: asyncio + [Link] + [Link](10) → 10 concurrent max
Retry: Exponential backoff: await [Link](2**attempt). Respect Retry-After headers.
Production: asyncio + async HTTP client + Semaphore + retries + timeouts + connection pooling + caching
KEY: 100 I/O calls → [Link]() with Semaphore. NOT multiprocessing. Add retries + timeout always.
26. Flask vs FastAPI vs Django
Flask: lightweight microframework. FastAPI: modern async API framework. Django: full-stack batteries-included.
Flask: Simple routing, flexible. Choose for: small APIs, microservices, prototypes, internal tools.
FastAPI: Auto validation (Pydantic), auto docs (/docs), async. Choose for: REST APIs, ML APIs, high-concurrency.
Django: ORM, admin panel, auth, migrations built-in. Choose for: large business apps, CMS, e-commerce.
KEY: Flask=small/flexible | FastAPI=modern APIs | Django=full-stack. Choose by requirements, not popularity.
27. WSGI vs ASGI — Uvicorn vs Gunicorn
WSGI = synchronous interface (Flask, Django). ASGI = asynchronous interface (FastAPI, Starlette).
Gunicorn: WSGI server + process manager. Used with Flask/Django. Can run ASGI with Uvicorn workers.
Uvicorn: ASGI server. Used directly with FastAPI. Fast, async-native.
Production: Gunicorn + UvicornWorker + FastAPI = best of both worlds. Process management + async execution.
KEY: WSGI=sync | ASGI=async+WebSocket. Production FastAPI: gunicorn -k [Link] main:app
28. Pydantic Validation + FastAPI Dependency Injection
Pydantic validates, coerces types, serializes. FastAPI uses Pydantic for all request/response models automatically.
Validation: class User(BaseModel): name: str; age: int → age='24' auto-converts to int. 'abc' → ValidationError.
field_validator: @field_validator('age') → custom validation logic. Raise ValueError to reject.
DI with Depends(): db = Depends(get_db) → FastAPI calls get_db() and injects result. Used for DB sessions, auth, config.
KEY: Pydantic=validate+convert+serialize | Depends()=inject dependencies per request. DI makes code testable and clean.
29. SQLAlchemy Core vs ORM + N+1 Problem
Core = SQL-centric, more control. ORM = object-centric, less boilerplate but can hide inefficiencies.
N+1 Problem: 1 query for 100 users + 100 queries for their orders = 101 queries. Caused by lazy loading.
Fix: joinedload(): .options(joinedload([Link])) → LEFT JOIN → 1 query total. Good for small relationships.
Fix: selectinload(): .options(selectinload([Link])) → WHERE IN → 2 queries total. Better for large relationships.
KEY: N+1 = most common ORM performance bug. Fix with eager loading. Use EXPLAIN ANALYZE to detect slow queries.
30. Pandas Vectorization, apply vs map, groupby Performance
Vectorized operations use optimized C/NumPy code → much faster than Python loops. Avoid iterrows().
Performance rank: Vectorized > map() > apply() > iterrows(). Always prefer vectorized: df['bonus'] = df['salary'] * 0.1
map(): Series-only value mapping/replacement. df['gender'].map({'M': 'Male', 'F': 'Female'})
groupby tips: Use built-in agg (sum, mean, count). Select columns before groupby. Convert strings to category dtype.
KEY: Never loop over rows in Pandas. Use vectorized ops. If you must apply(), check if vectorization is possible first.
31. NumPy Arrays vs Python Lists + Broadcasting + Views vs Copies
NumPy arrays: contiguous memory, homogeneous, C-optimized. Lists: pointers to objects, flexible but slow for math.
Broadcasting: arr + 10 → adds 10 to each element without loop. NumPy expands shape automatically.
View: arr[:5] → shares memory. Modifying view modifies original. Fast, no extra memory.
Copy: [Link]() → independent new array. Changes don't affect original. Use when isolation needed.
KEY: NumPy speed = contiguous memory + C operations. Slicing creates VIEW (not copy). Use .copy() for independence.
32. Serialization — Pickle vs JSON vs MsgPack vs Protobuf
Serialization converts objects to storable/transmittable format. Choice depends on performance, readability, language support.
JSON: Human readable, language-independent, REST API standard. Larger size, slower than binary.
Pickle: Python-only, supports any Python object. NEVER unpickle untrusted data (can execute arbitrary code).
MsgPack: Binary JSON — smaller and faster. Cross-language. Good for caching, microservices.
Protobuf: Schema-based binary. Fastest, smallest. Used in gRPC, distributed systems. Strong typing.
KEY: JSON=APIs | Pickle=Python internal (UNSAFE external) | MsgPack=compact binary | Protobuf=fastest/gRPC
33. Logging, Config, Secrets in Python Services
Use logging module (not print). Load config from env vars. NEVER hardcode secrets in source code.
Log levels: DEBUG < INFO < WARNING < ERROR < CRITICAL. Use [Link]() in except → includes stack trace.
Config: [Link]('DB_URL') or Pydantic BaseSettings. Different values per environment (dev/staging/prod).
Secrets: Never in code. Use env vars for small deployments. AWS Secrets Manager / HashiCorp Vault for production.
KEY: print()=dev only. logging=production. Secrets in env vars or secret manager. Never in Git or hardcoded.
34. pytest — Fixtures, Parametrize, Mocking, Coverage
pytest is the standard Python testing framework. Fixtures provide reusable setup. Parametrize runs one test with many inputs.
Fixture: @[Link] def db(): return Database() → inject into tests. Scope: function/module/session.
Parametrize: @[Link]('a,b,result', [(1,2,3),(2,3,5)]) → 2 test cases from 1 function.
Mock: @patch('[Link]') mock_get.return_value = ... → no real network calls in tests.
Coverage: pytest --cov → % of lines executed. 80-90% realistic target. 100% coverage ≠ bug-free code.
KEY: Fixture=setup | Parametrize=multiple inputs | Mock=isolate dependencies | Coverage=find untested code
35. Optimize Slow Pandas Merge on 50M Rows
Large merges are slow due to memory and computation. Fix: reduce data, fix dtypes, use proper tools.
Step 1: [Link](memory_usage='deep') → check memory. Reduce int64→int32 where possible. Strings → category dtype.
Step 2: Keep only needed columns before merge. Ensure merge key types match. Check for duplicate keys.
Step 3: Use inner join if outer not needed. Consider Parquet (columnar, compressed) over CSV.
Step 4: Dask/Polars for larger-than-RAM. Push joins to database (SQL) instead of loading everything.
KEY: Fastest Pandas merge = the merge you avoid by doing it in the database. Check dtypes first.
36. Python Profiling — cProfile, line_profiler, memory_profiler
Never optimize by guessing. Profile first. cProfile=function timing. line_profiler=line timing. memory_profiler=memory.
cProfile: [Link]('process()') → shows calls, tottime (self only), cumtime (self + children).
line_profiler: @profile + kernprof -l [Link] → shows time per line. Use AFTER cProfile identifies slow function.
memory_profiler: @profile + python -m memory_profiler → shows memory per line. Use for OOM, leaks.
KEY: Flow: cProfile → find slow function → line_profiler → find slow line. memory_profiler for memory issues.
37. Common Python Performance Pitfalls
Most performance issues come from bad algorithms, DB queries, or I/O — not Python syntax.
String concat: ''.join(parts) not result += part in loop. Each += creates new string object.
List for lookup: Use set or dict for membership checks. list: O(n), set: O(1).
iterrows(): Never use iterrows() on large DataFrames. Use vectorized ops.
Blocking async: [Link]() in async = blocks event loop. Use await [Link]() or asyncio.to_thread().
KEY: Optimize: algorithm > DB > I/O > data structures > micro-opts. Profile before optimizing. Don't guess.
38. pip, poetry, uv, [Link], [Link]
pip=default installer. [Link]=dependency list. [Link]=modern standard. poetry/uv=full managers.
[Link]: pip freeze > [Link] → pin versions. pip install -r [Link] to install.
[Link]: Centralizes project metadata + deps + build config + tool config. Modern Python standard.
poetry/uv: Poetry: dependency manager + lock file + virtualenv + packaging. uv: Rust-based, very fast.
KEY: Modern projects: [Link] + uv or poetry. Legacy: pip + [Link] still common.
39. venv, virtualenv, Conda, Pipenv
Virtual environments isolate dependencies per project, preventing version conflicts between projects.
venv: Built into Python: python -m venv myenv. Activate then use pip. Lightweight, no install needed.
Conda: Package manager + env manager. Handles non-Python dependencies (C libs, CUDA). Best for data science/ML.
Pipenv: pip + virtualenv combined. Uses Pipfile + [Link]. Largely replaced by poetry/uv today.
KEY: Backend project → venv or uv. Data science / ML → Conda. Legacy → virtualenv. All serve same core purpose.
40. Dockerizing Python Apps — Layers, .dockerignore, Multi-Stage
Docker packages app + dependencies + runtime into portable containers. Each Dockerfile instruction = a layer (cached).
Layer caching: COPY [Link] first → RUN pip install → COPY source. Dependencies cached if req unchanged.
.dockerignore: Exclude: venv/, __pycache__/, .git/, .env, *.log → smaller image, faster build, safer.
Multi-stage: FROM python:3.12 AS builder ... FROM python:3.12-slim → only copy build output → smaller final image.
KEY: Always: slim base image + .dockerignore + COPY requirements before source + multi-stage for production.
41. mypy and Type Hints — TypedDict, Protocol, Generic
Type hints annotate expected types. Python doesn't enforce them at runtime. mypy performs static analysis before execution.
TypedDict: class User(TypedDict): name: str; age: int → type-safe dict with known keys. mypy validates usage.
Protocol: from typing import Protocol → structural typing: class just needs matching methods, no inheritance.
Generic: class Box(Generic[T]) → Box[int] or Box[str]. Type-safe reusable containers.
KEY: Type hints = documentation + IDE support. mypy = catch bugs before runtime. Protocol > ABC for flexible APIs.
42. CI/CD — ruff, black, isort, bandit, safety, pytest
CI/CD automates: lint → format check → security scan → tests → build → deploy on every code push.
ruff: Fast linter (written in Rust). Detects unused imports, code smells, style violations.
black + isort: black: auto-formats code. isort: auto-sorts imports. Both enforce consistent style.
bandit + safety: bandit: scans code for security issues (hardcoded passwords, unsafe functions). safety: checks deps for CVEs.
KEY: Pipeline: Push → ruff → black → bandit → safety → pytest → coverage → docker build → deploy.
43. Microservice Patterns — Circuit Breaker, Retry, Idempotency
Distributed systems expect failures. Retry handles transient failures. Circuit breaker prevents cascade failures.
Retry + Backoff: Try 3x with delays: 1s, 2s, 4s. Use exponential backoff. Respect Retry-After headers.
Circuit Breaker: States: Closed (normal) → Open (stop calls after N failures) → Half-Open (test recovery). pybreaker library.
Idempotency: Same request executed N times = same result. Use unique txn IDs. Critical for payments/orders.
KEY: Retry=transient failures | Circuit Breaker=cascade prevention | Idempotency=duplicate safety. All three together.
44. Celery + Redis/RabbitMQ — Background Tasks + Retries
Celery = distributed task queue. Decouples long tasks from API response. Producer → Broker → Worker.
Basic: @[Link] def send_email(email): ... → send_email.delay(email) → queues task, returns immediately.
Retry: @[Link](autoretry_for=(Exception,), retry_backoff=True, max_retries=5) → auto-retries on failure.
Idempotency: Tasks may execute multiple times (crash/retry). Use transaction IDs to detect and skip duplicates.
KEY: Celery = background processing. Redis=simple broker. RabbitMQ=reliable/complex. Always: retries + idempotency.
45. Debug Python API with 5s p95 Latency
p95 = 95% of requests faster than this value. 5s p95 = 5% of requests take >5s. Never guess — profile first.
Step 1: Add tracing/metrics. Break request into: DB time + external API time + app time + network.
Step 2: Check DB: slow queries (EXPLAIN ANALYZE), missing indexes, N+1 queries, connection pool exhaustion.
Step 3: Check external APIs: timeouts, async opportunities ([Link]). Check caching opportunities.
Step 4: Profile CPU (cProfile), memory (memory_profiler). Check async blocking ([Link] in async code).
KEY: Measure first. Usually: DB > external APIs > CPU > memory. Fixing N+1 often gives 10x improvement instantly.
46. SOLID Principles with Python Examples
Five design principles for maintainable, flexible, testable code.
S: SRP: One class = one responsibility. Split UserRepo, EmailService, ReportService from User class.
O: OCP: Add new payment type by creating new class, not editing existing if-else chain.
L: LSP: Child should replace parent without breaking behavior. Penguin should NOT extend FlyingBird.
I: ISP: Robot shouldn't implement eat() just because it extends Worker. Split into Workable + Eatable.
D: DIP: UserService takes db: Database (abstract), not MySQL directly. Inject the dependency.
KEY: SOLID ≠ more classes. SOLID = less coupling. DIP most commonly asked. Use dependency injection.
47. Design Patterns — Singleton, Factory, Strategy, Observer, Repository
Reusable solutions to recurring design problems. Use when they solve a real problem, not by default.
Singleton: One instance only. class uses _instance = None in __new__. Use: logger, config. Drawback: global state.
Factory: [Link]('postgres') → returns right DB. Hides creation logic.
Strategy: PaymentStrategy ABC with StripePayment, PayPalPayment subclasses. Swap algorithm without if-else chain.
Observer: [Link]() → all [Link](). Event systems, notifications, WebSockets.
Repository: UserRepository handles SQLAlchemy. UserService handles business logic. Separates DB from logic.
KEY: Strategy = most common in interviews. Repository = clean architecture. Singleton = use carefully (global state risk).
48. Exception Handling — Custom Exceptions, Chaining, finally vs else
Catch specific exceptions. Never use bare except: pass. Use finally for cleanup.
else: Runs only if NO exception occurred. Use for code that shouldn't run on error.
finally: Always runs. Use for cleanup: close file, release DB connection, release lock.
Custom exceptions: class PaymentFailedError(Exception): pass → domain-specific, readable, catchable by type.
Chaining: raise RuntimeError('msg') from original_error → preserves root cause in traceback.
KEY: try→except specific→else (success)→finally (always). Chain exceptions to preserve root cause.
49. Unit vs Integration vs Contract Tests
Test pyramid: many unit tests → fewer integration → very few contract tests. Fast feedback loop.
Unit Test: Tests single function/class in isolation. Uses mocks for DB/APIs. Fast, simple, reliable.
Integration Test: Tests components together (API + real DB). Slower but catches schema mismatches, config errors.
Contract Test: Verifies service-to-service API compatibility. Consumer defines expected response. Provider verifies.
KEY: Unit=business logic | Integration=component interaction | Contract=service agreements. Mock external deps in units.
50. Design a Rate Limiter — Data Structures + Concurrency
Rate limiter: controls requests per user/IP per time window. Returns HTTP 429 when limit exceeded.
Sliding Window: Store request timestamps in deque per user. Remove expired. Count remaining. O(1) ops.
Token Bucket: Bucket refills N tokens/second. Each request consumes 1 token. Allows bursts. Common in production.
Concurrency: Single server: [Link]() around counter. Distributed: Redis INCR (atomic) for shared state.
Dict + Deque: users = {'user1': deque()} → O(1) user lookup + O(1) deque operations.
KEY: Single server → Lock + deque. Multi-server → Redis atomic ops. Semaphore controls concurrency. 429 on exceed.