Interview Q&A Python All Levels

Python Interview Questions & Answers

Python interview questions covering data types, OOP, decorators, generators, asyncio, memory management, and common libraries — Basic to Advanced.

18 min read 25 Questions
25 Total Questions
11 Basic
10 Intermediate
4 Advanced
Level:
Q1
What is Python and what are its key features?
Basic

Ans: Python is a high-level, interpreted, dynamically typed, general-purpose programming language designed for readability and simplicity.

FeatureDescription
InterpretedCode runs line-by-line without a compile step
Dynamically typedVariable types are checked at runtime, not compile time
Garbage collectedAutomatic memory management via reference counting + GC
Multi-paradigmSupports OOP, functional, and procedural styles
Extensive stdlib“Batteries included” — huge standard library out of the box
Cross-platformRuns on Windows, macOS, Linux without modification
Q2
What is the difference between a list, tuple, set, and dictionary in Python?
Basic

Ans:

TypeOrderedMutableDuplicatesKey-ValueSyntax
list[1, 2, 3]
tuple(1, 2, 3)
set{1, 2, 3}
dict✅ (3.7+)Keys: ❌{"a": 1}
my_list  = [1, 2, 2, 3]      # Ordered, mutable, allows duplicates
my_tuple = (1, 2, 2, 3)      # Ordered, immutable
my_set   = {1, 2, 3}         # Unordered, no duplicates
my_dict  = {"name": "Alice", "age": 30}  # Key-value pairs

# When to use each:
# list   → ordered sequence that changes (shopping cart)
# tuple  → fixed data (coordinates, RGB values)
# set    → membership testing, deduplication
# dict   → lookup by key (configuration, JSON-like data)
Q3
What is the difference between mutable and immutable objects in Python?
Basic

Ans: Below is the difference between mutabke and immutable object:

  • Mutable objects can be changed after creation.
  • Immutable objects cannot.
MutableImmutable
listint, float, bool
dictstr
settuple
bytearrayfrozenset, bytes
# Mutable — same object modified in place
lst = [1, 2, 3]
print(id(lst))   # e.g., 140234567890
lst.append(4)
print(id(lst))   # SAME id — modified in place

# Immutable — new object created
s = "hello"
print(id(s))     # e.g., 140234567000
s = s + " world"
print(id(s))     # DIFFERENT id — new string object

# Gotcha: mutable default arguments
def add_item(item, lst=[]):   # BAD — shared across calls!
    lst.append(item)
    return lst

def add_item(item, lst=None): # GOOD
    if lst is None:
        lst = []
    lst.append(item)
    return lst
Q4
What is a Python decorator and how do you create one?
Intermediate

Ans: A decorator is a function that wraps another function to extend its behaviour without modifying the original code. It implements the Decorator design pattern.

import functools
import time

# Basic decorator
def timer(func):
    @functools.wraps(func)  # Preserves original function metadata
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)
        end = time.perf_counter()
        print(f"{func.__name__} took {end - start:.4f}s")
        return result
    return wrapper

@timer
def slow_function(n):
    """Simulate work."""
    return sum(range(n))

slow_function(1_000_000)
# Output: slow_function took 0.0234s
# Decorator with arguments
def retry(max_attempts=3, delay=1.0):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(1, max_attempts + 1):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    if attempt == max_attempts:
                        raise
                    time.sleep(delay)
        return wrapper
    return decorator

@retry(max_attempts=5, delay=0.5)
def call_external_api():
    # Retries automatically on failure
    ...

Common built-in decorators:

  • @property — getter/setter on class attributes
  • @staticmethod — method that doesn’t receive self
  • @classmethod — method that receives the class (cls) instead of instance
  • @functools.lru_cache — memoization
Q5
What is a generator in Python? How does it differ from a regular function?
Intermediate

Ans:

A generator is a function that uses yield to return values lazily — one at a time — instead of computing and returning all values at once.

# Regular function — loads all values into memory
def get_squares_list(n):
    return [x**2 for x in range(n)]   # Creates list of n items in memory

