Python Topic
Advanced Language Features & Internals
1. What are Python's decorators under the hood? How does @functools.wraps work?
-
Answer:
Decorators are essentially syntactic sugar for function transformation.@decoratoris equivalent tofunc = decorator(func).-
Under the hood: A decorator is a callable that takes a function and returns a new function (or any callable), replacing the original.
-
@functools.wraps: When you decorate a function, you lose its original metadata (__name__,__doc__,__module__).wrapsis a decorator itself that copies this metadata from the original function to the wrapper. It's implemented by updating the__dict__of the wrapper function.
python
from functools import wraps def my_decorator(func): @wraps(func) # Preserves func's metadata def wrapper(*args, **kwargs): return func(*args, **kwargs) return wrapper -
2. Explain the difference between @staticmethod and @classmethod. When would you use each?
-
Answer:
-
@classmethod: Takes the class (cls) as the first implicit argument. It can access and modify class state. Used for alternative constructors or operations that require the class but not a specific instance. -
@staticmethod: Does not take any implicit first argument. It behaves like a plain function but belongs to the class's namespace. Used for utility functions that have a logical connection to the class but don't need class or instance data.
python
class MyClass: @classmethod def from_string(cls, string): # cls is MyClass, can create an instance return cls(...) # Acts as an alternative constructor @staticmethod def is_valid(data): # No access to cls or self, just a utility return isinstance(data, int) -
3. What are Coroutines and how do they differ from Generators?
-
Answer:
-
Generators are primarily used for producing iteration (data production) using
yield. They are a subset of coroutines. -
Coroutines are more general and used for consuming data and supporting more complex concurrency patterns. They can both
yieldandreceivevalues usingyield(orawaitinasyncio). While generators are pull-based (data is consumed), coroutines are push-based (data can be sent to them). -
Modern Python: The
asynciolibrary usesasync/awaitsyntax to build coroutines that are more readable and less error-prone than the older generator-based coroutines.
-
4. How does Python's garbage collection handle reference cycles?
-
Answer:
Reference counting alone cannot collect reference cycles (e.g., object A refers to B, B refers to A). Python employs a generational garbage collector (GC) as a secondary mechanism.-
Generations: Objects are grouped into three generations (0, 1, 2). New objects go into generation 0.
-
Cycle Detection: The GC periodically runs, starting with the youngest generation. It traces object references to identify unreachable cycles, even if their reference count isn't zero.
-
Collection: If an object survives a GC collection, it's moved to the next, older generation. This is efficient because most objects die young.
-
5. What is the collections.abc module used for? Give examples.
-
Answer:
This module provides abstract base classes (ABCs) to define and test if a class provides a specific interface (like a sequence, mapping, or iterator). It helps in implementing custom container types correctly.-
Use Cases:
-
Inheritance: Create a custom list by inheriting from
collections.abc.MutableSequence. You only need to implement__getitem__,__setitem__,__delitem__,__len__, andinsert, and you getappend,pop, etc., for free. -
Type Checking:
isinstance(my_obj, collections.abc.Iterable)is more reliable than checking for__iter__directly.
-
python
from collections.abc import MutableSequence class MyList(MutableSequence): def __init__(self): self._data = [] def __getitem__(self, index): return self._data[index] def __setitem__(self, index, value): self._data[index] = value def __delitem__(self, index): del self._data[index] def __len__(self): return len(self._data) def insert(self, index, value): self._data.insert(index, value) # MyList automatically has .append(), .pop(), etc. -
Concurrency & Performance Deep Dive
6. What are the key differences between asyncio.create_task(), asyncio.gather(), and asyncio.as_completed()?
-
Answer:
-
asyncio.create_task(coro): Schedules a coroutine to run "in the background" and returns aTaskobject. It starts the task but doesn't wait for it to complete. Used for fire-and-forget concurrency. -
asyncio.gather(*coros): Runs multiple coroutines concurrently and waits for all to finish. It returns a list of results in the same order as the input. You can handle exceptions collectively. -
asyncio.as_completed(coros): Returns an iterator that yields coroutines/futures as they complete. This allows you to process results in the order they finish, not the order they were submitted.
-
7. How does the selectors module work, and what problem does it solve?
-
Answer:
Theselectorsmodule provides high-level I/O multiplexing. It allows a single thread to monitor multiple file descriptors (sockets, files) for I/O readiness.-
Problem Solved: Building efficient, single-threaded concurrent servers without the overhead of threading. It's the foundation for
asyncio's event loop. -
How it works: You register file objects with a selector, specifying events you're interested in (e.g.,
EVENT_READ,EVENT_WRITE). Theselect()method blocks until one or more file objects are ready, then returns a list of ready objects.
-
8. Explain the concept of "Lazy Evaluation" in Python. Where is it used?
-
Answer:
Lazy evaluation defers the computation of a value until it is actually needed. This saves memory and can improve performance.-
Examples:
-
Generators:
(x for x in range(10))doesn't compute all values upfront. -
range(): In Python 3,rangeis a lazy sequence; it doesn't generate all numbers in memory. -
itertoolsfunctions:count,cycle,isliceare all lazy. -
Django Querysets: They are lazy; the database query isn't executed until the data is iterated over.
-
-
9. What is the purpose of the __call__ method? Give a practical use case.
-
Answer:
The__call__method allows an instance of a class to be called as a function.-
Use Case 1: Functors (Function Objects): Creating stateful functions where the behavior depends on internal state.
python
class Adder: def __init__(self, n): self.n = n def __call__(self, x): return self.n + x add5 = Adder(5) print(add5(10)) # Output: 15 (the instance is callable!)-
Use Case 2: Decorator Classes: Creating decorators that maintain state across calls.
python
class CountCalls: def __init__(self, func): self.func = func self.count = 0 def __call__(self, *args, **kwargs): self.count += 1 return self.func(*args, **kwargs) @CountCalls def say_hello(): print("Hello!") -
Testing & Debugging Advanced
10. What are fixtures in pytest? How do they help in testing?
-
Answer:
Fixtures are functions that provide a fixed, reliable baseline for tests. They run before (and after) the test functions that use them.-
Help they provide:
-
Setup/Teardown: Manage test dependencies (database connections, temporary files, API clients).
-
Dependency Injection: Pass reusable data or objects to tests without repetition.
-
Scope: Control how often a fixture is executed (e.g.,
function,class,module,session).
-
python
import pytest @pytest.fixture def database_connection(): conn = create_connection() # Setup yield conn # Provide value to test conn.close() # Teardown def test_query(database_connection): # Fixture is injected result = database_connection.execute("...") assert result is not None -
11. How do you mock an object in Python tests? Explain the unittest.mock module.
-
Answer:
unittest.mockallows you to replace parts of your system under test with mock objects and make assertions about how they were used.-
Mock: A flexible object that can be used to replace any object. You can set its return value, side effects, and check if it was called. -
patch: A decorator/context manager used to replace a target with aMockobject for the duration of a test. It's the most common way to inject mocks.
python
from unittest.mock import patch, MagicMock @patch('my_module.requests.get') # Patch where the object is used def test_api_call(mock_get): mock_get.return_value.status_code = 200 # Configure the mock result = my_function_that_uses_requests() mock_get.assert_called_once_with('https://api.example.com') assert result == 200 -
12. What is "property-based testing" and which libraries support it in Python?
-
Answer:
Property-based testing verifies that certain properties or invariants hold for a wide range of automatically generated input data, rather than just for specific examples.-
How it works: The library (like
Hypothesis) generates many random inputs that match a specified strategy. The test is run against all these inputs, trying to find a minimal failing case. -
Libraries:
Hypothesisis the main library. It helps find edge cases you might not have considered.
python
from hypothesis import given, strategies as st @given(st.integers(), st.integers()) def test_commutative_property(a, b): assert my_add(a, b) == my_add(b, a) # Should hold for all integers -
System Design & Architecture
13. How would you design a caching system in Python? What are the trade-offs?
-
Answer:
-
In-Memory (e.g.,
functools.lru_cache, dict): Fastest, but doesn't persist across process restarts and consumes application memory. -
External (e.g., Redis, Memcached): Shared across processes/machines, can persist, but adds network latency and dependency on another service.
-
File-based: Simple, persistent, but slow and not suitable for high-frequency writes.
-
Trade-offs: Consider performance vs. persistence, memory usage vs. scalability, and complexity vs. features (e.g., TTL, eviction policies).
-
14. Explain the concept of Dependency Injection (DI) in Python. How can it be implemented?
-
Answer:
DI is a pattern where an object receives its dependencies from an external source rather than creating them itself. This promotes loose coupling and testability.-
Implementation:
-
Constructor Injection: Pass dependencies via the
__init__method. This is the most common and Pythonic way. -
Framework: Use a DI container/library like
dependency-injectorfor complex applications to manage object creation and wiring.
-
python
# Without DI (tight coupling) class Service: def __init__(self): self.db = PostgreSQLDatabase() # Hard-coded dependency # With DI (loose coupling) class Service: def __init__(self, database): # Dependency is injected self.db = database # Easy to test def test_service(): mock_db = Mock() service = Service(mock_db) # Inject a mock -
15. What are the key principles of API design in Python web frameworks?
-
Answer:
-
RESTful Principles: Use HTTP methods correctly (GET, POST, PUT, DELETE), statelessness, proper resource naming (
/users/{id}). -
Validation: Always validate and sanitize input data (using Pydantic, Marshmallow, or framework-built-in tools).
-
Error Handling: Return consistent, informative error responses with appropriate HTTP status codes.
-
Documentation: Use tools like OpenAPI/Swagger (with FastAPI, DRF) for auto-generated documentation.
-
Versioning: Plan for API versioning from the start (e.g., in the URL
/v1/usersor headers). -
Authentication & Authorization: Implement robust, standard auth (OAuth2, JWT).
-
16. How do you manage configuration in a large Python application?
-
Answer:
-
Environment Variables: For secrets (API keys, DB URLs) and environment-specific settings. Use
os.getenv()or libraries likepydantic-settings. -
Configuration Files: For non-secret settings. Use
.ini,.yaml,.toml, or.json. Libraries likepydanticcan load and validate these. -
12-Factor App Method: Strictly use environment variables, as they are easy to change between deployments and are language-agnostic.
-
Hierarchical Configuration: Combine multiple sources (defaults → config file → environment variables → command-line arguments).
-
Ecosystem & Libraries
17. When would you use NumPy over built-in Python lists? What is its core advantage?
-
Answer:
Use NumPy for numerical computations on large, homogeneous data arrays.-
Core Advantages:
-
Performance: Operations are implemented in C and are vectorized, meaning they operate on entire arrays without Python loops. This is orders of magnitude faster.
-
Memory Efficiency: NumPy arrays are densely packed in memory, while Python lists are arrays of pointers to Python objects, which is much heavier.
-
Functionality: Provides a vast collection of mathematical functions (linear algebra, Fourier transforms, random number generation) optimized for arrays.
-
-
18. Explain the difference between Pandas .loc, .iloc, and .at/.iat.
-
Answer:
-
.loc: Label-based indexing. You use the row and column labels.df.loc['row_label', 'col_label']. Slicing is inclusive of the end. -
.iloc: Integer position-based indexing. You use the row and column integer positions (0-based).df.iloc[0, 1]. Slicing is exclusive of the end (like Python lists). -
.at/.iat: Scalar accessors for fast access to a single value.df.at['row_label', 'col_label']is much faster than.locfor getting/setting a single value. Similarly,df.iat[0, 1]is the integer-position version.
-
19. What is the role of the __init__.py file in modern Python? How has it changed?
-
Answer:
-
Traditional Role (Pre-Python 3.3): It was required to make a directory a Python package. It could also contain package initialization code and define
__all__forfrom package import *. -
Modern Role (Implicit Namespace Packages): Since Python 3.3,
__init__.pyis not required to create a package. Directories without it are called "namespace packages." However, it's still widely used for:-
Running initialization code for the package.
-
Defining
__all__. -
Conveniently importing submodule contents to the package level (e.g.,
from .submodule import MyClassin__init__.pyallows users to dofrom package import MyClass).
-
-
20. Describe a time you had to optimize a slow Python application. What was your process and what tools did you use?
-
Answer:
(This is another behavioral question. Structure your answer clearly.)-
1. Identify the Bottleneck: "First, I profiled the code to find the slow part. I used
cProfileto get a high-level view andline_profilerfor a line-by-line breakdown of the critical function." -
2. Analyze the Cause: "The profiling revealed the bottleneck was a nested
forloop performing a naive calculation on a large list." -
3. Implement a Solution: "I refactored the algorithm. I replaced the list with a NumPy array and used vectorized operations, which eliminated the Python loops and leveraged optimized C code under the hood. This gave us a 50x speedup."
-
4. Verify and Test: "I verified the results were identical and ran the test suite to ensure no regressions. I also used
memory_profilerto confirm we hadn't increased memory usage."
-
These questions dive deeper into architecture, performance tuning, and advanced language mechanics, which are crucial topics for senior-level Python roles. Good luck with your preparation!
Comments (0)
No comments yet. Be the first to comment!