🐍 Advanced Python Interview Questions
This lesson targets mid-to-senior Python roles. Topics include closures, itertools, asyncio, type hints, dataclasses, the descriptor protocol, metaclasses, threading vs multiprocessing, the GIL, testing with pytest, and functional programming patterns. These questions separate Python users from Python engineers.
Questions & Answers
01 What is a closure in Python? When is it useful? ►
Closures A closure is a function that remembers the variables from its enclosing scope even after that scope has finished executing. The inner function “closes over” the outer function’s variables.
def make_counter(start=0):
count = start # variable in the enclosing scope
def counter():
nonlocal count # needed to modify (not just read) the outer variable
count += 1
return count
return counter # return the inner function (not the result of calling it)
c1 = make_counter()
c2 = make_counter(10)
print(c1()) # 1
print(c1()) # 2
print(c2()) # 11 โ c2 has its own independent 'count'
# Closures underlie decorators and factories
def multiplier(factor):
return lambda x: x * factor # lambda closes over 'factor'
double = multiplier(2)
triple = multiplier(3)
double(5) # 10
triple(5) # 15
# Common mistake โ closure in a loop
funcs = [lambda: i for i in range(3)]
[f() for f in funcs] # [2, 2, 2] โ all closures share the same 'i'!
# Fix: capture the value at creation time
funcs = [lambda i=i: i for i in range(3)] # default argument
[f() for f in funcs] # [0, 1, 2] โ correct
02 What is asyncio? How does async/await work in Python? ►
Async asyncio is Python’s built-in library for writing concurrent code using coroutines. It runs on a single thread with an event loop โ when a coroutine awaits an I/O operation, the event loop runs other coroutines instead of blocking.
import asyncio
import aiohttp
# Coroutine โ declared with async def, uses await inside
async def fetch_url(session, url):
async with session.get(url) as response:
return await response.json()
async def main():
urls = ["https://api.example.com/users/1",
"https://api.example.com/users/2",
"https://api.example.com/users/3"]
async with aiohttp.ClientSession() as session:
# Sequential โ slow (waits for each request)
# results = [await fetch_url(session, url) for url in urls]
# Concurrent โ all requests start simultaneously
tasks = [asyncio.create_task(fetch_url(session, url)) for url in urls]
results = await asyncio.gather(*tasks)
return results
# Run the event loop
asyncio.run(main())
# asyncio primitives
await asyncio.sleep(1) # non-blocking sleep
await asyncio.wait_for(coro, timeout=5.0) # timeout
asyncio.create_task(background_job()) # fire-and-forget
# Async iteration
async def async_range(n):
for i in range(n):
await asyncio.sleep(0) # yield to event loop
yield i
async for value in async_range(5):
print(value)
asyncio vs threading: asyncio is single-threaded and excels at I/O-bound concurrency (many simultaneous network requests). Threading is better for blocking I/O with libraries that don’t support async. Neither helps CPU-bound work โ use multiprocessing for that.
03 What are Python type hints and the typing module? ►
Type Hints Type hints annotate variables and function signatures with expected types. They don’t affect runtime behaviour but enable static analysis tools (mypy, pyright) and improve IDE support and documentation.
from typing import Optional, Union, List, Dict, Tuple, Any, Callable
from collections.abc import Iterator, Generator, Sequence
# Function annotations
def greet(name: str, times: int = 1) -> str:
return f"Hello, {name}! " * times
# Variable annotations
count: int = 0
names: list[str] = [] # Python 3.9+ โ built-in generics (no need to import List)
# Optional (can be the type OR None)
def find_user(user_id: int) -> Optional[str]: # or: str | None (Python 3.10+)
...
# Union (one of multiple types)
def process(value: int | str) -> str: # Python 3.10+ union syntax
return str(value)
# TypedDict โ dict with specified key types
from typing import TypedDict
class UserDict(TypedDict):
name: str
age: int
email: str
# dataclass with types โ self-documenting
from dataclasses import dataclass, field
@dataclass
class User:
name: str
age: int
tags: list[str] = field(default_factory=list)
email: str | None = None
u = User(name="Alice", age=30)
# Protocol โ structural typing ("duck typing" + static checking)
from typing import Protocol
class Drawable(Protocol):
def draw(self) -> None: ...
def render(obj: Drawable) -> None:
obj.draw() # any object with .draw() method works
04 What is the GIL (Global Interpreter Lock)? How does it affect concurrency? ►
Concurrency The GIL is a mutex in CPython that allows only one thread to execute Python bytecode at a time. It protects CPython’s memory management (reference counting) from race conditions.
What the GIL affects:
- CPU-bound tasks โ threads do NOT run in true parallel for CPU-intensive Python code. Two threads on a 2-core machine won’t actually double speed for computation.
- I/O-bound tasks โ threads DO work well. During I/O (network, file), the GIL is released, letting other threads run. This is why threading works fine for web scrapers, API clients.
import threading, multiprocessing, time
# CPU-bound example
def cpu_task(n):
return sum(i ** 2 for i in range(n))
# Threading โ NOT faster for CPU work (GIL)
t1 = threading.Thread(target=cpu_task, args=(10_000_000,))
t2 = threading.Thread(target=cpu_task, args=(10_000_000,))
# t1 and t2 actually alternate, not parallel
# Multiprocessing โ TRUE parallelism (separate processes, no GIL)
p1 = multiprocessing.Process(target=cpu_task, args=(10_000_000,))
p2 = multiprocessing.Process(target=cpu_task, args=(10_000_000,))
# p1 and p2 run on separate CPU cores simultaneously
# Summary:
# I/O-bound โ threading or asyncio
# CPU-bound โ multiprocessing (or C extensions, Cython, PyPy)
# Python 3.13 introduced experimental No-GIL mode (free-threaded CPython)
# python3.13t --enable-experimental-free-threaded โ truly parallel threads
05 What is the difference between threading and multiprocessing in Python? ►
Concurrency
import threading
import multiprocessing
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor
# Threading โ shared memory, GIL, I/O-bound
def io_task(url):
return requests.get(url).status_code
with ThreadPoolExecutor(max_workers=10) as executor:
results = list(executor.map(io_task, urls)) # 10 concurrent requests
# Multiprocessing โ separate processes, no GIL, CPU-bound
def cpu_task(data):
return sum(x ** 2 for x in data)
with ProcessPoolExecutor(max_workers=4) as executor: # 4 CPU cores
results = list(executor.map(cpu_task, data_chunks))
# Key differences:
# Threading: shared memory, cheap to create, GIL limits CPU parallelism
# Multiprocessing: separate memory spaces, higher overhead (spawn process),
# true CPU parallelism, must serialise data (pickle)
# Inter-process communication
from multiprocessing import Queue, Pipe, Value, Array
q = Queue()
q.put({"result": 42})
data = q.get()
# Shared memory (Python 3.8+)
from multiprocessing import shared_memory
shm = shared_memory.SharedMemory(create=True, size=1024)
# Zero-copy data sharing between processes
06 What are dataclasses in Python? How do they improve class definitions? ►
OOP The @dataclass decorator (Python 3.7+) automatically generates __init__, __repr__, __eq__ and optionally __lt__, __hash__, __slots__ from class variable annotations โ eliminating boilerplate.
from dataclasses import dataclass, field, asdict, astuple
from typing import ClassVar
@dataclass(order=True, frozen=False)
class Product:
# ClassVar โ not an instance field
category: ClassVar[str] = "General"
name: str
price: float
tags: list[str] = field(default_factory=list) # mutable default
_id: int = field(default=0, repr=False) # hide from repr
sort_index: float = field(init=False, repr=False) # not in __init__
def __post_init__(self):
self.sort_index = self.price # run after __init__
p1 = Product("Widget", 9.99)
p2 = Product("Gadget", 19.99, tags=["tech"])
print(p1) # Product(name='Widget', price=9.99, tags=[])
print(p1 == p2) # False โ __eq__ compares all fields
print(p1 < p2) # True โ order=True adds comparison via sort_index
print(asdict(p1)) # {'name': 'Widget', 'price': 9.99, 'tags': []}
# frozen=True โ immutable (hashable, can be used as dict key)
@dataclass(frozen=True)
class Point:
x: float
y: float
p = Point(1.0, 2.0)
# p.x = 5.0 # FrozenInstanceError
# slots=True (Python 3.10+) โ uses __slots__ for memory efficiency
@dataclass(slots=True)
class SlottedPoint:
x: float
y: float
07 What is itertools and what are its most useful functions? ►
Standard Library The itertools module provides memory-efficient iterator building blocks for working with iterables โ all lazy (yield values on demand, no intermediate lists).
import itertools
# Infinite iterators
itertools.count(10, 2) # 10, 12, 14, 16, ... (infinite counter)
itertools.cycle("ABC") # A, B, C, A, B, C, ... (infinite cycle)
itertools.repeat(42, times=3) # 42, 42, 42 (repeat N times, or infinite)
# Combinatorics
list(itertools.product("AB", repeat=2)) # [('A','A'),('A','B'),('B','A'),('B','B')]
list(itertools.permutations("ABC", 2)) # all 2-item ordered permutations
list(itertools.combinations("ABC", 2)) # [('A','B'),('A','C'),('B','C')] โ no repeats
list(itertools.combinations_with_replacement("AB",2)) # [('A','A'),('A','B'),('B','B')]
# Grouping and slicing
list(itertools.islice(range(100), 5, 15, 2)) # [5,7,9,11,13] โ slice without list
list(itertools.chain([1,2], [3,4], [5,6])) # [1,2,3,4,5,6] โ flatten iterables
data = [("A",1),("A",2),("B",3),("B",4),("A",5)]
for key, group in itertools.groupby(sorted(data, key=lambda x: x[0]), key=lambda x: x[0]):
print(key, list(group)) # A [('A',1),('A',2)] | B [('B',3),('B',4)]
list(itertools.accumulate([1,2,3,4,5])) # [1,3,6,10,15] โ running sum
list(itertools.accumulate([1,2,3,4,5], max)) # [1,2,3,4,5] โ running max
list(itertools.starmap(pow, [(2,3),(3,2),(4,2)])) # [8,9,16]
list(itertools.compress("ABCDEF", [1,0,1,0,1,0])) # ['A','C','E']
list(itertools.dropwhile(lambda x: x < 5, [1,4,6,4,1])) # [6,4,1]
list(itertools.takewhile(lambda x: x < 5, [1,4,6,4,1])) # [1,4]
list(itertools.pairwise([1,2,3,4,5])) # [(1,2),(2,3),(3,4),(4,5)] Python 3.10+
08 What is functools and its most useful utilities? ►
Standard Library The functools module provides higher-order functions and tools for working with functions.
import functools
# lru_cache โ memoisation (cache function results)
@functools.lru_cache(maxsize=128) # maxsize=None for unlimited
def fibonacci(n):
if n < 2: return n
return fibonacci(n-1) + fibonacci(n-2)
fibonacci(100) # fast โ previously computed values cached
fibonacci.cache_info() # CacheInfo(hits=98, misses=101, maxsize=128, currsize=101)
fibonacci.cache_clear() # clear the cache
# Python 3.9+: @functools.cache (unbounded lru_cache)
@functools.cache
def fib(n):
if n < 2: return n
return fib(n-1) + fib(n-2)
# partial โ fix some arguments of a function
def power(base, exponent):
return base ** exponent
square = functools.partial(power, exponent=2)
cube = functools.partial(power, exponent=3)
square(5) # 25
cube(3) # 27
# reduce โ fold a list into a single value
functools.reduce(lambda acc, x: acc + x, [1,2,3,4,5]) # 15
functools.reduce(lambda acc, x: acc * x, [1,2,3,4,5]) # 120
# total_ordering โ define __eq__ and one comparison, get all others
@functools.total_ordering
class Student:
def __init__(self, gpa): self.gpa = gpa
def __eq__(self, other): return self.gpa == other.gpa
def __lt__(self, other): return self.gpa < other.gpa
# __le__, __gt__, __ge__ generated automatically
09 What are Python’s property, getter, setter, and deleter? ►
OOP The @property decorator creates managed attributes โ you control what happens when an attribute is read, written, or deleted. This is the Pythonic way to add validation and computed attributes without breaking the API.
class Temperature:
def __init__(self, celsius: float = 0):
self._celsius = celsius # store in "private" attribute
@property
def celsius(self) -> float: # getter โ called on t.celsius
return self._celsius
@celsius.setter
def celsius(self, value: float): # setter โ called on t.celsius = x
if value < -273.15:
raise ValueError(f"Temperature {value}ยฐC is below absolute zero")
self._celsius = value
@celsius.deleter
def celsius(self): # deleter โ called on del t.celsius
del self._celsius
@property
def fahrenheit(self) -> float: # computed (read-only) property
return self._celsius * 9/5 + 32
@property
def kelvin(self) -> float:
return self._celsius + 273.15
t = Temperature(25)
print(t.celsius) # 25 โ calls getter
print(t.fahrenheit) # 77.0 โ computed
t.celsius = 100 # calls setter
t.celsius = -300 # raises ValueError
# t.fahrenheit = 100 # AttributeError: can't set attribute (no setter)
10 What is the descriptor protocol in Python? ►
OOP A descriptor is any object that implements __get__, __set__, or __delete__. Descriptors power Python’s attribute access system โ @property, @staticmethod, @classmethod, and super() are all implemented as descriptors.
class Validator:
"""Data descriptor โ validates type and range on assignment"""
def __set_name__(self, owner, name):
self.name = name
self.private = f"_{name}"
def __get__(self, obj, objtype=None):
if obj is None:
return self # accessing on class returns the descriptor itself
return getattr(obj, self.private, None)
def __set__(self, obj, value):
raise NotImplementedError
class PositiveNumber(Validator):
def __set__(self, obj, value):
if not isinstance(value, (int, float)) or value <= 0:
raise ValueError(f"{self.name} must be positive, got {value}")
setattr(obj, self.private, value)
class Circle:
radius = PositiveNumber() # descriptor instance
def __init__(self, radius):
self.radius = radius # calls PositiveNumber.__set__
@property
def area(self):
return 3.14159 * self.radius ** 2
c = Circle(5)
print(c.radius) # 5 โ calls __get__
c.radius = 10 # calls __set__ โ validates
c.radius = -1 # raises ValueError
# Non-data descriptor (only __get__) vs Data descriptor (has __set__ or __delete__)
# Data descriptors take precedence over instance __dict__
# Non-data descriptors are overridden by instance __dict__
11 What are metaclasses in Python? ►
Advanced OOP A metaclass is “the class of a class”. Just as instances are created by classes, classes are created by metaclasses. The default metaclass is type. Metaclasses control class creation โ used for ORMs, API frameworks, and enforcement of class contracts.
# Everything is an object โ even classes
type(42) # <class 'int'>
type(int) # <class 'type'> โ int is an instance of type!
type(type) # <class 'type'> โ type is its own metaclass
# type() creates classes dynamically
Dog = type("Dog", (object,), {
"sound": "Woof",
"speak": lambda self: f"{self.sound}!"
})
d = Dog()
d.speak() # "Woof!"
# Custom metaclass โ enforce that all public methods are documented
class DocEnforcer(type):
def __new__(mcs, name, bases, namespace):
for attr_name, attr_val in namespace.items():
if callable(attr_val) and not attr_name.startswith("_"):
if not attr_val.__doc__:
raise TypeError(f"{name}.{attr_name} must have a docstring")
return super().__new__(mcs, name, bases, namespace)
class MyAPI(metaclass=DocEnforcer):
def create(self):
"""Create a resource.""" # โ
has docstring
pass
def delete(self): # โ missing docstring โ TypeError at class creation
pass
# Python's ABCMeta uses metaclasses to create abstract base classes
from abc import ABCMeta, abstractmethod
class Shape(metaclass=ABCMeta):
@abstractmethod
def area(self) -> float: ...
12 What is pytest and how do you write effective tests? ►
Testing pytest is the standard Python testing framework โ more powerful and less boilerplate than the built-in unittest.
pip install pytest pytest-cov pytest-mock
# test_calculator.py
import pytest
from calculator import add, divide
# Basic test โ function starting with test_
def test_add_positive_numbers():
assert add(2, 3) == 5
def test_add_negative_numbers():
assert add(-2, -3) == -5
# Test for exceptions
def test_divide_by_zero():
with pytest.raises(ZeroDivisionError, match="division by zero"):
divide(10, 0)
# Parametrize โ run one test with multiple inputs
@pytest.mark.parametrize("a, b, expected", [
(2, 3, 5),
(0, 5, 5),
(-1, 1, 0),
(100, -50, 50),
])
def test_add(a, b, expected):
assert add(a, b) == expected
# Fixtures โ reusable setup (dependency injection)
@pytest.fixture
def db_connection():
conn = create_test_db()
yield conn # test runs here
conn.close() # teardown after test
@pytest.fixture(scope="session") # shared across all tests in session
def app_config():
return {"env": "test", "debug": True}
def test_user_creation(db_connection):
user = User.create(db_connection, name="Alice")
assert user.id is not None
# Mocking with pytest-mock
def test_sends_email(mocker):
mock_send = mocker.patch("myapp.email.send_email")
register_user("alice@example.com")
mock_send.assert_called_once_with("alice@example.com", "Welcome!")
# Run: pytest -v --cov=myapp --cov-report=html
13 What is the collections module and its most useful classes? ►
Standard Library The collections module provides specialised container types that extend the built-ins.
from collections import Counter, defaultdict, OrderedDict, deque, namedtuple, ChainMap
# Counter โ count hashable objects
words = ["apple", "banana", "apple", "cherry", "banana", "apple"]
c = Counter(words)
print(c) # Counter({'apple': 3, 'banana': 2, 'cherry': 1})
c.most_common(2) # [('apple', 3), ('banana', 2)]
c.update(["apple"]) # Counter({'apple': 4, ...})
c["orange"] # 0 โ no KeyError for missing keys
# defaultdict โ auto-creates missing keys
from collections import defaultdict
graph = defaultdict(list)
graph["A"].append("B") # no KeyError โ "A" auto-initialised to []
word_groups = defaultdict(set)
for word in words:
word_groups[len(word)].add(word)
# deque โ O(1) appends/pops from both ends (vs O(n) for list.insert(0, x))
q = deque(maxlen=3) # fixed-size sliding window
q.append(1); q.append(2); q.append(3); q.append(4) # maxlen=3: [2,3,4]
q.appendleft(0) # [0,2,3]
q.rotate(1) # [3,0,2]
# namedtuple โ tuple with named fields (immutable, memory-efficient)
Point = namedtuple("Point", ["x", "y"])
p = Point(3, 4)
print(p.x, p.y) # 3 4
print(p._asdict()) # OrderedDict([('x', 3), ('y', 4)])
# ChainMap โ combine multiple dicts without copying
defaults = {"color": "blue", "size": "md"}
overrides = {"color": "red"}
config = ChainMap(overrides, defaults)
config["color"] # "red" โ overrides wins; "size": "md" from defaults
14 What is the difference between __str__ and __repr__? ►
OOP
__repr__โ developer-facing representation. Goal: unambiguous, ideally valid Python that recreates the object. Called byrepr(obj)and in the REPL. Fallback when__str__is not defined.__str__โ user-facing, human-readable representation. Called bystr(obj)andprint(obj). Should be informative and concise.
class Point:
def __init__(self, x, y):
self.x, self.y = x, y
def __repr__(self):
# Ideally: eval(repr(p)) == p (recreatable)
return f"Point({self.x!r}, {self.y!r})"
def __str__(self):
# Human-friendly
return f"({self.x}, {self.y})"
p = Point(3, 4)
repr(p) # "Point(3, 4)" โ can recreate: eval("Point(3, 4)") == p
str(p) # "(3, 4)" โ user-friendly
print(p) # (3, 4) โ uses __str__
# In containers: __repr__ is used for items
print([p, Point(1, 2)]) # [Point(3, 4), Point(1, 2)] โ uses __repr__
# f-string controls:
f"{p}" # "(3, 4)" โ uses __str__
f"{p!r}" # "Point(3, 4)" โ forces __repr__
f"{p!s}" # "(3, 4)" โ forces __str__
# Rule: always implement __repr__ at minimum.
# Implement __str__ separately when the user-facing display should differ.
15 What is Python’s abc module and abstract base classes? ►
OOP Abstract Base Classes (ABCs) define interfaces โ they declare methods that subclasses must implement. You cannot instantiate an ABC directly if it has abstract methods.
from abc import ABC, abstractmethod
from typing import Iterator
class DataSource(ABC):
"""Abstract interface for all data sources."""
@abstractmethod
def connect(self) -> None:
"""Establish the connection."""
...
@abstractmethod
def read(self, query: str) -> list[dict]:
"""Execute a query and return results."""
...
@abstractmethod
def close(self) -> None:
"""Release the connection."""
...
def fetch_all(self, query: str) -> list[dict]:
"""Concrete method โ uses abstract interface."""
self.connect()
try:
return self.read(query)
finally:
self.close()
# Concrete implementation โ must implement all abstract methods
class PostgresSource(DataSource):
def connect(self): ...
def read(self, query): ...
def close(self): ...
class MongoSource(DataSource):
def connect(self): ...
def read(self, query): ...
def close(self): ...
# DataSource() # TypeError: Can't instantiate abstract class
pg = PostgresSource() # โ
# isinstance check with ABCs
isinstance(pg, DataSource) # True
issubclass(PostgresSource, DataSource) # True
# Virtual subclass โ register a class as implementing an ABC
# without inheriting from it
DataSource.register(dict)
isinstance({}, DataSource) # True (but doesn't enforce methods)
16 How does Python’s memory model work? What is reference counting? ►
Memory CPython uses reference counting as its primary garbage collection mechanism. Every object has a reference count โ when it reaches zero, the memory is immediately freed.
import sys
import gc
a = [1, 2, 3]
sys.getrefcount(a) # at least 2 (a + the getrefcount argument)
b = a # refcount โ 3
del b # refcount โ 2
del a # refcount โ 1 (held by getrefcount call frame), then 0 โ freed
# Circular reference โ reference counting alone can't handle this
class Node:
def __init__(self): self.other = None
n1, n2 = Node(), Node()
n1.other = n2 # n2 refcount: 2
n2.other = n1 # n1 refcount: 2
del n1, n2 # refcounts drop to 1, not 0 โ memory NOT freed!
# The cyclic garbage collector handles this
# Cyclic GC โ detect and collect reference cycles
gc.collect() # manually trigger cyclic GC
gc.get_count() # (gen0_objects, gen1_objects, gen2_objects)
gc.disable() # disable for performance-critical code with no cycles
# weakref โ reference without incrementing refcount (observer patterns)
import weakref
class Cache:
pass
cache = Cache()
ref = weakref.ref(cache) # weak reference
ref() # <Cache object> โ if still alive
del cache
ref() # None โ object was GC'd
17 What is Python’s __slots__ and when should you use it? ►
Memory By default, Python stores instance attributes in a per-instance __dict__ (a hash table). __slots__ replaces this with a fixed array of attribute slots โ reducing memory usage by 40-50% and speeding up attribute access.
class PointWithDict:
def __init__(self, x, y):
self.x = x
self.y = y
class PointWithSlots:
__slots__ = ("x", "y") # fixed set of attributes
def __init__(self, x, y):
self.x = x
self.y = y
import sys
p1 = PointWithDict(1.0, 2.0)
p2 = PointWithSlots(1.0, 2.0)
print(sys.getsizeof(p1)) # ~48 bytes (+ __dict__ ~232 bytes)
print(sys.getsizeof(p2)) # ~56 bytes (no __dict__)
# Slots prevent adding new attributes dynamically
p2.z = 3.0 # AttributeError: 'PointWithSlots' has no attribute 'z'
# Real impact โ 1 million points
points_dict = [PointWithDict(i, i) for i in range(1_000_000)]
points_slots = [PointWithSlots(i, i) for i in range(1_000_000)]
# points_slots uses ~280MB less RAM
# When to use __slots__:
# - Classes instantiated millions of times (data records, game entities)
# - When memory is a concern
# - When you want to prevent accidental attribute creation
# Caveats:
# - Cannot pickle easily (need __getstate__/__setstate__)
# - Cannot use __weakref__ without adding it to __slots__
# - Inheritance: parent must also use __slots__ for full benefit
18 What is Python’s pathlib module? ►
Standard Library pathlib (Python 3.4+) provides object-oriented filesystem paths โ a cleaner, more Pythonic alternative to os.path.
from pathlib import Path
# Create path objects
p = Path("/home/user/project")
p = Path.cwd() # current working directory
p = Path.home() # home directory
# Build paths with / operator (cross-platform)
config = Path("project") / "config" / "settings.json"
# project/config/settings.json
# Path properties
p = Path("/home/user/project/src/main.py")
p.name # "main.py"
p.stem # "main"
p.suffix # ".py"
p.suffixes # [".py"]
p.parent # Path("/home/user/project/src")
p.parents[1] # Path("/home/user/project")
p.parts # ('/', 'home', 'user', 'project', 'src', 'main.py')
# Existence and type checks
p.exists()
p.is_file()
p.is_dir()
# Read and write (no open() needed for simple cases)
text = Path("data.txt").read_text(encoding="utf-8")
Path("output.txt").write_text("Hello World", encoding="utf-8")
data = Path("data.bin").read_bytes()
# Create directories
Path("output/reports").mkdir(parents=True, exist_ok=True)
# Glob and rglob
list(Path(".").glob("*.py")) # current dir .py files
list(Path(".").rglob("*.py")) # recursive โ all subdirs
list(Path(".").glob("test_*.py")) # test files
# Iterate a directory
for f in Path("src").iterdir():
if f.is_file():
print(f.name)
19 What is Python’s logging module and how do you use it in production? ►
Standard Library The logging module is the standard way to add logging to Python applications. Never use print() in production code โ logging supports levels, handlers, formatters, and structured output.
import logging
import logging.config
# Basic setup (development)
logging.basicConfig(
level=logging.DEBUG,
format="%(asctime)s %(levelname)-8s %(name)s:%(lineno)d โ %(message)s",
datefmt="%Y-%m-%d %H:%M:%S"
)
logger = logging.getLogger(__name__) # always use __name__ for the logger
logger.debug("Detailed debug info")
logger.info("Server started on port 8000")
logger.warning("Disk usage above 80%%")
logger.error("Failed to connect to DB: %s", error) # lazy formatting
logger.critical("System shutting down")
logger.exception("Unhandled error", exc_info=True) # includes traceback
# Production setup โ structured JSON logging (use python-json-logger)
LOGGING_CONFIG = {
"version": 1,
"disable_existing_loggers": False,
"handlers": {
"console": {
"class": "logging.StreamHandler",
"formatter": "json"
},
"file": {
"class": "logging.handlers.RotatingFileHandler",
"filename": "app.log",
"maxBytes": 10_000_000, # 10MB
"backupCount": 5,
"formatter": "json"
}
},
"root": {"level": "INFO", "handlers": ["console", "file"]}
}
logging.config.dictConfig(LOGGING_CONFIG)
20 What are Python’s map(), filter(), and reduce()? ►
Functional
from functools import reduce numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] # map() โ apply a function to every item (lazy iterator) squares = list(map(lambda x: x ** 2, numbers)) # [1, 4, 9, 16, 25, 36, 49, 64, 81, 100] # map with multiple iterables products = list(map(lambda x, y: x * y, [1,2,3], [4,5,6])) # [4, 10, 18] # filter() โ keep items where function returns True (lazy iterator) evens = list(filter(lambda x: x % 2 == 0, numbers)) # [2, 4, 6, 8, 10] filter(None, [0, 1, None, 2, "", "hello", False]) # [1, 2, "hello"] โ removes falsy values # reduce() โ fold iterable into single value (from functools) total = reduce(lambda acc, x: acc + x, numbers) # 55 product = reduce(lambda acc, x: acc * x, numbers) # 3628800 maximum = reduce(lambda acc, x: acc if acc > x else x, numbers) # 10 # Modern Pythonic alternative โ comprehensions are often preferred squares = [x ** 2 for x in numbers] # vs map() evens = [x for x in numbers if x % 2 == 0] # vs filter() total = sum(numbers) # vs reduce() for addition product = 1 for x in numbers: product *= x # vs reduce() for multiplication
21 What is Python’s enum module? ►
Standard Library The enum module provides enumeration types โ a set of symbolic names bound to unique values. Enums are more readable and safer than raw strings or magic numbers.
from enum import Enum, IntEnum, Flag, auto, unique
# Basic enum
class Color(Enum):
RED = 1
GREEN = 2
BLUE = 3
Color.RED # <Color.RED: 1>
Color.RED.name # "RED"
Color.RED.value # 1
Color(1) # <Color.RED: 1> โ lookup by value
Color["RED"] # <Color.RED: 1> โ lookup by name
list(Color) # [Color.RED, Color.GREEN, Color.BLUE]
Color.RED == Color.RED # True
Color.RED == 1 # False โ no implicit comparison with int
# auto() โ auto-assign values
class Direction(Enum):
NORTH = auto() # 1
SOUTH = auto() # 2
EAST = auto() # 3
WEST = auto() # 4
# IntEnum โ members are also integers (useful for comparisons)
class Priority(IntEnum):
LOW = 1
MEDIUM = 2
HIGH = 3
Priority.HIGH > Priority.LOW # True
# @unique โ raises ValueError if any two members share a value
@unique
class Status(Enum):
PENDING = "pending"
ACTIVE = "active"
CANCELLED = "cancelled"
# Flag โ bitwise operations for permission-style flags
class Permission(Flag):
READ = auto() # 1
WRITE = auto() # 2
EXECUTE = auto() # 4
user_perms = Permission.READ | Permission.WRITE # READ|WRITE
Permission.READ in user_perms # True
📝 Knowledge Check
Test your understanding of advanced Python patterns and standard library features.