# Generator — yields one value at a time (lazy)
def get_squares_gen(n):
    for x in range(n):
        yield x**2

# Memory comparison
import sys
lst = get_squares_list(1_000_000)
gen = get_squares_gen(1_000_000)

print(sys.getsizeof(lst))  # ~8 MB
print(sys.getsizeof(gen))  # 112 bytes!
# Generator expressions (like list comprehensions but lazy)
squares = (x**2 for x in range(1000))   # Generator
squares_list = [x**2 for x in range(1000)]  # List

# Infinite generator (impossible with a list)
def fibonacci():
    a, b = 0, 1
    while True:
        yield a
        a, b = b, a + b

fib = fibonacci()
print([next(fib) for _ in range(10)])
# [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

Use generators when:

  • Processing large files line-by-line
  • Producing an infinite sequence
  • Building data processing pipelines
Q6
Explain *args and **kwargs in Python.
Basic

Ans:

  • *args — collects extra positional arguments into a tuple
  • **kwargs — collects extra keyword arguments into a dict
def demo(*args, **kwargs):
    print(f"args:   {args}")    # tuple
    print(f"kwargs: {kwargs}")  # dict

demo(1, 2, 3, name="Alice", role="DevOps")
# args:   (1, 2, 3)
# kwargs: {'name': 'Alice', 'role': 'DevOps'}
# Passing to another function (forwarding)
def log_call(func):
    def wrapper(*args, **kwargs):
        print(f"Calling {func.__name__} with {args} {kwargs}")
        return func(*args, **kwargs)
    return wrapper

# Unpacking with * and **
numbers = [1, 2, 3]
print(*numbers)        # same as print(1, 2, 3)

config = {"sep": "-", "end": "\n"}
print(*numbers, **config)   # 1-2-3

Correct order in function signature: def f(positional, *args, keyword_only, **kwargs)

Q7
What is the Python GIL (Global Interpreter Lock) and how does it affect concurrency?
Advanced

Ans:

The GIL is a mutex in CPython that allows only one thread to execute Python bytecode at a time — even on multi-core hardware.

Thread 1: ──[GIL acquired]──[run bytecode]──[GIL released]──waiting──
Thread 2: ──waiting────────────────────────[GIL acquired]──[run]──

Impact:

  • CPU-bound tasks (number crunching) → threads don’t help; use multiprocessing instead
  • I/O-bound tasks (network, disk) → threads work fine; the GIL is released during I/O waits
import threading
import multiprocessing

# CPU-bound: multiprocessing wins (bypasses GIL — each process has its own GIL)
def cpu_work(n):
    return sum(i**2 for i in range(n))

# I/O-bound: threading works fine (GIL released during sleep/network/file I/O)
import requests
import concurrent.futures

urls = ["https://api.example.com/data"] * 10

with concurrent.futures.ThreadPoolExecutor(max_workers=5) as pool:
    responses = list(pool.map(requests.get, urls))
# Python 3.13+ introduces a "free-threaded" mode (experimental)
# that removes the GIL — but this is still stabilizing.

# Current best practice:
# CPU-bound → multiprocessing.Pool
# I/O-bound → asyncio or threading
# Mixed     → asyncio + run_in_executor for CPU parts
Q8
What is the difference between shallow copy and deep copy?
Intermediate

Ans: A shallow copy creates a new outer object but shares references to nested objects. A deep copy recursively copies all nested objects, making the copy completely independent.

import copy

original = {"name": "Alice", "scores": [90, 85, 78]}

# Shallow copy — copies the outer object, inner objects are shared
shallow = copy.copy(original)
shallow["name"] = "Bob"       # Does NOT affect original
shallow["scores"].append(100) # DOES affect original (same list object!)

print(original["name"])    # Alice (unchanged)
print(original["scores"])  # [90, 85, 78, 100] ← affected!

# Deep copy — recursively copies all nested objects
original = {"name": "Alice", "scores": [90, 85, 78]}
deep = copy.deepcopy(original)
deep["scores"].append(100)

print(original["scores"])  # [90, 85, 78] ← NOT affected
Outer ObjectNested Objects
Assignment (=)Same referenceSame reference
copy.copy() (shallow)New objectSame reference
copy.deepcopy() (deep)New objectNew objects (recursive)
Q9
Explain Python's context manager (the `with` statement).
Intermediate

Ans:

A context manager guarantees cleanup code runs even if an exception occurs. It implements __enter__ and __exit__ dunder methods.

# Without context manager (fragile)
f = open("file.txt", "r")
try:
    data = f.read()
finally:
    f.close()   # Must manually close — could be missed

# With context manager (safe)
with open("file.txt", "r") as f:
    data = f.read()
# f.close() called automatically, even on exception
# Creating your own context manager (class-based)
class DatabaseConnection:
    def __enter__(self):
        self.conn = connect_to_db()
        return self.conn

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.conn.close()
        return False  # Don't suppress exceptions

with DatabaseConnection() as db:
    db.execute("SELECT 1")
# Creating context managers with @contextmanager (simpler)
from contextlib import contextmanager

@contextmanager
def managed_resource():
    resource = acquire_resource()
    try:
        yield resource          # Code inside 'with' block runs here
    finally:
        release_resource(resource)  # Always runs

with managed_resource() as r:
    r.do_something()
Q10
What is list comprehension and when should you use a generator expression instead?
Basic

Ans:

# List comprehension — creates a full list in memory
squares = [x**2 for x in range(10) if x % 2 == 0]
# [0, 4, 16, 36, 64]

# Dict comprehension
word_lengths = {word: len(word) for word in ["aws", "kubernetes", "docker"]}
# {'aws': 3, 'kubernetes': 10, 'docker': 6}

# Set comprehension
unique_lengths = {len(word) for word in ["cat", "bat", "door", "mat"]}
# {3, 4}

# Generator expression (lazy — use () not [])
squares_gen = (x**2 for x in range(10_000_000))
# No memory allocated yet — computed on demand

# Rule of thumb:
# List comprehension → need all results at once, or reuse multiple times
# Generator expression → single-pass consumption, large/infinite datasets
# Generator expressions compose well (pipeline)
lines = (line.strip() for line in open("huge.log"))
errors = (line for line in lines if "ERROR" in line)
first_10 = list(itertools.islice(errors, 10))
# Memory: only one line loaded at a time
Q11
What is the difference between `__str__` and `__repr__`?
Intermediate

Ans:

MethodPurposeCalled byAudience
__repr__Unambiguous, developer representationrepr(), REPLDevelopers / debugging
__str__Human-readable, user-friendlyprint(), str()End users
from datetime import datetime

dt = datetime.now()
print(repr(dt))  # datetime.datetime(2024, 1, 15, 14, 30, 45, 123456)
print(str(dt))   # 2024-01-15 14:30:45.123456

class ServerConfig:
    def __init__(self, host, port):
        self.host = host
        self.port = port

    def __repr__(self):
        # Should be unambiguous — ideally recreatable
        return f"ServerConfig(host='{self.host}', port={self.port})"

    def __str__(self):
        # Human-friendly
        return f"{self.host}:{self.port}"

cfg = ServerConfig("localhost", 8080)
print(repr(cfg))  # ServerConfig(host='localhost', port=8080)
print(str(cfg))   # localhost:8080
print(cfg)        # localhost:8080  (uses __str__)

Rule: Always define __repr__. Define __str__ only when the user-facing format differs.

Q12
What are Python's `classmethod` and `staticmethod` decorators?
Intermediate

Ans:

class Order:
    TAX_RATE = 0.08

    def __init__(self, amount):
        self.amount = amount

    def total(self):
        """Instance method — has access to self and the instance."""
        return self.amount * (1 + self.TAX_RATE)

    @classmethod
    def from_dict(cls, data: dict):
        """Class method — has access to cls (the class itself, not the instance).
        Useful as alternative constructors."""
        return cls(data["amount"])

    @staticmethod
    def is_valid_amount(amount: float) -> bool:
        """Static method — no access to self or cls.
        A utility function that logically belongs to the class."""
        return amount > 0

# Usage
order = Order(100)
order2 = Order.from_dict({"amount": 200})    # classmethod
print(Order.is_valid_amount(-5))             # False — staticmethod
ReceivesAccess toUse case
Instance methodselfInstance + classMost methods
@classmethodclsClass onlyAlternative constructors
@staticmethodNothingNothingUtility functions
Q13
How does Python's memory management work? What is reference counting?
Advanced

Ans:

Python uses two main mechanisms:

1. Reference Counting — Every object tracks how many references point to it. When the count drops to 0, memory is freed immediately.

import sys

x = [1, 2, 3]
print(sys.getrefcount(x))  # 2 (x + the getrefcount argument)

y = x          # Reference count becomes 3
del x          # Reference count becomes 2
y = None       # Reference count becomes 0 → object freed

2. Cyclic Garbage Collector — Reference counting cannot handle circular references:

# Circular reference — neither object reaches refcount 0
a = {}
b = {}
a["other"] = b
b["other"] = a

# Python's gc module detects and cleans these up
import gc
gc.collect()  # Manually trigger cycle detection

Memory optimization tools:

# __slots__ — prevents __dict__ creation, saves 40-50% memory per instance
class Point:
    __slots__ = ["x", "y"]  # No __dict__, no arbitrary attributes
    def __init__(self, x, y):
        self.x = x
        self.y = y

# For millions of objects, this matters:
import sys
class PointDict:
    def __init__(self, x, y): self.x = x; self.y = y
class PointSlots:
    __slots__ = ["x", "y"]
    def __init__(self, x, y): self.x = x; self.y = y

print(sys.getsizeof(PointDict(1,2)))   # ~48 bytes + dict overhead
print(sys.getsizeof(PointSlots(1,2)))  # ~56 bytes total (no dict)
Q14
What is `asyncio` in Python and when would you use it?
Advanced

Ans:

asyncio is Python’s built-in library for asynchronous I/O using a single-threaded event loop. It uses async/await syntax to write non-blocking code.

import asyncio
import aiohttp   # pip install aiohttp

# WITHOUT asyncio — sequential (slow)
# Each request waits for the previous one to complete
# 10 requests × 200ms each = 2000ms total

# WITH asyncio — concurrent (fast)
# All requests fire "simultaneously" — 10 requests ≈ 200ms total

async def fetch(session, url):
    async with session.get(url) as response:
        return await response.json()

async def fetch_all(urls):
    async with aiohttp.ClientSession() as session:
        tasks = [fetch(session, url) for url in urls]
        results = await asyncio.gather(*tasks)  # Run all concurrently
    return results

urls = ["https://api.example.com/user/1",
        "https://api.example.com/user/2",
        "https://api.example.com/user/3"]

results = asyncio.run(fetch_all(urls))
# asyncio vs threading vs multiprocessing
# I/O-bound, many connections  → asyncio (best — low overhead)
# I/O-bound, simple            → threading (easier)
# CPU-bound computation        → multiprocessing (bypasses GIL)

Key concepts:

  • async def → defines a coroutine (doesn’t run until awaited)
  • await → suspends the current coroutine, yields control to event loop
  • asyncio.gather() → runs multiple coroutines concurrently
  • asyncio.run() → entry point — starts the event loop
Q15
What are Python type hints and why should you use them?
Intermediate

Ans:

Type hints are annotations that declare the expected types of variables, function parameters, and return values. They don’t affect runtime but enable static analysis tools.

from typing import Optional, List, Dict, Union, Tuple
from dataclasses import dataclass

# Basic type hints
def greet(name: str, times: int = 1) -> str:
    return f"Hello, {name}! " * times

# Complex types
def process_users(users: List[Dict[str, str]]) -> Dict[str, int]:
    return {u["name"]: len(u["email"]) for u in users}

# Optional (can be None)
def find_user(user_id: int) -> Optional[str]:
    return db.get(user_id)   # Returns str or None

# Python 3.10+ — union with |
def parse_value(val: str | int | None) -> float:
    return float(val) if val is not None else 0.0
# dataclass — combines type hints with automatic __init__, __repr__
@dataclass
class ServiceConfig:
    host: str
    port: int
    timeout: float = 30.0
    tags: List[str] = None

    def __post_init__(self):
        if self.tags is None:
            self.tags = []

cfg = ServiceConfig(host="localhost", port=8080)
print(cfg)  # ServiceConfig(host='localhost', port=8080, timeout=30.0, tags=[])

Benefits:

  • IDEs provide better autocomplete and error detection
  • Tools like mypy catch type errors before runtime
  • Code is self-documenting
Q16
What is the difference between `is` and `==` in Python?
Basic

Ans:

  • == checks value equality (do they hold the same data?)
  • is checks identity equality (are they the exact same object in memory?)
a = [1, 2, 3]
b = [1, 2, 3]
c = a

print(a == b)  # True  — same values
print(a is b)  # False — different objects
print(a is c)  # True  — same object (c points to a)
# Integer caching — CPython caches small integers (-5 to 256)
x = 256
y = 256
print(x is y)  # True  — same cached object

x = 257
y = 257
print(x is y)  # False — outside cache range, different objects

# Correct usage of `is`
if response is None:    # ✅ Correct — None is a singleton
    handle_empty()

if response == None:    # ⚠️ Works but less Pythonic
    handle_empty()

Rule: Use is only for None, True, and False (singletons). Use == for everything else.

Q17
What is exception handling in Python? Explain try/except/else/finally.
Basic

Ans:

import logging

def read_config(path: str) -> dict:
    try:
        # Code that may raise an exception
        with open(path) as f:
            return json.load(f)

    except FileNotFoundError:
        # Specific exception — file doesn't exist
        logging.error(f"Config file not found: {path}")
        return {}

    except json.JSONDecodeError as e:
        # Another specific exception — bad JSON
        logging.error(f"Invalid JSON in {path}: {e}")
        raise ValueError(f"Config file is malformed: {path}") from e

    except (PermissionError, OSError) as e:
        # Multiple exception types in one clause
        raise RuntimeError(f"Cannot read config: {e}") from e

    else:
        # Runs ONLY if no exception was raised in try
        logging.info(f"Config loaded successfully from {path}")

    finally:
        # ALWAYS runs — cleanup code (even if exception propagates)
        logging.debug("read_config complete")
# Custom exceptions
class InsufficientFundsError(ValueError):
    def __init__(self, balance: float, amount: float):
        self.balance = balance
        self.amount = amount
        super().__init__(f"Cannot withdraw {amount}: balance is {balance}")

def withdraw(balance: float, amount: float) -> float:
    if amount > balance:
        raise InsufficientFundsError(balance, amount)
    return balance - amount
Q18
What are Python's OOP pillars? Explain inheritance, encapsulation, polymorphism, and abstraction.
Intermediate

Ans:

from abc import ABC, abstractmethod

# ABSTRACTION — abstract base class defines interface, not implementation
class NotificationService(ABC):
    @abstractmethod
    def send(self, message: str, recipient: str) -> bool:
        """Must be implemented by subclasses."""
        ...

    def send_with_retry(self, message, recipient, max_retries=3):
        """Concrete method available to all subclasses."""
        for attempt in range(max_retries):
            if self.send(message, recipient):
                return True
        return False

# INHERITANCE — subclass extends/overrides base class
class EmailService(NotificationService):
    def __init__(self, smtp_host: str):
        self._smtp_host = smtp_host   # ENCAPSULATION — _ prefix = private by convention

    def send(self, message: str, recipient: str) -> bool:
        # Concrete implementation
        print(f"Email to {recipient}: {message}")
        return True

class SlackService(NotificationService):
    def __init__(self, webhook_url: str):
        self.__webhook = webhook_url   # __ name mangling = stronger encapsulation

    def send(self, message: str, recipient: str) -> bool:
        print(f"Slack to #{recipient}: {message}")
        return True

# POLYMORPHISM — same interface, different behaviour
services: list[NotificationService] = [
    EmailService("smtp.gmail.com"),
    SlackService("https://hooks.slack.com/..."),
]

alert = "Deployment complete!"
for service in services:
    service.send(alert, "devops-team")  # Calls the right implementation automatically
Q19
What is a Python lambda function and when should you use it?
Basic

Ans:

A lambda is an anonymous single-expression function. It’s syntactic sugar for a simple function.

# Regular function
def double(x):
    return x * 2

# Equivalent lambda
double = lambda x: x * 2

# Lambdas shine as inline callbacks
numbers = [5, 2, 8, 1, 9, 3]
numbers.sort(key=lambda x: -x)   # Sort descending
# [9, 8, 5, 3, 2, 1]

users = [{"name": "Charlie", "age": 30}, {"name": "Alice", "age": 25}]
users.sort(key=lambda u: u["name"])   # Sort by name

# With filter() and map()
evens = list(filter(lambda x: x % 2 == 0, range(10)))  # [0, 2, 4, 6, 8]
squares = list(map(lambda x: x**2, [1, 2, 3, 4]))       # [1, 4, 9, 16]

When NOT to use lambdas:

  • When the logic is complex (use a named function for readability)
  • When you need docstrings or type hints
  • When you’ll reuse the function (assign to a variable instead)
Q20
What is the difference between `@staticmethod` property and `@property` in Python?
Intermediate

Ans:

@property turns a method into a read-only attribute with getter/setter/deleter control.

class Temperature:
    def __init__(self, celsius: float):
        self._celsius = celsius   # Internal storage

    @property
    def celsius(self) -> float:
        """Getter."""
        return self._celsius

    @celsius.setter
    def celsius(self, value: float):
        """Setter — validates before storing."""
        if value < -273.15:
            raise ValueError(f"Temperature below absolute zero: {value}")
        self._celsius = value

    @celsius.deleter
    def celsius(self):
        del self._celsius

    @property
    def fahrenheit(self) -> float:
        """Computed property — no setter needed."""
        return self._celsius * 9/5 + 32

t = Temperature(100)
print(t.fahrenheit)   # 212.0 — looks like an attribute, runs code
t.celsius = -274      # Raises ValueError
t.celsius = 0
print(t.fahrenheit)   # 32.0
Q21
What are Python virtual environments and why are they important?
Basic

Ans:

A virtual environment is an isolated Python environment with its own interpreter and packages — separate from the system Python.

# Create virtual environment
python -m venv venv

# Activate (Linux/macOS)
source venv/bin/activate

# Activate (Windows)
venv\Scripts\activate

# Install packages (only in this venv, not globally)
pip install fastapi uvicorn

# Freeze dependencies
pip freeze > requirements.txt

# Deactivate
deactivate

Why they matter:

  • Dependency isolation — Project A needs Django 3.x, Project B needs Django 4.x — both work on the same machine
  • Reproducibilityrequirements.txt + venv ensures everyone runs the same versions
  • No root access required — install packages without sudo
# Modern tooling
# uv — ultra-fast alternative to pip + venv (recommended)
pip install uv
uv venv
uv pip install fastapi

# poetry — dependency management + virtual envs
poetry init
poetry add fastapi
poetry shell
Q22
What is the `__init__.py` file and what is a Python package?
Basic

Ans:

A package is a directory containing Python modules and an __init__.py file. The __init__.py marks the directory as a package and can initialize shared state.

myapp/
├── __init__.py          # Makes myapp a package
├── config.py
├── models/
│   ├── __init__.py      # Makes models a sub-package
│   ├── user.py
│   └── order.py
└── services/
    ├── __init__.py
    └── payment.py
# myapp/__init__.py — expose public API
from .config import settings
from .models.user import User

__version__ = "1.0.0"
__all__ = ["User", "settings"]  # Controls what 'from myapp import *' exports

# Usage
from myapp import User           # Works because of __init__.py
from myapp.models.order import Order  # Direct import also works

In Python 3.3+, __init__.py is optional for “namespace packages”, but best practice is to include it for regular packages to be explicit.

Q23
How do you handle file I/O in Python? What is the difference between text mode and binary mode?
Basic

Ans:

# Reading a file
with open("data.txt", "r", encoding="utf-8") as f:
    content = f.read()            # Entire file as string
    # OR
    lines = f.readlines()         # List of lines
    # OR (memory efficient for large files)
    for line in f:                # Iterator — one line at a time
        process(line.strip())

# Writing a file
with open("output.txt", "w", encoding="utf-8") as f:
    f.write("Hello, World!\n")
    f.writelines(["line1\n", "line2\n"])

# Appending
with open("log.txt", "a") as f:
    f.write("New log entry\n")

# Binary mode — for images, PDFs, executables
with open("image.png", "rb") as f:
    data = f.read()   # bytes, not str

with open("copy.png", "wb") as f:
    f.write(data)
ModeDescription
"r"Read text (default)
"w"Write text (overwrites)
"a"Append text
"rb"Read binary
"wb"Write binary
"r+"Read and write
Q24
What is the difference between `multiprocessing` and `threading` in Python?
Advanced

Ans:

import threading
import multiprocessing
import time

# I/O-bound task — threading is fine (GIL released during I/O)
def download_file(url):
    import requests
    return requests.get(url).content

with concurrent.futures.ThreadPoolExecutor(max_workers=10) as pool:
    results = list(pool.map(download_file, urls))

# CPU-bound task — multiprocessing bypasses GIL
def compute_intensive(n):
    return sum(i**2 for i in range(n))

with multiprocessing.Pool(processes=4) as pool:
    results = pool.map(compute_intensive, [10**7] * 8)
Featurethreadingmultiprocessing
ParallelismConcurrent (GIL limits true parallel)True parallel (separate processes)
MemoryShared memorySeparate memory space
CommunicationShared objects (with locks)Queues, Pipes, shared memory
OverheadLowHigher (process startup)
Best forI/O-boundCPU-bound
GIL affectedYesNo
# ProcessPoolExecutor — modern way to use multiprocessing
from concurrent.futures import ProcessPoolExecutor

with ProcessPoolExecutor(max_workers=4) as pool:
    results = list(pool.map(compute_intensive, [10**7] * 8))
Q25
What are Python's built-in data structures for counting and ordering? Explain `Counter`, `defaultdict`, and `OrderedDict`.
Intermediate

Ans:

from collections import Counter, defaultdict, OrderedDict, deque

# Counter — count occurrences
words = ["aws", "docker", "aws", "kubernetes", "docker", "aws"]
count = Counter(words)
print(count)                    # Counter({'aws': 3, 'docker': 2, 'kubernetes': 1})
print(count.most_common(2))     # [('aws', 3), ('docker', 2)]
count.update(["aws", "helm"])   # Add more counts

# defaultdict — dict with default value for missing keys (no KeyError)
graph = defaultdict(list)
graph["A"].append("B")   # No need to check if "A" exists
graph["A"].append("C")
print(graph)  # defaultdict(<class 'list'>, {'A': ['B', 'C']})

# Versus regular dict:
try:
    regular = {}
    regular["A"].append("B")  # KeyError!
except KeyError:
    regular["A"] = ["B"]      # Must manually initialize

# deque — double-ended queue with O(1) append/pop from both ends
queue = deque(maxlen=5)      # Fixed-size sliding window
for i in range(10):
    queue.append(i)          # Auto-evicts oldest when full
print(queue)                 # deque([5, 6, 7, 8, 9], maxlen=5)

queue.appendleft(99)         # Add to left
queue.rotate(2)              # Rotate elements

Add More Questions to This Guide

Know a question that should be here? Share it and help the community!

Open Google Form