From d9621dbd9ffcee8307f9563db7317fe2723e2d5a Mon Sep 17 00:00:00 2001 From: Devinterview-io <76989322+Devinterview-io@users.noreply.github.com> Date: Sat, 24 Jan 2026 15:12:44 -0500 Subject: [PATCH 1/7] Update python interview questions --- README.md | 550 +++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 441 insertions(+), 109 deletions(-) diff --git a/README.md b/README.md index e6c44ef..1270c1b 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# 100 Core Python Interview Questions in 2026 +# Top 100 Python Interview Questions

@@ -13,271 +13,603 @@ ## 1. What are the _key features_ of _Python_? -**Python** is a versatile and popular programming language known for its simplicity, **elegant syntax**, and a vast ecosystem of libraries. Let's look at some of the key features that make Python stand out. +**Python** is a versatile and popular programming language known for its simplicity, **elegant syntax**, and a vast ecosystem of libraries. As of 2026, its relevance and adoption continue to grow across various industries. Let's look at some of the key features that make Python stand out. ### Key Features of Python #### 1. Interpreted and Interactive -Python uses an interpreter, allowing developers to run code **line-by-line**, making it ideal for rapid prototyping and debugging. +Python code is executed **line-by-line** by an interpreter, not compiled to machine code before runtime. This characteristic facilitates **rapid prototyping**, experimentation, and easier debugging, as errors are typically detected only when that specific line of code is executed. Python also provides an interactive shell where developers can execute commands and see results immediately. + +```python +# Example of interactive mode +>>> print("Hello, Python!") +Hello, Python! +>>> x = 10 +>>> y = 20 +>>> x + y +30 +``` #### 2. Easy to Learn and Read -Python's **clean, readable syntax**, often resembling plain English, reduces the cognitive load for beginners and experienced developers alike. +Python's **clean, readable syntax** closely resembles plain English, significantly reducing the cognitive load for both beginners and experienced developers. Its mandatory use of **significant whitespace** (indentation) for defining code blocks enforces a consistent and highly readable code style, making it easier to maintain and understand code written by others. #### 3. Cross-Platform Compatibility -Python is versatile, running on various platforms, such as Windows, Linux, and macOS, without requiring platform-specific modifications. +Python is a **cross-platform language**, meaning code written on one operating system (e.g., Windows) can run seamlessly on others (e.g., Linux, macOS) without requiring platform-specific modifications. This "write once, run anywhere" capability is facilitated by the Python interpreter being available for diverse platforms. #### 4. Modular and Scalable -Developers can organize their code into modular packages and reusabale functions. +Python promotes modular programming through **modules** and **packages**. Developers can organize code into reusable files (modules) and directories of modules (packages), which can then be easily imported and used in other parts of a project. This modularity enhances code organization, reusability, and maintainability, making Python suitable for developing **large-scale and complex applications**. + +```python +# Example of importing a module +import math + +radius = 5 +area = math.pi * (radius ** 2) +print(f"Area of circle: {area}") +``` #### 5. Rich Library Ecosystem -The Python Package Index (PyPI) hosts over 260,000 libraries, providing solutions for tasks ranging from web development to data analytics. +The Python Package Index (PyPI) is a vast repository hosting over **500,000 libraries and frameworks** (and growing rapidly). This **extensive ecosystem** provides ready-to-use solutions for almost any programming task, from: +* **Web Development**: Django, Flask, FastAPI +* **Data Science & Analytics**: NumPy, Pandas, SciPy +* **Machine Learning & AI**: TensorFlow, PyTorch, scikit-learn +* **Automation**: Selenium, Ansible +* **GUI Development**: PyQt, Kivy, Tkinter -#### 6. Exceptionally Versatile +This rich collection significantly speeds up development and innovation. -From web applications to scientific computing, Python is equally proficient in diverse domains. +#### 6. General-Purpose and Versatile -#### 7. Memory Management +Python is a **general-purpose programming language**, meaning it is not specialized for any single domain but is rather highly adaptable to a multitude of applications. Its versatility allows it to be effectively used in areas such as web and desktop applications, scientific and numerical computing, artificial intelligence and machine learning, data analysis, network programming, scripting, and automation. -Python seamlessly allocates and manages memory, shielding developers from low-level tasks, such as memory deallocation. +#### 7. Automatic Memory Management + +Python features **automatic memory management**, relieving developers from manual memory allocation and deallocation tasks. It primarily uses **reference counting** and a **generational garbage collector** to automatically detect and reclaim memory occupied by objects that are no longer referenced. This significantly reduces the chances of memory leaks and simplifies application development. #### 8. Dynamically Typed -Python infers the data type of a variable during execution, easing the declartion and manipulation of variables. +Python is a **dynamically typed language**, which means the type of a variable is determined at runtime, not at compile time. Developers do not need to explicitly declare the data type of a variable; Python infers it during execution. This offers flexibility and faster development cycles, though it can sometimes lead to type-related errors only detectable at runtime. + +```python +# Example of dynamic typing +my_variable = 10 # my_variable is an integer +print(type(my_variable)) # Output: + +my_variable = "hello" # Now my_variable is a string +print(type(my_variable)) # Output: +``` -#### 9. Object-Oriented +#### 9. Object-Oriented Programming (OOP) -Python supports object-oriented paradigms, where everything is an **object**, offering attributes and methods to manipulate data. +Python is a **multi-paradigm language** that fully supports and leverages the **object-oriented programming (OOP)** paradigm. In Python, everything is an **object**, and it facilitates concepts like **classes**, **objects**, **inheritance**, **polymorphism**, and **encapsulation**. This object-oriented nature helps in structuring complex applications into manageable and reusable components. -#### 10. Extensible +#### 10. Extensible & Embeddable -With its C-language API, developers can integrate performance-critical tasks and existing C modules with Python. +Python is highly **extensible**, allowing developers to integrate code written in other languages, particularly C and C++, for performance-critical tasks. This is commonly done using the **CPython C API**, `ctypes` module, or tools like **Cython**. Conversely, Python is also **embeddable**, meaning the Python interpreter can be integrated into applications written in other languages, allowing them to execute Python code and leverage its scripting capabilities.
## 2. How is _Python_ executed? -**Python** source code is processed through various steps before it can be executed. Let's explore the key stages in this process. +### How Python is Executed + +Python source code undergoes a multi-stage process involving both **compilation** and **interpretation** before it can be executed. Understanding these stages is key to comprehending Python's performance characteristics and portability. ### Compilation & Interpretation -Python code goes through both **compilation** and **interpretation**. +Python employs a hybrid execution model: -- **Bytecode Compilation**: High-level Python code is transformed into low-level bytecode by the Python interpreter with the help of a compiler. Bytecode is a set of instructions that Python's virtual machine (PVM) can understand and execute. - -- **On-the-fly Interpretation**: The PVM reads and executes bytecode instructions in a step-by-step manner. - -This dual approach known as "compile and then interpret" is what sets Python (and certain other languages) apart. +- **Bytecode Compilation**: When a Python program is run, the source code (`.py` files) is first translated into an intermediate format called **bytecode**. This process is performed by the Python interpreter's **compiler component**. Bytecode is a low-level, platform-independent set of instructions, typically stored in `.pyc` files (Python compiled files) within `__pycache__` directories for faster loading on subsequent runs. +- **Bytecode Interpretation**: The generated bytecode is then executed by the **Python Virtual Machine (PVM)**. The PVM is a runtime engine that reads and executes bytecode instructions one by one. This step-by-step execution is the "interpretation" phase. + +This "compile-to-bytecode then interpret" approach is characteristic of many virtual machine-based languages and offers significant advantages. ### Bytecode versus Machine Code Execution -While some programming languages compile directly to machine code, Python compiles to bytecode. This bytecode is then executed by the Python virtual machine. This extra step of bytecode execution **can make Python slower** in certain use-cases when compared to languages that compile directly to machine code. +Some programming languages (like C++ or Rust) compile directly to **machine code**, which is specific to a computer's architecture (e.g., x86, ARM) and can be executed directly by the CPU. Python, on the other hand, compiles to **bytecode**. -The advantage, however, is that bytecode is platform-independent. A Python program can be run on any machine with a compatible PVM, ensuring cross-platform support. +- **Performance**: The extra layer of abstraction introduced by the PVM interpreting bytecode *can* make standard Python implementations (like CPython) slower in certain CPU-bound scenarios compared to languages that compile directly to optimized machine code. However, constant improvements in CPython's interpreter and standard library, along with the increasing use of optimized C extensions, significantly mitigate this in many real-world applications. +- **Portability**: The primary advantage of bytecode is its **platform independence**. A Python program's bytecode can be executed on any machine that has a compatible PVM, regardless of the underlying operating system or hardware architecture. This ensures excellent cross-platform support. ### Source Code to Bytecode: Compilation Steps -1. **Lexical Analysis**: The source code is broken down into tokens, identifying characters and symbols for Python to understand. -2. **Syntax Parsing**: Tokens are structured into a parse tree to establish the code's syntax and grammar. -3. **Semantic Analysis**: Code is analyzed for its meaning and context, ensuring it's logically sound. -4. **Bytecode Generation**: Based on the previous steps, bytecode instructions are created. +The process of translating Python source code into bytecode involves several standard compiler phases: -### Just-In-Time (JIT) Compilation +1. #### Lexical Analysis (Scanning) + The source code is broken down into a stream of **tokens**. Tokens are the smallest meaningful units in the language, such as keywords (`def`, `return`), identifiers (`example_func`), operators (`*`), and literals (`15`, `20`). +2. #### Syntax Parsing + The stream of tokens is then analyzed to ensure it conforms to Python's grammatical rules. This phase constructs an **Abstract Syntax Tree (AST)**, which is a hierarchical representation of the program's structure. +3. #### Semantic Analysis + The AST is checked for semantic correctness. This involves tasks like ensuring variables are defined before use, type checking (where applicable), and verifying that operations are valid for their operands. Python performs some semantic checks at this stage, though many type-related checks occur at runtime due to its dynamic nature. +4. #### Bytecode Generation + Finally, based on the validated AST, the **bytecode instructions** are generated. These instructions are tailored for execution by the PVM. -While Python typically uses a combination of interpretation and compilation, **JIT** boosts efficiency by selectively compiling parts of the program that are frequently used or could benefit from optimization. +### Just-In-Time (JIT) Compilation and Alternative Implementations -JIT compiles sections of the program to machine code on-the-fly. This direct machine code generation for frequently executed parts can significantly speed up those segments, blurring the line between traditional interpreters and compilers. +While the default CPython interpreter primarily relies on bytecode interpretation, the concept of **Just-In-Time (JIT) compilation** is highly relevant to Python's execution ecosystem. + +- **JIT's Role**: A JIT compiler compiles parts of the bytecode into native machine code *during runtime*, typically focusing on frequently executed sections (hot paths). This can significantly boost performance by eliminating the overhead of bytecode interpretation for those sections. +- **CPython vs. JIT**: It's crucial to note that **standard CPython does not include a JIT compiler** as of 2026. CPython's performance improvements mostly come from optimizing its interpreter loop, specializing opcodes, and better memory management. +- **Alternative Implementations**: JIT compilation is a core feature of alternative Python implementations like **PyPy**. PyPy's JIT compiler often delivers substantial speedups for pure Python code compared to CPython, especially for long-running processes with repetitive computations. Other projects and experimental CPython forks might explore JIT integration in the future, but it's not a standard feature of the mainstream CPython distribution. + +Therefore, when discussing "how Python is executed," it's generally understood to refer to CPython's bytecode interpretation model, while acknowledging that other implementations leverage JIT for enhanced performance. ### Code Example: Disassembly of Bytecode +Python's `dis` module allows us to inspect the bytecode generated for a function or code object, providing a direct view into the instructions the PVM will execute. + ```python import dis -def example_func(): - return 15 * 20 +def calculate_product(): + a = 15 + b = 20 + return a * b # Disassemble to view bytecode instructions -dis.dis(example_func) +dis.dis(calculate_product) ``` -Disassembling code using Python's `dis` module can reveal the underlying bytecode instructions that the PVM executes. Here's the disassembled output for the above code: +Here's the disassembled output for the `calculate_product` function: ```plaintext - 4 0 LOAD_CONST 2 (300) - 2 RETURN_VALUE + 4 0 LOAD_CONST 1 (15) # Load integer 15 + 2 STORE_FAST 0 (a) # Store it in local variable 'a' + + 5 4 LOAD_CONST 2 (20) # Load integer 20 + 6 STORE_FAST 1 (b) # Store it in local variable 'b' + + 6 8 LOAD_FAST 0 (a) # Load value of 'a' + 10 LOAD_FAST 1 (b) # Load value of 'b' + 12 BINARY_MULTIPLY # Multiply the top two values on stack + 14 RETURN_VALUE # Return the result ``` + +This output clearly shows the PVM instructions for loading constants, storing them in local variables, loading the variables again, performing the multiplication, and finally returning the value. This granular view demonstrates the step-by-step nature of bytecode execution.
## 3. What is _PEP 8_ and why is it important? -**PEP 8** is a style guide for Python code that promotes code consistency, readability, and maintainability. It's named after Python Enhancement Proposal (PEP), the mechanism used to propose and standardize changes to the Python language. - -PEP 8 is not a set-in-stone rule book, but it provides general guidelines that help developers across the Python community write code that's visually consistent and thus easier to understand. - -### Key Design Principles +### What is PEP 8 and why is it important? -PEP 8 emphasizes: +**PEP 8** is the official style guide for Python code. Its primary purpose is to promote code consistency, readability, and maintainability across the Python community. The name stands for **P**ython **E**nhancement **P**roposal, and PEPs are the primary mechanism for proposing and standardizing new features, changes, and conventions for the Python language itself. -- **Readability**: Code should be easy to read and understand, even by someone who didn't write it. -- **Consistency**: Codebase should adhere to a predictable style so there's little cognitive load in reading or making changes. -- **One Way to Do It**: Instead of offering multiple ways to write the same construct, PEP 8 advocates for a single, idiomatic style. +While not a rigid set of mandatory rules, PEP 8 provides comprehensive guidelines that help developers write Python code that is visually uniform and thus significantly easier to understand, navigate, and debug for anyone reading it. Adherence to PEP 8 fosters a common coding style, reducing cognitive load when moving between different Python projects or collaborating with other developers. -### Base Rules +### Key Design Principles -- **Indentation**: Use 4 spaces for each level of logical indentation. -- **Line Length**: Keep lines of code limited to 79 characters. This number is a guideline; longer lines are acceptable in certain contexts. -- **Blank Lines**: Use them to separate logical sections but not excessively. +PEP 8's recommendations are rooted in several fundamental design principles: + +* #### Readability + Code should be as easy to read and understand as plain English, even by someone unfamiliar with the codebase or the original author. Clear and consistent formatting significantly contributes to this. +* #### Consistency + A codebase should adhere to a predictable and uniform style. This minimizes surprises and allows developers to focus on the logic rather than deciphering varied formatting choices. +* #### Maintainability + Consistent and readable code is inherently easier to maintain, update, and debug over time. It reduces the effort required to onboard new team members and ensures the longevity of the software. +* #### Explicit over Implicit + Python's philosophy often favors explicit code over implicit code. PEP 8 extends this by encouraging clear and unambiguous formatting. + +### Core Guidelines + +PEP 8 covers a wide range of formatting conventions. Some of the most frequently applied guidelines include: + +* #### Indentation + Use **4 spaces** per level of logical indentation. Tabs should be avoided entirely for indentation. + ```python + # PEP 8 compliant indentation + def example_function(arg1, arg2): + if arg1 > arg2: + print("arg1 is greater") + else: + print("arg2 is greater or equal") + ``` +* #### Line Length + Limit all lines of code to a maximum of **79 characters**. For docstrings and comments, the limit is often recommended to be **72 characters**. This guideline improves readability, especially when viewing code on smaller screens or in side-by-side diffs. While the official PEP 8 recommendation remains 79 characters, it's worth noting that many modern Python projects, especially those using auto-formatters like `Black` or `Ruff`, often adopt slightly longer line lengths (e.g., 88 or 120 characters) for practical reasons, as long as readability isn't compromised. +* #### Blank Lines + Use blank lines to separate logical sections of code, such as functions, classes, and larger blocks within functions. Generally, two blank lines separate top-level function and class definitions, and one blank line separates method definitions within a class. Avoid excessive blank lines. +* #### Imports + Imports should typically be on separate lines and grouped as follows: standard library imports, third-party imports, and local application-specific imports. Each group should be separated by a blank line. + ```python + import os + import sys + + import requests + from numpy import array + + from my_package.my_module import MyClass + ``` ### Naming Styles -- **Class Names**: Prefer `CamelCase`. -- **Function and Variable Names**: Use `lowercase_with_underscores`. -- **Module Names**: Keep them short and in `lowercase`. +Naming conventions are critical for understanding the purpose of different code elements. + +* #### Class Names + Use `CapWords` (also known as `CamelCase`), where each word in the name starts with a capital letter. + * `class MyClassName:` + * `class HTTPRequest:` +* #### Function and Variable Names + Use `snake_case` (all lowercase with words separated by underscores). + * `def my_function():` + * `variable_name = 10` +* #### Module Names + Use short, all-lowercase names. Underscores can be used if it improves readability for multi-word module names. + * `import mymodule` + * `import another_module` +* #### Constants + Use `ALL_CAPS_WITH_UNDERSCORES` to denote constants (variables whose values are intended to remain unchanged throughout the program's execution). + * `MAX_CONNECTIONS = 100` + * `DEFAULT_TIMEOUT = 30` ### Documentation -- Use triple quotes for documentation strings. -- Comments should be on their own line and explain the reason for the following code block. +* #### Docstrings + Use triple-quoted strings (`"""Docstring content"""` or `'''Docstring content'''`) for documentation strings (docstrings) for modules, classes, functions, and methods. These explain the purpose and usage of the code. +* #### Comments + Comments (`#`) should be used to explain *why* certain code exists or how a complex piece of code works, rather than just *what* it does (which should be evident from the code itself). They should be concise, up-to-date, and on their own line preceding the code they refer to. ### Whitespace Usage -- **Operators**: Surround them with a single space. -- **Commas**: Follow them with a space. +Appropriate use of whitespace enhances readability around operators and punctuation. + +* #### Operators + Surround binary operators (e.g., `=`, `+`, `-`, `*`, `/`, `==`, `>`, `<`, `and`, `or`) with a single space on either side. + * `x = 1 + 2` + * `if a == b:` +* #### Commas, Semicolons, Colons + Always follow a comma, semicolon, or colon with a space. Avoid spaces before them. + * `my_list = [1, 2, 3]` + * `def func(arg1, arg2):` +* #### Parentheses, Brackets, Braces + Avoid extraneous whitespace immediately inside parentheses, brackets, or braces. + * `my_list[index]` (not `my_list[ index ]`) + * `my_tuple = (1, 2)` (not `my_tuple = ( 1, 2 )`) -### Example: Directory Walker +### Example: PEP 8 Compliant Directory Walker -Here is the `PEP8` compliant code: +The following Python code snippet adheres to several PEP 8 guidelines, demonstrating proper indentation, naming conventions, and spacing. ```python import os -def walk_directory(path): - for dirpath, dirnames, filenames in os.walk(path): +def walk_directory_and_print_files(base_path): + """ + Recursively walks a given directory path and prints the full path of each file found. + + Args: + base_path (str): The starting directory path to walk. + """ + for dirpath, dirnames, filenames in os.walk(base_path): for filename in filenames: file_path = os.path.join(dirpath, filename) - print(file_path) + # This print statement is for demonstration; in a real app, + # you might process the file_path further. + print(f"Found file: {file_path}") -walk_directory('/path/to/directory') +# Example usage (replace with an actual path for execution) +# Ensure the path exists, e.g., 'C:/Users/YourUser/Documents' or '/home/user/my_docs' +if __name__ == "__main__": + # You might get this path from user input or configuration + test_path = './my_test_dir' # Current directory example + + # Create a dummy directory and file for demonstration if it doesn't exist + if not os.path.exists(test_path): + os.makedirs(test_path) + with open(os.path.join(test_path, 'example.txt'), 'w') as f: + f.write("Hello PEP 8!") + + walk_directory_and_print_files(test_path) ```
## 4. How is memory allocation and garbage collection handled in _Python_? -In Python, **both memory allocation** and **garbage collection** are handled discretely. +In Python, **memory allocation** and **garbage collection** are handled automatically and transparently by the Python interpreter, abstracting these complexities away from the developer. This significantly reduces the likelihood of memory-related bugs like leaks or segmentation faults. ### Memory Allocation -- The "heap" is the pool of memory for storing objects. The Python memory manager allocates and deallocates this space as needed. +Python manages its own private heap space, where all Python objects (e.g., integers, strings, lists, dictionaries, custom objects) reside. The **Python Memory Manager** is responsible for allocating and deallocating memory within this heap. + +#### Specialized Allocators + +To optimize performance for various object sizes, Python employs a tiered memory allocation strategy: -- In latest Python versions, the `obmalloc` system is responsible for small object allocations. This system preallocates small and medium-sized memory blocks to manage frequently created small objects. +* **`pymalloc` (or `obmalloc`)**: For small and medium-sized objects (typically less than 512 bytes), Python uses a specialized allocator known as `pymalloc` (historically) or more generally, the `obmalloc` system in CPython. This system pre-allocates large blocks of memory from the operating system (called **arenas**) and then subdivides them into smaller fixed-size **pools** and **blocks**. This greatly reduces the overhead of frequent small allocations and deallocations, as Python reuses these internal memory blocks efficiently. This mechanism is crucial for the performance of typical Python programs that create many small objects. +* **System `malloc`**: For larger objects (greater than 512 bytes) or when the `pymalloc` system cannot satisfy a request, Python directly falls back to the underlying operating system's memory allocator (e.g., `malloc` and `free` from the C standard library like `Glibc`). -- The `allocator` abstracts the system-level memory management, employing memory management libraries like `Glibc` to interact with the operating system. +#### Stack vs. Heap -- Larger blocks of memory are primarily obtained directly from the operating system. +It's important to distinguish between the **stack** and the **heap**: -- **Stack** and **Heap** separation is joined by "Pool Allocator" for internal use. +* **Stack**: The call stack stores function call frames, local variables, and references to objects. Variables on the stack are typically primitives or pointers/references to objects. +* **Heap**: This is where all actual Python objects are stored. Python's memory manager exclusively deals with the **Python private heap**. When a Python variable is created, it's typically a reference on the stack pointing to an object on the heap. + +```python +# Example of object creation and memory allocation +my_list = [1, 2, 3] # A list object is allocated on the heap. + # 'my_list' is a reference on the stack pointing to this object. + +my_dict = {'a': 1, 'b': 2} # A dictionary object is allocated on the heap. + +# Small integers (and some strings) are often interned for efficiency, +# meaning they are pre-allocated and reused. +x = 10 +y = 10 # x and y might point to the same integer object on the heap +``` ### Garbage Collection -Python employs a method called **reference counting** along with a **cycle-detecting garbage collector**. +Python employs a hybrid approach to garbage collection, combining **reference counting** with a **cycle-detecting garbage collector**. #### Reference Counting -- Every object has a reference count. When an object's count drops to zero, it is immediately deallocated. +* **Mechanism**: Every Python object maintains a **reference count**, which tracks how many references (variables, container elements, etc.) point to it. When an object is created, its reference count is 1. When a new reference points to it, the count increments; when a reference is removed (e.g., variable goes out of scope, `del` keyword used, reference reassigned), the count decrements. +* **Deallocation**: When an object's reference count drops to zero, it means no active references point to it, making it unreachable. The object is then immediately deallocated, and its memory is returned to the `pymalloc` system or the OS. +* **Efficiency**: Reference counting is generally very efficient and allows for prompt memory reclamation. Most objects are deallocated as soon as they become unreachable. + +```python +import sys + +# An empty list object is created +a = [] +print(f"Reference count for a after creation: {sys.getrefcount(a) - 1}") # -1 for temporary ref from getrefcount -- This mechanism is swift, often releasing objects instantly without the need for garbage collection. +# Another reference to the same list object +b = a +print(f"Reference count after b = a: {sys.getrefcount(a) - 1}") -- However, it can be insufficient in handling **circular references**. +# Remove reference b +del b +print(f"Reference count after del b: {sys.getrefcount(a) - 1}") + +# Once 'a' is also deleted or reassigned, the count will drop to 0, +# and the list object will be deallocated. +``` #### Cycle-Detecting Garbage Collector -- Python has a separate garbage collector that periodically identifies and deals with circular references. +* **Problem**: Reference counting alone cannot detect and reclaim memory occupied by **circular references**. A circular reference occurs when two or more objects refer to each other, forming a closed loop, even if no external references point to the cycle. In such cases, the reference count of each object within the cycle might never drop to zero, leading to a memory leak. +* **Solution**: Python includes a separate, optional, **generational garbage collector** designed to identify and collect these uncollectable cycles. + * **Generations**: The collector categorizes objects into three generations (0, 1, and 2) based on their age. New objects start in generation 0. If an object survives a collection in generation 0, it's promoted to generation 1, and so on. Older generations are collected less frequently because objects that have survived longer are less likely to be part of a temporary cycle. + * **Collection Process**: When invoked (either automatically or manually via `gc.collect()`), the collector traverses object graphs to find unreachable cycles. It temporarily unlinks references within potential cycles to see if any object's reference count drops to zero. If so, those objects are deemed part of an uncollectable cycle and are deallocated. +* **Performance**: Cycle detection is a more computationally intensive process than reference counting, so it runs much less frequently. The `gc` module provides an interface to control and inspect the garbage collector. + +```python +import gc + +class Node: + def __init__(self, value): + self.value = value + self.next = None # Reference to another Node + # print(f"Node {self.value} created") # For demonstration + + def __del__(self): + # This destructor will be called when the object is truly deallocated + print(f"Node {self.value} destroyed") -- This is, however, a more time-consuming process and is invoked less frequently than reference counting. +# Disable automatic garbage collection for clearer demonstration +gc.disable() -### Memory Management in Python vs. C +n1 = Node(1) +n2 = Node(2) -Python handles memory management quite differently from languages like C or C++: +n1.next = n2 +n2.next = n1 # Circular reference: n1 -> n2 -> n1 -- In Python, the developer isn't directly responsible for memory allocations or deallocations, reducing the likelihood of memory-related bugs. +print("References created. Objects n1 and n2 still exist in memory.") -- The memory manager in Python is what's known as a **"general-purpose memory manager"** that can be slower than the dedicated memory managers of C or C++ in certain contexts. +del n1 +del n2 # Variables n1 and n2 are deleted, but the objects themselves are not __del__-ed -- Python, especially due to the existence of a garbage collector, might have memory overhead compared to C or C++ where manual memory management often results in minimal overhead is one of the factors that might contribute to Python's sometimes slower performance. +print("Local references n1 and n2 are gone, but objects still exist due to circularity.") -- The level of memory efficiency isn't as high as that of C or C++. This is because Python is designed to be convenient and easy to use, often at the expense of some performance optimization. +# The objects n1 and n2 are still alive because their reference counts within the cycle +# prevent them from being zero, even though no external references point to the cycle. + +print("\nRunning cycle-detecting garbage collector...") +gc.collect() # This will detect the cycle and deallocate the objects, + # triggering their __del__ methods. + +gc.enable() # Re-enable garbage collection +``` + +### Memory Management in Python vs. C/C++ + +Python's approach to memory management differs significantly from lower-level languages like C or C++: + +* **Automation vs. Manual Control**: Python provides **automatic memory management**, relieving developers from explicitly allocating (`malloc`/`new`) or deallocating (`free`/`delete`) memory. This contrasts sharply with C/C++, where manual memory management is a core responsibility. +* **Reduced Bugs**: The automation in Python greatly reduces the risk of common memory-related bugs such as memory leaks, double-frees, or dangling pointers. +* **Performance and Overhead**: + * Python's general-purpose memory manager, combined with the overhead of dynamic typing, reference counting, and the cycle-detecting garbage collector, can result in higher memory consumption and potentially slower performance compared to finely tuned C/C++ applications. + * Each Python object also carries additional overhead (e.g., type information, reference count) that isn't present in raw C data structures. +* **Developer Productivity**: The trade-off is a significant boost in **developer productivity** and ease of use, as developers can focus on application logic rather than intricate memory handling.
## 5. What are the _built-in data types_ in _Python_? -Python offers numerous **built-in data types** that provide varying functionalities and utilities. +Python offers numerous **built-in data types** that provide varying functionalities and utilities. These types are fundamental to programming in Python and can be broadly categorized into immutable and mutable types, depending on whether their state can be changed after creation. ### Immutable Data Types +Immutable objects cannot be modified after they are created. Any operation that appears to modify an immutable object actually creates a new object. #### 1. int - Represents a whole number, such as 42 or -10. +Represents **whole numbers** (integers) of arbitrary precision. There is no practical limit to how large an integer value can be, other than the available memory. +```python +x = 42 +print(type(x)) # +``` #### 2. float - Represents a decimal number, like 3.14 or -0.01. +Represents **floating-point numbers** (decimal numbers). Python's `float` type typically implements the IEEE 754 double-precision standard. +```python +pi = 3.14159 +print(type(pi)) # +``` #### 3. complex - Comprises a real and an imaginary part, like 3 + 4j. +Represents **complex numbers**, comprising a real and an imaginary part. The imaginary part is denoted by `j` or `J`. +```python +z = 3 + 4j +print(type(z)) # +``` #### 4. bool - Represents a boolean value, True or False. +Represents **boolean values**, which can be either `True` or `False`. `bool` is a subclass of `int`, where `True` is equivalent to `1` and `False` to `0`. +```python +is_active = True +is_empty = False +print(type(is_active)) # +``` #### 5. str - A sequence of unicode characters enclosed within quotes. +Represents **sequences of Unicode characters**, used for text. String literals are enclosed within single quotes, double quotes, or triple quotes. +```python +message = "Hello, Python!" +print(type(message)) # +``` #### 6. tuple - An ordered collection of items, often heterogeneous, enclosed within parentheses. +An **ordered, immutable collection** of items. Tuples can contain heterogeneous data types and are typically enclosed within parentheses. +```python +coords = (10, 20, 'north') +print(type(coords)) # +``` #### 7. frozenset - A set of unique, immutable objects, similar to sets, enclosed within curly braces. +An **immutable version of a `set`**. It is a collection of unique, hashable objects and cannot be modified after creation. `frozenset` objects are created using the `frozenset()` constructor. +```python +immutable_items = frozenset([1, 2, 3, 2]) +print(immutable_items) # frozenset({1, 2, 3}) +print(type(immutable_items)) # +``` #### 8. bytes - Represents a group of 8-bit bytes, often used with binary data, enclosed within brackets. +Represents an **immutable sequence of 8-bit bytes**. It is primarily used to handle binary data. `bytes` literals are prefixed with `b`. +```python +binary_data = b'hello' +print(type(binary_data)) # +``` + +#### 9. NoneType +The type of the **`None` object**, which indicates the absence of a value or a null value. `None` is a singleton object. +```python +result = None +print(type(result)) # +``` -#### 9. bytearray - Resembles the 'bytes' type but allows mutable changes. +#### 10. type +The **metaclass** for all new-style classes in Python. It is the type of types themselves. +```python +t = type(10) # t is +print(type(t)) # +``` -#### 10. NoneType - Indicates the absence of a value. +#### 11. object +The **base class** from which all other classes in Python implicitly or explicitly inherit. It is the root of the class hierarchy. +```python +o = object() +print(type(o)) # +``` ### Mutable Data Types +Mutable objects can be modified after they are created. Changes made to a mutable object directly affect the object's state. #### 1. list - A versatile ordered collection that can contain different data types and offers dynamic sizing, enclosed within square brackets. +An **ordered, mutable collection** of items. Lists are highly versatile, can contain heterogeneous data types, and offer dynamic sizing. They are enclosed within square brackets. +```python +my_list = [1, 'two', 3.0, [4, 5]] +my_list.append(6) +print(type(my_list)) # +``` #### 2. set - Represents a unique set of objects and is characterized by curly braces. +An **unordered, mutable collection of unique, hashable objects**. Sets are useful for operations like membership testing, removing duplicates, and mathematical set operations. They are characterized by curly braces. +```python +unique_numbers = {1, 2, 3, 2} +unique_numbers.add(4) +print(unique_numbers) # {1, 2, 3, 4} (order may vary) +print(type(unique_numbers)) # +``` #### 3. dict - A versatile key-value paired collection enclosed within braces. +A **key-value paired collection** that is mutable and since Python 3.7+, insertion-ordered. Keys must be unique and hashable, while values can be of any type. Dictionaries are enclosed within curly braces. +```python +person = {'name': 'Alice', 'age': 30, 'city': 'New York'} +person['age'] = 31 +print(type(person)) # +``` -#### 4. memoryview - Points to the memory used by another object, aiding efficient viewing and manipulation of data. +#### 4. bytearray +A **mutable version of the `bytes` type**. It represents a mutable sequence of 8-bit bytes and allows in-place modification of binary data. +```python +mutable_binary = bytearray(b'world') +mutable_binary[0] = ord('W') +print(mutable_binary) # bytearray(b'World') +print(type(mutable_binary)) # +``` -#### 5. array - Offers storage for a specified type of data, similar to lists but with dedicated built-in functionalities. +#### 5. memoryview +A **built-in type that exposes the buffer protocol** of an object (like `bytes` or `bytearray`). It allows direct access to an object's internal data without copying, aiding efficient viewing and manipulation of large data blocks. +```python +data = bytearray(b'abcdef') +mv = memoryview(data) +print(mv[0]) # 97 (ASCII for 'a') +mv[0] = 100 # Change 'a' to 'd' +print(data) # bytearray(b'dbcdef') +print(type(mv)) # +``` -#### 6. deque - A double-ended queue distinguished by optimized insertion and removal operations from both its ends. +#### 6. array.array +The `array` module provides an **array type that can store a sequence of items of a single, specified numeric type**. It is similar to a `list` but is more memory-efficient for storing large numbers of homogeneous elements. It must be imported from the `array` module. +```python +import array +my_array = array.array('i', [1, 2, 3, 4]) # 'i' for signed int +my_array.append(5) +print(type(my_array)) # +``` -#### 7. object - The base object from which all classes inherit. +#### 7. collections.deque +The `collections` module provides `deque` (double-ended queue), which is a **list-like container with optimized appends and pops from both ends** with approximately O(1) performance. It must be imported from the `collections` module. +```python +from collections import deque +my_deque = deque([1, 2, 3]) +my_deque.appendleft(0) # [0, 1, 2, 3] +my_deque.pop() # 3 +print(type(my_deque)) # +``` #### 8. types.SimpleNamespace - Grants the capability to assign attributes to it. +The `types` module provides `SimpleNamespace`, which is a **simple `object` subclass that allows arbitrary attribute assignment**. It's useful for creating lightweight data objects. It must be imported from the `types` module. +```python +import types +ns = types.SimpleNamespace(a=1, b='hello') +ns.c = [1, 2, 3] +print(ns.a, ns.b, ns.c) # 1 hello [1, 2, 3] +print(type(ns)) # +``` #### 9. types.ModuleType - Represents a module body containing attributes. +The `types` module provides `ModuleType`, which is the **type of all modules**. It represents the actual module object loaded into Python. It must be imported from the `types` module. +```python +import sys +module_type_example = type(sys) +print(module_type_example) # +print(type(module_type_example)) # (ModuleType itself is a type) +``` #### 10. types.FunctionType - Defines a particular kind of function. +The `types` module provides `FunctionType`, which is the **type of user-defined functions**. It represents callable objects created with `def`. It must be imported from the `types` module. +```python +import types +def my_function(): + pass +func_type_example = type(my_function) +print(func_type_example) # +print(type(func_type_example)) # (FunctionType itself is a type) +```
## 6. Explain the difference between a _mutable_ and _immutable_ object. From 546333d84bc9d289d631ce6d5ad8fcfec85f2a58 Mon Sep 17 00:00:00 2001 From: Devinterview-io <76989322+Devinterview-io@users.noreply.github.com> Date: Sat, 24 Jan 2026 16:08:08 -0500 Subject: [PATCH 2/7] Update python interview questions --- README.md | 1258 ++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 996 insertions(+), 262 deletions(-) diff --git a/README.md b/README.md index 1270c1b..ece7412 100644 --- a/README.md +++ b/README.md @@ -614,587 +614,1321 @@ print(type(func_type_example)) # (FunctionType itself is a type) ## 6. Explain the difference between a _mutable_ and _immutable_ object. -Let's look at the difference between **mutable** and **immutable** objects. +Let's look at the difference between **mutable** and **immutable** objects in Python. This distinction is fundamental to understanding how data is stored, manipulated, and passed around in Python programs. -### Key Distinctions +### Core Definition + +The primary difference lies in whether an object's state can be altered after its creation. -- **Mutable Objects**: Can be modified after creation. -- **Immutable Objects**: Cannot be modified after creation. +#### Mutable Objects +A **mutable object** is an object whose state or content can be changed after it has been created. When a mutable object is modified, its identity (memory address) remains the same, but its value changes. Any variable referencing this object will see the change. + +#### Immutable Objects +An **immutable object** is an object whose state or content cannot be changed after it has been created. If an operation appears to "modify" an immutable object, it actually creates a *new* object with the desired changes, and the original object remains untouched. Variables are then re-bound to this new object. ### Common Examples -- **Mutable**: Lists, Sets, Dictionaries -- **Immutable**: Tuples, Strings, Numbers +#### Mutable Types +* **Lists** (`list`): Ordered, changeable sequence of items. +* **Sets** (`set`): Unordered collection of unique, mutable items. +* **Dictionaries** (`dict`): Unordered collection of key-value pairs, where keys must be immutable and unique. +* **Bytearrays** (`bytearray`): Mutable sequence of bytes. +* Most custom class instances (unless specifically designed to be immutable). -### Code Example: Immutability in Python +#### Immutable Types +* **Numbers** (`int`, `float`, `complex`): Integers, floating-point numbers, and complex numbers. +* **Strings** (`str`): Ordered, immutable sequence of characters. +* **Tuples** (`tuple`): Ordered, immutable sequence of items. (Note: A tuple itself is immutable, but if it contains mutable objects, those mutable objects can still be changed). +* **Frozensets** (`frozenset`): Unordered collection of unique, immutable items. +* **Bytes** (`bytes`): Immutable sequence of bytes. +* **Booleans** (`bool`): `True` and `False`. +* **None** (`NoneType`). -Here is the Python code: +### Code Example: Illustrating Mutability and Immutability in Python ```python -# Immutable objects (int, str, tuple) +import sys + +# --- Immutable objects (int, str, tuple) --- +print("--- Immutable Objects ---") num = 42 -text = "Hello, World!" -my_tuple = (1, 2, 3) +text = "Hello" +my_tuple = (1, [2, 3], 4) # Tuple itself is immutable, but contains a mutable list + +print(f"Initial num: {num}, id: {id(num)}") +print(f"Initial text: '{text}', id: {id(text)}") +print(f"Initial my_tuple: {my_tuple}, id: {id(my_tuple)}") + +# Attempting to "modify" num (reassignment creates a new object) +num += 10 # This is syntactic sugar for num = num + 10 +print(f"After num += 10: {num}, id: {id(num)} (id changed, new object created)") + +try: + # Trying to modify a string (will raise TypeError) + # Strings do not support item assignment + # text[0] = 'M' + pass # Commenting out to allow script to run, but conceptually this fails +except TypeError as e: + print(f"Error modifying string: {e}") -# Trying to modify will raise an error try: - num += 10 - text[0] = 'M' # This will raise a TypeError - my_tuple[0] = 100 # This will also raise a TypeError + # Trying to modify a tuple (will raise TypeError) + my_tuple[0] = 100 except TypeError as e: - print(f"Error: {e}") + print(f"Error modifying tuple element: {e}") + +# However, elements *within* a mutable object inside an immutable tuple can be changed +print(f"Mutable list inside tuple before change: {my_tuple[1]}, id: {id(my_tuple[1])}") +my_tuple[1].append(5) # This modifies the list object, not the tuple +print(f"Mutable list inside tuple after change: {my_tuple[1]}, id: {id(my_tuple[1])} (id of list unchanged)") +print(f"my_tuple after internal list modification: {my_tuple}, id: {id(my_tuple)} (tuple id unchanged)") + -# Mutable objects (list, set, dict) +# --- Mutable objects (list, dict, set) --- +print("\n--- Mutable Objects ---") my_list = [1, 2, 3] my_dict = {'a': 1, 'b': 2} +my_set = {10, 20} -# Can be modified without issues -my_list.append(4) -del my_dict['a'] +print(f"Initial my_list: {my_list}, id: {id(my_list)}") +print(f"Initial my_dict: {my_dict}, id: {id(my_dict)}") +print(f"Initial my_set: {my_set}, id: {id(my_set)}") -# Checking the changes -print(my_list) # Output: [1, 2, 3, 4] -print(my_dict) # Output: {'b': 2} +# Modifications happen in-place; object ID remains the same +my_list.append(4) +my_dict['c'] = 3 +my_set.add(30) + +print(f"After append my_list: {my_list}, id: {id(my_list)} (id unchanged)") +print(f"After add 'c' my_dict: {my_dict}, id: {id(my_dict)} (id unchanged)") +print(f"After add 30 my_set: {my_set}, id: {id(my_set)} (id unchanged)") + +# Example of aliasing with mutable objects +list_a = [1, 2] +list_b = list_a # list_b now refers to the *same* object as list_a +print(f"list_a: {list_a}, id: {id(list_a)}") +print(f"list_b: {list_b}, id: {id(list_b)}") + +list_b.append(3) # Modifies the object referenced by both list_a and list_b +print(f"After list_b.append(3):") +print(f"list_a: {list_a}, id: {id(list_a)}") # list_a is also changed! +print(f"list_b: {list_b}, id: {id(list_b)}") ``` -### Benefits & Trade-Offs +### Key Implications and Use Cases -**Immutability** offers benefits such as **safety** in concurrent environments and facilitating **predictable behavior**. +#### Hashability +* **Immutable objects** are **hashable**. This means they have a hash value that never changes during their lifetime. This property is crucial for using them as keys in dictionaries and elements in sets, as these data structures rely on hashing for efficient lookups. +* **Mutable objects** are **not hashable** by default because their hash value would change if their content changed, making them unreliable for lookup operations. Consequently, mutable objects like lists, sets, and dictionaries cannot be used as dictionary keys or set members. -**Mutability**, on the other hand, often improves **performance** by avoiding copy overhead and redundant computations. +#### Thread Safety +* **Immutable objects** are inherently **thread-safe**. Since their state cannot change after creation, multiple threads can access them concurrently without the need for locks or synchronization mechanisms, simplifying concurrent programming. +* **Mutable objects** are **not thread-safe** by default. If multiple threads access and modify the same mutable object without proper synchronization, race conditions can occur, leading to unpredictable behavior and data corruption. -### Impact on Operations +#### Predictability and Debugging +* **Immutable objects** contribute to more **predictable code**. Their fixed state makes it easier to reason about program flow and trace data transformations, as an object's value will not unexpectedly change from another part of the program. This often leads to fewer bugs related to side effects. +* **Mutable objects**, while powerful, can introduce **side effects**. If a mutable object is passed around and modified by different functions, it can be challenging to track its state and debug issues arising from unintended modifications. -- **Reading and Writing**: Immutable objects typically favor **reading** over **writing**, promoting a more straightforward and predictable code flow. +#### Performance and Memory +* **Mutable objects** can be more **memory-efficient** for in-place updates, especially for large datasets. Modifying a mutable object avoids the overhead of creating a new object and garbage collecting the old one. +* **Immutable objects** might incur memory and performance overhead if "modifications" lead to frequent creation of new objects. However, immutability can also enable optimizations such as object sharing (multiple references pointing to the same immutable object) or caching of computed values, as their state is guaranteed not to change. Python internally caches small integers and strings for this reason. -- **Memory and Performance**: Mutability can be more efficient in terms of memory usage and performance, especially concerning large datasets, thanks to in-place updates. - -Choosing between the two depends on the program's needs, such as the required data integrity and the trade-offs between predictability and performance. +Choosing between mutable and immutable objects depends on the specific requirements of the program, balancing considerations like data integrity, concurrent access patterns, and performance characteristics.
## 7. How do you _handle exceptions_ in _Python_? -**Exception handling** is a fundamental aspect of Python, and it safeguards your code against unexpected errors or conditions. Key components of exception handling in Python include: +**Exception handling** is a fundamental aspect of Python, and it safeguards your code against unexpected errors or conditions. It allows programs to gracefully recover from errors, rather than crashing, and provides mechanisms to perform necessary cleanup operations. Key components of exception handling in Python include: ### Components -- **Try**: The section of code where exceptions might occur is placed within a `try` block. +- **Try**: The section of code where exceptions might occur is placed within a `try` block. If an exception occurs in this block, the execution flow is immediately transferred to the corresponding `except` block. -- **Except**: Any possible exceptions that are `raised` by the `try` block are caught and handled in the `except` block. +- **Except**: Any possible exceptions that are `raised` by the `try` block are caught and handled in the `except` block. You can specify which type of exception to catch, or catch multiple types. -- **Finally**: This block ensures a piece of code always executes, regardless of whether an exception occurred. It's commonly used for cleanup operations, such as closing files or database connections. +- **Finally**: This block ensures a piece of code always executes, regardless of whether an exception occurred in the `try` block or was caught by an `except` block. It's commonly used for cleanup operations, such as closing files or database connections, to ensure resources are properly released. ### Generic Exception Handling vs. Handling Specific Exceptions -It's good practice to **handle** specific exceptions. However, a more **general** approach can also be taken. When doing the latter, ensure the general exception handling is at the end of the chain, as shown here: +It's good practice to **handle** specific exceptions to provide tailored responses to different error conditions. However, a more **general** approach can also be taken. When combining specific and general exception handlers, ensure that the most specific `except` blocks come first, and the more **general** `except Exception as e` block is placed at the end of the chain. This ensures that specific error conditions are addressed before being caught by a broader handler. ```python try: risky_operation() except IndexError: # Handle specific exception types first. handle_index_error() +except ValueError: # Another specific exception + handle_value_error() except Exception as e: # More general exception must come last. - handle_generic_error() + # This will catch any other exception not caught by the specific handlers above + print(f"An unexpected error occurred: {e}") + handle_generic_error(e) finally: - cleanup() + cleanup() # This always executes ``` ### Raising Exceptions -Use this mechanism to **trigger and manage** exceptions under specific circumstances. This can be particularly useful when building custom classes or functions where specific conditions should be met. +You can use the `raise` statement to **trigger and manage** exceptions under specific circumstances. This is particularly useful when building custom classes or functions where specific conditions should be met, and a failure to meet them warrants an immediate halt and error notification. **Raise** a specific exception: ```python def divide(a, b): if b == 0: + # Raising a built-in exception with a custom message raise ZeroDivisionError("Divisor cannot be zero") return a / b try: result = divide(4, 0) except ZeroDivisionError as e: - print(e) + print(f"Error: {e}") # Output: Error: Divisor cannot be zero +except Exception as e: + print(f"An unexpected error occurred: {e}") ``` **Raise a general exception**: +While generally recommended to raise specific exceptions (either built-in or custom), you can also raise the base `Exception` class directly for generic error conditions. ```python -def some_risky_operation(): - if condition: - raise Exception("Some generic error occurred") +def some_risky_operation(condition: bool): + if condition: + raise Exception("Some generic error occurred due to a specific condition.") + return "Operation successful" + +try: + print(some_risky_operation(True)) +except Exception as e: + print(f"Caught a general exception: {e}") # Output: Caught a general exception: Some generic error occurred due to a specific condition. ``` ### Using `with` for Resource Management -The `with` keyword provides a more efficient and clean way to handle resources, like files, ensuring their proper closure when operations are complete or in case of any exceptions. The resource should implement a `context manager`, typically by having `__enter__` and `__exit__` methods. +The `with` keyword provides a more efficient and clean way to handle resources, like files or network connections, ensuring their proper acquisition and release (closure) when operations are complete or in case of any exceptions. Resources that can be used with `with` must implement the `context manager` protocol, typically by having `__enter__` and `__exit__` methods. Here's an example using a file: ```python -with open("example.txt", "r") as file: - data = file.read() -# File is automatically closed when the block is exited. +try: + with open("example.txt", "r") as file: + data = file.read() + # File is automatically closed when the 'with' block is exited, + # even if an exception occurs during file reading. + print("File content read successfully.") +except FileNotFoundError: + print("Error: The file 'example.txt' was not found.") +except IOError as e: + print(f"Error reading file: {e}") ``` -### Silence with `pass`, `continue`, or `else` +### Controlling Flow with `else`, `pass`, and `continue` + +These keywords offer different strategies for controlling program flow within or around exception handling blocks, allowing for conditional execution or ignoring certain exceptions. + +#### `else` with `try-except` blocks -There are times when not raising an exception is appropriate. You can use `pass` or `continue` in an exception block when you want to essentially ignore an exception and proceed with the rest of your code. +The `else` block after a `try-except` block will only be executed if **no exceptions** are raised within the `try` block. It serves as a place for code that *must* run only upon successful completion of the `try` block, before the `finally` block (if present). -- **`pass`**: Simply does nothing. It acts as a placeholder. +```python +def perform_division(a, b): + try: + result = a / b + except ZeroDivisionError: + print("Cannot divide by zero!") + else: + # This code runs only if no exception occurred in the try block + print(f"Division successful. Result: {result}") + return result + finally: + print("Attempted division operation finished.") - ```python - try: - risky_operation() - except SomeSpecificException: - pass - ``` +perform_division(10, 2) +# Output: +# Division successful. Result: 5.0 +# Attempted division operation finished. -- **`continue`**: This keyword is generally used in loops. It moves to the next iteration without executing the code that follows it within the block. +perform_division(10, 0) +# Output: +# Cannot divide by zero! +# Attempted division operation finished. +``` + +#### `pass` + +The `pass` statement is a null operation; nothing happens when it executes. It acts as a placeholder where a statement is syntactically required but you don't want any action to be performed, effectively ignoring an exception. While sometimes useful for prototyping, ignoring exceptions in production code should be done cautiously, as it can mask underlying issues. + +```python +def risky_operation(): + raise ValueError("Something went wrong!") + +try: + risky_operation() +except ValueError: + # Explicitly ignoring a specific ValueError + pass # No action taken, program continues normally +print("Program continues after potentially ignored exception.") +``` - ```python - for item in my_list: - try: - perform_something(item) - except ExceptionType: - continue - ``` +#### `continue` -- **`else` with `try-except` blocks**: The `else` block after a `try-except` block will only be executed if no exceptions are raised within the `try` block +The `continue` keyword is generally used within loops. When executed inside an `except` block within a loop, it immediately skips the rest of the current iteration of the loop and proceeds to the next iteration. This is useful when processing a list of items and you want to skip an item that causes an exception without stopping the entire loop. - ```python - try: - some_function() - except SpecificException: - handle_specific_exception() - else: - no_exception_raised() - ``` +```python +data_items = [10, 2, 0, 5, 'error', 1] + +for item in data_items: + try: + result = 100 / item + print(f"Result for {item}: {result}") + except (ZeroDivisionError, TypeError) as e: + print(f"Skipping item {item} due with error: {e}") + continue # Move to the next item in the loop + except Exception as e: + print(f"An unexpected error occurred for item {item}: {e}") + continue +``` -### Callback Function: `ExceptionHook` +### Callback Function: `sys.excepthook` -Python 3 introduced the better handling of uncaught exceptions by providing an optional function for printing stack traces. The `sys.excepthook` can be set to match any exception in the module as long as it has a `hook` attribute. +The `sys.excepthook` is a configurable function in the `sys` module that allows you to customize how Python handles *uncaught* exceptions (exceptions that propagate all the way up without being caught by any `except` block). By default, `sys.excepthook` points to `sys.__excepthook__`, which prints the standard Python traceback to `sys.stderr`. You can replace it with your own function to log errors, display custom messages, send notifications, or perform other actions before the program potentially terminates. -Here's an example for this test module: +Here's an example: ```python -# test.py +# test_hook.py import sys -def excepthook(type, value, traceback): - print("Unhandled exception:", type, value) - # Call the default exception hook - sys.__excepthook__(type, value, traceback) +def custom_excepthook(exc_type, exc_value, exc_traceback): + """ + A custom exception hook that logs the unhandled exception + and then calls the default hook. + """ + print(f"*** UNHANDLED EXCEPTION CAUGHT BY CUSTOM HOOK ***") + print(f"Type: {exc_type.__name__}") + print(f"Value: {exc_value}") + # You could log this to a file, send an email, etc. + + # Call the original excepthook to print the standard traceback + sys.__excepthook__(exc_type, exc_value, exc_traceback) + +# Set our custom hook +sys.excepthook = custom_excepthook -sys.excepthook = excepthook +def problematic_function(): + print("Inside problematic_function") + result = 1 / 0 # This will raise a ZeroDivisionError + +if __name__ == "__main__": + print("Calling problematic_function...") + problematic_function() + print("This line will not be reached due to unhandled exception.") -def test_exception_hook(): - throw_some_exception() ``` -When run, calling `test_exception_hook` will print "Unhandled exception: ..." +When `test_hook.py` is executed, the output will first show the custom message from `custom_excepthook` followed by the standard Python traceback, because `sys.__excepthook__` was called. -_Note_: `sys.excepthook` will not capture exceptions raised as the result of interactive prompt commands, such as SyntaxError or KeyboardInterrupt. +_Note_: `sys.excepthook` will not capture exceptions raised as the result of interactive prompt commands, such as `SyntaxError` or `KeyboardInterrupt`, nor does it affect exceptions caught by `try...except` blocks. It specifically targets exceptions that would otherwise lead to program termination with a traceback.
## 8. What is the difference between _list_ and _tuple_? -**Lists** and **Tuples** in Python share many similarities, such as being sequences and supporting indexing. - -However, these data structures differ in key ways: +**Lists** and **Tuples** in Python share many similarities, such as being ordered sequences and supporting indexing, slicing, and iteration. However, they are fundamentally different in their core characteristics, making them suitable for different use cases. ### Key Distinctions -- **Mutability**: Lists are mutable, allowing you to add, remove, or modify elements after creation. Tuples, once created, are immutable. +- **Mutability**: This is the most significant difference. + * **Lists** are **mutable**, meaning their elements can be added, removed, or modified after the list has been created. They are dynamic and can change in size and content. + * **Tuples** are **immutable**. Once a tuple is created, its size and the elements it contains cannot be changed. While you cannot modify, add, or remove items from a tuple, it's important to note that if a tuple contains mutable objects (e.g., a list), the contents of those mutable objects *can* be changed. The tuple itself still holds the *reference* to that object, and that reference doesn't change. -- **Performance**: Lists are generally slower than tuples, most apparent in tasks like iteration and function calls. +- **Performance and Memory Usage**: + * **Tuples** are generally more performant and consume less memory than lists for the same number of elements. This is because their fixed size allows Python to make certain optimizations. They have less overhead as they don't need to allocate space for growth or maintain methods for modification. + * **Lists**, being mutable, require more memory and can be slightly slower due to the overhead of supporting dynamic operations (resizing, adding, removing elements). -- **Syntax**: Lists are defined with square brackets `[]`, whereas tuples use parentheses `()`. +- **Syntax**: + * **Lists** are defined using square brackets `[]`. + * **Tuples** are defined using parentheses `()`. A single-element tuple requires a trailing comma (e.g., `(item,)`) to distinguish it from a simple parenthesized expression. ### When to Use Each -- **Lists** are ideal for collections that may change in size and content. They are the preferred choice for storing data elements. +- **Lists** are ideal for collections of items that may change over time, such as a collection of user inputs, a dynamic queue, or a list of items to be processed. They are the preferred choice when you need a modifiable sequence. -- **Tuples**, due to their immutability and enhanced performance, are a good choice for representing fixed sets of related data. +- **Tuples**, due to their immutability and enhanced performance, are a good choice for: + * Representing fixed sets of related data, like a record or a point in a coordinate system (e.g., `(x, y)`). + * Function arguments and return values, especially when returning multiple items from a function. + * Using as keys in dictionaries (provided all elements within the tuple are themselves immutable). + * Data that should not be changed, ensuring data integrity. ### Syntax #### List: Example +Lists demonstrate mutability through operations like appending, removing, or modifying elements. + ```python my_list = ["apple", "banana", "cherry"] +print(f"Initial list: {my_list}") + +# Adding an element my_list.append("date") +print(f"After append: {my_list}") + +# Modifying an element my_list[1] = "blackberry" +print(f"After modification: {my_list}") + +# Removing an element +my_list.remove("apple") +print(f"After removal: {my_list}") ``` #### Tuple: Example +Tuples are created with parentheses. Attempts to modify them will result in a `TypeError`. + ```python my_tuple = (1, 2, 3, 4) -# Unpacking a tuple +print(f"Initial tuple: {my_tuple}") + +# Unpacking a tuple (a common operation) a, b, c, d = my_tuple +print(f"Unpacked values: a={a}, b={b}, c={c}, d={d}") + +# Attempting to modify a tuple (will raise a TypeError) +try: + my_tuple[0] = 5 +except TypeError as e: + print(f"Error attempting to modify tuple: {e}") + +# Example of a tuple containing a mutable object +nested_tuple = (1, [2, 3], 4) +print(f"Nested tuple: {nested_tuple}") + +# The list inside the tuple *can* be modified, even though the tuple itself is immutable +nested_tuple[1].append(5) +print(f"Nested tuple after modifying inner list: {nested_tuple}") + +# The tuple itself still refers to the same list object; its reference hasn't changed. ```
## 9. How do you create a _dictionary_ in _Python_? -**Python dictionaries** are versatile data structures, offering key-based access for rapid lookups. Let's explore various data within dictionaries and techniques to create and manipulate them. +**Python dictionaries** are highly versatile and fundamental data structures that store data in `key:value` pairs, enabling efficient data retrieval. This section will explore the core concepts and various methods for creating dictionaries in Python. ### Key Concepts -- A **dictionary** in Python contains a collection of `key:value` pairs. -- **Keys** must be unique and are typically immutable, such as strings, numbers, or tuples. -- **Values** can be of any type, and they can be duplicated. +- A **dictionary** in Python is an unordered (as of Python 3.7+, insertion order is preserved, making them *ordered* in practice for CPython, but officially unordered for language specification prior to 3.7) collection of `key:value` pairs. +- **Keys** must be unique within a dictionary and typically immutable types, such as strings, numbers (integers, floats), or tuples containing only immutable elements. Attempting to use a mutable object (like a list) as a key will raise a `TypeError`. +- **Values** can be of any data type (strings, numbers, lists, other dictionaries, custom objects, etc.) and can be duplicated across different keys. ### Creating a Dictionary -You can use several methods to create a dictionary: - -1. **Literal Definition**: Define key-value pairs within curly braces { }. +Python offers several straightforward and efficient methods to create dictionaries: -2. **From Key-Value Pairs**: Use the `dict()` constructor or the `{key: value}` shorthand. - -3. **Using the `dict()` Constructor**: This can accept another dictionary, a sequence of key-value pairs, or named arguments. - -4. **Comprehensions**: This is a concise way to create dictionaries using a single line of code. - -5. **`zip()` Function**: This creates a dictionary by zipping two lists, where the first list corresponds to the keys, and the second to the values. +1. **Dictionary Literal**: The most common and direct way to define a dictionary, by enclosing `key:value` pairs within curly braces `{}`. +2. **`dict()` Constructor**: A versatile constructor that can accept different types of arguments to form a dictionary: + * An iterable of `key:value` pairs (e.g., a list of tuples or another dictionary's `items()`). + * Keyword arguments, where the argument names become string keys and their corresponding values become dictionary values. + * Another dictionary, creating a shallow copy. +3. **Dictionary Comprehensions**: A concise and powerful syntax for creating dictionaries dynamically from other iterables, often involving transformations or conditional logic. +4. **`dict.fromkeys()` Method**: Useful for creating dictionaries with a sequence of keys and assigning all of them a specified default value (or `None` if no value is provided). +5. **Using `zip()` with `dict()`**: A common pattern where the `zip()` function pairs elements from two sequences (one for keys, one for values), and the resulting iterable of pairs is then passed to the `dict()` constructor. ### Examples -#### Dictionary Literal Definition +#### 1. Dictionary Literal -Here is a Python code: +This is the most idiomatic way to create a dictionary. ```python -# Dictionary literal definition -student = { - "name": "John Doe", - "age": 21, - "courses": ["Math", "Physics"] +# Creating a dictionary using a literal +student_profile = { + "name": "Alice Smith", + "age": 20, + "major": "Computer Science", + "courses": ["Data Structures", "Algorithms", "Databases"], + "is_enrolled": True } + +print(f"Student Profile (Literal): {student_profile}") +# Expected output: Student Profile (Literal): {'name': 'Alice Smith', 'age': 20, 'major': 'Computer Science', 'courses': ['Data Structures', 'Algorithms', 'Databases'], 'is_enrolled': True} ``` -#### From Key-Value Pairs +#### 2. Using the `dict()` Constructor -Here is the Python code: +##### From an Iterable of Key-Value Pairs + +The `dict()` constructor can take a sequence of two-element iterables (like tuples or lists), where each inner iterable represents a `(key, value)` pair. ```python -# Using the `dict()` constructor -student_dict = dict([ - ("name", "John Doe"), - ("age", 21), - ("courses", ["Math", "Physics"]) +# From a list of tuples +faculty_info = dict([ + ("dean", "Dr. Emily White"), + ("department_head", "Prof. Robert Green"), + ("number_of_professors", 15) ]) +print(f"Faculty Info (from list of tuples): {faculty_info}") +# Expected output: Faculty Info (from list of tuples): {'dean': 'Dr. Emily White', 'department_head': 'Prof. Robert Green', 'number_of_professors': 15} +``` -# Using the shorthand syntax -student_dict_short = { - "name": "John Doe", - "age": 21, - "courses": ["Math", "Physics"] -} +##### From Keyword Arguments + +When using named arguments, the argument names become string keys, and their values become the dictionary values. + +```python +# From keyword arguments +city_data = dict(name="New York", population=8400000, country="USA") +print(f"City Data (from keyword arguments): {city_data}") +# Expected output: City Data (from keyword arguments): {'name': 'New York', 'population': 8400000, 'country': 'USA'} ``` -#### Using `zip()` +##### From Another Dictionary (Copying) -Here is a Python code: +This creates a shallow copy of an existing dictionary. ```python -keys = ["a", "b", "c"] -values = [1, 2, 3] +# From an existing dictionary (creating a copy) +student_profile_copy = dict(student_profile) # Using the previously defined student_profile +print(f"Student Profile Copy: {student_profile_copy}") +# Expected output: Student Profile Copy: {'name': 'Alice Smith', 'age': 20, 'major': 'Computer Science', 'courses': ['Data Structures', 'Algorithms', 'Databases'], 'is_enrolled': True} +``` -zipped = zip(keys, values) -dict_from_zip = dict(zipped) # Result: {"a": 1, "b": 2, "c": 3} +#### 3. Dictionary Comprehensions + +Dictionary comprehensions provide a concise way to create dictionaries from expressions and `for` loops. + +```python +# Creating a dictionary from a list of numbers, mapping number to its square +squares = {num: num**2 for num in range(1, 6)} +print(f"Squares Dictionary: {squares}") +# Expected output: Squares Dictionary: {1: 1, 2: 4, 3: 9, 4: 16, 5: 25} + +# Creating a dictionary from two lists, with a condition +employees = ["John", "Jane", "Mike"] +salaries = [60000, 75000, 50000] +employee_salaries = {emp: sal for emp, sal in zip(employees, salaries) if sal > 55000} +print(f"Employee Salaries (filtered): {employee_salaries}") +# Expected output: Employee Salaries (filtered): {'John': 60000, 'Jane': 75000} ``` -#### Using `dict()` Constructor +#### 4. `dict.fromkeys()` Method -Here is a Python code: +This method takes an iterable of keys and an optional `value`. All keys in the new dictionary will be mapped to this `value`. If `value` is omitted, `None` is used. ```python -# Sequence of key-value pairs -student_dict2 = dict(name="Jane Doe", age=22, courses=["Biology", "Chemistry"]) +# Creating a dictionary with default values +default_score = 0 +student_scores = dict.fromkeys(["Alice", "Bob", "Charlie"], default_score) +print(f"Student Scores (fromkeys): {student_scores}") +# Expected output: Student Scores (fromkeys): {'Alice': 0, 'Bob': 0, 'Charlie': 0} + +# Creating a dictionary with keys and default value None +project_tasks = dict.fromkeys(["Design", "Development", "Testing"]) +print(f"Project Tasks (fromkeys with None): {project_tasks}") +# Expected output: Project Tasks (fromkeys with None): {'Design': None, 'Development': None, 'Testing': None} +``` -# From another dictionary -student_dict_combined = dict(student, **student_dict2) +#### 5. Using `zip()` with `dict()` + +The `zip()` function pairs corresponding elements from multiple iterables. When combined with `dict()`, it's excellent for creating a dictionary from separate key and value lists. + +```python +keys = ["product_id", "name", "price"] +values = [101, "Laptop", 1200.50] + +# Using zip() to combine keys and values, then converting to a dictionary +product_details = dict(zip(keys, values)) +print(f"Product Details (using zip): {product_details}") +# Expected output: Product Details (using zip): {'product_id': 101, 'name': 'Laptop', 'price': 1200.5} + +# Example with different length lists (zip stops at the shortest) +colors = ["red", "green", "blue", "yellow"] +hex_codes = ["#FF0000", "#00FF00", "#0000FF"] # Shorter list +color_map = dict(zip(colors, hex_codes)) +print(f"Color Map (zip with different lengths): {color_map}") +# Expected output: Color Map (zip with different lengths): {'red': '#FF0000', 'green': '#00FF00', 'blue': '#0000FF'} ```
## 10. What is the difference between _==_ and _is operator_ in _Python_? -Both the **`==`** and **`is`** operators in Python are used for comparison, but they function differently. +Both the **`==`** and **`is`** operators in Python are used for comparison, but they serve fundamentally different purposes, focusing on distinct aspects of objects. + +### Understanding `==` (Equality Operator) -- The **`==`** operator checks for **value equality**. -- The **`is`** operator, on the other hand, validates **object identity**, +The **`==`** operator checks for **value equality**. It determines whether two objects have the same content or value. -In Python, every object is unique, identifiable by its memory address. The **`is`** operator uses this memory address to check if two objects are the same, indicating they both point to the exact same instance in memory. +#### How `==` Works +When `a == b` is evaluated, Python essentially asks: "Do these two objects represent the same value?" -- **`is`**: Compares the memory address or identity of two objects. -- **`==`**: Compares the content or value of two objects. +For built-in types: +* **Numbers, strings, tuples:** `==` compares their literal values. +* **Lists, dictionaries, sets:** `==` recursively compares their elements. + +For custom objects: +The behavior of `==` can be customized by implementing the special method `__eq__(self, other)` within the class definition. If `__eq__` is not implemented, Python's default behavior for custom objects is to check for identity (similar to `is`), but this is rarely the desired behavior for value comparison. + +#### Example of `==` +```python +# Comparing built-in types +a = [1, 2, 3] +b = [1, 2, 3] +c = [4, 5, 6] +d = a + +print(f"a == b: {a == b}") # Output: True (same content) +print(f"a == c: {a == c}") # Output: False (different content) +print(f"a == d: {a == d}") # Output: True (same content) + +# Comparing custom objects with __eq__ +class Point: + def __init__(self, x, y): + self.x = x + self.y = y + + def __eq__(self, other): + if not isinstance(other, Point): + return NotImplemented + return self.x == other.x and self.y == other.y + +p1 = Point(1, 2) +p2 = Point(1, 2) +p3 = Point(3, 4) + +print(f"p1 == p2: {p1 == p2}") # Output: True (due to __eq__ implementation) +print(f"p1 == p3: {p1 == p3}") # Output: False +``` -While **`is`** is primarily used for **None** checks, it's generally advisable to use **`==`** for most other comparisons. +### Understanding `is` (Identity Operator) -### Tips for Using Operators +The **`is`** operator checks for **object identity**. It determines whether two variables refer to the *exact same object* in memory. -- **`==`**: Use for equality comparisons, like when comparing numeric or string values. -- **`is`**: Use for comparing membership or when dealing with singletons like **None**. +#### How `is` Works +When `a is b` is evaluated, Python asks: "Are `a` and `b` references to the very same object instance?" This is equivalent to checking if `id(a) == id(b)`. The `id()` built-in function returns a unique identifier for an object, which is typically its memory address during its lifetime. + +The `is` operator **cannot be overloaded** for custom classes. Its behavior is fixed: it always compares object identities. + +#### Example of `is` +```python +# Comparing built-in types +a = [1, 2, 3] +b = [1, 2, 3] # 'b' is a new list object, even if content is identical +c = a # 'c' refers to the exact same list object as 'a' + +print(f"a is b: {a is b}") # Output: False (different objects in memory) +print(f"a is c: {a is c}") # Output: True (same object in memory) + +# Comparing custom objects +class Point: + def __init__(self, x, y): + self.x = x + self.y = y + +p1 = Point(1, 2) +p2 = Point(1, 2) +p3 = p1 + +print(f"p1 is p2: {p1 is p2}") # Output: False (p1 and p2 are different objects) +print(f"p1 is p3: {p1 is p3}") # Output: True (p1 and p3 refer to the same object) +``` + +### Key Differences Summarized + +* **`==`**: Compares the **value** or **content** of objects. Can be customized with `__eq__`. +* **`is`**: Compares the **identity** (memory address) of objects. Cannot be customized. + +### When to Use Which Operator + +#### Use `==` for Value Comparison +* You should almost always use **`==`** when you want to compare if two objects have the same content or value. This is the most common use case for comparison. +* **Examples:** Comparing numbers, strings, lists, dictionaries, or instances of custom classes where `__eq__` is defined to represent value equality. + +#### Use `is` for Identity Comparison +* **Singletons:** The `is` operator is most reliably and commonly used to check if an object is `None`, `True`, or `False`, as these are singletons in Python (there is only ever one instance of each). + ```python + my_variable = None + if my_variable is None: + print("Variable is None") + ``` +* **Checking for the exact same object:** If you specifically need to verify that two variables refer to the exact same object in memory, perhaps to detect alias relationships or for performance optimizations. + +#### Important Caveats for `is` +Python performs certain optimizations, such as **object interning**, for immutable types like small integers (typically -5 to 256), `True`, `False`, `None`, and sometimes short strings. This can lead to situations where `is` returns `True` for objects that might logically be considered distinct values, but which Python has optimized to point to the same memory location. + +```python +# Integer interning +x = 100 +y = 100 +print(f"x is y (small int): {x is y}") # Output: True (due to interning) + +a = 1000 +b = 1000 +print(f"a is b (large int): {a is b}") # Output: False (not always interned) + +# String interning (can be unpredictable for non-literal strings) +s1 = "hello" +s2 = "hello" +print(f"s1 is s2 (literal string): {s1 is s2}") # Output: True (often interned) + +s3 = "hello world" +s4 = "hello world" +print(f"s3 is s4 (longer string): {s3 is s4}") # Output: False (interning less likely) + +s5 = "hello" +s6 = "he" + "llo" +print(f"s5 is s6 (concatenated string): {s5 is s6}") # Output: True (optimised at compile time) +``` +Because of these optimizations, relying on `is` for value comparison, especially with numbers and strings, can lead to inconsistent and difficult-to-debug behavior. Always use `==` for comparing values unless you have a specific reason to check for object identity.
## 11. How does a _Python function_ work? -**Python functions** are the building blocks of code organization, often serving predefined tasks within modules and scripts. They enable reusability, modularity, and encapsulation. +**Python functions** are fundamental constructs for organizing code, embodying principles of reusability, modularity, and encapsulation. They allow developers to break down complex problems into smaller, manageable units, each designed to perform a specific task. -### Key Components +### Core Concepts of a Python Function -- **Function Signature**: Denoted by the `def` keyword, it includes the function name, parameters, and an optional return type. -- **Function Body**: This section carries the core logic, often comprising conditional checks, loops, and method invocations. -- **Return Statement**: The function's output is determined by this statement. When None is specified, the function returns by default. -- **Local Variables**: These variables are scoped to the function and are only accessible within it. +Python functions operate on several core principles: -### Execution Process +* **Code Reusability**: Functions define a block of code that can be executed multiple times from different parts of a program, avoiding code duplication. +* **Modularity**: They promote the division of a program into independent, self-contained modules, making the codebase easier to understand, maintain, and debug. +* **Encapsulation**: Functions encapsulate their internal logic and data (local variables), minimizing interference with other parts of the program. +* **First-Class Objects**: In Python, functions are first-class objects, meaning they can be assigned to variables, passed as arguments to other functions, and returned as values from other functions. This enables powerful programming paradigms like higher-order functions and decorators. -When a function is called: +### Anatomy of a Python Function -1. **Stack Allocation**: A stack frame, also known as an activation record, is created to manage the function's execution. This frame contains details like the function's parameters, local variables, and **instruction pointer**. - -2. **Parameter Binding**: The arguments passed during the function call are bound to the respective parameters defined in the function header. +A Python function typically consists of the following parts: -3. **Function Execution**: Control is transferred to the function body. The statements in the body are executed in a sequential manner until the function hits a return statement or the end of the function body. +#### Function Signature +The signature defines the function's interface. It begins with the `def` keyword, followed by the function's name, a pair of parentheses enclosing its parameters, and a colon `:`. Modern Python (post-PEP 484) also supports optional **type hints** for parameters and the return value to improve code readability and allow static analysis. -4. **Return**: If a return statement is encountered, the function evaluates the expression following the `return` and hands the value back to the caller. The stack frame of the function is then popped from the call stack. +```python +def greet(name: str, message: str = "Hello") -> str: + # Function body + pass +``` +Here, `greet` is the function name, `name` and `message` are parameters (`message` has a default value), and `-> str` indicates the function is expected to return a string. + +#### Function Body +This is an indented block of code that contains the logic performed when the function is called. It can include conditional statements, loops, other function calls, and any other valid Python statements. + +#### Return Statement +The `return` statement specifies the value that the function sends back to its caller. If a function reaches its end without an explicit `return` statement, or if `return` is used without an expression, it implicitly returns `None`. + +```python +def add(a: int, b: int) -> int: + result = a + b + return result -5. **Post Execution**: If there's no `return` statement, or if the function ends without evaluating any return statement, `None` is implicitly returned. +def do_nothing(): + # This function implicitly returns None + pass + +value1 = add(5, 3) # value1 will be 8 +value2 = do_nothing() # value2 will be None +``` -### Local Variable Scope +#### Docstrings +While not part of the execution, a **docstring** (a multi-line string immediately after the function signature) is a crucial component for documentation. It explains what the function does, its parameters, and what it returns. -- **Function Parameters**: These are a precursor to local variables and are instantiated with the values passed during function invocation. -- **Local Variables**: Created using an assignment statement inside the function and cease to exist when the function execution ends. -- **Nested Scopes**: In functions within functions (closures), non-local variables - those defined in the enclosing function - are accessible but not modifiable by the inner function, without using the `nonlocal` keyword. +```python +def calculate_area(radius: float) -> float: + """ + Calculates the area of a circle given its radius. -### Global Visibility + Args: + radius (float): The radius of the circle. -If a variable is not defined within a function, the Python runtime will look for it in the global scope. This behavior enables functions to access and even modify global variables. + Returns: + float: The calculated area. + """ + import math + return math.pi * radius**2 +``` -### Avoiding Side Effects +### How a Python Function Executes + +When a function is called, a specific sequence of actions occurs: + +1. #### Call Stack and Stack Frames + Upon a function call, a new **stack frame** (also known as an activation record) is created on the program's **call stack**. This stack frame is a dedicated memory region that stores information relevant to that specific function invocation, including: + * The function's parameters. + * Its local variables. + * The return address (the location in the calling code to resume execution after the function completes). + * Other management information. + +2. #### Parameter Binding + The arguments passed during the function call are bound to the corresponding **parameters** defined in the function's signature. Python supports various ways to pass arguments, including positional arguments, keyword arguments, default values, and arbitrary argument lists (`*args` for positional, `**kwargs` for keyword). + +3. #### Execution Flow + Control is transferred to the function's body. Statements within the function body are executed sequentially. If an exception occurs, the execution might be interrupted and propagated up the call stack. + +4. #### Returning Control + When a `return` statement is encountered, the function evaluates the expression (if any) following `return` and sends this value back to the caller. Subsequently, the function's stack frame is **popped** from the call stack, releasing the memory allocated for its local variables and parameters. Execution in the caller resumes from the return address. If no `return` statement is met or the function body completes, `None` is implicitly returned. + +### Variable Scope and Lifetime (LEGB Rule) + +Python uses the **LEGB rule** to determine the order in which it looks for names (variables, functions, classes) during resolution: + +* **L**ocal: Names assigned within the current function (e.g., `x = 10` inside a `def`). This also includes parameters. +* **E**nclosing function locals: Names in the local scope of any enclosing function (for nested functions). +* **G**lobal: Names assigned at the top-level of a module (outside any function). +* **B**uilt-in: Names pre-assigned in Python's built-in module (e.g., `print`, `len`, `str`). + +#### Local Scope (L) +Variables defined inside a function (including its parameters) are **local** to that function. They are created when the function is called and destroyed when the function completes execution. They are not accessible from outside the function. + +```python +def my_function(): + local_var = "I am local" + print(local_var) # Accessible here + +my_function() +# print(local_var) # This would raise a NameError +``` -Functions offer a level of encapsulation, potentially reducing side effects by ensuring that data and variables are managed within a controlled environment. Such containment can help enhance the robustness and predictability of a codebase. As a best practice, minimizing the reliance on global variables can lead to more maintainable, reusable, and testable code. +#### Enclosing Function Local Scope (E) +In nested functions, the inner function can access variables from its **enclosing (outer) function's scope**. These are often called **non-local** variables. To modify a non-local variable from an inner function, the `nonlocal` keyword must be used. Without `nonlocal`, an assignment inside the inner function would create a new local variable, shadowing the outer one. + +```python +def outer_function(): + enclosing_var = "Outer variable" + + def inner_function(): + nonlocal enclosing_var # Declare intent to modify + enclosing_var = "Modified by inner" + print(f"Inside inner: {enclosing_var}") + + inner_function() + print(f"Inside outer: {enclosing_var}") + +outer_function() +# Output: +# Inside inner: Modified by inner +# Inside outer: Modified by inner +``` + +#### Global Scope (G) +Variables defined at the top level of a script or module are **global**. Functions can read global variables directly. However, to **modify** a global variable from within a function, the `global` keyword must be explicitly used. Otherwise, an assignment will create a new local variable with the same name, leaving the global variable unchanged. + +```python +global_var = "I am global" + +def modify_global(): + global global_var # Declare intent to modify + global_var = "I was modified" + print(f"Inside function: {global_var}") + +def create_local_with_same_name(): + global_var = "I am a local masking the global" # Creates a new local variable + print(f"Inside function (local): {global_var}") + +print(f"Before call: {global_var}") +modify_global() +print(f"After modify_global: {global_var}") + +create_local_with_same_name() +print(f"After create_local_with_same_name: {global_var}") +# Output: +# Before call: I am global +# Inside function: I was modified +# After modify_global: I was modified +# Inside function (local): I am a local masking the global +# After create_local_with_same_name: I was modified (Global variable remains unchanged here) +``` + +### Best Practices: Minimizing Side Effects + +Functions offer a crucial level of encapsulation, which can significantly reduce **side effects**. A side effect occurs when a function modifies something outside its local scope (e.g., a global variable, a mutable argument, or an external system). + +While Python's flexibility allows functions to access and modify global/non-local variables (with `global`/`nonlocal`), over-reliance on this can lead to: + +* **Harder Debugging**: It becomes difficult to trace where and when a variable's value changes. +* **Reduced Readability**: The function's behavior might depend on external state not explicitly passed as arguments. +* **Lower Testability**: Functions with side effects are harder to test in isolation, as their output depends on the global state. +* **Less Reusability**: Functions tightly coupled to specific global states are less portable. + +As a best practice, strive for **pure functions** where possible: functions that, given the same inputs, always produce the same output and cause no side effects. Minimizing the use of `global` and `nonlocal` keywords, and favoring passing data through arguments and returning results, leads to more robust, predictable, and maintainable codebases.
## 12. What is a _lambda function_, and where would you use it? -A **Lambda function**, or **lambda**, for short, is a small anonymous function defined using the `lambda` keyword in Python. +### What is a Lambda Function? + +A **lambda function**, often simply called a **lambda**, is a small, anonymous function defined in Python using the `lambda` keyword. Unlike regular functions defined with `def`, a lambda function does not require a name, making it suitable for short, one-off operations. Its body is restricted to a single expression, the result of which is implicitly returned. -While you can certainly use named functions when you need a function for something in Python, there are places where a lambda expression is more suitable. +While traditional named functions serve most programming needs, lambda expressions provide a concise alternative for simple, functional constructs where a full `def` statement might be considered overly verbose. ### Distinctive Features -- **Anonymity**: Lambdas are not given a name in the traditional sense, making them suited for one-off uses in your codebase. -- **Single Expression Body**: Their body is limited to a single expression. This can be an advantage for brevity but a restriction for larger, more complex functions. -- **Implicit Return**: There's no need for an explicit `return` statement. -- **Conciseness**: Lambdas streamline the definition of straightforward functions. +* #### Anonymity + Lambdas are inherently anonymous; they are not bound to an identifier (name) in the same way `def` functions are. This makes them ideal for situations where a function is needed transiently and will not be reused elsewhere in the codebase. + +* #### Single Expression Body + The core characteristic of a lambda is its limitation to a single expression. This means its body cannot contain multiple statements (like `if`, `for`, `while`, or `return`). It computes and returns the value of that single expression. + * Example: `lambda x, y: x + y` is valid. + * Example: `lambda x: if x > 0: return x` is **invalid** because it contains a statement (`if`) and an explicit `return`. + +* #### Implicit Return + There is no need for an explicit `return` statement within a lambda. The value produced by the single expression in its body is automatically returned. + +* #### Conciseness + Lambdas enable the definition of straightforward functions in a compact, inline form, which can improve readability for simple operations when used appropriately. ### Common Use Cases -- **Map, Filter, and Reduce**: Functions like `map` can take a lambda as a parameter, allowing you to define simple transformations on the fly. For example, doubling each element of a list can be achieved with `list(map(lambda x: x*2, my_list))`. -- **List Comprehensions**: They are a more Pythonic way of running the same `map` or `filter` operations, often seen as an alternative to lambdas and `map`. -- **Sorting**: Lambdas can serve as a custom key function, offering flexibility in sort orders. -- **Callbacks**: Often used in events where a function is needed to be executed when an action occurs (e.g., button click). -- **Simple Functions**: For functions that are so basic that giving them a name, especially in more procedural code, would be overkill. +Lambdas shine in functional programming paradigms or when a small, throwaway function is required as an argument to a higher-order function. + +* #### Map, Filter, and Reduce + Functions like `map()`, `filter()`, and `functools.reduce()` often take a function as an argument to apply a transformation or filter criteria to an iterable. Lambdas provide a convenient way to define these simple operations inline. + + ```python + # Example: Doubling each element in a list using map and lambda + my_list = [1, 2, 3, 4] + doubled_list = list(map(lambda x: x * 2, my_list)) + print(doubled_list) # Output: [2, 4, 6, 8] + + # Example: Filtering even numbers using filter and lambda + even_numbers = list(filter(lambda x: x % 2 == 0, my_list)) + print(even_numbers) # Output: [2, 4] + ``` + +* #### Sorting with Custom Keys + The `sort()` method of lists or the built-in `sorted()` function can accept a `key` argument, which is a function that extracts a comparison key from each element in the list. Lambdas are perfect for defining custom sorting logic on the fly. + + ```python + # Example: Sorting a list of tuples by the second element + data = [('apple', 10), ('banana', 5), ('cherry', 15)] + sorted_data = sorted(data, key=lambda item: item[1]) + print(sorted_data) # Output: [('banana', 5), ('apple', 10), ('cherry', 15)] + + # Example: Sorting a list of strings by their length + words = ["python", "is", "awesome"] + sorted_words = sorted(words, key=lambda s: len(s)) + print(sorted_words) # Output: ['is', 'python', 'awesome'] + ``` + +* #### Callbacks + In event-driven programming (e.g., GUI frameworks, web frameworks), lambdas are frequently used as callback functions that are executed when a specific action occurs (e.g., a button click, an API response). + + ```python + # Conceptual example for a GUI button click + # button.on_click(lambda event: print("Button clicked!")) + ``` + +* #### Simple Functions + For very basic, self-contained functions that are used immediately and not intended for reuse, a lambda can prevent the clutter of defining a named function. + +### Notable Limitations and Alternatives -### Notable Limitations +While useful, lambdas have limitations that often make named functions or other Pythonic constructs preferable. -- **Lack of Verbose Readability**: Named functions are generally preferred when their intended use is obvious from the name. Lambdas can make code harder to understand if they're complex or not used in a recognizable pattern. -- **No Formal Documentation**: While the function's purpose should be apparent from its content, a named function makes it easier to provide direct documentation. Lambdas would need a separate verbal explanation, typically in the code or comments. +* #### Lack of Verbose Readability + If the logic within a lambda becomes even slightly complex, it can quickly become difficult to read and understand. Named functions with clear names and docstrings generally offer superior readability and maintainability for anything beyond trivial operations. + +* #### No Formal Documentation + Lambdas do not support docstrings, making it harder to document their purpose directly within the function itself. Their intent must be inferred from context or explained via external comments, which is less ideal. + +* #### Restriction to a Single Expression + As previously mentioned, lambdas cannot contain statements (assignments, `if`/`else` statements, loops, `try`/`except` blocks, etc.). This severely limits their complexity. For instance, you cannot define a lambda that prints something and then returns a value, as `print()` is a statement. While conditional *expressions* (`x if condition else y`) are allowed, they can quickly become unreadable. + +* #### Alternatives like List Comprehensions + For many scenarios involving `map()` or `filter()` with simple lambdas, Python's **list comprehensions** (and generator expressions, set comprehensions, dictionary comprehensions) often provide a more readable and Pythonic alternative. + + ```python + # Using map and lambda + my_list = [1, 2, 3, 4] + doubled_list_lambda = list(map(lambda x: x * 2, my_list)) + + # Using a list comprehension (often preferred for clarity) + doubled_list_comprehension = [x * 2 for x in my_list] + + print(doubled_list_lambda) # Output: [2, 4, 6, 8] + print(doubled_list_comprehension) # Output: [2, 4, 6, 8] + ``` + +In conclusion, lambda functions are a powerful tool for writing concise, anonymous functions for simple, single-expression tasks, particularly when passed as arguments to higher-order functions. However, for more complex logic, better readability, or reusability, a traditional `def` function or other Pythonic constructs are usually the superior choice.
## 13. Explain _*args_ and _**kwargs_ in _Python_. -In Python, `*args` and `**kwargs` are often used to pass a variable number of arguments to a function. +In Python, `*args` and `**kwargs` are powerful constructs used to pass a **variable number of arguments** to a function. They enable flexible function definitions that can accept an arbitrary quantity of inputs. + +The `*args` syntax collects a variable number of **positional arguments** into a **tuple**, while `**kwargs` does the same for **keyword arguments** into a **dictionary**. + +Here are the key features, use-cases, and their respective code examples, along with important related concepts. + +### `*args`: Variable Positional Arguments -`*args` collects a variable number of positional arguments into a **tuple**, while `**kwargs` does the same for keyword arguments into a **dictionary**. +The single asterisk `*` operator, when used in a function definition, indicates that an arbitrary number of positional arguments can be accepted. -Here are the key features, use-cases, and their respective code examples. +#### How it Works -### **\*args**: Variable Number of Positional Arguments +The name `*args` is a widely adopted **convention**. The asterisk `*` itself is the operator that tells Python to collect all remaining positional arguments into a single **tuple**. This tuple can then be iterated over within the function. -- **How it Works**: The name `*args` is a convention. The asterisk (*) tells Python to put any remaining positional arguments it receives into a tuple. +#### Use-Cases -- **Use-Case**: When the number of arguments needed is uncertain. +* When the number of arguments a function needs to operate on is **uncertain** or can vary. +* To create **flexible functions** that can handle different call signatures for positional data. +* For **forwarding arguments** from one function to another. -#### Code Example: "*args" +#### Code Example: `*args` ```python def sum_all(*args): + """ + Calculates the sum of an arbitrary number of integers. + The collected arguments are available as a tuple named 'args'. + """ result = 0 + print(f"Type of args: {type(args)}") # Output: Type of args: for num in args: result += num return result -print(sum_all(1, 2, 3, 4)) # Output: 10 +print(sum_all(1, 2, 3, 4)) # Output: 10 +print(sum_all(10, 20, 30, 40, 50)) # Output: 150 ``` -### **\*\*kwargs**: Variable Number of Keyword Arguments +### `**kwargs`: Variable Keyword Arguments + +The double asterisk `**` operator, when used in a function definition, allows a function to accept an arbitrary number of keyword arguments. + +#### How it Works -- **How it Works**: The double asterisk (**) is used to capture keyword arguments and their values into a dictionary. +Similar to `*args`, `**kwargs` is a **convention**, and the double asterisk `**` is the operator. It captures all remaining keyword arguments and their values into a **dictionary**. The keys of this dictionary are the keyword argument names (as strings), and the values are their corresponding argument values. -- **Use-Case**: When a function should accept an arbitrary number of keyword arguments. +#### Use-Cases -#### Code Example: "**kwargs" +* When a function needs to accept an **arbitrary number of named parameters**. +* To create highly **configurable functions** where options can be passed as keyword arguments. +* For **building flexible APIs** where different settings can be provided. +* For **forwarding keyword arguments** from one function to another, especially in decorators or object initializers. + +#### Code Example: `**kwargs` ```python -def print_values(**kwargs): +def print_user_info(**kwargs): + """ + Prints user information passed as keyword arguments. + The collected arguments are available as a dictionary named 'kwargs'. + """ + print(f"Type of kwargs: {type(kwargs)}") # Output: Type of kwargs: + if not kwargs: + print("No user information provided.") + return + + print("User Info:") for key, value in kwargs.items(): - print(f"{key}: {value}") + print(f" {key.replace('_', ' ').title()}: {value}") # Keyword arguments are captured as a dictionary -print_values(name="John", age=30, city="New York") +print_user_info(name="Alice", age=25, city="London", occupation="Engineer") # Output: -# name: John -# age: 30 -# city: New York +# Type of kwargs: +# User Info: +# Name: Alice +# Age: 25 +# City: London +# Occupation: Engineer + +print_user_info() # Output: No user information provided. ``` + +### Argument Order and Combination + +When defining a function that uses a mix of standard arguments, `*args`, and `**kwargs`, there is a specific order that must be followed: + +1. **Standard positional arguments** +2. **`*args`** (to capture additional positional arguments) +3. **Keyword-only arguments** (arguments specified after `*args` that *must* be passed by keyword) +4. **`**kwargs`** (to capture additional keyword arguments) + +#### Code Example: Combining `*args` and `**kwargs` + +```python +def configure_system(system_name, *settings, debug_mode=False, **options): + """ + Configures a system with a name, a list of settings, an optional debug mode, + and arbitrary additional configuration options. + """ + print(f"Configuring system: {system_name}") + print(f" Settings (tuple): {settings}") + print(f" Debug Mode (kw-only): {debug_mode}") + print(f" Additional Options (dict): {options}") + +configure_system("WebServer", "security_patch", "logging_level", + debug_mode=True, timeout=30, users_limit=100) +# Output: +# Configuring system: WebServer +# Settings (tuple): ('security_patch', 'logging_level') +# Debug Mode (kw-only): True +# Additional Options (dict): {'timeout': 30, 'users_limit': 100} + +configure_system("Database", "replication_enabled") +# Output: +# Configuring system: Database +# Settings (tuple): ('replication_enabled',) +# Debug Mode (kw-only): False +# Additional Options (dict): {} +``` + +### Unpacking Arguments (The Duality) + +The `*` and `**` operators have a dual purpose: they can also be used to **unpack** iterables and dictionaries, respectively, when **calling a function**. This is the inverse operation of collecting arguments. + +#### Unpacking with `*` + +Using `*` before an iterable (like a list or tuple) in a function call unpacks its elements, treating them as individual positional arguments. + +```python +def multiply(a, b, c): + return a * b * c + +numbers = [2, 3, 4] +print(multiply(*numbers)) # Unpacks [2, 3, 4] into 2, 3, 4. Output: 24 + +coordinates = (10, 20, 30) +print(multiply(*coordinates)) # Unpacks (10, 20, 30) into 10, 20, 30. Output: 6000 +``` + +#### Unpacking with `**` + +Using `**` before a dictionary in a function call unpacks its key-value pairs, treating them as individual keyword arguments. + +```python +def describe_person(name, age, city): + print(f"{name} is {age} years old and lives in {city}.") + +person_data = {"name": "Charlie", "age": 40, "city": "Paris"} +describe_person(**person_data) # Unpacks dictionary into name="Charlie", age=40, city="Paris". +# Output: Charlie is 40 years old and lives in Paris. + +config = {"name": "Server1", "port": 8080, "timeout": 60} + +# Can be combined with specific args, as long as there are no conflicts +def connect(name, port, **extra_options): + print(f"Connecting to {name} on port {port} with options: {extra_options}") + +connect(**config) +# Output: Connecting to Server1 on port 8080 with options: {'timeout': 60} +``` + +### Key Benefits + +* **Flexibility:** Functions can be designed to accept varying numbers of inputs without needing multiple overloaded definitions. +* **Code Reusability:** Promotes more generic and adaptable function designs. +* **Argument Forwarding:** Simplifies passing arguments from a wrapper function to an inner function, which is particularly useful in decorators or when building API layers.
## 14. What are _decorators_ in _Python_? -In Python, a **decorator** is a design pattern and a feature that allows you to modify functions and methods dynamically. This is done primarily to keep the code clean, maintainable, and DRY (Don't Repeat Yourself). +In Python, a **decorator** is a powerful design pattern and a feature that allows you to modify or extend the behavior of functions or methods dynamically without permanently altering their source code. This is a form of **metaprogramming**, primarily used to keep the code **clean**, **maintainable**, and adhere to the **DRY** (Don't Repeat Yourself) principle. ### How Decorators Work -- Decorators wrap a target function, allowing you to execute custom code before and after that function. -- They are typically **higher-order functions** that take a function as an argument and return a new function. -- This paradigm of "functions that modify functions" is often referred to as **metaprogramming**. +* Decorators are essentially **callable objects** (usually functions) that take another function as an argument. +* They *wrap* the **target function**, allowing you to execute custom code both *before* and *after* the target function's execution. +* The decorator then returns a new function (or another callable object) that typically replaces the original function. This makes them **higher-order functions**. ### Common Use Cases -- **Authorization and Authentication**: Control user access. -- **Logging**: Record function calls and their parameters. -- **Caching**: Store previous function results for quick access. -- **Validation**: Verify input parameters or function output. -- **Task Scheduling**: Execute a function at a specific time or on an event. -- **Counting and Profiling**: Keep track of the number of function calls and their execution time. +Decorators have numerous practical applications, enhancing code modularity and reusability: + +* **Authorization and Authentication**: Restricting access to certain functions based on user roles or permissions. +* **Logging**: Recording function calls, arguments, return values, or exceptions for debugging and auditing. +* **Caching**: Storing the results of expensive function calls to avoid recomputing them for the same inputs. +* **Validation**: Checking input parameters or function output against specified criteria. +* **Task Scheduling**: Executing a function at a specific time or on a particular event. +* **Counting and Profiling**: Keeping track of the number of times a function is called or measuring its execution time. +* **Retries**: Automatically retrying a function call if it fails (e.g., due to network issues). ### Using Decorators in Code -Here is the Python code: +Python's `@decorator` syntax provides **syntactic sugar** for applying decorators. ```python from functools import wraps +import time -# 1. Basic Decorator -def my_decorator(func): - @wraps(func) # Ensures the original function's metadata is preserved +# 1. Basic Decorator (Timer Example) +def timer(func): + """A decorator that measures the execution time of a function.""" + @wraps(func) # Preserves the original function's metadata def wrapper(*args, **kwargs): - print('Something is happening before the function is called.') + start_time = time.perf_counter() result = func(*args, **kwargs) - print('Something is happening after the function is called.') + end_time = time.perf_counter() + print(f'Function {func.__name__!r} executed in {(end_time - start_time):.4f}s') return result return wrapper -@my_decorator -def say_hello(): - print('Hello!') +@timer +def long_running_function(): + """Simulates a function that takes some time to execute.""" + time.sleep(0.5) + print('Long running function completed.') -say_hello() +long_running_function() # Output: Long running function completed. \n Function 'long_running_function' executed in X.XXXs -# 2. Decorators with Arguments -def decorator_with_args(arg1, arg2): +# 2. Decorators with Arguments (Parameterized Decorator) +def log_level(level): + """A decorator factory that creates a decorator to log function calls at a specific level.""" def actual_decorator(func): @wraps(func) def wrapper(*args, **kwargs): - print(f'Arguments passed to decorator: {arg1}, {arg2}') + print(f'[{level.upper()}] Calling {func.__name__!r} with args: {args}, kwargs: {kwargs}') result = func(*args, **kwargs) + print(f'[{level.upper()}] {func.__name__!r} returned: {result}') return result return wrapper return actual_decorator -@decorator_with_args('arg1', 'arg2') -def my_function(): - print('I am decorated!') +@log_level('info') +def add(a, b): + return a + b -my_function() +@log_level('debug') +def multiply(x, y): + return x * y + +print(add(5, 3)) # Output: [INFO] Calling 'add' with args: (5, 3), kwargs: {} \n [INFO] 'add' returned: 8 \n 8 +print(multiply(4, 2)) # Output: [DEBUG] Calling 'multiply' with args: (4, 2), kwargs: {} \n [DEBUG] 'multiply' returned: 8 \n 8 ``` ### Decorator Syntax in Python -The `@decorator` syntax is a convenient shortcut for: +The `@decorator` syntax above is a compact and readable way to apply a decorator. It is entirely equivalent to, and merely **syntactic sugar** for, the following: ```python def say_hello(): print('Hello!') -say_hello = my_decorator(say_hello) + +# Applying the decorator manually +say_hello = timer(say_hello) # assuming 'timer' from the example above + +say_hello() # This would now also print the execution time ``` -### Role of **functools.wraps** +### Role of `functools.wraps` -When defining decorators, particularly those that return functions, it is good practice to use `@wraps(func)` from the `functools` module. This ensures that the original function's metadata, such as its name and docstring, is preserved. +When defining decorators, especially those that return inner functions, it is crucial to use `@wraps(func)` from the `functools` module. This decorator ensures that the original function's **metadata** (like its `__name__`, `__doc__`, `__module__`, and argument lists) is correctly preserved on the wrapper function. Without `@wraps`, the decorated function would appear to be the wrapper function, which can hinder debugging, documentation tools, and introspection.
## 15. How can you create a _module_ in _Python_? -You can **create** a Python module through one of two methods: +To create a module in Python, you essentially save Python code in a file with a `.py` extension. This file then becomes a module that can be imported and used in other Python programs or modules. + +A **Python module** is a file containing Python definitions and statements. Modules are a fundamental mechanism for organizing related code into logical units, promoting reusability, and avoiding name collisions. + +### How to Create a Python Module + +The primary and most common way to create a module is as follows: -- **Define**: Begin with saving a Python file with `.py` extension. This file will automatically function as a module. +1. **Save Python Code as a `.py` File**: + Simply write your Python code (functions, classes, variables, etc.) in a text file and save it with a `.py` extension. For instance, if you save your code as `my_module.py`, then `my_module` becomes the name of your module. -- **Create a Blank Module**: Start an empty file with no extension. Name the file using the accepted module syntax, e.g., `__init__ `, for it to act as a module. +2. **Implicitly as Part of a Package (using `__init__.py`)**: + While not a standalone module itself in the same sense as a `.py` file, an `__init__.py` file *is* a module. Its primary purpose is to mark a directory as a **Python package**. A package is a way to organize related modules into a directory hierarchy. When a directory contains an `__init__.py` file (even an empty one), Python treats that directory as a package, and any `.py` files inside it become modules within that package. + *Note*: As of Python 3.3, `__init__.py` is optional for *namespace packages*, but it is still commonly used and required for regular packages. -Next, use **import** to access the module and its functionality. +### Accessing a Module + +Once a module is created, you can access its functionality using the `import` statement in another Python script or interactive session. ### Code Example: Creating a `math_operations` Module +Let's illustrate by creating a module named `math_operations`. + #### Module Definition -Save the below `math_operations.py` file : +Save the following code in a file named `math_operations.py`: ```python +# math_operations.py + def add(x, y): + """Adds two numbers and returns the sum.""" return x + y def subtract(x, y): + """Subtracts the second number from the first.""" return x - y def multiply(x, y): + """Multiplies two numbers.""" return x * y def divide(x, y): + """Divides the first number by the second. Handles division by zero.""" + if y == 0: + raise ValueError("Cannot divide by zero") return x / y + +PI = 3.14159 # A global variable within the module ``` #### Module Usage -You can use `math_operations` module by using import as shown below: +You can use the `math_operations` module by importing it into another Python file (e.g., `main_app.py`) or an interactive interpreter: ```python -import math_operations +# main_app.py -result = math_operations.add(4, 5) -print(result) +import math_operations # Imports the entire module -result = math_operations.divide(10, 5) -print(result) -``` +# Access functions and variables using the module name +result_add = math_operations.add(4, 5) +print(f"4 + 5 = {result_add}") # Output: 4 + 5 = 9 -Even though it is not required in the later **versions of Python**, you can also use statement `from math_operations import *` to import all the members such as functions and classes at once: +result_divide = math_operations.divide(10, 5) +print(f"10 / 5 = {result_divide}") # Output: 10 / 5 = 2.0 -```python -from math_operations import * # Not recommended generally due to name collisions and readability concerns +print(f"Value of PI: {math_operations.PI}") # Output: Value of PI: 3.14159 -result = add(3, 2) -print(result) -``` +# Using specific imports for brevity (recommended for specific functions/classes) +from math_operations import multiply, subtract -### Best Practice -Before submitting the code, let's make sure to follow the **Best Practice**: +result_multiply = multiply(6, 7) +print(f"6 * 7 = {result_multiply}") # Output: 6 * 7 = 42 -- **Avoid Global Variables**: Use a `main()` function. -- **Guard Against Code Execution on Import**: To avoid unintended side effects, use: +result_subtract = subtract(15, 8) +print(f"15 - 8 = {result_subtract}") # Output: 15 - 8 = 7 -```python -if __name__ == "__main__": - main() +# Importing all members (generally discouraged) +# from math_operations import * # Not recommended generally due to potential name collisions and readability concerns + +# try: +# result_zero_divide = math_operations.divide(10, 0) +# print(result_zero_divide) +# except ValueError as e: +# print(f"Error: {e}") # Output: Error: Cannot divide by zero ``` -This makes sure that the block of code following `if __name__ == "__main__":` is only executed when the module is run directly and not when imported as a module in another program. +### Best Practices for Module Creation + +When creating modules, consider the following best practices: + +* **Modularity**: Keep modules focused on a single responsibility or a coherent set of related functionalities. +* **Documentation**: Use docstrings for modules, functions, and classes to explain their purpose, arguments, and return values. This greatly enhances maintainability. +* **Guard Against Code Execution on Import**: It's common for modules to contain code that should only run when the module is executed directly (as a script) and not when it's imported into another program. Use the `if __name__ == "__main__":` block for this: + + ```python + # my_module.py + + def my_function(): + print("This function is part of the module.") + + def main(): + """Main execution logic when the module is run as a script.""" + print("This code runs only when my_module.py is executed directly.") + my_function() + # Additional script-specific logic here + + if __name__ == "__main__": + main() + ``` + This ensures that the code inside the `main()` function (or directly under the `if` block) is only executed when `my_module.py` is run as the primary script. If `my_module.py` is imported into another file, the `main()` function will not be called automatically, preventing unintended side effects.
From d23005d99dba3d318190d6e8d4ac3e8d6a1f1c8d Mon Sep 17 00:00:00 2001 From: Devinterview-io <76989322+Devinterview-io@users.noreply.github.com> Date: Sun, 25 Jan 2026 15:20:10 -0500 Subject: [PATCH 3/7] Update python interview questions --- README.md | 1830 +++++++++++------------------------------------------ 1 file changed, 376 insertions(+), 1454 deletions(-) diff --git a/README.md b/README.md index ece7412..7453e9a 100644 --- a/README.md +++ b/README.md @@ -13,1922 +13,844 @@ ## 1. What are the _key features_ of _Python_? -**Python** is a versatile and popular programming language known for its simplicity, **elegant syntax**, and a vast ecosystem of libraries. As of 2026, its relevance and adoption continue to grow across various industries. Let's look at some of the key features that make Python stand out. +**Python** is a versatile and dominant programming language known for its simplicity, **elegant syntax**, and massive ecosystem. Below are the defining features that maintain its popularity in 2026. ### Key Features of Python #### 1. Interpreted and Interactive - -Python code is executed **line-by-line** by an interpreter, not compiled to machine code before runtime. This characteristic facilitates **rapid prototyping**, experimentation, and easier debugging, as errors are typically detected only when that specific line of code is executed. Python also provides an interactive shell where developers can execute commands and see results immediately. - -```python -# Example of interactive mode ->>> print("Hello, Python!") -Hello, Python! ->>> x = 10 ->>> y = 20 ->>> x + y -30 -``` +Python executes code **line-by-line** via an interpreter, facilitating rapid prototyping and debugging. Modern versions utilize a specializing **adaptive interpreter** to optimize performance during execution. #### 2. Easy to Learn and Read - -Python's **clean, readable syntax** closely resembles plain English, significantly reducing the cognitive load for both beginners and experienced developers. Its mandatory use of **significant whitespace** (indentation) for defining code blocks enforces a consistent and highly readable code style, making it easier to maintain and understand code written by others. +Python's **clean syntax** resembles plain English and relies on indentation for structure. This high readability significantly reduces the cognitive load for both beginners and maintenance engineers. #### 3. Cross-Platform Compatibility - -Python is a **cross-platform language**, meaning code written on one operating system (e.g., Windows) can run seamlessly on others (e.g., Linux, macOS) without requiring platform-specific modifications. This "write once, run anywhere" capability is facilitated by the Python interpreter being available for diverse platforms. +Python is platform-independent, running seamlessly on Windows, Linux, macOS, and **WebAssembly (WASM)** environments without requiring platform-specific code modifications. #### 4. Modular and Scalable - -Python promotes modular programming through **modules** and **packages**. Developers can organize code into reusable files (modules) and directories of modules (packages), which can then be easily imported and used in other parts of a project. This modularity enhances code organization, reusability, and maintainability, making Python suitable for developing **large-scale and complex applications**. - -```python -# Example of importing a module -import math - -radius = 5 -area = math.pi * (radius ** 2) -print(f"Area of circle: {area}") -``` +The language encourages modularity through packages and modules, allowing developers to organize code into **reusable** functions and classes for scalable application architectures. #### 5. Rich Library Ecosystem +The Python Package Index (PyPI) hosts **hundreds of thousands** of libraries, providing immediate solutions for Artificial Intelligence, Data Science, Web Development, and Cloud Computing. -The Python Package Index (PyPI) is a vast repository hosting over **500,000 libraries and frameworks** (and growing rapidly). This **extensive ecosystem** provides ready-to-use solutions for almost any programming task, from: -* **Web Development**: Django, Flask, FastAPI -* **Data Science & Analytics**: NumPy, Pandas, SciPy -* **Machine Learning & AI**: TensorFlow, PyTorch, scikit-learn -* **Automation**: Selenium, Ansible -* **GUI Development**: PyQt, Kivy, Tkinter - -This rich collection significantly speeds up development and innovation. - -#### 6. General-Purpose and Versatile - -Python is a **general-purpose programming language**, meaning it is not specialized for any single domain but is rather highly adaptable to a multitude of applications. Its versatility allows it to be effectively used in areas such as web and desktop applications, scientific and numerical computing, artificial intelligence and machine learning, data analysis, network programming, scripting, and automation. +#### 6. Exceptionally Versatile +Python serves as a general-purpose language, equally proficient in scripting, building complex web applications, and performing high-performance **scientific computing**. #### 7. Automatic Memory Management +Python manages memory automatically via a private heap and built-in **Garbage Collection**, shielding developers from low-level details like manual memory deallocation. -Python features **automatic memory management**, relieving developers from manual memory allocation and deallocation tasks. It primarily uses **reference counting** and a **generational garbage collector** to automatically detect and reclaim memory occupied by objects that are no longer referenced. This significantly reduces the chances of memory leaks and simplifies application development. - -#### 8. Dynamically Typed - -Python is a **dynamically typed language**, which means the type of a variable is determined at runtime, not at compile time. Developers do not need to explicitly declare the data type of a variable; Python infers it during execution. This offers flexibility and faster development cycles, though it can sometimes lead to type-related errors only detectable at runtime. - -```python -# Example of dynamic typing -my_variable = 10 # my_variable is an integer -print(type(my_variable)) # Output: - -my_variable = "hello" # Now my_variable is a string -print(type(my_variable)) # Output: -``` - -#### 9. Object-Oriented Programming (OOP) +#### 8. Dynamically Typed with Gradual Typing +Python infers data types at runtime (dynamic typing). However, modern Python embraces **Type Hints**, enabling static analysis and robust tooling while retaining runtime flexibility. -Python is a **multi-paradigm language** that fully supports and leverages the **object-oriented programming (OOP)** paradigm. In Python, everything is an **object**, and it facilitates concepts like **classes**, **objects**, **inheritance**, **polymorphism**, and **encapsulation**. This object-oriented nature helps in structuring complex applications into manageable and reusable components. +#### 9. Object-Oriented +Python strictly follows object-oriented paradigms. Everything, including functions and data types, is an **object**, supporting inheritance, polymorphism, and encapsulation. -#### 10. Extensible & Embeddable - -Python is highly **extensible**, allowing developers to integrate code written in other languages, particularly C and C++, for performance-critical tasks. This is commonly done using the **CPython C API**, `ctypes` module, or tools like **Cython**. Conversely, Python is also **embeddable**, meaning the Python interpreter can be integrated into applications written in other languages, allowing them to execute Python code and leverage its scripting capabilities. +#### 10. Extensible and Embeddable +Python allows integration with C/C++ for performance-critical tasks. It is also **embeddable**, allowing integration into other applications to provide scripting interfaces.
## 2. How is _Python_ executed? -### How Python is Executed - -Python source code undergoes a multi-stage process involving both **compilation** and **interpretation** before it can be executed. Understanding these stages is key to comprehending Python's performance characteristics and portability. +### Execution Overview +Python is a hybrid language that utilizes a two-step process involving **compilation** to an intermediate format followed by **interpretation**. This design balances development speed with portability. ### Compilation & Interpretation +The standard execution flow in CPython (the reference implementation) consists of: -Python employs a hybrid execution model: - -- **Bytecode Compilation**: When a Python program is run, the source code (`.py` files) is first translated into an intermediate format called **bytecode**. This process is performed by the Python interpreter's **compiler component**. Bytecode is a low-level, platform-independent set of instructions, typically stored in `.pyc` files (Python compiled files) within `__pycache__` directories for faster loading on subsequent runs. -- **Bytecode Interpretation**: The generated bytecode is then executed by the **Python Virtual Machine (PVM)**. The PVM is a runtime engine that reads and executes bytecode instructions one by one. This step-by-step execution is the "interpretation" phase. - -This "compile-to-bytecode then interpret" approach is characteristic of many virtual machine-based languages and offers significant advantages. - -### Bytecode versus Machine Code Execution - -Some programming languages (like C++ or Rust) compile directly to **machine code**, which is specific to a computer's architecture (e.g., x86, ARM) and can be executed directly by the CPU. Python, on the other hand, compiles to **bytecode**. - -- **Performance**: The extra layer of abstraction introduced by the PVM interpreting bytecode *can* make standard Python implementations (like CPython) slower in certain CPU-bound scenarios compared to languages that compile directly to optimized machine code. However, constant improvements in CPython's interpreter and standard library, along with the increasing use of optimized C extensions, significantly mitigate this in many real-world applications. -- **Portability**: The primary advantage of bytecode is its **platform independence**. A Python program's bytecode can be executed on any machine that has a compatible PVM, regardless of the underlying operating system or hardware architecture. This ensures excellent cross-platform support. - -### Source Code to Bytecode: Compilation Steps - -The process of translating Python source code into bytecode involves several standard compiler phases: - -1. #### Lexical Analysis (Scanning) - The source code is broken down into a stream of **tokens**. Tokens are the smallest meaningful units in the language, such as keywords (`def`, `return`), identifiers (`example_func`), operators (`*`), and literals (`15`, `20`). -2. #### Syntax Parsing - The stream of tokens is then analyzed to ensure it conforms to Python's grammatical rules. This phase constructs an **Abstract Syntax Tree (AST)**, which is a hierarchical representation of the program's structure. -3. #### Semantic Analysis - The AST is checked for semantic correctness. This involves tasks like ensuring variables are defined before use, type checking (where applicable), and verifying that operations are valid for their operands. Python performs some semantic checks at this stage, though many type-related checks occur at runtime due to its dynamic nature. -4. #### Bytecode Generation - Finally, based on the validated AST, the **bytecode instructions** are generated. These instructions are tailored for execution by the PVM. - -### Just-In-Time (JIT) Compilation and Alternative Implementations +- **Bytecode Compilation**: The Python compiler translates high-level source code (`.py`) into **bytecode**. Bytecode represents a set of platform-independent instructions optimized for the Python Virtual Machine (PVM). These compiled files are often cached in the `__pycache__` directory (as `.pyc` files) to speed up subsequent startups. + +- **Interpretation**: The PVM acts as a loop that iterates through bytecode instructions, executing them one by one. It abstracts the underlying hardware, ensuring the code runs on any operating system. -While the default CPython interpreter primarily relies on bytecode interpretation, the concept of **Just-In-Time (JIT) compilation** is highly relevant to Python's execution ecosystem. +### Source Code to Bytecode: Key Steps +1. **Lexical Analysis (Tokenization)**: The source code is broken down into a stream of atomic elements called tokens (keywords, identifiers, literals). +2. **Parsing**: Tokens are organized into an **Abstract Syntax Tree (AST)**, a tree representation of the code's logical structure. +3. **Symbol Table Generation**: The compiler analyzes the AST to define scopes and variable bindings (semantic analysis). +4. **Bytecode Generation**: The AST is traversed to emit the final bytecode instructions encapsulated in code objects. -- **JIT's Role**: A JIT compiler compiles parts of the bytecode into native machine code *during runtime*, typically focusing on frequently executed sections (hot paths). This can significantly boost performance by eliminating the overhead of bytecode interpretation for those sections. -- **CPython vs. JIT**: It's crucial to note that **standard CPython does not include a JIT compiler** as of 2026. CPython's performance improvements mostly come from optimizing its interpreter loop, specializing opcodes, and better memory management. -- **Alternative Implementations**: JIT compilation is a core feature of alternative Python implementations like **PyPy**. PyPy's JIT compiler often delivers substantial speedups for pure Python code compared to CPython, especially for long-running processes with repetitive computations. Other projects and experimental CPython forks might explore JIT integration in the future, but it's not a standard feature of the mainstream CPython distribution. +### Modern Optimization: Adaptive Interpreter & JIT +Since Python 3.11 (and enhanced in 3.13+), the execution model has evolved to include dynamic optimization: -Therefore, when discussing "how Python is executed," it's generally understood to refer to CPython's bytecode interpretation model, while acknowledging that other implementations leverage JIT for enhanced performance. +- **Tier 1: Adaptive Interpretation**: The PVM monitors code as it runs. If specific instructions are executed frequently with consistent types (e.g., binary addition of two integers), the interpreter "specializes" them on-the-fly, replacing generic bytecode with faster, type-specific versions. +- **Tier 2: JIT Compilation**: Modern CPython utilizes a **Copy-and-Patch JIT** (Just-In-Time) compiler. For "hot" code paths, specialized bytecode is translated into machine code micro-operations, significantly reducing the overhead of the interpretation loop. -### Code Example: Disassembly of Bytecode - -Python's `dis` module allows us to inspect the bytecode generated for a function or code object, providing a direct view into the instructions the PVM will execute. +### Code Example: Bytecode Inspection +We can use the `dis` module to view the underlying bytecode. The example below also demonstrates **constant folding**, an optimization where the compiler pre-calculates constant expressions. ```python import dis -def calculate_product(): - a = 15 - b = 20 - return a * b +def example_func(): + return 15 * 20 # Disassemble to view bytecode instructions -dis.dis(calculate_product) +dis.dis(example_func) ``` -Here's the disassembled output for the `calculate_product` function: - +**Output:** ```plaintext - 4 0 LOAD_CONST 1 (15) # Load integer 15 - 2 STORE_FAST 0 (a) # Store it in local variable 'a' - - 5 4 LOAD_CONST 2 (20) # Load integer 20 - 6 STORE_FAST 1 (b) # Store it in local variable 'b' - - 6 8 LOAD_FAST 0 (a) # Load value of 'a' - 10 LOAD_FAST 1 (b) # Load value of 'b' - 12 BINARY_MULTIPLY # Multiply the top two values on stack - 14 RETURN_VALUE # Return the result + 4 0 LOAD_CONST 1 (300) + 2 RETURN_VALUE ``` - -This output clearly shows the PVM instructions for loading constants, storing them in local variables, loading the variables again, performing the multiplication, and finally returning the value. This granular view demonstrates the step-by-step nature of bytecode execution. +*(The compiler computed `15 * 20` -> `300` during compilation, issuing a single `LOAD_CONST` instruction rather than a runtime multiplication.)*
## 3. What is _PEP 8_ and why is it important? -### What is PEP 8 and why is it important? +### What is PEP 8? -**PEP 8** is the official style guide for Python code. Its primary purpose is to promote code consistency, readability, and maintainability across the Python community. The name stands for **P**ython **E**nhancement **P**roposal, and PEPs are the primary mechanism for proposing and standardizing new features, changes, and conventions for the Python language itself. +**PEP 8** is the official style guide for Python code. It stands for *Python Enhancement Proposal 8* and provides conventions for writing clear, consistent, and standard Python. Its primary goal is to improve the readability of code and make it indistinguishable across different developers. -While not a rigid set of mandatory rules, PEP 8 provides comprehensive guidelines that help developers write Python code that is visually uniform and thus significantly easier to understand, navigate, and debug for anyone reading it. Adherence to PEP 8 fosters a common coding style, reducing cognitive load when moving between different Python projects or collaborating with other developers. +In 2026, while strict manual adherence is less common, PEP 8 remains the foundational configuration for modern auto-formatters (like **Ruff** or **Black**) and linters. ### Key Design Principles -PEP 8's recommendations are rooted in several fundamental design principles: - -* #### Readability - Code should be as easy to read and understand as plain English, even by someone unfamiliar with the codebase or the original author. Clear and consistent formatting significantly contributes to this. -* #### Consistency - A codebase should adhere to a predictable and uniform style. This minimizes surprises and allows developers to focus on the logic rather than deciphering varied formatting choices. -* #### Maintainability - Consistent and readable code is inherently easier to maintain, update, and debug over time. It reduces the effort required to onboard new team members and ensures the longevity of the software. -* #### Explicit over Implicit - Python's philosophy often favors explicit code over implicit code. PEP 8 extends this by encouraging clear and unambiguous formatting. - -### Core Guidelines - -PEP 8 covers a wide range of formatting conventions. Some of the most frequently applied guidelines include: - -* #### Indentation - Use **4 spaces** per level of logical indentation. Tabs should be avoided entirely for indentation. - ```python - # PEP 8 compliant indentation - def example_function(arg1, arg2): - if arg1 > arg2: - print("arg1 is greater") - else: - print("arg2 is greater or equal") - ``` -* #### Line Length - Limit all lines of code to a maximum of **79 characters**. For docstrings and comments, the limit is often recommended to be **72 characters**. This guideline improves readability, especially when viewing code on smaller screens or in side-by-side diffs. While the official PEP 8 recommendation remains 79 characters, it's worth noting that many modern Python projects, especially those using auto-formatters like `Black` or `Ruff`, often adopt slightly longer line lengths (e.g., 88 or 120 characters) for practical reasons, as long as readability isn't compromised. -* #### Blank Lines - Use blank lines to separate logical sections of code, such as functions, classes, and larger blocks within functions. Generally, two blank lines separate top-level function and class definitions, and one blank line separates method definitions within a class. Avoid excessive blank lines. -* #### Imports - Imports should typically be on separate lines and grouped as follows: standard library imports, third-party imports, and local application-specific imports. Each group should be separated by a blank line. - ```python - import os - import sys - - import requests - from numpy import array - - from my_package.my_module import MyClass - ``` +PEP 8 emphasizes: + +- **Readability**: Code is read much more often than it is written. +- **Consistency**: A uniform style reduces cognitive load, allowing developers to focus on logic rather than formatting. +- **Explicit Layout**: Visual cues (indentation, spacing) should reflect the logical structure of the code. + +### Base Rules + +- **Indentation**: Use **4 spaces** for each indentation level. Do not use tabs. +- **Line Length**: The official limit is **79 characters** to facilitate side-by-side code review. (Note: Many modern teams extend this to 88 or 100 characters via configuration). +- **Blank Lines**: Use two blank lines to separate top-level functions and classes; use one blank line for method definitions inside classes. +- **Imports**: Place imports at the top of the file, on separate lines. ### Naming Styles -Naming conventions are critical for understanding the purpose of different code elements. - -* #### Class Names - Use `CapWords` (also known as `CamelCase`), where each word in the name starts with a capital letter. - * `class MyClassName:` - * `class HTTPRequest:` -* #### Function and Variable Names - Use `snake_case` (all lowercase with words separated by underscores). - * `def my_function():` - * `variable_name = 10` -* #### Module Names - Use short, all-lowercase names. Underscores can be used if it improves readability for multi-word module names. - * `import mymodule` - * `import another_module` -* #### Constants - Use `ALL_CAPS_WITH_UNDERSCORES` to denote constants (variables whose values are intended to remain unchanged throughout the program's execution). - * `MAX_CONNECTIONS = 100` - * `DEFAULT_TIMEOUT = 30` +- **Class Names**: Use `CapWords` (PascalCase). +- **Functions and Variables**: Use `snake_case` (lowercase with underscores). +- **Constants**: Use `UPPER_CASE_WITH_UNDERSCORES`. +- **Modules**: Keep names short and `lowercase`. ### Documentation -* #### Docstrings - Use triple-quoted strings (`"""Docstring content"""` or `'''Docstring content'''`) for documentation strings (docstrings) for modules, classes, functions, and methods. These explain the purpose and usage of the code. -* #### Comments - Comments (`#`) should be used to explain *why* certain code exists or how a complex piece of code works, rather than just *what* it does (which should be evident from the code itself). They should be concise, up-to-date, and on their own line preceding the code they refer to. +- **Docstrings**: Use triple double-quotes (`"""`) for all public modules, functions, classes, and methods. +- **Comments**: Comments should be complete sentences. Inline comments should be separated by at least two spaces from the code. ### Whitespace Usage -Appropriate use of whitespace enhances readability around operators and punctuation. +- **Operators**: Surround binary operators with a single space (e.g., `x = y + 1`). +- **Grouping**: Avoid extraneous whitespace inside parentheses, brackets, or braces (e.g., `call(arg)` not `call( arg )`). -* #### Operators - Surround binary operators (e.g., `=`, `+`, `-`, `*`, `/`, `==`, `>`, `<`, `and`, `or`) with a single space on either side. - * `x = 1 + 2` - * `if a == b:` -* #### Commas, Semicolons, Colons - Always follow a comma, semicolon, or colon with a space. Avoid spaces before them. - * `my_list = [1, 2, 3]` - * `def func(arg1, arg2):` -* #### Parentheses, Brackets, Braces - Avoid extraneous whitespace immediately inside parentheses, brackets, or braces. - * `my_list[index]` (not `my_list[ index ]`) - * `my_tuple = (1, 2)` (not `my_tuple = ( 1, 2 )`) +### Example: Directory Walker -### Example: PEP 8 Compliant Directory Walker - -The following Python code snippet adheres to several PEP 8 guidelines, demonstrating proper indentation, naming conventions, and spacing. +The following code illustrates PEP 8 compliance, including standard spacing, naming conventions, and modern type hints: ```python import os -def walk_directory_and_print_files(base_path): - """ - Recursively walks a given directory path and prints the full path of each file found. - - Args: - base_path (str): The starting directory path to walk. - """ - for dirpath, dirnames, filenames in os.walk(base_path): +def walk_directory(path: str) -> None: + """Traverse the directory and print all file paths.""" + for dirpath, _, filenames in os.walk(path): for filename in filenames: file_path = os.path.join(dirpath, filename) - # This print statement is for demonstration; in a real app, - # you might process the file_path further. - print(f"Found file: {file_path}") + print(file_path) -# Example usage (replace with an actual path for execution) -# Ensure the path exists, e.g., 'C:/Users/YourUser/Documents' or '/home/user/my_docs' if __name__ == "__main__": - # You might get this path from user input or configuration - test_path = './my_test_dir' # Current directory example - - # Create a dummy directory and file for demonstration if it doesn't exist - if not os.path.exists(test_path): - os.makedirs(test_path) - with open(os.path.join(test_path, 'example.txt'), 'w') as f: - f.write("Hello PEP 8!") - - walk_directory_and_print_files(test_path) + walk_directory('/path/to/directory') ```
## 4. How is memory allocation and garbage collection handled in _Python_? -In Python, **memory allocation** and **garbage collection** are handled automatically and transparently by the Python interpreter, abstracting these complexities away from the developer. This significantly reduces the likelihood of memory-related bugs like leaks or segmentation faults. +In Python, **both memory allocation** and **garbage collection** are handled discretely. ### Memory Allocation -Python manages its own private heap space, where all Python objects (e.g., integers, strings, lists, dictionaries, custom objects) reside. The **Python Memory Manager** is responsible for allocating and deallocating memory within this heap. +- The "heap" is the pool of memory for storing objects. The Python memory manager allocates and deallocates this space as needed. -#### Specialized Allocators +- In latest Python versions, the `obmalloc` system is responsible for small object allocations. This system preallocates small and medium-sized memory blocks to manage frequently created small objects. -To optimize performance for various object sizes, Python employs a tiered memory allocation strategy: +- The `allocator` abstracts the system-level memory management, employing memory management libraries like `Glibc` to interact with the operating system. -* **`pymalloc` (or `obmalloc`)**: For small and medium-sized objects (typically less than 512 bytes), Python uses a specialized allocator known as `pymalloc` (historically) or more generally, the `obmalloc` system in CPython. This system pre-allocates large blocks of memory from the operating system (called **arenas**) and then subdivides them into smaller fixed-size **pools** and **blocks**. This greatly reduces the overhead of frequent small allocations and deallocations, as Python reuses these internal memory blocks efficiently. This mechanism is crucial for the performance of typical Python programs that create many small objects. -* **System `malloc`**: For larger objects (greater than 512 bytes) or when the `pymalloc` system cannot satisfy a request, Python directly falls back to the underlying operating system's memory allocator (e.g., `malloc` and `free` from the C standard library like `Glibc`). +- Larger blocks of memory are primarily obtained directly from the operating system. -#### Stack vs. Heap - -It's important to distinguish between the **stack** and the **heap**: - -* **Stack**: The call stack stores function call frames, local variables, and references to objects. Variables on the stack are typically primitives or pointers/references to objects. -* **Heap**: This is where all actual Python objects are stored. Python's memory manager exclusively deals with the **Python private heap**. When a Python variable is created, it's typically a reference on the stack pointing to an object on the heap. - -```python -# Example of object creation and memory allocation -my_list = [1, 2, 3] # A list object is allocated on the heap. - # 'my_list' is a reference on the stack pointing to this object. - -my_dict = {'a': 1, 'b': 2} # A dictionary object is allocated on the heap. - -# Small integers (and some strings) are often interned for efficiency, -# meaning they are pre-allocated and reused. -x = 10 -y = 10 # x and y might point to the same integer object on the heap -``` +- **Stack** and **Heap** separation is joined by "Pool Allocator" for internal use. ### Garbage Collection -Python employs a hybrid approach to garbage collection, combining **reference counting** with a **cycle-detecting garbage collector**. +Python employs a method called **reference counting** along with a **cycle-detecting garbage collector**. #### Reference Counting -* **Mechanism**: Every Python object maintains a **reference count**, which tracks how many references (variables, container elements, etc.) point to it. When an object is created, its reference count is 1. When a new reference points to it, the count increments; when a reference is removed (e.g., variable goes out of scope, `del` keyword used, reference reassigned), the count decrements. -* **Deallocation**: When an object's reference count drops to zero, it means no active references point to it, making it unreachable. The object is then immediately deallocated, and its memory is returned to the `pymalloc` system or the OS. -* **Efficiency**: Reference counting is generally very efficient and allows for prompt memory reclamation. Most objects are deallocated as soon as they become unreachable. - -```python -import sys - -# An empty list object is created -a = [] -print(f"Reference count for a after creation: {sys.getrefcount(a) - 1}") # -1 for temporary ref from getrefcount +- Every object has a reference count. When an object's count drops to zero, it is immediately deallocated. -# Another reference to the same list object -b = a -print(f"Reference count after b = a: {sys.getrefcount(a) - 1}") +- This mechanism is swift, often releasing objects instantly without the need for garbage collection. -# Remove reference b -del b -print(f"Reference count after del b: {sys.getrefcount(a) - 1}") - -# Once 'a' is also deleted or reassigned, the count will drop to 0, -# and the list object will be deallocated. -``` +- However, it can be insufficient in handling **circular references**. #### Cycle-Detecting Garbage Collector -* **Problem**: Reference counting alone cannot detect and reclaim memory occupied by **circular references**. A circular reference occurs when two or more objects refer to each other, forming a closed loop, even if no external references point to the cycle. In such cases, the reference count of each object within the cycle might never drop to zero, leading to a memory leak. -* **Solution**: Python includes a separate, optional, **generational garbage collector** designed to identify and collect these uncollectable cycles. - * **Generations**: The collector categorizes objects into three generations (0, 1, and 2) based on their age. New objects start in generation 0. If an object survives a collection in generation 0, it's promoted to generation 1, and so on. Older generations are collected less frequently because objects that have survived longer are less likely to be part of a temporary cycle. - * **Collection Process**: When invoked (either automatically or manually via `gc.collect()`), the collector traverses object graphs to find unreachable cycles. It temporarily unlinks references within potential cycles to see if any object's reference count drops to zero. If so, those objects are deemed part of an uncollectable cycle and are deallocated. -* **Performance**: Cycle detection is a more computationally intensive process than reference counting, so it runs much less frequently. The `gc` module provides an interface to control and inspect the garbage collector. - -```python -import gc - -class Node: - def __init__(self, value): - self.value = value - self.next = None # Reference to another Node - # print(f"Node {self.value} created") # For demonstration +- Python has a separate garbage collector that periodically identifies and deals with circular references. - def __del__(self): - # This destructor will be called when the object is truly deallocated - print(f"Node {self.value} destroyed") +- This is, however, a more time-consuming process and is invoked less frequently than reference counting. -# Disable automatic garbage collection for clearer demonstration -gc.disable() +### Memory Management in Python vs. C -n1 = Node(1) -n2 = Node(2) +Python handles memory management quite differently from languages like C or C++: -n1.next = n2 -n2.next = n1 # Circular reference: n1 -> n2 -> n1 +- In Python, the developer isn't directly responsible for memory allocations or deallocations, reducing the likelihood of memory-related bugs. -print("References created. Objects n1 and n2 still exist in memory.") +- The memory manager in Python is what's known as a **"general-purpose memory manager"** that can be slower than the dedicated memory managers of C or C++ in certain contexts. -del n1 -del n2 # Variables n1 and n2 are deleted, but the objects themselves are not __del__-ed - -print("Local references n1 and n2 are gone, but objects still exist due to circularity.") - -# The objects n1 and n2 are still alive because their reference counts within the cycle -# prevent them from being zero, even though no external references point to the cycle. - -print("\nRunning cycle-detecting garbage collector...") -gc.collect() # This will detect the cycle and deallocate the objects, - # triggering their __del__ methods. - -gc.enable() # Re-enable garbage collection -``` +- Python, especially due to the existence of a garbage collector, might have memory overhead compared to C or C++ where manual memory management often results in minimal overhead is one of the factors that might contribute to Python's sometimes slower performance. -### Memory Management in Python vs. C/C++ - -Python's approach to memory management differs significantly from lower-level languages like C or C++: - -* **Automation vs. Manual Control**: Python provides **automatic memory management**, relieving developers from explicitly allocating (`malloc`/`new`) or deallocating (`free`/`delete`) memory. This contrasts sharply with C/C++, where manual memory management is a core responsibility. -* **Reduced Bugs**: The automation in Python greatly reduces the risk of common memory-related bugs such as memory leaks, double-frees, or dangling pointers. -* **Performance and Overhead**: - * Python's general-purpose memory manager, combined with the overhead of dynamic typing, reference counting, and the cycle-detecting garbage collector, can result in higher memory consumption and potentially slower performance compared to finely tuned C/C++ applications. - * Each Python object also carries additional overhead (e.g., type information, reference count) that isn't present in raw C data structures. -* **Developer Productivity**: The trade-off is a significant boost in **developer productivity** and ease of use, as developers can focus on application logic rather than intricate memory handling. +- The level of memory efficiency isn't as high as that of C or C++. This is because Python is designed to be convenient and easy to use, often at the expense of some performance optimization.
## 5. What are the _built-in data types_ in _Python_? -Python offers numerous **built-in data types** that provide varying functionalities and utilities. These types are fundamental to programming in Python and can be broadly categorized into immutable and mutable types, depending on whether their state can be changed after creation. +Python offers numerous **built-in data types** that provide varying functionalities and utilities. ### Immutable Data Types -Immutable objects cannot be modified after they are created. Any operation that appears to modify an immutable object actually creates a new object. #### 1. int -Represents **whole numbers** (integers) of arbitrary precision. There is no practical limit to how large an integer value can be, other than the available memory. -```python -x = 42 -print(type(x)) # -``` + Represents a whole number, such as 42 or -10. #### 2. float -Represents **floating-point numbers** (decimal numbers). Python's `float` type typically implements the IEEE 754 double-precision standard. -```python -pi = 3.14159 -print(type(pi)) # -``` + Represents a decimal number, like 3.14 or -0.01. #### 3. complex -Represents **complex numbers**, comprising a real and an imaginary part. The imaginary part is denoted by `j` or `J`. -```python -z = 3 + 4j -print(type(z)) # -``` + Comprises a real and an imaginary part, like 3 + 4j. #### 4. bool -Represents **boolean values**, which can be either `True` or `False`. `bool` is a subclass of `int`, where `True` is equivalent to `1` and `False` to `0`. -```python -is_active = True -is_empty = False -print(type(is_active)) # -``` + Represents a boolean value, True or False. #### 5. str -Represents **sequences of Unicode characters**, used for text. String literals are enclosed within single quotes, double quotes, or triple quotes. -```python -message = "Hello, Python!" -print(type(message)) # -``` + A sequence of unicode characters enclosed within quotes. #### 6. tuple -An **ordered, immutable collection** of items. Tuples can contain heterogeneous data types and are typically enclosed within parentheses. -```python -coords = (10, 20, 'north') -print(type(coords)) # -``` + An ordered collection of items, often heterogeneous, enclosed within parentheses. #### 7. frozenset -An **immutable version of a `set`**. It is a collection of unique, hashable objects and cannot be modified after creation. `frozenset` objects are created using the `frozenset()` constructor. -```python -immutable_items = frozenset([1, 2, 3, 2]) -print(immutable_items) # frozenset({1, 2, 3}) -print(type(immutable_items)) # -``` + A set of unique, immutable objects, similar to sets, enclosed within curly braces. #### 8. bytes -Represents an **immutable sequence of 8-bit bytes**. It is primarily used to handle binary data. `bytes` literals are prefixed with `b`. -```python -binary_data = b'hello' -print(type(binary_data)) # -``` + Represents a group of 8-bit bytes, often used with binary data, enclosed within brackets. -#### 9. NoneType -The type of the **`None` object**, which indicates the absence of a value or a null value. `None` is a singleton object. -```python -result = None -print(type(result)) # -``` +#### 9. bytearray + Resembles the 'bytes' type but allows mutable changes. -#### 10. type -The **metaclass** for all new-style classes in Python. It is the type of types themselves. -```python -t = type(10) # t is -print(type(t)) # -``` - -#### 11. object -The **base class** from which all other classes in Python implicitly or explicitly inherit. It is the root of the class hierarchy. -```python -o = object() -print(type(o)) # -``` +#### 10. NoneType + Indicates the absence of a value. ### Mutable Data Types -Mutable objects can be modified after they are created. Changes made to a mutable object directly affect the object's state. #### 1. list -An **ordered, mutable collection** of items. Lists are highly versatile, can contain heterogeneous data types, and offer dynamic sizing. They are enclosed within square brackets. -```python -my_list = [1, 'two', 3.0, [4, 5]] -my_list.append(6) -print(type(my_list)) # -``` + A versatile ordered collection that can contain different data types and offers dynamic sizing, enclosed within square brackets. #### 2. set -An **unordered, mutable collection of unique, hashable objects**. Sets are useful for operations like membership testing, removing duplicates, and mathematical set operations. They are characterized by curly braces. -```python -unique_numbers = {1, 2, 3, 2} -unique_numbers.add(4) -print(unique_numbers) # {1, 2, 3, 4} (order may vary) -print(type(unique_numbers)) # -``` + Represents a unique set of objects and is characterized by curly braces. #### 3. dict -A **key-value paired collection** that is mutable and since Python 3.7+, insertion-ordered. Keys must be unique and hashable, while values can be of any type. Dictionaries are enclosed within curly braces. -```python -person = {'name': 'Alice', 'age': 30, 'city': 'New York'} -person['age'] = 31 -print(type(person)) # -``` + A versatile key-value paired collection enclosed within braces. -#### 4. bytearray -A **mutable version of the `bytes` type**. It represents a mutable sequence of 8-bit bytes and allows in-place modification of binary data. -```python -mutable_binary = bytearray(b'world') -mutable_binary[0] = ord('W') -print(mutable_binary) # bytearray(b'World') -print(type(mutable_binary)) # -``` +#### 4. memoryview + Points to the memory used by another object, aiding efficient viewing and manipulation of data. -#### 5. memoryview -A **built-in type that exposes the buffer protocol** of an object (like `bytes` or `bytearray`). It allows direct access to an object's internal data without copying, aiding efficient viewing and manipulation of large data blocks. -```python -data = bytearray(b'abcdef') -mv = memoryview(data) -print(mv[0]) # 97 (ASCII for 'a') -mv[0] = 100 # Change 'a' to 'd' -print(data) # bytearray(b'dbcdef') -print(type(mv)) # -``` +#### 5. array + Offers storage for a specified type of data, similar to lists but with dedicated built-in functionalities. -#### 6. array.array -The `array` module provides an **array type that can store a sequence of items of a single, specified numeric type**. It is similar to a `list` but is more memory-efficient for storing large numbers of homogeneous elements. It must be imported from the `array` module. -```python -import array -my_array = array.array('i', [1, 2, 3, 4]) # 'i' for signed int -my_array.append(5) -print(type(my_array)) # -``` +#### 6. deque + A double-ended queue distinguished by optimized insertion and removal operations from both its ends. -#### 7. collections.deque -The `collections` module provides `deque` (double-ended queue), which is a **list-like container with optimized appends and pops from both ends** with approximately O(1) performance. It must be imported from the `collections` module. -```python -from collections import deque -my_deque = deque([1, 2, 3]) -my_deque.appendleft(0) # [0, 1, 2, 3] -my_deque.pop() # 3 -print(type(my_deque)) # -``` +#### 7. object + The base object from which all classes inherit. #### 8. types.SimpleNamespace -The `types` module provides `SimpleNamespace`, which is a **simple `object` subclass that allows arbitrary attribute assignment**. It's useful for creating lightweight data objects. It must be imported from the `types` module. -```python -import types -ns = types.SimpleNamespace(a=1, b='hello') -ns.c = [1, 2, 3] -print(ns.a, ns.b, ns.c) # 1 hello [1, 2, 3] -print(type(ns)) # -``` + Grants the capability to assign attributes to it. #### 9. types.ModuleType -The `types` module provides `ModuleType`, which is the **type of all modules**. It represents the actual module object loaded into Python. It must be imported from the `types` module. -```python -import sys -module_type_example = type(sys) -print(module_type_example) # -print(type(module_type_example)) # (ModuleType itself is a type) -``` + Represents a module body containing attributes. #### 10. types.FunctionType -The `types` module provides `FunctionType`, which is the **type of user-defined functions**. It represents callable objects created with `def`. It must be imported from the `types` module. -```python -import types -def my_function(): - pass -func_type_example = type(my_function) -print(func_type_example) # -print(type(func_type_example)) # (FunctionType itself is a type) -``` + Defines a particular kind of function.
## 6. Explain the difference between a _mutable_ and _immutable_ object. -Let's look at the difference between **mutable** and **immutable** objects in Python. This distinction is fundamental to understanding how data is stored, manipulated, and passed around in Python programs. - -### Core Definition - -The primary difference lies in whether an object's state can be altered after its creation. +Let's look at the difference between **mutable** and **immutable** objects. -#### Mutable Objects -A **mutable object** is an object whose state or content can be changed after it has been created. When a mutable object is modified, its identity (memory address) remains the same, but its value changes. Any variable referencing this object will see the change. +### Key Distinctions -#### Immutable Objects -An **immutable object** is an object whose state or content cannot be changed after it has been created. If an operation appears to "modify" an immutable object, it actually creates a *new* object with the desired changes, and the original object remains untouched. Variables are then re-bound to this new object. +- **Mutable Objects**: Can be modified after creation. +- **Immutable Objects**: Cannot be modified after creation. ### Common Examples -#### Mutable Types -* **Lists** (`list`): Ordered, changeable sequence of items. -* **Sets** (`set`): Unordered collection of unique, mutable items. -* **Dictionaries** (`dict`): Unordered collection of key-value pairs, where keys must be immutable and unique. -* **Bytearrays** (`bytearray`): Mutable sequence of bytes. -* Most custom class instances (unless specifically designed to be immutable). +- **Mutable**: Lists, Sets, Dictionaries +- **Immutable**: Tuples, Strings, Numbers -#### Immutable Types -* **Numbers** (`int`, `float`, `complex`): Integers, floating-point numbers, and complex numbers. -* **Strings** (`str`): Ordered, immutable sequence of characters. -* **Tuples** (`tuple`): Ordered, immutable sequence of items. (Note: A tuple itself is immutable, but if it contains mutable objects, those mutable objects can still be changed). -* **Frozensets** (`frozenset`): Unordered collection of unique, immutable items. -* **Bytes** (`bytes`): Immutable sequence of bytes. -* **Booleans** (`bool`): `True` and `False`. -* **None** (`NoneType`). +### Code Example: Immutability in Python -### Code Example: Illustrating Mutability and Immutability in Python +Here is the Python code: ```python -import sys - -# --- Immutable objects (int, str, tuple) --- -print("--- Immutable Objects ---") +# Immutable objects (int, str, tuple) num = 42 -text = "Hello" -my_tuple = (1, [2, 3], 4) # Tuple itself is immutable, but contains a mutable list - -print(f"Initial num: {num}, id: {id(num)}") -print(f"Initial text: '{text}', id: {id(text)}") -print(f"Initial my_tuple: {my_tuple}, id: {id(my_tuple)}") - -# Attempting to "modify" num (reassignment creates a new object) -num += 10 # This is syntactic sugar for num = num + 10 -print(f"After num += 10: {num}, id: {id(num)} (id changed, new object created)") - -try: - # Trying to modify a string (will raise TypeError) - # Strings do not support item assignment - # text[0] = 'M' - pass # Commenting out to allow script to run, but conceptually this fails -except TypeError as e: - print(f"Error modifying string: {e}") +text = "Hello, World!" +my_tuple = (1, 2, 3) +# Trying to modify will raise an error try: - # Trying to modify a tuple (will raise TypeError) - my_tuple[0] = 100 + num += 10 + text[0] = 'M' # This will raise a TypeError + my_tuple[0] = 100 # This will also raise a TypeError except TypeError as e: - print(f"Error modifying tuple element: {e}") - -# However, elements *within* a mutable object inside an immutable tuple can be changed -print(f"Mutable list inside tuple before change: {my_tuple[1]}, id: {id(my_tuple[1])}") -my_tuple[1].append(5) # This modifies the list object, not the tuple -print(f"Mutable list inside tuple after change: {my_tuple[1]}, id: {id(my_tuple[1])} (id of list unchanged)") -print(f"my_tuple after internal list modification: {my_tuple}, id: {id(my_tuple)} (tuple id unchanged)") - + print(f"Error: {e}") -# --- Mutable objects (list, dict, set) --- -print("\n--- Mutable Objects ---") +# Mutable objects (list, set, dict) my_list = [1, 2, 3] my_dict = {'a': 1, 'b': 2} -my_set = {10, 20} -print(f"Initial my_list: {my_list}, id: {id(my_list)}") -print(f"Initial my_dict: {my_dict}, id: {id(my_dict)}") -print(f"Initial my_set: {my_set}, id: {id(my_set)}") - -# Modifications happen in-place; object ID remains the same +# Can be modified without issues my_list.append(4) -my_dict['c'] = 3 -my_set.add(30) - -print(f"After append my_list: {my_list}, id: {id(my_list)} (id unchanged)") -print(f"After add 'c' my_dict: {my_dict}, id: {id(my_dict)} (id unchanged)") -print(f"After add 30 my_set: {my_set}, id: {id(my_set)} (id unchanged)") - -# Example of aliasing with mutable objects -list_a = [1, 2] -list_b = list_a # list_b now refers to the *same* object as list_a -print(f"list_a: {list_a}, id: {id(list_a)}") -print(f"list_b: {list_b}, id: {id(list_b)}") - -list_b.append(3) # Modifies the object referenced by both list_a and list_b -print(f"After list_b.append(3):") -print(f"list_a: {list_a}, id: {id(list_a)}") # list_a is also changed! -print(f"list_b: {list_b}, id: {id(list_b)}") +del my_dict['a'] + +# Checking the changes +print(my_list) # Output: [1, 2, 3, 4] +print(my_dict) # Output: {'b': 2} ``` -### Key Implications and Use Cases +### Benefits & Trade-Offs -#### Hashability -* **Immutable objects** are **hashable**. This means they have a hash value that never changes during their lifetime. This property is crucial for using them as keys in dictionaries and elements in sets, as these data structures rely on hashing for efficient lookups. -* **Mutable objects** are **not hashable** by default because their hash value would change if their content changed, making them unreliable for lookup operations. Consequently, mutable objects like lists, sets, and dictionaries cannot be used as dictionary keys or set members. +**Immutability** offers benefits such as **safety** in concurrent environments and facilitating **predictable behavior**. -#### Thread Safety -* **Immutable objects** are inherently **thread-safe**. Since their state cannot change after creation, multiple threads can access them concurrently without the need for locks or synchronization mechanisms, simplifying concurrent programming. -* **Mutable objects** are **not thread-safe** by default. If multiple threads access and modify the same mutable object without proper synchronization, race conditions can occur, leading to unpredictable behavior and data corruption. +**Mutability**, on the other hand, often improves **performance** by avoiding copy overhead and redundant computations. -#### Predictability and Debugging -* **Immutable objects** contribute to more **predictable code**. Their fixed state makes it easier to reason about program flow and trace data transformations, as an object's value will not unexpectedly change from another part of the program. This often leads to fewer bugs related to side effects. -* **Mutable objects**, while powerful, can introduce **side effects**. If a mutable object is passed around and modified by different functions, it can be challenging to track its state and debug issues arising from unintended modifications. +### Impact on Operations -#### Performance and Memory -* **Mutable objects** can be more **memory-efficient** for in-place updates, especially for large datasets. Modifying a mutable object avoids the overhead of creating a new object and garbage collecting the old one. -* **Immutable objects** might incur memory and performance overhead if "modifications" lead to frequent creation of new objects. However, immutability can also enable optimizations such as object sharing (multiple references pointing to the same immutable object) or caching of computed values, as their state is guaranteed not to change. Python internally caches small integers and strings for this reason. +- **Reading and Writing**: Immutable objects typically favor **reading** over **writing**, promoting a more straightforward and predictable code flow. -Choosing between mutable and immutable objects depends on the specific requirements of the program, balancing considerations like data integrity, concurrent access patterns, and performance characteristics. +- **Memory and Performance**: Mutability can be more efficient in terms of memory usage and performance, especially concerning large datasets, thanks to in-place updates. + +Choosing between the two depends on the program's needs, such as the required data integrity and the trade-offs between predictability and performance.
## 7. How do you _handle exceptions_ in _Python_? -**Exception handling** is a fundamental aspect of Python, and it safeguards your code against unexpected errors or conditions. It allows programs to gracefully recover from errors, rather than crashing, and provides mechanisms to perform necessary cleanup operations. Key components of exception handling in Python include: +**Exception handling** is a fundamental aspect of Python, and it safeguards your code against unexpected errors or conditions. Key components of exception handling in Python include: ### Components -- **Try**: The section of code where exceptions might occur is placed within a `try` block. If an exception occurs in this block, the execution flow is immediately transferred to the corresponding `except` block. +- **Try**: The section of code where exceptions might occur is placed within a `try` block. -- **Except**: Any possible exceptions that are `raised` by the `try` block are caught and handled in the `except` block. You can specify which type of exception to catch, or catch multiple types. +- **Except**: Any possible exceptions that are `raised` by the `try` block are caught and handled in the `except` block. -- **Finally**: This block ensures a piece of code always executes, regardless of whether an exception occurred in the `try` block or was caught by an `except` block. It's commonly used for cleanup operations, such as closing files or database connections, to ensure resources are properly released. +- **Finally**: This block ensures a piece of code always executes, regardless of whether an exception occurred. It's commonly used for cleanup operations, such as closing files or database connections. ### Generic Exception Handling vs. Handling Specific Exceptions -It's good practice to **handle** specific exceptions to provide tailored responses to different error conditions. However, a more **general** approach can also be taken. When combining specific and general exception handlers, ensure that the most specific `except` blocks come first, and the more **general** `except Exception as e` block is placed at the end of the chain. This ensures that specific error conditions are addressed before being caught by a broader handler. +It's good practice to **handle** specific exceptions. However, a more **general** approach can also be taken. When doing the latter, ensure the general exception handling is at the end of the chain, as shown here: ```python try: risky_operation() except IndexError: # Handle specific exception types first. handle_index_error() -except ValueError: # Another specific exception - handle_value_error() except Exception as e: # More general exception must come last. - # This will catch any other exception not caught by the specific handlers above - print(f"An unexpected error occurred: {e}") - handle_generic_error(e) + handle_generic_error() finally: - cleanup() # This always executes + cleanup() ``` ### Raising Exceptions -You can use the `raise` statement to **trigger and manage** exceptions under specific circumstances. This is particularly useful when building custom classes or functions where specific conditions should be met, and a failure to meet them warrants an immediate halt and error notification. +Use this mechanism to **trigger and manage** exceptions under specific circumstances. This can be particularly useful when building custom classes or functions where specific conditions should be met. **Raise** a specific exception: ```python def divide(a, b): if b == 0: - # Raising a built-in exception with a custom message raise ZeroDivisionError("Divisor cannot be zero") return a / b try: result = divide(4, 0) except ZeroDivisionError as e: - print(f"Error: {e}") # Output: Error: Divisor cannot be zero -except Exception as e: - print(f"An unexpected error occurred: {e}") + print(e) ``` **Raise a general exception**: -While generally recommended to raise specific exceptions (either built-in or custom), you can also raise the base `Exception` class directly for generic error conditions. ```python -def some_risky_operation(condition: bool): - if condition: - raise Exception("Some generic error occurred due to a specific condition.") - return "Operation successful" - -try: - print(some_risky_operation(True)) -except Exception as e: - print(f"Caught a general exception: {e}") # Output: Caught a general exception: Some generic error occurred due to a specific condition. +def some_risky_operation(): + if condition: + raise Exception("Some generic error occurred") ``` ### Using `with` for Resource Management -The `with` keyword provides a more efficient and clean way to handle resources, like files or network connections, ensuring their proper acquisition and release (closure) when operations are complete or in case of any exceptions. Resources that can be used with `with` must implement the `context manager` protocol, typically by having `__enter__` and `__exit__` methods. +The `with` keyword provides a more efficient and clean way to handle resources, like files, ensuring their proper closure when operations are complete or in case of any exceptions. The resource should implement a `context manager`, typically by having `__enter__` and `__exit__` methods. Here's an example using a file: ```python -try: - with open("example.txt", "r") as file: - data = file.read() - # File is automatically closed when the 'with' block is exited, - # even if an exception occurs during file reading. - print("File content read successfully.") -except FileNotFoundError: - print("Error: The file 'example.txt' was not found.") -except IOError as e: - print(f"Error reading file: {e}") +with open("example.txt", "r") as file: + data = file.read() +# File is automatically closed when the block is exited. ``` -### Controlling Flow with `else`, `pass`, and `continue` +### Silence with `pass`, `continue`, or `else` -These keywords offer different strategies for controlling program flow within or around exception handling blocks, allowing for conditional execution or ignoring certain exceptions. +There are times when not raising an exception is appropriate. You can use `pass` or `continue` in an exception block when you want to essentially ignore an exception and proceed with the rest of your code. -#### `else` with `try-except` blocks +- **`pass`**: Simply does nothing. It acts as a placeholder. -The `else` block after a `try-except` block will only be executed if **no exceptions** are raised within the `try` block. It serves as a place for code that *must* run only upon successful completion of the `try` block, before the `finally` block (if present). + ```python + try: + risky_operation() + except SomeSpecificException: + pass + ``` -```python -def perform_division(a, b): - try: - result = a / b - except ZeroDivisionError: - print("Cannot divide by zero!") - else: - # This code runs only if no exception occurred in the try block - print(f"Division successful. Result: {result}") - return result - finally: - print("Attempted division operation finished.") - -perform_division(10, 2) -# Output: -# Division successful. Result: 5.0 -# Attempted division operation finished. - -perform_division(10, 0) -# Output: -# Cannot divide by zero! -# Attempted division operation finished. -``` +- **`continue`**: This keyword is generally used in loops. It moves to the next iteration without executing the code that follows it within the block. -#### `pass` + ```python + for item in my_list: + try: + perform_something(item) + except ExceptionType: + continue + ``` -The `pass` statement is a null operation; nothing happens when it executes. It acts as a placeholder where a statement is syntactically required but you don't want any action to be performed, effectively ignoring an exception. While sometimes useful for prototyping, ignoring exceptions in production code should be done cautiously, as it can mask underlying issues. +- **`else` with `try-except` blocks**: The `else` block after a `try-except` block will only be executed if no exceptions are raised within the `try` block -```python -def risky_operation(): - raise ValueError("Something went wrong!") - -try: - risky_operation() -except ValueError: - # Explicitly ignoring a specific ValueError - pass # No action taken, program continues normally -print("Program continues after potentially ignored exception.") -``` - -#### `continue` - -The `continue` keyword is generally used within loops. When executed inside an `except` block within a loop, it immediately skips the rest of the current iteration of the loop and proceeds to the next iteration. This is useful when processing a list of items and you want to skip an item that causes an exception without stopping the entire loop. - -```python -data_items = [10, 2, 0, 5, 'error', 1] - -for item in data_items: - try: - result = 100 / item - print(f"Result for {item}: {result}") - except (ZeroDivisionError, TypeError) as e: - print(f"Skipping item {item} due with error: {e}") - continue # Move to the next item in the loop - except Exception as e: - print(f"An unexpected error occurred for item {item}: {e}") - continue -``` + ```python + try: + some_function() + except SpecificException: + handle_specific_exception() + else: + no_exception_raised() + ``` -### Callback Function: `sys.excepthook` +### Callback Function: `ExceptionHook` -The `sys.excepthook` is a configurable function in the `sys` module that allows you to customize how Python handles *uncaught* exceptions (exceptions that propagate all the way up without being caught by any `except` block). By default, `sys.excepthook` points to `sys.__excepthook__`, which prints the standard Python traceback to `sys.stderr`. You can replace it with your own function to log errors, display custom messages, send notifications, or perform other actions before the program potentially terminates. +Python 3 introduced the better handling of uncaught exceptions by providing an optional function for printing stack traces. The `sys.excepthook` can be set to match any exception in the module as long as it has a `hook` attribute. -Here's an example: +Here's an example for this test module: ```python -# test_hook.py +# test.py import sys -def custom_excepthook(exc_type, exc_value, exc_traceback): - """ - A custom exception hook that logs the unhandled exception - and then calls the default hook. - """ - print(f"*** UNHANDLED EXCEPTION CAUGHT BY CUSTOM HOOK ***") - print(f"Type: {exc_type.__name__}") - print(f"Value: {exc_value}") - # You could log this to a file, send an email, etc. +def excepthook(type, value, traceback): + print("Unhandled exception:", type, value) + # Call the default exception hook + sys.__excepthook__(type, value, traceback) - # Call the original excepthook to print the standard traceback - sys.__excepthook__(exc_type, exc_value, exc_traceback) - -# Set our custom hook -sys.excepthook = custom_excepthook - -def problematic_function(): - print("Inside problematic_function") - result = 1 / 0 # This will raise a ZeroDivisionError - -if __name__ == "__main__": - print("Calling problematic_function...") - problematic_function() - print("This line will not be reached due to unhandled exception.") +sys.excepthook = excepthook +def test_exception_hook(): + throw_some_exception() ``` -When `test_hook.py` is executed, the output will first show the custom message from `custom_excepthook` followed by the standard Python traceback, because `sys.__excepthook__` was called. +When run, calling `test_exception_hook` will print "Unhandled exception: ..." -_Note_: `sys.excepthook` will not capture exceptions raised as the result of interactive prompt commands, such as `SyntaxError` or `KeyboardInterrupt`, nor does it affect exceptions caught by `try...except` blocks. It specifically targets exceptions that would otherwise lead to program termination with a traceback. +_Note_: `sys.excepthook` will not capture exceptions raised as the result of interactive prompt commands, such as SyntaxError or KeyboardInterrupt.
## 8. What is the difference between _list_ and _tuple_? -**Lists** and **Tuples** in Python share many similarities, such as being ordered sequences and supporting indexing, slicing, and iteration. However, they are fundamentally different in their core characteristics, making them suitable for different use cases. +**Lists** and **Tuples** in Python share many similarities, such as being sequences and supporting indexing. + +However, these data structures differ in key ways: ### Key Distinctions -- **Mutability**: This is the most significant difference. - * **Lists** are **mutable**, meaning their elements can be added, removed, or modified after the list has been created. They are dynamic and can change in size and content. - * **Tuples** are **immutable**. Once a tuple is created, its size and the elements it contains cannot be changed. While you cannot modify, add, or remove items from a tuple, it's important to note that if a tuple contains mutable objects (e.g., a list), the contents of those mutable objects *can* be changed. The tuple itself still holds the *reference* to that object, and that reference doesn't change. +- **Mutability**: Lists are mutable, allowing you to add, remove, or modify elements after creation. Tuples, once created, are immutable. -- **Performance and Memory Usage**: - * **Tuples** are generally more performant and consume less memory than lists for the same number of elements. This is because their fixed size allows Python to make certain optimizations. They have less overhead as they don't need to allocate space for growth or maintain methods for modification. - * **Lists**, being mutable, require more memory and can be slightly slower due to the overhead of supporting dynamic operations (resizing, adding, removing elements). +- **Performance**: Lists are generally slower than tuples, most apparent in tasks like iteration and function calls. -- **Syntax**: - * **Lists** are defined using square brackets `[]`. - * **Tuples** are defined using parentheses `()`. A single-element tuple requires a trailing comma (e.g., `(item,)`) to distinguish it from a simple parenthesized expression. +- **Syntax**: Lists are defined with square brackets `[]`, whereas tuples use parentheses `()`. ### When to Use Each -- **Lists** are ideal for collections of items that may change over time, such as a collection of user inputs, a dynamic queue, or a list of items to be processed. They are the preferred choice when you need a modifiable sequence. +- **Lists** are ideal for collections that may change in size and content. They are the preferred choice for storing data elements. -- **Tuples**, due to their immutability and enhanced performance, are a good choice for: - * Representing fixed sets of related data, like a record or a point in a coordinate system (e.g., `(x, y)`). - * Function arguments and return values, especially when returning multiple items from a function. - * Using as keys in dictionaries (provided all elements within the tuple are themselves immutable). - * Data that should not be changed, ensuring data integrity. +- **Tuples**, due to their immutability and enhanced performance, are a good choice for representing fixed sets of related data. ### Syntax #### List: Example -Lists demonstrate mutability through operations like appending, removing, or modifying elements. - ```python my_list = ["apple", "banana", "cherry"] -print(f"Initial list: {my_list}") - -# Adding an element my_list.append("date") -print(f"After append: {my_list}") - -# Modifying an element my_list[1] = "blackberry" -print(f"After modification: {my_list}") - -# Removing an element -my_list.remove("apple") -print(f"After removal: {my_list}") ``` #### Tuple: Example -Tuples are created with parentheses. Attempts to modify them will result in a `TypeError`. - ```python my_tuple = (1, 2, 3, 4) -print(f"Initial tuple: {my_tuple}") - -# Unpacking a tuple (a common operation) +# Unpacking a tuple a, b, c, d = my_tuple -print(f"Unpacked values: a={a}, b={b}, c={c}, d={d}") - -# Attempting to modify a tuple (will raise a TypeError) -try: - my_tuple[0] = 5 -except TypeError as e: - print(f"Error attempting to modify tuple: {e}") - -# Example of a tuple containing a mutable object -nested_tuple = (1, [2, 3], 4) -print(f"Nested tuple: {nested_tuple}") - -# The list inside the tuple *can* be modified, even though the tuple itself is immutable -nested_tuple[1].append(5) -print(f"Nested tuple after modifying inner list: {nested_tuple}") - -# The tuple itself still refers to the same list object; its reference hasn't changed. ```
## 9. How do you create a _dictionary_ in _Python_? -**Python dictionaries** are highly versatile and fundamental data structures that store data in `key:value` pairs, enabling efficient data retrieval. This section will explore the core concepts and various methods for creating dictionaries in Python. +**Python dictionaries** are versatile data structures, offering key-based access for rapid lookups. Let's explore various data within dictionaries and techniques to create and manipulate them. ### Key Concepts -- A **dictionary** in Python is an unordered (as of Python 3.7+, insertion order is preserved, making them *ordered* in practice for CPython, but officially unordered for language specification prior to 3.7) collection of `key:value` pairs. -- **Keys** must be unique within a dictionary and typically immutable types, such as strings, numbers (integers, floats), or tuples containing only immutable elements. Attempting to use a mutable object (like a list) as a key will raise a `TypeError`. -- **Values** can be of any data type (strings, numbers, lists, other dictionaries, custom objects, etc.) and can be duplicated across different keys. +- A **dictionary** in Python contains a collection of `key:value` pairs. +- **Keys** must be unique and are typically immutable, such as strings, numbers, or tuples. +- **Values** can be of any type, and they can be duplicated. ### Creating a Dictionary -Python offers several straightforward and efficient methods to create dictionaries: +You can use several methods to create a dictionary: -1. **Dictionary Literal**: The most common and direct way to define a dictionary, by enclosing `key:value` pairs within curly braces `{}`. -2. **`dict()` Constructor**: A versatile constructor that can accept different types of arguments to form a dictionary: - * An iterable of `key:value` pairs (e.g., a list of tuples or another dictionary's `items()`). - * Keyword arguments, where the argument names become string keys and their corresponding values become dictionary values. - * Another dictionary, creating a shallow copy. -3. **Dictionary Comprehensions**: A concise and powerful syntax for creating dictionaries dynamically from other iterables, often involving transformations or conditional logic. -4. **`dict.fromkeys()` Method**: Useful for creating dictionaries with a sequence of keys and assigning all of them a specified default value (or `None` if no value is provided). -5. **Using `zip()` with `dict()`**: A common pattern where the `zip()` function pairs elements from two sequences (one for keys, one for values), and the resulting iterable of pairs is then passed to the `dict()` constructor. +1. **Literal Definition**: Define key-value pairs within curly braces { }. -### Examples +2. **From Key-Value Pairs**: Use the `dict()` constructor or the `{key: value}` shorthand. -#### 1. Dictionary Literal +3. **Using the `dict()` Constructor**: This can accept another dictionary, a sequence of key-value pairs, or named arguments. -This is the most idiomatic way to create a dictionary. - -```python -# Creating a dictionary using a literal -student_profile = { - "name": "Alice Smith", - "age": 20, - "major": "Computer Science", - "courses": ["Data Structures", "Algorithms", "Databases"], - "is_enrolled": True -} +4. **Comprehensions**: This is a concise way to create dictionaries using a single line of code. -print(f"Student Profile (Literal): {student_profile}") -# Expected output: Student Profile (Literal): {'name': 'Alice Smith', 'age': 20, 'major': 'Computer Science', 'courses': ['Data Structures', 'Algorithms', 'Databases'], 'is_enrolled': True} -``` +5. **`zip()` Function**: This creates a dictionary by zipping two lists, where the first list corresponds to the keys, and the second to the values. -#### 2. Using the `dict()` Constructor +### Examples -##### From an Iterable of Key-Value Pairs +#### Dictionary Literal Definition -The `dict()` constructor can take a sequence of two-element iterables (like tuples or lists), where each inner iterable represents a `(key, value)` pair. +Here is a Python code: ```python -# From a list of tuples -faculty_info = dict([ - ("dean", "Dr. Emily White"), - ("department_head", "Prof. Robert Green"), - ("number_of_professors", 15) -]) -print(f"Faculty Info (from list of tuples): {faculty_info}") -# Expected output: Faculty Info (from list of tuples): {'dean': 'Dr. Emily White', 'department_head': 'Prof. Robert Green', 'number_of_professors': 15} +# Dictionary literal definition +student = { + "name": "John Doe", + "age": 21, + "courses": ["Math", "Physics"] +} ``` -##### From Keyword Arguments +#### From Key-Value Pairs -When using named arguments, the argument names become string keys, and their values become the dictionary values. +Here is the Python code: ```python -# From keyword arguments -city_data = dict(name="New York", population=8400000, country="USA") -print(f"City Data (from keyword arguments): {city_data}") -# Expected output: City Data (from keyword arguments): {'name': 'New York', 'population': 8400000, 'country': 'USA'} -``` - -##### From Another Dictionary (Copying) - -This creates a shallow copy of an existing dictionary. +# Using the `dict()` constructor +student_dict = dict([ + ("name", "John Doe"), + ("age", 21), + ("courses", ["Math", "Physics"]) +]) -```python -# From an existing dictionary (creating a copy) -student_profile_copy = dict(student_profile) # Using the previously defined student_profile -print(f"Student Profile Copy: {student_profile_copy}") -# Expected output: Student Profile Copy: {'name': 'Alice Smith', 'age': 20, 'major': 'Computer Science', 'courses': ['Data Structures', 'Algorithms', 'Databases'], 'is_enrolled': True} +# Using the shorthand syntax +student_dict_short = { + "name": "John Doe", + "age": 21, + "courses": ["Math", "Physics"] +} ``` -#### 3. Dictionary Comprehensions +#### Using `zip()` -Dictionary comprehensions provide a concise way to create dictionaries from expressions and `for` loops. +Here is a Python code: ```python -# Creating a dictionary from a list of numbers, mapping number to its square -squares = {num: num**2 for num in range(1, 6)} -print(f"Squares Dictionary: {squares}") -# Expected output: Squares Dictionary: {1: 1, 2: 4, 3: 9, 4: 16, 5: 25} - -# Creating a dictionary from two lists, with a condition -employees = ["John", "Jane", "Mike"] -salaries = [60000, 75000, 50000] -employee_salaries = {emp: sal for emp, sal in zip(employees, salaries) if sal > 55000} -print(f"Employee Salaries (filtered): {employee_salaries}") -# Expected output: Employee Salaries (filtered): {'John': 60000, 'Jane': 75000} -``` - -#### 4. `dict.fromkeys()` Method - -This method takes an iterable of keys and an optional `value`. All keys in the new dictionary will be mapped to this `value`. If `value` is omitted, `None` is used. +keys = ["a", "b", "c"] +values = [1, 2, 3] -```python -# Creating a dictionary with default values -default_score = 0 -student_scores = dict.fromkeys(["Alice", "Bob", "Charlie"], default_score) -print(f"Student Scores (fromkeys): {student_scores}") -# Expected output: Student Scores (fromkeys): {'Alice': 0, 'Bob': 0, 'Charlie': 0} - -# Creating a dictionary with keys and default value None -project_tasks = dict.fromkeys(["Design", "Development", "Testing"]) -print(f"Project Tasks (fromkeys with None): {project_tasks}") -# Expected output: Project Tasks (fromkeys with None): {'Design': None, 'Development': None, 'Testing': None} +zipped = zip(keys, values) +dict_from_zip = dict(zipped) # Result: {"a": 1, "b": 2, "c": 3} ``` -#### 5. Using `zip()` with `dict()` +#### Using `dict()` Constructor -The `zip()` function pairs corresponding elements from multiple iterables. When combined with `dict()`, it's excellent for creating a dictionary from separate key and value lists. +Here is a Python code: ```python -keys = ["product_id", "name", "price"] -values = [101, "Laptop", 1200.50] - -# Using zip() to combine keys and values, then converting to a dictionary -product_details = dict(zip(keys, values)) -print(f"Product Details (using zip): {product_details}") -# Expected output: Product Details (using zip): {'product_id': 101, 'name': 'Laptop', 'price': 1200.5} - -# Example with different length lists (zip stops at the shortest) -colors = ["red", "green", "blue", "yellow"] -hex_codes = ["#FF0000", "#00FF00", "#0000FF"] # Shorter list -color_map = dict(zip(colors, hex_codes)) -print(f"Color Map (zip with different lengths): {color_map}") -# Expected output: Color Map (zip with different lengths): {'red': '#FF0000', 'green': '#00FF00', 'blue': '#0000FF'} +# Sequence of key-value pairs +student_dict2 = dict(name="Jane Doe", age=22, courses=["Biology", "Chemistry"]) + +# From another dictionary +student_dict_combined = dict(student, **student_dict2) ```
## 10. What is the difference between _==_ and _is operator_ in _Python_? -Both the **`==`** and **`is`** operators in Python are used for comparison, but they serve fundamentally different purposes, focusing on distinct aspects of objects. - -### Understanding `==` (Equality Operator) - -The **`==`** operator checks for **value equality**. It determines whether two objects have the same content or value. - -#### How `==` Works -When `a == b` is evaluated, Python essentially asks: "Do these two objects represent the same value?" - -For built-in types: -* **Numbers, strings, tuples:** `==` compares their literal values. -* **Lists, dictionaries, sets:** `==` recursively compares their elements. - -For custom objects: -The behavior of `==` can be customized by implementing the special method `__eq__(self, other)` within the class definition. If `__eq__` is not implemented, Python's default behavior for custom objects is to check for identity (similar to `is`), but this is rarely the desired behavior for value comparison. +Both the **`==`** and **`is`** operators in Python are used for comparison, but they function differently. -#### Example of `==` -```python -# Comparing built-in types -a = [1, 2, 3] -b = [1, 2, 3] -c = [4, 5, 6] -d = a - -print(f"a == b: {a == b}") # Output: True (same content) -print(f"a == c: {a == c}") # Output: False (different content) -print(f"a == d: {a == d}") # Output: True (same content) - -# Comparing custom objects with __eq__ -class Point: - def __init__(self, x, y): - self.x = x - self.y = y - - def __eq__(self, other): - if not isinstance(other, Point): - return NotImplemented - return self.x == other.x and self.y == other.y - -p1 = Point(1, 2) -p2 = Point(1, 2) -p3 = Point(3, 4) - -print(f"p1 == p2: {p1 == p2}") # Output: True (due to __eq__ implementation) -print(f"p1 == p3: {p1 == p3}") # Output: False -``` - -### Understanding `is` (Identity Operator) - -The **`is`** operator checks for **object identity**. It determines whether two variables refer to the *exact same object* in memory. - -#### How `is` Works -When `a is b` is evaluated, Python asks: "Are `a` and `b` references to the very same object instance?" This is equivalent to checking if `id(a) == id(b)`. The `id()` built-in function returns a unique identifier for an object, which is typically its memory address during its lifetime. - -The `is` operator **cannot be overloaded** for custom classes. Its behavior is fixed: it always compares object identities. - -#### Example of `is` -```python -# Comparing built-in types -a = [1, 2, 3] -b = [1, 2, 3] # 'b' is a new list object, even if content is identical -c = a # 'c' refers to the exact same list object as 'a' - -print(f"a is b: {a is b}") # Output: False (different objects in memory) -print(f"a is c: {a is c}") # Output: True (same object in memory) - -# Comparing custom objects -class Point: - def __init__(self, x, y): - self.x = x - self.y = y - -p1 = Point(1, 2) -p2 = Point(1, 2) -p3 = p1 - -print(f"p1 is p2: {p1 is p2}") # Output: False (p1 and p2 are different objects) -print(f"p1 is p3: {p1 is p3}") # Output: True (p1 and p3 refer to the same object) -``` - -### Key Differences Summarized - -* **`==`**: Compares the **value** or **content** of objects. Can be customized with `__eq__`. -* **`is`**: Compares the **identity** (memory address) of objects. Cannot be customized. +- The **`==`** operator checks for **value equality**. +- The **`is`** operator, on the other hand, validates **object identity**, -### When to Use Which Operator +In Python, every object is unique, identifiable by its memory address. The **`is`** operator uses this memory address to check if two objects are the same, indicating they both point to the exact same instance in memory. -#### Use `==` for Value Comparison -* You should almost always use **`==`** when you want to compare if two objects have the same content or value. This is the most common use case for comparison. -* **Examples:** Comparing numbers, strings, lists, dictionaries, or instances of custom classes where `__eq__` is defined to represent value equality. +- **`is`**: Compares the memory address or identity of two objects. +- **`==`**: Compares the content or value of two objects. -#### Use `is` for Identity Comparison -* **Singletons:** The `is` operator is most reliably and commonly used to check if an object is `None`, `True`, or `False`, as these are singletons in Python (there is only ever one instance of each). - ```python - my_variable = None - if my_variable is None: - print("Variable is None") - ``` -* **Checking for the exact same object:** If you specifically need to verify that two variables refer to the exact same object in memory, perhaps to detect alias relationships or for performance optimizations. +While **`is`** is primarily used for **None** checks, it's generally advisable to use **`==`** for most other comparisons. -#### Important Caveats for `is` -Python performs certain optimizations, such as **object interning**, for immutable types like small integers (typically -5 to 256), `True`, `False`, `None`, and sometimes short strings. This can lead to situations where `is` returns `True` for objects that might logically be considered distinct values, but which Python has optimized to point to the same memory location. +### Tips for Using Operators -```python -# Integer interning -x = 100 -y = 100 -print(f"x is y (small int): {x is y}") # Output: True (due to interning) - -a = 1000 -b = 1000 -print(f"a is b (large int): {a is b}") # Output: False (not always interned) - -# String interning (can be unpredictable for non-literal strings) -s1 = "hello" -s2 = "hello" -print(f"s1 is s2 (literal string): {s1 is s2}") # Output: True (often interned) - -s3 = "hello world" -s4 = "hello world" -print(f"s3 is s4 (longer string): {s3 is s4}") # Output: False (interning less likely) - -s5 = "hello" -s6 = "he" + "llo" -print(f"s5 is s6 (concatenated string): {s5 is s6}") # Output: True (optimised at compile time) -``` -Because of these optimizations, relying on `is` for value comparison, especially with numbers and strings, can lead to inconsistent and difficult-to-debug behavior. Always use `==` for comparing values unless you have a specific reason to check for object identity. +- **`==`**: Use for equality comparisons, like when comparing numeric or string values. +- **`is`**: Use for comparing membership or when dealing with singletons like **None**.
## 11. How does a _Python function_ work? -**Python functions** are fundamental constructs for organizing code, embodying principles of reusability, modularity, and encapsulation. They allow developers to break down complex problems into smaller, manageable units, each designed to perform a specific task. - -### Core Concepts of a Python Function +**Python functions** are the building blocks of code organization, often serving predefined tasks within modules and scripts. They enable reusability, modularity, and encapsulation. -Python functions operate on several core principles: +### Key Components -* **Code Reusability**: Functions define a block of code that can be executed multiple times from different parts of a program, avoiding code duplication. -* **Modularity**: They promote the division of a program into independent, self-contained modules, making the codebase easier to understand, maintain, and debug. -* **Encapsulation**: Functions encapsulate their internal logic and data (local variables), minimizing interference with other parts of the program. -* **First-Class Objects**: In Python, functions are first-class objects, meaning they can be assigned to variables, passed as arguments to other functions, and returned as values from other functions. This enables powerful programming paradigms like higher-order functions and decorators. +- **Function Signature**: Denoted by the `def` keyword, it includes the function name, parameters, and an optional return type. +- **Function Body**: This section carries the core logic, often comprising conditional checks, loops, and method invocations. +- **Return Statement**: The function's output is determined by this statement. When None is specified, the function returns by default. +- **Local Variables**: These variables are scoped to the function and are only accessible within it. -### Anatomy of a Python Function +### Execution Process -A Python function typically consists of the following parts: +When a function is called: -#### Function Signature -The signature defines the function's interface. It begins with the `def` keyword, followed by the function's name, a pair of parentheses enclosing its parameters, and a colon `:`. Modern Python (post-PEP 484) also supports optional **type hints** for parameters and the return value to improve code readability and allow static analysis. +1. **Stack Allocation**: A stack frame, also known as an activation record, is created to manage the function's execution. This frame contains details like the function's parameters, local variables, and **instruction pointer**. + +2. **Parameter Binding**: The arguments passed during the function call are bound to the respective parameters defined in the function header. -```python -def greet(name: str, message: str = "Hello") -> str: - # Function body - pass -``` -Here, `greet` is the function name, `name` and `message` are parameters (`message` has a default value), and `-> str` indicates the function is expected to return a string. +3. **Function Execution**: Control is transferred to the function body. The statements in the body are executed in a sequential manner until the function hits a return statement or the end of the function body. -#### Function Body -This is an indented block of code that contains the logic performed when the function is called. It can include conditional statements, loops, other function calls, and any other valid Python statements. +4. **Return**: If a return statement is encountered, the function evaluates the expression following the `return` and hands the value back to the caller. The stack frame of the function is then popped from the call stack. -#### Return Statement -The `return` statement specifies the value that the function sends back to its caller. If a function reaches its end without an explicit `return` statement, or if `return` is used without an expression, it implicitly returns `None`. +5. **Post Execution**: If there's no `return` statement, or if the function ends without evaluating any return statement, `None` is implicitly returned. -```python -def add(a: int, b: int) -> int: - result = a + b - return result +### Local Variable Scope -def do_nothing(): - # This function implicitly returns None - pass +- **Function Parameters**: These are a precursor to local variables and are instantiated with the values passed during function invocation. +- **Local Variables**: Created using an assignment statement inside the function and cease to exist when the function execution ends. +- **Nested Scopes**: In functions within functions (closures), non-local variables - those defined in the enclosing function - are accessible but not modifiable by the inner function, without using the `nonlocal` keyword. -value1 = add(5, 3) # value1 will be 8 -value2 = do_nothing() # value2 will be None -``` +### Global Visibility -#### Docstrings -While not part of the execution, a **docstring** (a multi-line string immediately after the function signature) is a crucial component for documentation. It explains what the function does, its parameters, and what it returns. +If a variable is not defined within a function, the Python runtime will look for it in the global scope. This behavior enables functions to access and even modify global variables. -```python -def calculate_area(radius: float) -> float: - """ - Calculates the area of a circle given its radius. - - Args: - radius (float): The radius of the circle. - - Returns: - float: The calculated area. - """ - import math - return math.pi * radius**2 -``` +### Avoiding Side Effects -### How a Python Function Executes - -When a function is called, a specific sequence of actions occurs: - -1. #### Call Stack and Stack Frames - Upon a function call, a new **stack frame** (also known as an activation record) is created on the program's **call stack**. This stack frame is a dedicated memory region that stores information relevant to that specific function invocation, including: - * The function's parameters. - * Its local variables. - * The return address (the location in the calling code to resume execution after the function completes). - * Other management information. - -2. #### Parameter Binding - The arguments passed during the function call are bound to the corresponding **parameters** defined in the function's signature. Python supports various ways to pass arguments, including positional arguments, keyword arguments, default values, and arbitrary argument lists (`*args` for positional, `**kwargs` for keyword). - -3. #### Execution Flow - Control is transferred to the function's body. Statements within the function body are executed sequentially. If an exception occurs, the execution might be interrupted and propagated up the call stack. - -4. #### Returning Control - When a `return` statement is encountered, the function evaluates the expression (if any) following `return` and sends this value back to the caller. Subsequently, the function's stack frame is **popped** from the call stack, releasing the memory allocated for its local variables and parameters. Execution in the caller resumes from the return address. If no `return` statement is met or the function body completes, `None` is implicitly returned. - -### Variable Scope and Lifetime (LEGB Rule) - -Python uses the **LEGB rule** to determine the order in which it looks for names (variables, functions, classes) during resolution: - -* **L**ocal: Names assigned within the current function (e.g., `x = 10` inside a `def`). This also includes parameters. -* **E**nclosing function locals: Names in the local scope of any enclosing function (for nested functions). -* **G**lobal: Names assigned at the top-level of a module (outside any function). -* **B**uilt-in: Names pre-assigned in Python's built-in module (e.g., `print`, `len`, `str`). - -#### Local Scope (L) -Variables defined inside a function (including its parameters) are **local** to that function. They are created when the function is called and destroyed when the function completes execution. They are not accessible from outside the function. - -```python -def my_function(): - local_var = "I am local" - print(local_var) # Accessible here - -my_function() -# print(local_var) # This would raise a NameError -``` - -#### Enclosing Function Local Scope (E) -In nested functions, the inner function can access variables from its **enclosing (outer) function's scope**. These are often called **non-local** variables. To modify a non-local variable from an inner function, the `nonlocal` keyword must be used. Without `nonlocal`, an assignment inside the inner function would create a new local variable, shadowing the outer one. - -```python -def outer_function(): - enclosing_var = "Outer variable" - - def inner_function(): - nonlocal enclosing_var # Declare intent to modify - enclosing_var = "Modified by inner" - print(f"Inside inner: {enclosing_var}") - - inner_function() - print(f"Inside outer: {enclosing_var}") - -outer_function() -# Output: -# Inside inner: Modified by inner -# Inside outer: Modified by inner -``` - -#### Global Scope (G) -Variables defined at the top level of a script or module are **global**. Functions can read global variables directly. However, to **modify** a global variable from within a function, the `global` keyword must be explicitly used. Otherwise, an assignment will create a new local variable with the same name, leaving the global variable unchanged. - -```python -global_var = "I am global" - -def modify_global(): - global global_var # Declare intent to modify - global_var = "I was modified" - print(f"Inside function: {global_var}") - -def create_local_with_same_name(): - global_var = "I am a local masking the global" # Creates a new local variable - print(f"Inside function (local): {global_var}") - -print(f"Before call: {global_var}") -modify_global() -print(f"After modify_global: {global_var}") - -create_local_with_same_name() -print(f"After create_local_with_same_name: {global_var}") -# Output: -# Before call: I am global -# Inside function: I was modified -# After modify_global: I was modified -# Inside function (local): I am a local masking the global -# After create_local_with_same_name: I was modified (Global variable remains unchanged here) -``` - -### Best Practices: Minimizing Side Effects - -Functions offer a crucial level of encapsulation, which can significantly reduce **side effects**. A side effect occurs when a function modifies something outside its local scope (e.g., a global variable, a mutable argument, or an external system). - -While Python's flexibility allows functions to access and modify global/non-local variables (with `global`/`nonlocal`), over-reliance on this can lead to: - -* **Harder Debugging**: It becomes difficult to trace where and when a variable's value changes. -* **Reduced Readability**: The function's behavior might depend on external state not explicitly passed as arguments. -* **Lower Testability**: Functions with side effects are harder to test in isolation, as their output depends on the global state. -* **Less Reusability**: Functions tightly coupled to specific global states are less portable. - -As a best practice, strive for **pure functions** where possible: functions that, given the same inputs, always produce the same output and cause no side effects. Minimizing the use of `global` and `nonlocal` keywords, and favoring passing data through arguments and returning results, leads to more robust, predictable, and maintainable codebases. +Functions offer a level of encapsulation, potentially reducing side effects by ensuring that data and variables are managed within a controlled environment. Such containment can help enhance the robustness and predictability of a codebase. As a best practice, minimizing the reliance on global variables can lead to more maintainable, reusable, and testable code.
## 12. What is a _lambda function_, and where would you use it? -### What is a Lambda Function? - -A **lambda function**, often simply called a **lambda**, is a small, anonymous function defined in Python using the `lambda` keyword. Unlike regular functions defined with `def`, a lambda function does not require a name, making it suitable for short, one-off operations. Its body is restricted to a single expression, the result of which is implicitly returned. +A **Lambda function**, or **lambda**, for short, is a small anonymous function defined using the `lambda` keyword in Python. -While traditional named functions serve most programming needs, lambda expressions provide a concise alternative for simple, functional constructs where a full `def` statement might be considered overly verbose. +While you can certainly use named functions when you need a function for something in Python, there are places where a lambda expression is more suitable. ### Distinctive Features -* #### Anonymity - Lambdas are inherently anonymous; they are not bound to an identifier (name) in the same way `def` functions are. This makes them ideal for situations where a function is needed transiently and will not be reused elsewhere in the codebase. - -* #### Single Expression Body - The core characteristic of a lambda is its limitation to a single expression. This means its body cannot contain multiple statements (like `if`, `for`, `while`, or `return`). It computes and returns the value of that single expression. - * Example: `lambda x, y: x + y` is valid. - * Example: `lambda x: if x > 0: return x` is **invalid** because it contains a statement (`if`) and an explicit `return`. - -* #### Implicit Return - There is no need for an explicit `return` statement within a lambda. The value produced by the single expression in its body is automatically returned. - -* #### Conciseness - Lambdas enable the definition of straightforward functions in a compact, inline form, which can improve readability for simple operations when used appropriately. +- **Anonymity**: Lambdas are not given a name in the traditional sense, making them suited for one-off uses in your codebase. +- **Single Expression Body**: Their body is limited to a single expression. This can be an advantage for brevity but a restriction for larger, more complex functions. +- **Implicit Return**: There's no need for an explicit `return` statement. +- **Conciseness**: Lambdas streamline the definition of straightforward functions. ### Common Use Cases -Lambdas shine in functional programming paradigms or when a small, throwaway function is required as an argument to a higher-order function. +- **Map, Filter, and Reduce**: Functions like `map` can take a lambda as a parameter, allowing you to define simple transformations on the fly. For example, doubling each element of a list can be achieved with `list(map(lambda x: x*2, my_list))`. +- **List Comprehensions**: They are a more Pythonic way of running the same `map` or `filter` operations, often seen as an alternative to lambdas and `map`. +- **Sorting**: Lambdas can serve as a custom key function, offering flexibility in sort orders. +- **Callbacks**: Often used in events where a function is needed to be executed when an action occurs (e.g., button click). +- **Simple Functions**: For functions that are so basic that giving them a name, especially in more procedural code, would be overkill. -* #### Map, Filter, and Reduce - Functions like `map()`, `filter()`, and `functools.reduce()` often take a function as an argument to apply a transformation or filter criteria to an iterable. Lambdas provide a convenient way to define these simple operations inline. +### Notable Limitations - ```python - # Example: Doubling each element in a list using map and lambda - my_list = [1, 2, 3, 4] - doubled_list = list(map(lambda x: x * 2, my_list)) - print(doubled_list) # Output: [2, 4, 6, 8] - - # Example: Filtering even numbers using filter and lambda - even_numbers = list(filter(lambda x: x % 2 == 0, my_list)) - print(even_numbers) # Output: [2, 4] - ``` - -* #### Sorting with Custom Keys - The `sort()` method of lists or the built-in `sorted()` function can accept a `key` argument, which is a function that extracts a comparison key from each element in the list. Lambdas are perfect for defining custom sorting logic on the fly. - - ```python - # Example: Sorting a list of tuples by the second element - data = [('apple', 10), ('banana', 5), ('cherry', 15)] - sorted_data = sorted(data, key=lambda item: item[1]) - print(sorted_data) # Output: [('banana', 5), ('apple', 10), ('cherry', 15)] - - # Example: Sorting a list of strings by their length - words = ["python", "is", "awesome"] - sorted_words = sorted(words, key=lambda s: len(s)) - print(sorted_words) # Output: ['is', 'python', 'awesome'] - ``` - -* #### Callbacks - In event-driven programming (e.g., GUI frameworks, web frameworks), lambdas are frequently used as callback functions that are executed when a specific action occurs (e.g., a button click, an API response). - - ```python - # Conceptual example for a GUI button click - # button.on_click(lambda event: print("Button clicked!")) - ``` - -* #### Simple Functions - For very basic, self-contained functions that are used immediately and not intended for reuse, a lambda can prevent the clutter of defining a named function. - -### Notable Limitations and Alternatives - -While useful, lambdas have limitations that often make named functions or other Pythonic constructs preferable. - -* #### Lack of Verbose Readability - If the logic within a lambda becomes even slightly complex, it can quickly become difficult to read and understand. Named functions with clear names and docstrings generally offer superior readability and maintainability for anything beyond trivial operations. - -* #### No Formal Documentation - Lambdas do not support docstrings, making it harder to document their purpose directly within the function itself. Their intent must be inferred from context or explained via external comments, which is less ideal. - -* #### Restriction to a Single Expression - As previously mentioned, lambdas cannot contain statements (assignments, `if`/`else` statements, loops, `try`/`except` blocks, etc.). This severely limits their complexity. For instance, you cannot define a lambda that prints something and then returns a value, as `print()` is a statement. While conditional *expressions* (`x if condition else y`) are allowed, they can quickly become unreadable. - -* #### Alternatives like List Comprehensions - For many scenarios involving `map()` or `filter()` with simple lambdas, Python's **list comprehensions** (and generator expressions, set comprehensions, dictionary comprehensions) often provide a more readable and Pythonic alternative. - - ```python - # Using map and lambda - my_list = [1, 2, 3, 4] - doubled_list_lambda = list(map(lambda x: x * 2, my_list)) - - # Using a list comprehension (often preferred for clarity) - doubled_list_comprehension = [x * 2 for x in my_list] - - print(doubled_list_lambda) # Output: [2, 4, 6, 8] - print(doubled_list_comprehension) # Output: [2, 4, 6, 8] - ``` - -In conclusion, lambda functions are a powerful tool for writing concise, anonymous functions for simple, single-expression tasks, particularly when passed as arguments to higher-order functions. However, for more complex logic, better readability, or reusability, a traditional `def` function or other Pythonic constructs are usually the superior choice. +- **Lack of Verbose Readability**: Named functions are generally preferred when their intended use is obvious from the name. Lambdas can make code harder to understand if they're complex or not used in a recognizable pattern. +- **No Formal Documentation**: While the function's purpose should be apparent from its content, a named function makes it easier to provide direct documentation. Lambdas would need a separate verbal explanation, typically in the code or comments.
## 13. Explain _*args_ and _**kwargs_ in _Python_. -In Python, `*args` and `**kwargs` are powerful constructs used to pass a **variable number of arguments** to a function. They enable flexible function definitions that can accept an arbitrary quantity of inputs. - -The `*args` syntax collects a variable number of **positional arguments** into a **tuple**, while `**kwargs` does the same for **keyword arguments** into a **dictionary**. - -Here are the key features, use-cases, and their respective code examples, along with important related concepts. +In Python, `*args` and `**kwargs` are often used to pass a variable number of arguments to a function. -### `*args`: Variable Positional Arguments +`*args` collects a variable number of positional arguments into a **tuple**, while `**kwargs` does the same for keyword arguments into a **dictionary**. -The single asterisk `*` operator, when used in a function definition, indicates that an arbitrary number of positional arguments can be accepted. +Here are the key features, use-cases, and their respective code examples. -#### How it Works +### **\*args**: Variable Number of Positional Arguments -The name `*args` is a widely adopted **convention**. The asterisk `*` itself is the operator that tells Python to collect all remaining positional arguments into a single **tuple**. This tuple can then be iterated over within the function. +- **How it Works**: The name `*args` is a convention. The asterisk (*) tells Python to put any remaining positional arguments it receives into a tuple. -#### Use-Cases +- **Use-Case**: When the number of arguments needed is uncertain. -* When the number of arguments a function needs to operate on is **uncertain** or can vary. -* To create **flexible functions** that can handle different call signatures for positional data. -* For **forwarding arguments** from one function to another. - -#### Code Example: `*args` +#### Code Example: "*args" ```python def sum_all(*args): - """ - Calculates the sum of an arbitrary number of integers. - The collected arguments are available as a tuple named 'args'. - """ result = 0 - print(f"Type of args: {type(args)}") # Output: Type of args: for num in args: result += num return result -print(sum_all(1, 2, 3, 4)) # Output: 10 -print(sum_all(10, 20, 30, 40, 50)) # Output: 150 +print(sum_all(1, 2, 3, 4)) # Output: 10 ``` -### `**kwargs`: Variable Keyword Arguments - -The double asterisk `**` operator, when used in a function definition, allows a function to accept an arbitrary number of keyword arguments. +### **\*\*kwargs**: Variable Number of Keyword Arguments -#### How it Works +- **How it Works**: The double asterisk (**) is used to capture keyword arguments and their values into a dictionary. -Similar to `*args`, `**kwargs` is a **convention**, and the double asterisk `**` is the operator. It captures all remaining keyword arguments and their values into a **dictionary**. The keys of this dictionary are the keyword argument names (as strings), and the values are their corresponding argument values. +- **Use-Case**: When a function should accept an arbitrary number of keyword arguments. -#### Use-Cases - -* When a function needs to accept an **arbitrary number of named parameters**. -* To create highly **configurable functions** where options can be passed as keyword arguments. -* For **building flexible APIs** where different settings can be provided. -* For **forwarding keyword arguments** from one function to another, especially in decorators or object initializers. - -#### Code Example: `**kwargs` +#### Code Example: "**kwargs" ```python -def print_user_info(**kwargs): - """ - Prints user information passed as keyword arguments. - The collected arguments are available as a dictionary named 'kwargs'. - """ - print(f"Type of kwargs: {type(kwargs)}") # Output: Type of kwargs: - if not kwargs: - print("No user information provided.") - return - - print("User Info:") +def print_values(**kwargs): for key, value in kwargs.items(): - print(f" {key.replace('_', ' ').title()}: {value}") + print(f"{key}: {value}") # Keyword arguments are captured as a dictionary -print_user_info(name="Alice", age=25, city="London", occupation="Engineer") -# Output: -# Type of kwargs: -# User Info: -# Name: Alice -# Age: 25 -# City: London -# Occupation: Engineer - -print_user_info() # Output: No user information provided. -``` - -### Argument Order and Combination - -When defining a function that uses a mix of standard arguments, `*args`, and `**kwargs`, there is a specific order that must be followed: - -1. **Standard positional arguments** -2. **`*args`** (to capture additional positional arguments) -3. **Keyword-only arguments** (arguments specified after `*args` that *must* be passed by keyword) -4. **`**kwargs`** (to capture additional keyword arguments) - -#### Code Example: Combining `*args` and `**kwargs` - -```python -def configure_system(system_name, *settings, debug_mode=False, **options): - """ - Configures a system with a name, a list of settings, an optional debug mode, - and arbitrary additional configuration options. - """ - print(f"Configuring system: {system_name}") - print(f" Settings (tuple): {settings}") - print(f" Debug Mode (kw-only): {debug_mode}") - print(f" Additional Options (dict): {options}") - -configure_system("WebServer", "security_patch", "logging_level", - debug_mode=True, timeout=30, users_limit=100) +print_values(name="John", age=30, city="New York") # Output: -# Configuring system: WebServer -# Settings (tuple): ('security_patch', 'logging_level') -# Debug Mode (kw-only): True -# Additional Options (dict): {'timeout': 30, 'users_limit': 100} - -configure_system("Database", "replication_enabled") -# Output: -# Configuring system: Database -# Settings (tuple): ('replication_enabled',) -# Debug Mode (kw-only): False -# Additional Options (dict): {} -``` - -### Unpacking Arguments (The Duality) - -The `*` and `**` operators have a dual purpose: they can also be used to **unpack** iterables and dictionaries, respectively, when **calling a function**. This is the inverse operation of collecting arguments. - -#### Unpacking with `*` - -Using `*` before an iterable (like a list or tuple) in a function call unpacks its elements, treating them as individual positional arguments. - -```python -def multiply(a, b, c): - return a * b * c - -numbers = [2, 3, 4] -print(multiply(*numbers)) # Unpacks [2, 3, 4] into 2, 3, 4. Output: 24 - -coordinates = (10, 20, 30) -print(multiply(*coordinates)) # Unpacks (10, 20, 30) into 10, 20, 30. Output: 6000 +# name: John +# age: 30 +# city: New York ``` - -#### Unpacking with `**` - -Using `**` before a dictionary in a function call unpacks its key-value pairs, treating them as individual keyword arguments. - -```python -def describe_person(name, age, city): - print(f"{name} is {age} years old and lives in {city}.") - -person_data = {"name": "Charlie", "age": 40, "city": "Paris"} -describe_person(**person_data) # Unpacks dictionary into name="Charlie", age=40, city="Paris". -# Output: Charlie is 40 years old and lives in Paris. - -config = {"name": "Server1", "port": 8080, "timeout": 60} - -# Can be combined with specific args, as long as there are no conflicts -def connect(name, port, **extra_options): - print(f"Connecting to {name} on port {port} with options: {extra_options}") - -connect(**config) -# Output: Connecting to Server1 on port 8080 with options: {'timeout': 60} -``` - -### Key Benefits - -* **Flexibility:** Functions can be designed to accept varying numbers of inputs without needing multiple overloaded definitions. -* **Code Reusability:** Promotes more generic and adaptable function designs. -* **Argument Forwarding:** Simplifies passing arguments from a wrapper function to an inner function, which is particularly useful in decorators or when building API layers.
## 14. What are _decorators_ in _Python_? -In Python, a **decorator** is a powerful design pattern and a feature that allows you to modify or extend the behavior of functions or methods dynamically without permanently altering their source code. This is a form of **metaprogramming**, primarily used to keep the code **clean**, **maintainable**, and adhere to the **DRY** (Don't Repeat Yourself) principle. +In Python, a **decorator** is a design pattern and a feature that allows you to modify functions and methods dynamically. This is done primarily to keep the code clean, maintainable, and DRY (Don't Repeat Yourself). ### How Decorators Work -* Decorators are essentially **callable objects** (usually functions) that take another function as an argument. -* They *wrap* the **target function**, allowing you to execute custom code both *before* and *after* the target function's execution. -* The decorator then returns a new function (or another callable object) that typically replaces the original function. This makes them **higher-order functions**. +- Decorators wrap a target function, allowing you to execute custom code before and after that function. +- They are typically **higher-order functions** that take a function as an argument and return a new function. +- This paradigm of "functions that modify functions" is often referred to as **metaprogramming**. ### Common Use Cases -Decorators have numerous practical applications, enhancing code modularity and reusability: - -* **Authorization and Authentication**: Restricting access to certain functions based on user roles or permissions. -* **Logging**: Recording function calls, arguments, return values, or exceptions for debugging and auditing. -* **Caching**: Storing the results of expensive function calls to avoid recomputing them for the same inputs. -* **Validation**: Checking input parameters or function output against specified criteria. -* **Task Scheduling**: Executing a function at a specific time or on a particular event. -* **Counting and Profiling**: Keeping track of the number of times a function is called or measuring its execution time. -* **Retries**: Automatically retrying a function call if it fails (e.g., due to network issues). +- **Authorization and Authentication**: Control user access. +- **Logging**: Record function calls and their parameters. +- **Caching**: Store previous function results for quick access. +- **Validation**: Verify input parameters or function output. +- **Task Scheduling**: Execute a function at a specific time or on an event. +- **Counting and Profiling**: Keep track of the number of function calls and their execution time. ### Using Decorators in Code -Python's `@decorator` syntax provides **syntactic sugar** for applying decorators. +Here is the Python code: ```python from functools import wraps -import time -# 1. Basic Decorator (Timer Example) -def timer(func): - """A decorator that measures the execution time of a function.""" - @wraps(func) # Preserves the original function's metadata +# 1. Basic Decorator +def my_decorator(func): + @wraps(func) # Ensures the original function's metadata is preserved def wrapper(*args, **kwargs): - start_time = time.perf_counter() + print('Something is happening before the function is called.') result = func(*args, **kwargs) - end_time = time.perf_counter() - print(f'Function {func.__name__!r} executed in {(end_time - start_time):.4f}s') + print('Something is happening after the function is called.') return result return wrapper -@timer -def long_running_function(): - """Simulates a function that takes some time to execute.""" - time.sleep(0.5) - print('Long running function completed.') +@my_decorator +def say_hello(): + print('Hello!') -long_running_function() # Output: Long running function completed. \n Function 'long_running_function' executed in X.XXXs +say_hello() -# 2. Decorators with Arguments (Parameterized Decorator) -def log_level(level): - """A decorator factory that creates a decorator to log function calls at a specific level.""" +# 2. Decorators with Arguments +def decorator_with_args(arg1, arg2): def actual_decorator(func): @wraps(func) def wrapper(*args, **kwargs): - print(f'[{level.upper()}] Calling {func.__name__!r} with args: {args}, kwargs: {kwargs}') + print(f'Arguments passed to decorator: {arg1}, {arg2}') result = func(*args, **kwargs) - print(f'[{level.upper()}] {func.__name__!r} returned: {result}') return result return wrapper return actual_decorator -@log_level('info') -def add(a, b): - return a + b - -@log_level('debug') -def multiply(x, y): - return x * y +@decorator_with_args('arg1', 'arg2') +def my_function(): + print('I am decorated!') -print(add(5, 3)) # Output: [INFO] Calling 'add' with args: (5, 3), kwargs: {} \n [INFO] 'add' returned: 8 \n 8 -print(multiply(4, 2)) # Output: [DEBUG] Calling 'multiply' with args: (4, 2), kwargs: {} \n [DEBUG] 'multiply' returned: 8 \n 8 +my_function() ``` ### Decorator Syntax in Python -The `@decorator` syntax above is a compact and readable way to apply a decorator. It is entirely equivalent to, and merely **syntactic sugar** for, the following: +The `@decorator` syntax is a convenient shortcut for: ```python def say_hello(): print('Hello!') - -# Applying the decorator manually -say_hello = timer(say_hello) # assuming 'timer' from the example above - -say_hello() # This would now also print the execution time +say_hello = my_decorator(say_hello) ``` -### Role of `functools.wraps` +### Role of **functools.wraps** -When defining decorators, especially those that return inner functions, it is crucial to use `@wraps(func)` from the `functools` module. This decorator ensures that the original function's **metadata** (like its `__name__`, `__doc__`, `__module__`, and argument lists) is correctly preserved on the wrapper function. Without `@wraps`, the decorated function would appear to be the wrapper function, which can hinder debugging, documentation tools, and introspection. +When defining decorators, particularly those that return functions, it is good practice to use `@wraps(func)` from the `functools` module. This ensures that the original function's metadata, such as its name and docstring, is preserved.
## 15. How can you create a _module_ in _Python_? -To create a module in Python, you essentially save Python code in a file with a `.py` extension. This file then becomes a module that can be imported and used in other Python programs or modules. - -A **Python module** is a file containing Python definitions and statements. Modules are a fundamental mechanism for organizing related code into logical units, promoting reusability, and avoiding name collisions. - -### How to Create a Python Module - -The primary and most common way to create a module is as follows: - -1. **Save Python Code as a `.py` File**: - Simply write your Python code (functions, classes, variables, etc.) in a text file and save it with a `.py` extension. For instance, if you save your code as `my_module.py`, then `my_module` becomes the name of your module. +You can **create** a Python module through one of two methods: -2. **Implicitly as Part of a Package (using `__init__.py`)**: - While not a standalone module itself in the same sense as a `.py` file, an `__init__.py` file *is* a module. Its primary purpose is to mark a directory as a **Python package**. A package is a way to organize related modules into a directory hierarchy. When a directory contains an `__init__.py` file (even an empty one), Python treats that directory as a package, and any `.py` files inside it become modules within that package. - *Note*: As of Python 3.3, `__init__.py` is optional for *namespace packages*, but it is still commonly used and required for regular packages. +- **Define**: Begin with saving a Python file with `.py` extension. This file will automatically function as a module. -### Accessing a Module +- **Create a Blank Module**: Start an empty file with no extension. Name the file using the accepted module syntax, e.g., `__init__ `, for it to act as a module. -Once a module is created, you can access its functionality using the `import` statement in another Python script or interactive session. +Next, use **import** to access the module and its functionality. ### Code Example: Creating a `math_operations` Module -Let's illustrate by creating a module named `math_operations`. - #### Module Definition -Save the following code in a file named `math_operations.py`: +Save the below `math_operations.py` file : ```python -# math_operations.py - def add(x, y): - """Adds two numbers and returns the sum.""" return x + y def subtract(x, y): - """Subtracts the second number from the first.""" return x - y def multiply(x, y): - """Multiplies two numbers.""" return x * y def divide(x, y): - """Divides the first number by the second. Handles division by zero.""" - if y == 0: - raise ValueError("Cannot divide by zero") return x / y - -PI = 3.14159 # A global variable within the module ``` #### Module Usage -You can use the `math_operations` module by importing it into another Python file (e.g., `main_app.py`) or an interactive interpreter: +You can use `math_operations` module by using import as shown below: ```python -# main_app.py - -import math_operations # Imports the entire module - -# Access functions and variables using the module name -result_add = math_operations.add(4, 5) -print(f"4 + 5 = {result_add}") # Output: 4 + 5 = 9 +import math_operations -result_divide = math_operations.divide(10, 5) -print(f"10 / 5 = {result_divide}") # Output: 10 / 5 = 2.0 +result = math_operations.add(4, 5) +print(result) -print(f"Value of PI: {math_operations.PI}") # Output: Value of PI: 3.14159 - -# Using specific imports for brevity (recommended for specific functions/classes) -from math_operations import multiply, subtract - -result_multiply = multiply(6, 7) -print(f"6 * 7 = {result_multiply}") # Output: 6 * 7 = 42 - -result_subtract = subtract(15, 8) -print(f"15 - 8 = {result_subtract}") # Output: 15 - 8 = 7 - -# Importing all members (generally discouraged) -# from math_operations import * # Not recommended generally due to potential name collisions and readability concerns - -# try: -# result_zero_divide = math_operations.divide(10, 0) -# print(result_zero_divide) -# except ValueError as e: -# print(f"Error: {e}") # Output: Error: Cannot divide by zero +result = math_operations.divide(10, 5) +print(result) ``` -### Best Practices for Module Creation +Even though it is not required in the later **versions of Python**, you can also use statement `from math_operations import *` to import all the members such as functions and classes at once: -When creating modules, consider the following best practices: +```python +from math_operations import * # Not recommended generally due to name collisions and readability concerns -* **Modularity**: Keep modules focused on a single responsibility or a coherent set of related functionalities. -* **Documentation**: Use docstrings for modules, functions, and classes to explain their purpose, arguments, and return values. This greatly enhances maintainability. -* **Guard Against Code Execution on Import**: It's common for modules to contain code that should only run when the module is executed directly (as a script) and not when it's imported into another program. Use the `if __name__ == "__main__":` block for this: +result = add(3, 2) +print(result) +``` - ```python - # my_module.py +### Best Practice +Before submitting the code, let's make sure to follow the **Best Practice**: - def my_function(): - print("This function is part of the module.") +- **Avoid Global Variables**: Use a `main()` function. +- **Guard Against Code Execution on Import**: To avoid unintended side effects, use: - def main(): - """Main execution logic when the module is run as a script.""" - print("This code runs only when my_module.py is executed directly.") - my_function() - # Additional script-specific logic here +```python +if __name__ == "__main__": + main() +``` - if __name__ == "__main__": - main() - ``` - This ensures that the code inside the `main()` function (or directly under the `if` block) is only executed when `my_module.py` is run as the primary script. If `my_module.py` is imported into another file, the `main()` function will not be called automatically, preventing unintended side effects. +This makes sure that the block of code following `if __name__ == "__main__":` is only executed when the module is run directly and not when imported as a module in another program.
From 08b998ad08f311641a25037736bd90e7bc34a84c Mon Sep 17 00:00:00 2001 From: Devinterview-io <76989322+Devinterview-io@users.noreply.github.com> Date: Sun, 25 Jan 2026 17:18:11 -0500 Subject: [PATCH 4/7] Update python interview questions --- README.md | 863 ++++++++++++++++++++++++++++-------------------------- 1 file changed, 452 insertions(+), 411 deletions(-) diff --git a/README.md b/README.md index 7453e9a..9f3899a 100644 --- a/README.md +++ b/README.md @@ -13,517 +13,505 @@ ## 1. What are the _key features_ of _Python_? -**Python** is a versatile and dominant programming language known for its simplicity, **elegant syntax**, and massive ecosystem. Below are the defining features that maintain its popularity in 2026. +**Python** is a versatile and popular high-level programming language known for its simplicity, **elegant syntax**, and a vast ecosystem of libraries. Let's look at the key features that make Python stand out. ### Key Features of Python #### 1. Interpreted and Interactive -Python executes code **line-by-line** via an interpreter, facilitating rapid prototyping and debugging. Modern versions utilize a specializing **adaptive interpreter** to optimize performance during execution. +Python executes code **line-by-line** via an interpreter, making it ideal for rapid prototyping and debugging. Modern versions also incorporate Just-In-Time (JIT) compilation for improved performance. #### 2. Easy to Learn and Read -Python's **clean syntax** resembles plain English and relies on indentation for structure. This high readability significantly reduces the cognitive load for both beginners and maintenance engineers. +Python's **clean, readable syntax**, often resembling plain English, reduces the cognitive load for beginners and experienced developers alike by using indentation to define code blocks. #### 3. Cross-Platform Compatibility -Python is platform-independent, running seamlessly on Windows, Linux, macOS, and **WebAssembly (WASM)** environments without requiring platform-specific code modifications. +Python is portable, running on various platforms such as Windows, Linux, and macOS without requiring platform-specific modifications. #### 4. Modular and Scalable -The language encourages modularity through packages and modules, allowing developers to organize code into **reusable** functions and classes for scalable application architectures. +Developers can organize their code into modular packages and **reusable** functions, promoting better structure and maintainability for large applications. #### 5. Rich Library Ecosystem -The Python Package Index (PyPI) hosts **hundreds of thousands** of libraries, providing immediate solutions for Artificial Intelligence, Data Science, Web Development, and Cloud Computing. +The Python Package Index (PyPI) hosts **hundreds of thousands** of libraries, providing robust solutions for tasks ranging from web development to artificial intelligence. #### 6. Exceptionally Versatile -Python serves as a general-purpose language, equally proficient in scripting, building complex web applications, and performing high-performance **scientific computing**. +From web applications to scientific computing and automation, Python is a general-purpose language equally proficient in diverse domains. -#### 7. Automatic Memory Management -Python manages memory automatically via a private heap and built-in **Garbage Collection**, shielding developers from low-level details like manual memory deallocation. +#### 7. Memory Management +Python automatically allocates and manages memory using a private heap and **garbage collection**, shielding developers from low-level tasks like manual deallocation. -#### 8. Dynamically Typed with Gradual Typing -Python infers data types at runtime (dynamic typing). However, modern Python embraces **Type Hints**, enabling static analysis and robust tooling while retaining runtime flexibility. +#### 8. Dynamically Typed +Python infers the data type of a variable during execution, easing the **declaration** and manipulation of variables, while supporting optional static type hinting. #### 9. Object-Oriented -Python strictly follows object-oriented paradigms. Everything, including functions and data types, is an **object**, supporting inheritance, polymorphism, and encapsulation. +Python supports object-oriented paradigms where everything is an **object**, offering attributes and methods to encapsulate and manipulate data efficiently. -#### 10. Extensible and Embeddable -Python allows integration with C/C++ for performance-critical tasks. It is also **embeddable**, allowing integration into other applications to provide scripting interfaces. +#### 10. Extensible +With its C-language API, developers can integrate performance-critical tasks and existing C/C++ modules directly with Python code.
## 2. How is _Python_ executed? -### Execution Overview -Python is a hybrid language that utilizes a two-step process involving **compilation** to an intermediate format followed by **interpretation**. This design balances development speed with portability. +**Python** source code undergoes a multi-stage process involving compilation, interpretation, and dynamic optimization before execution. Below are the technical details of this workflow as of 2026. ### Compilation & Interpretation -The standard execution flow in CPython (the reference implementation) consists of: -- **Bytecode Compilation**: The Python compiler translates high-level source code (`.py`) into **bytecode**. Bytecode represents a set of platform-independent instructions optimized for the Python Virtual Machine (PVM). These compiled files are often cached in the `__pycache__` directory (as `.pyc` files) to speed up subsequent startups. - -- **Interpretation**: The PVM acts as a loop that iterates through bytecode instructions, executing them one by one. It abstracts the underlying hardware, ensuring the code runs on any operating system. +Python (specifically the standard **CPython** implementation) utilizes a hybrid execution model: -### Source Code to Bytecode: Key Steps -1. **Lexical Analysis (Tokenization)**: The source code is broken down into a stream of atomic elements called tokens (keywords, identifiers, literals). -2. **Parsing**: Tokens are organized into an **Abstract Syntax Tree (AST)**, a tree representation of the code's logical structure. -3. **Symbol Table Generation**: The compiler analyzes the AST to define scopes and variable bindings (semantic analysis). -4. **Bytecode Generation**: The AST is traversed to emit the final bytecode instructions encapsulated in code objects. +- **Bytecode Compilation**: The source code is first compiled into **Bytecode**, a low-level, platform-independent intermediate representation. This bytecode is a sequence of instructions for the Python Virtual Machine (PVM), often cached in `.pyc` files. +- **The PVM Loop**: The PVM is a stack-based interpreter that iterates through bytecode instructions and executes them sequentially. -### Modern Optimization: Adaptive Interpreter & JIT -Since Python 3.11 (and enhanced in 3.13+), the execution model has evolved to include dynamic optimization: +### The Compilation Pipeline -- **Tier 1: Adaptive Interpretation**: The PVM monitors code as it runs. If specific instructions are executed frequently with consistent types (e.g., binary addition of two integers), the interpreter "specializes" them on-the-fly, replacing generic bytecode with faster, type-specific versions. -- **Tier 2: JIT Compilation**: Modern CPython utilizes a **Copy-and-Patch JIT** (Just-In-Time) compiler. For "hot" code paths, specialized bytecode is translated into machine code micro-operations, significantly reducing the overhead of the interpretation loop. +The transformation from source code to executable bytecode involves strict steps: -### Code Example: Bytecode Inspection -We can use the `dis` module to view the underlying bytecode. The example below also demonstrates **constant folding**, an optimization where the compiler pre-calculates constant expressions. +1. **Lexical Analysis**: The tokenizer breaks the source text into individual **tokens** (identifiers, keywords, operators). +2. **Parsing**: Tokens are structured into an **Abstract Syntax Tree (AST)**, which validates the grammar and syntax. +3. **Bytecode Generation**: The compiler traverses the AST, generates a Control Flow Graph (CFG), and emits the final bytecode instructions. + +### Adaptive Execution and JIT + +Modern Python (post-3.11) employs **Tiered Execution** to optimize performance dynamically: + +- **Tier 1: Adaptive Interpretation**: The PVM monitors code as it runs. If specific instructions are executed frequently with consistent types, the interpreter "quickens" them by replacing generic bytecode with **specialized instructions** optimized for those specific types. +- **Tier 2: Just-In-Time (JIT) Compilation**: For highly active code paths ("hot spots"), the JIT compiler (utilizing a copy-and-patch architecture) translates specialized bytecode into native machine code. This bypasses the interpreter loop entirely for those segments, significantly reducing overhead. + +### Bytecode versus Machine Code + +Unlike languages like C++ that compile directly to hardware-specific **Machine Code**, Python compiles to **Bytecode**. +- **Portability**: Bytecode allows Python programs to run on any device with a PVM, regardless of the underlying hardware. +- **Performance**: While the extra layer of interpretation historically caused latency, the modern **Adaptive Interpreter** and **JIT** bridge the gap by compiling hot code to machine code at runtime. + +### Code Example: Disassembly and Optimization + +We can inspect the bytecode using the `dis` module. The Python compiler performs **Peephole Optimization** during the compilation phase, such as pre-calculating constant expressions. ```python import dis def example_func(): + # The compiler evaluates 15 * 20 at compile-time (Constant Folding) return 15 * 20 # Disassemble to view bytecode instructions dis.dis(example_func) ``` -**Output:** +**Output**: +The output reveals that the multiplication does not happen at runtime; the result (`300`) is loaded directly. + ```plaintext 4 0 LOAD_CONST 1 (300) 2 RETURN_VALUE ``` -*(The compiler computed `15 * 20` -> `300` during compilation, issuing a single `LOAD_CONST` instruction rather than a runtime multiplication.)*
## 3. What is _PEP 8_ and why is it important? ### What is PEP 8? -**PEP 8** is the official style guide for Python code. It stands for *Python Enhancement Proposal 8* and provides conventions for writing clear, consistent, and standard Python. Its primary goal is to improve the readability of code and make it indistinguishable across different developers. - -In 2026, while strict manual adherence is less common, PEP 8 remains the foundational configuration for modern auto-formatters (like **Ruff** or **Black**) and linters. +**PEP 8** (Python Enhancement Proposal 8) is the standard style guide for writing Python code. Authored by Guido van Rossum, Barry Warsaw, and Nick Coghlan, it provides guidelines to ensure that Python code is formatted consistently across the entire ecosystem. -### Key Design Principles +### Why It Is Important -PEP 8 emphasizes: - -- **Readability**: Code is read much more often than it is written. -- **Consistency**: A uniform style reduces cognitive load, allowing developers to focus on logic rather than formatting. -- **Explicit Layout**: Visual cues (indentation, spacing) should reflect the logical structure of the code. +- **Readability**: Code is read much more often than it is written. PEP 8 ensures that code written by one developer appears familiar to another, reducing the cognitive load required to understand the logic. +- **Consistency**: Adhering to a uniform style makes codebases easier to maintain and review. +- **Tooling Standard**: Modern linters (e.g., `ruff`, `flake8`) and auto-formatters (e.g., `black`) rely on PEP 8 definitions to automate code quality checks. ### Base Rules -- **Indentation**: Use **4 spaces** for each indentation level. Do not use tabs. -- **Line Length**: The official limit is **79 characters** to facilitate side-by-side code review. (Note: Many modern teams extend this to 88 or 100 characters via configuration). -- **Blank Lines**: Use two blank lines to separate top-level functions and classes; use one blank line for method definitions inside classes. -- **Imports**: Place imports at the top of the file, on separate lines. +- **Indentation**: Use **4 spaces** per indentation level. Do not use tabs. +- **Line Length**: The official guideline is **79 characters** to facilitate side-by-side editing, though many modern teams adjust this limit (e.g., to 88 or 100 characters). +- **Blank Lines**: Surround top-level function and class definitions with two blank lines. Method definitions inside a class are separated by a single blank line. ### Naming Styles -- **Class Names**: Use `CapWords` (PascalCase). -- **Functions and Variables**: Use `snake_case` (lowercase with underscores). -- **Constants**: Use `UPPER_CASE_WITH_UNDERSCORES`. -- **Modules**: Keep names short and `lowercase`. - -### Documentation +- **Class Names**: Use `CapWords` (also known as PascalCase). +- **Functions & Variables**: Use `snake_case` (lowercase with underscores). +- **Constants**: Use `UPPER_CASE_WITH_UNDERSCORES`. +- **Non-public Attributes**: Use a leading underscore (e.g., `_internal_var`) to indicate internal use. -- **Docstrings**: Use triple double-quotes (`"""`) for all public modules, functions, classes, and methods. -- **Comments**: Comments should be complete sentences. Inline comments should be separated by at least two spaces from the code. +### Documentation & Whitespace -### Whitespace Usage +- **Docstrings**: Use triple double quotes (`"""`) for documentation strings. +- **Whitespace**: Avoid extraneous whitespace immediately inside parentheses, brackets, or braces. Always surround binary operators (like assignment `=` or addition `+`) with a single space. +- **Imports**: Place imports at the top of the file, on separate lines, grouped in order: standard library, third-party, and local application. -- **Operators**: Surround binary operators with a single space (e.g., `x = y + 1`). -- **Grouping**: Avoid extraneous whitespace inside parentheses, brackets, or braces (e.g., `call(arg)` not `call( arg )`). +### Example: Modern Compliant Code -### Example: Directory Walker - -The following code illustrates PEP 8 compliance, including standard spacing, naming conventions, and modern type hints: +The following example demonstrates PEP 8 spacing, naming, type hinting, and standard library usage (using `pathlib` instead of the older `os` module): ```python -import os +from pathlib import Path + +def list_files(base_path: str) -> None: + """ + Recursively print all file paths in the given directory. + """ + directory = Path(base_path) -def walk_directory(path: str) -> None: - """Traverse the directory and print all file paths.""" - for dirpath, _, filenames in os.walk(path): - for filename in filenames: - file_path = os.path.join(dirpath, filename) + # rglob is the modern, idiomatic way to walk directories + for file_path in directory.rglob('*'): + if file_path.is_file(): print(file_path) if __name__ == "__main__": - walk_directory('/path/to/directory') + list_files('/var/log') ```
## 4. How is memory allocation and garbage collection handled in _Python_? -In Python, **both memory allocation** and **garbage collection** are handled discretely. +In Python, memory management is automated via a **private heap** and a hybrid **garbage collection** mechanism. ### Memory Allocation -- The "heap" is the pool of memory for storing objects. The Python memory manager allocates and deallocates this space as needed. - -- In latest Python versions, the `obmalloc` system is responsible for small object allocations. This system preallocates small and medium-sized memory blocks to manage frequently created small objects. - -- The `allocator` abstracts the system-level memory management, employing memory management libraries like `Glibc` to interact with the operating system. - -- Larger blocks of memory are primarily obtained directly from the operating system. - -- **Stack** and **Heap** separation is joined by "Pool Allocator" for internal use. +* **Private Heap**: The Python Memory Manager ensures all objects and data structures are stored in a private heap, inaccessible to the programmer directly. +* **Pymalloc Strategy**: CPython utilizes a specialized allocator (optimized for objects smaller than 512 bytes) to reduce fragmentation and system calls. It organizes memory hierarchically: + 1. **Arenas**: Large, 256KB chunks mapped from the OS. + 2. **Pools**: 4KB subdivisions within arenas. + 3. **Blocks**: Fixed-size units within pools used to store data. +* **Raw Allocators**: For larger objects, Python bypasses the specialized allocator and interacts directly with the system's standard C allocator (e.g., `malloc`). ### Garbage Collection -Python employs a method called **reference counting** along with a **cycle-detecting garbage collector**. +Python primarily uses **Reference Counting**, supplemented by a **Generational Garbage Collector**. #### Reference Counting -- Every object has a reference count. When an object's count drops to zero, it is immediately deallocated. +* Every internal object carries a reference count (`ob_refcnt`). When this count drops to $0$, the memory is **immediately deallocated**. +* This mechanism is highly efficient for the majority of objects, releasing resources without waiting for a GC cycle. +* **Immortal Objects**: In modern Python (PEP 683), core objects (like `None`, `True`, and small integers) are marked as "Immortal." Their reference counts are not tracked, which improves performance and thread safety. -- This mechanism is swift, often releasing objects instantly without the need for garbage collection. - -- However, it can be insufficient in handling **circular references**. +```python +import sys +x = [] +# Output is typically 2: one for variable 'x', one for the getrefcount argument +print(sys.getrefcount(x)) +``` #### Cycle-Detecting Garbage Collector -- Python has a separate garbage collector that periodically identifies and deals with circular references. - -- This is, however, a more time-consuming process and is invoked less frequently than reference counting. +* Reference counting cannot resolve **circular references** (e.g., List A contains List B, and List B contains List A). +* A separate Cycle-Detecting GC runs periodically to identify these unreachable isolated subgraphs. +* **Generational Hypothesis**: The GC classifies objects into three generations (0, 1, 2). New objects start in Generation 0. If they survive a collection sweep, they are promoted to older generations. Older generations are scanned less frequently, minimizing runtime overhead. ### Memory Management in Python vs. C -Python handles memory management quite differently from languages like C or C++: - -- In Python, the developer isn't directly responsible for memory allocations or deallocations, reducing the likelihood of memory-related bugs. - -- The memory manager in Python is what's known as a **"general-purpose memory manager"** that can be slower than the dedicated memory managers of C or C++ in certain contexts. - -- Python, especially due to the existence of a garbage collector, might have memory overhead compared to C or C++ where manual memory management often results in minimal overhead is one of the factors that might contribute to Python's sometimes slower performance. - -- The level of memory efficiency isn't as high as that of C or C++. This is because Python is designed to be convenient and easy to use, often at the expense of some performance optimization. +* **Abstraction**: Python developers are freed from manual `malloc`/`free` calls, eliminating classes of bugs like memory leaks and dangling pointers common in C. +* **Overhead**: Python objects are C structures containing metadata (headers, reference counts), resulting in higher memory usage than equivalent raw C data types. +* **Performance**: While Python's allocator is optimized, the overhead of maintaining reference counts and running the cyclic GC makes it less memory-efficient and generally slower than C's manual management.
## 5. What are the _built-in data types_ in _Python_? -Python offers numerous **built-in data types** that provide varying functionalities and utilities. +Python provides a rich set of **built-in data types** generally categorized by their mutability—whether their value can be changed after creation. ### Immutable Data Types +These objects cannot be modified once created. #### 1. int - Represents a whole number, such as 42 or -10. +Represents arbitrary-precision integers. +```python +x = 42 +``` #### 2. float - Represents a decimal number, like 3.14 or -0.01. +Represents double-precision floating-point numbers. +```python +pi = 3.14159 +``` #### 3. complex - Comprises a real and an imaginary part, like 3 + 4j. +Comprises a real and an imaginary part, denoted with a `j` suffix. +```python +c = 3 + 4j +``` #### 4. bool - Represents a boolean value, True or False. +A subtype of `int` representing boolean truth values: `True` (1) and `False` (0). #### 5. str - A sequence of unicode characters enclosed within quotes. +An immutable sequence of Unicode code points (text). +```python +s = "Python 2026" +``` #### 6. tuple - An ordered collection of items, often heterogeneous, enclosed within parentheses. +An ordered, immutable collection of items, often heterogeneous. +```python +t = (1, "apple", 3.14) +``` #### 7. frozenset - A set of unique, immutable objects, similar to sets, enclosed within curly braces. +An immutable version of a `set`. It is hashable and can be used as a dictionary key. +```python +fs = frozenset([1, 2, 3]) +``` #### 8. bytes - Represents a group of 8-bit bytes, often used with binary data, enclosed within brackets. +An immutable sequence of integers in the range $0 \le x < 256$, used for binary data. +```python +b = b'binary data' +``` -#### 9. bytearray - Resembles the 'bytes' type but allows mutable changes. +#### 9. range +Represents an immutable sequence of numbers, typically used in loops. +```python +r = range(0, 10, 2) +``` #### 10. NoneType - Indicates the absence of a value. +Represents the null value or the absence of a value, accessed via the `None` keyword. ### Mutable Data Types +These objects support in-place modifications. #### 1. list - A versatile ordered collection that can contain different data types and offers dynamic sizing, enclosed within square brackets. +A dynamic, ordered sequence that can contain mixed data types. +```python +nums = [1, 2, 3] +nums.append(4) +``` #### 2. set - Represents a unique set of objects and is characterized by curly braces. +An unordered collection of unique, hashable objects. +```python +unique_ids = {101, 102, 103} +``` #### 3. dict - A versatile key-value paired collection enclosed within braces. - -#### 4. memoryview - Points to the memory used by another object, aiding efficient viewing and manipulation of data. - -#### 5. array - Offers storage for a specified type of data, similar to lists but with dedicated built-in functionalities. - -#### 6. deque - A double-ended queue distinguished by optimized insertion and removal operations from both its ends. - -#### 7. object - The base object from which all classes inherit. - -#### 8. types.SimpleNamespace - Grants the capability to assign attributes to it. +A mapping of hashable **keys** to arbitrary **values**. +```python +user = {'name': 'Alice', 'id': 42} +``` -#### 9. types.ModuleType - Represents a module body containing attributes. +#### 4. bytearray +A mutable sequence of integers in the range $0 \le x < 256$. It allows in-place modification of binary data. +```python +ba = bytearray(b'hello') +ba[0] = 87 # Changes 'h' to 'W' +``` -#### 10. types.FunctionType - Defines a particular kind of function. +#### 5. memoryview +Allows Python code to access the internal data of an object (like `bytes` or `bytearray`) without copying, supporting the buffer protocol. +```python +mv = memoryview(b'abc') +```
## 6. Explain the difference between a _mutable_ and _immutable_ object. -Let's look at the difference between **mutable** and **immutable** objects. - ### Key Distinctions -- **Mutable Objects**: Can be modified after creation. -- **Immutable Objects**: Cannot be modified after creation. +In Python, every variable holds a reference to an object instance. The distinction lies in whether that object's value can change after it is created. + +- **Mutable Objects**: The internal state can be modified in place. The object's memory address (identity) remains the same across modifications. +- **Immutable Objects**: The internal state cannot be changed. Any operation that appears to modify the value actually creates a **new object** with a new memory address. ### Common Examples -- **Mutable**: Lists, Sets, Dictionaries -- **Immutable**: Tuples, Strings, Numbers +- **Mutable**: `list`, `dict`, `set`, `bytearray` +- **Immutable**: `int`, `float`, `complex`, `str`, `tuple`, `frozenset`, `bytes` ### Code Example: Immutability in Python -Here is the Python code: +The following code demonstrates that modifying a **mutable** list keeps the same ID, whereas modifying an **immutable** integer changes the ID (rebinding). It also shows the `TypeError` raised when attempting structural changes on immutable types. ```python -# Immutable objects (int, str, tuple) -num = 42 -text = "Hello, World!" -my_tuple = (1, 2, 3) +# --- Mutable Object (List) --- +my_list = [1, 2] +print(f"Original List ID: {id(my_list)}") -# Trying to modify will raise an error -try: - num += 10 - text[0] = 'M' # This will raise a TypeError - my_tuple[0] = 100 # This will also raise a TypeError -except TypeError as e: - print(f"Error: {e}") +my_list.append(3) # Modifies in-place +print(f"Modified List ID: {id(my_list)}") # ID remains the same -# Mutable objects (list, set, dict) -my_list = [1, 2, 3] -my_dict = {'a': 1, 'b': 2} +# --- Immutable Object (Integer & Tuple) --- +num = 10 +print(f"Original Int ID: {id(num)}") -# Can be modified without issues -my_list.append(4) -del my_dict['a'] +num += 1 # Rebinds variable to a NEW object +print(f"New Int ID: {id(num)}") # ID changes -# Checking the changes -print(my_list) # Output: [1, 2, 3, 4] -print(my_dict) # Output: {'b': 2} +# Structural modification raises TypeError +my_tuple = (1, 2) +try: + my_tuple[0] = 100 +except TypeError as e: + print(f"Error: {e}") # Tuples do not support item assignment ``` ### Benefits & Trade-Offs -**Immutability** offers benefits such as **safety** in concurrent environments and facilitating **predictable behavior**. +**Immutability** ensures **hashability**, meaning immutable objects can serve as dictionary keys or set elements. It also provides inherent **thread safety**, as the state cannot change unexpectedly during concurrent execution. -**Mutability**, on the other hand, often improves **performance** by avoiding copy overhead and redundant computations. +**Mutability** provides better **performance** for large collections. Modifying a large list in place is $O(1)$ or amortized $O(1)$, whereas modifying an immutable sequence (like a string) often requires copying the entire structure, resulting in $O(N)$ complexity. ### Impact on Operations -- **Reading and Writing**: Immutable objects typically favor **reading** over **writing**, promoting a more straightforward and predictable code flow. - -- **Memory and Performance**: Mutability can be more efficient in terms of memory usage and performance, especially concerning large datasets, thanks to in-place updates. +- **Parameter Passing**: Python passes references by assignment. If a function modifies a **mutable** argument (e.g., appending to a list), the change is reflected globally. If it modifies an **immutable** argument, it only changes the local reference, leaving the external variable untouched. -Choosing between the two depends on the program's needs, such as the required data integrity and the trade-offs between predictability and performance. +- **Memory Efficiency**: **Mutable** objects allow in-place updates, reducing memory churn. However, Python caches small **immutable** objects (like small integers and interned strings) to optimize memory usage.
## 7. How do you _handle exceptions_ in _Python_? -**Exception handling** is a fundamental aspect of Python, and it safeguards your code against unexpected errors or conditions. Key components of exception handling in Python include: - -### Components - -- **Try**: The section of code where exceptions might occur is placed within a `try` block. +**Exception handling** is a fundamental mechanism in Python that safeguards code against runtime errors, ensuring robust execution flows. The core syntax relies on the `try`, `except`, `else`, and `finally` blocks. -- **Except**: Any possible exceptions that are `raised` by the `try` block are caught and handled in the `except` block. +### Core Components -- **Finally**: This block ensures a piece of code always executes, regardless of whether an exception occurred. It's commonly used for cleanup operations, such as closing files or database connections. +- **`try`**: The block containing code that might raise an exception. +- **`except`**: catche and handles exceptions raised within the `try` block. +- **`else`**: Executes only if the `try` block completes **without** raising an exception. +- **`finally`**: Executes unconditionally after the `try` (and `except`/`else`) blocks. It is primarily used for cleanup operations like closing connections. -### Generic Exception Handling vs. Handling Specific Exceptions +### Handling Strategies: Specific, Generic, and Groups -It's good practice to **handle** specific exceptions. However, a more **general** approach can also be taken. When doing the latter, ensure the general exception handling is at the end of the chain, as shown here: +Best practice dictates handling **specific** exceptions first. However, a catch-all is sometimes necessary at the end. Since Python 3.11+, handling multiple unrelated exceptions (often from asynchronous tasks) is supported via `ExceptionGroup` and the `except*` syntax. ```python try: risky_operation() -except IndexError: # Handle specific exception types first. - handle_index_error() -except Exception as e: # More general exception must come last. - handle_generic_error() +except (IndexError, KeyError): # Handle specific standard exceptions + handle_lookup_error() +except* ValueError: # Handle part of an ExceptionGroup (Python 3.11+) + handle_async_validation_error() +except Exception as e: # General catch-all (Must be last) + log_generic_error(e) finally: - cleanup() + cleanup_resources() ``` ### Raising Exceptions -Use this mechanism to **trigger and manage** exceptions under specific circumstances. This can be particularly useful when building custom classes or functions where specific conditions should be met. +Use the `raise` keyword to trigger exceptions manually when specific conditions are not met, or to re-propagate caught exceptions. **Raise** a specific exception: - ```python def divide(a, b): if b == 0: raise ZeroDivisionError("Divisor cannot be zero") return a / b - -try: - result = divide(4, 0) -except ZeroDivisionError as e: - print(e) ``` -**Raise a general exception**: - +**Re-raise** to propagate an error up the stack: ```python -def some_risky_operation(): - if condition: - raise Exception("Some generic error occurred") +try: + divide(10, 0) +except ZeroDivisionError: + logger.error("Calculation failed") + raise # Re-raises the active ZeroDivisionError ``` -### Using `with` for Resource Management +### Resource Management with `with` -The `with` keyword provides a more efficient and clean way to handle resources, like files, ensuring their proper closure when operations are complete or in case of any exceptions. The resource should implement a `context manager`, typically by having `__enter__` and `__exit__` methods. - -Here's an example using a file: +The `with` keyword is the standard for managing resources. It utilizes **Context Managers** (objects implementing `__enter__` and `__exit__`) to ensure resources are automatically released, regardless of whether the operation succeeds or fails. ```python with open("example.txt", "r") as file: data = file.read() -# File is automatically closed when the block is exited. +# File is automatically closed here, even if read() raises an error. ``` -### Silence with `pass`, `continue`, or `else` - -There are times when not raising an exception is appropriate. You can use `pass` or `continue` in an exception block when you want to essentially ignore an exception and proceed with the rest of your code. +### Control Flow: `pass`, `continue`, and `else` -- **`pass`**: Simply does nothing. It acts as a placeholder. +Control flow keywords allow you to fine-tune how the program proceeds after handling (or avoiding) an error. +- **`pass`**: Silently ignores the exception. ```python - try: - risky_operation() - except SomeSpecificException: + except IgnorableError: pass ``` - -- **`continue`**: This keyword is generally used in loops. It moves to the next iteration without executing the code that follows it within the block. - +- **`continue`**: Used in loops to skip the rest of the current iteration and proceed to the next one. ```python - for item in my_list: + for item in data: try: - perform_something(item) - except ExceptionType: + process(item) + except InvalidItemError: continue - ``` - -- **`else` with `try-except` blocks**: The `else` block after a `try-except` block will only be executed if no exceptions are raised within the `try` block - - ```python - try: - some_function() - except SpecificException: - handle_specific_exception() - else: - no_exception_raised() ``` +- **`else`**: Placing code in the `else` block prevents the `try` block from becoming too broad. It ensures that exceptions raised by the "success logic" are not caught by the `except` block intended for the "risky logic". -### Callback Function: `ExceptionHook` - -Python 3 introduced the better handling of uncaught exceptions by providing an optional function for printing stack traces. The `sys.excepthook` can be set to match any exception in the module as long as it has a `hook` attribute. +### Global Callback: `sys.excepthook` -Here's an example for this test module: +To handle **uncaught exceptions** globally (e.g., logging fatal errors before the program terminates), you can override `sys.excepthook`. ```python -# test.py import sys -def excepthook(type, value, traceback): - print("Unhandled exception:", type, value) - # Call the default exception hook - sys.__excepthook__(type, value, traceback) +def global_exception_handler(exc_type, value, traceback): + print(f"Unhandled critical error: {value}") + # Call the default hook to ensure standard behavior (printing stack trace) + sys.__excepthook__(exc_type, value, traceback) -sys.excepthook = excepthook - -def test_exception_hook(): - throw_some_exception() +sys.excepthook = global_exception_handler ``` -When run, calling `test_exception_hook` will print "Unhandled exception: ..." - -_Note_: `sys.excepthook` will not capture exceptions raised as the result of interactive prompt commands, such as SyntaxError or KeyboardInterrupt. +*Note*: `sys.excepthook` does not capture `SystemExit` or interactive session errors like `SyntaxError`.
## 8. What is the difference between _list_ and _tuple_? -**Lists** and **Tuples** in Python share many similarities, such as being sequences and supporting indexing. - -However, these data structures differ in key ways: +**Lists** and **Tuples** are both sequence data types in Python that support indexing and slicing. However, they serve different operational purposes. ### Key Distinctions -- **Mutability**: Lists are mutable, allowing you to add, remove, or modify elements after creation. Tuples, once created, are immutable. - -- **Performance**: Lists are generally slower than tuples, most apparent in tasks like iteration and function calls. - -- **Syntax**: Lists are defined with square brackets `[]`, whereas tuples use parentheses `()`. +- **Mutability**: **Lists** are mutable, allowing dynamic modification (addition, removal, or item assignment). **Tuples** are immutable; once created, their structure cannot be changed. +- **Performance**: **Tuples** are more memory-efficient and faster to iterate over than lists due to fixed memory allocation. +- **Syntax**: **Lists** use square brackets `[]`. **Tuples** use parentheses `()` (though the comma `,` is the primary defining operator). ### When to Use Each -- **Lists** are ideal for collections that may change in size and content. They are the preferred choice for storing data elements. - -- **Tuples**, due to their immutability and enhanced performance, are a good choice for representing fixed sets of related data. +- **Lists**: Ideal for **homogeneous** collections of data where the size or content may change (e.g., a stack of files). +- **Tuples**: Ideal for **heterogeneous** data structures representing a single logical record (e.g., coordinates $(x, y)$ or database rows), or when a hashable key is required for a dictionary. ### Syntax -#### List: Example +#### List: Mutable Sequence ```python my_list = ["apple", "banana", "cherry"] -my_list.append("date") -my_list[1] = "blackberry" +my_list.append("date") # Modification allowed +my_list[1] = "blackberry" # Item assignment allowed ``` -#### Tuple: Example +#### Tuple: Immutable Record ```python my_tuple = (1, 2, 3, 4) -# Unpacking a tuple -a, b, c, d = my_tuple +# my_tuple[0] = 5 # Raises TypeError +a, b, *rest = my_tuple # Tuple unpacking ```
## 9. How do you create a _dictionary_ in _Python_? -**Python dictionaries** are versatile data structures, offering key-based access for rapid lookups. Let's explore various data within dictionaries and techniques to create and manipulate them. - -### Key Concepts - -- A **dictionary** in Python contains a collection of `key:value` pairs. -- **Keys** must be unique and are typically immutable, such as strings, numbers, or tuples. -- **Values** can be of any type, and they can be duplicated. - -### Creating a Dictionary +Here is the updated answer regarding creating dictionaries in Python, tailored for technical accuracy in 2026. -You can use several methods to create a dictionary: +**Python dictionaries** are mutable, ordered (since Python 3.7+), and versatile data structures that map unique keys to values for efficient data retrieval. -1. **Literal Definition**: Define key-value pairs within curly braces { }. +### Key Concepts -2. **From Key-Value Pairs**: Use the `dict()` constructor or the `{key: value}` shorthand. +- A **dictionary** stores data in `key: value` pairs. +- **Keys** must be unique and **hashable** (immutable types like strings, numbers, or tuples). +- **Values** can be of any data type and can be duplicated. -3. **Using the `dict()` Constructor**: This can accept another dictionary, a sequence of key-value pairs, or named arguments. +### Creation Methods -4. **Comprehensions**: This is a concise way to create dictionaries using a single line of code. +There are several standard ways to instantiate a dictionary: -5. **`zip()` Function**: This creates a dictionary by zipping two lists, where the first list corresponds to the keys, and the second to the values. +1. **Literal Definition**: The most common method using curly braces `{}`. +2. **`dict()` Constructor**: Creates a dictionary from keyword arguments or an iterable of pairs. +3. **Dictionary Comprehension**: A concise, functional way to construct dictionaries dynamically. +4. **`zip()` Function**: Combines two iterables (keys and values) into a dictionary. +5. **`fromkeys()` Method**: Creates a new dictionary with specified keys and a default value. ### Examples #### Dictionary Literal Definition -Here is a Python code: +The most direct syntax using curly braces. ```python -# Dictionary literal definition +# Literal definition student = { "name": "John Doe", "age": 21, @@ -531,178 +519,204 @@ student = { } ``` -#### From Key-Value Pairs +#### The `dict()` Constructor -Here is the Python code: +You can pass keyword arguments or a list of tuples to the constructor. ```python -# Using the `dict()` constructor -student_dict = dict([ +# Using keyword arguments (keys must be valid identifiers) +student_kw = dict(name="Jane Doe", age=22, courses=["Biology"]) + +# Using a list of tuples (useful for keys that aren't valid identifiers) +student_tuples = dict([ ("name", "John Doe"), ("age", 21), - ("courses", ["Math", "Physics"]) + (101, "Room Number") ]) - -# Using the shorthand syntax -student_dict_short = { - "name": "John Doe", - "age": 21, - "courses": ["Math", "Physics"] -} ``` -#### Using `zip()` +#### Dictionary Comprehensions -Here is a Python code: +Similar to list comprehensions, this syntax generates a dictionary based on an expression. ```python -keys = ["a", "b", "c"] -values = [1, 2, 3] +numbers = [1, 2, 3, 4] -zipped = zip(keys, values) -dict_from_zip = dict(zipped) # Result: {"a": 1, "b": 2, "c": 3} +# Create a dictionary mapping numbers to their squares +squares = {x: x**2 for x in numbers} +# Result: {1: 1, 2: 4, 3: 9, 4: 16} ``` -#### Using `dict()` Constructor +#### Using `zip()` -Here is a Python code: +Useful when keys and values are in separate lists. ```python -# Sequence of key-value pairs -student_dict2 = dict(name="Jane Doe", age=22, courses=["Biology", "Chemistry"]) +keys = ["a", "b", "c"] +values = [1, 2, 3] -# From another dictionary -student_dict_combined = dict(student, **student_dict2) +# Zip combines them into pairs, dict converts them +dict_from_zip = dict(zip(keys, values)) +# Result: {"a": 1, "b": 2, "c": 3} ```
## 10. What is the difference between _==_ and _is operator_ in _Python_? -Both the **`==`** and **`is`** operators in Python are used for comparison, but they function differently. +Both the **`==`** and **`is`** operators in Python are used for comparison, but they distinguish between **value** and **identity**. + +- The **`==`** operator checks for **value equality**. It compares the data content of two objects to see if they are equivalent. +- The **`is`** operator checks for **object identity**. It validates whether two variables point to the exact same instance in memory (checking if their memory addresses are identical). -- The **`==`** operator checks for **value equality**. -- The **`is`** operator, on the other hand, validates **object identity**, +Internally, **`is`** is faster as it compares integer memory addresses (ids), whereas **`==`** generally invokes the object's `__eq__()` method, which can be arbitrarily complex. -In Python, every object is unique, identifiable by its memory address. The **`is`** operator uses this memory address to check if two objects are the same, indicating they both point to the exact same instance in memory. +### Code Illustration -- **`is`**: Compares the memory address or identity of two objects. -- **`==`**: Compares the content or value of two objects. +```python +# Two lists with the same content +list_a = [1, 2, 3] +list_b = [1, 2, 3] +list_c = list_a # Alias referring to the same object as list_a + +# Equality Check (==) +print(list_a == list_b) # True: The contents are the same. -While **`is`** is primarily used for **None** checks, it's generally advisable to use **`==`** for most other comparisons. +# Identity Check (is) +print(list_a is list_b) # False: They are two distinct objects in memory. +print(list_a is list_c) # True: They reference the exact same memory address. +``` -### Tips for Using Operators +### Usage Guidelines -- **`==`**: Use for equality comparisons, like when comparing numeric or string values. -- **`is`**: Use for comparing membership or when dealing with singletons like **None**. +- **`==`**: Use this for standard comparisons (e.g., numbers, strings, lists, custom objects). +- **`is`**: Use this specifically for checking against singletons like **`None`**, **`True`**, or **`False`**.
## 11. How does a _Python function_ work? -**Python functions** are the building blocks of code organization, often serving predefined tasks within modules and scripts. They enable reusability, modularity, and encapsulation. +**Python functions** are the fundamental units of code organization, facilitating reusability, modularity, and encapsulation. In modern Python, they are first-class objects that contain compiled bytecode. ### Key Components -- **Function Signature**: Denoted by the `def` keyword, it includes the function name, parameters, and an optional return type. -- **Function Body**: This section carries the core logic, often comprising conditional checks, loops, and method invocations. -- **Return Statement**: The function's output is determined by this statement. When None is specified, the function returns by default. -- **Local Variables**: These variables are scoped to the function and are only accessible within it. +- **Function Signature**: Defined by `def`, including the name, parameters, and optional **type hints** (standard in 2026). +- **Function Body**: The indented block containing logic, loops, and conditional checks. +- **Return Statement**: Terminates execution and outputs a value. Returns `None` implicitly if omitted. +- **Local Namespace**: A discrete scope for variables defined within the function. -### Execution Process +```python +def process_data(value: int) -> int: # Signature with Type Hints + result = value * 2 # Local variable + return result # Return statement +``` -When a function is called: +### Execution Process -1. **Stack Allocation**: A stack frame, also known as an activation record, is created to manage the function's execution. This frame contains details like the function's parameters, local variables, and **instruction pointer**. - -2. **Parameter Binding**: The arguments passed during the function call are bound to the respective parameters defined in the function header. +When a function is called, the Python interpreter performs the following: -3. **Function Execution**: Control is transferred to the function body. The statements in the body are executed in a sequential manner until the function hits a return statement or the end of the function body. +1. **Frame Allocation**: A **stack frame** (activation record) is created on the call stack. This object manages the function's local variables, arguments, and the **instruction pointer**. +2. **Parameter Binding**: Arguments passed by the caller are bound to the parameter names in the local namespace. +3. **Function Execution**: Control transfers to the function body. The interpreter executes the underlying **bytecode** sequentially. +4. **Return**: Upon encountering `return`, the expression is evaluated, and the value is passed back to the caller. The stack frame is popped and discarded. +5. **Post Execution**: If the end of the body is reached without a return statement, `None` is returned automatically. -4. **Return**: If a return statement is encountered, the function evaluates the expression following the `return` and hands the value back to the caller. The stack frame of the function is then popped from the call stack. +### Local Variable Scope -5. **Post Execution**: If there's no `return` statement, or if the function ends without evaluating any return statement, `None` is implicitly returned. +Python resolves names using the **LEGB** rule (Local, Enclosing, Global, Built-in). -### Local Variable Scope +- **Function Parameters**: Initialized with values passed during invocation, acting as local variables. +- **Local Variables**: Created via assignment inside the function; they exist only during execution. +- **Nested Scopes**: Inner functions (closures) can read variables from enclosing functions. Modification requires the `nonlocal` keyword. -- **Function Parameters**: These are a precursor to local variables and are instantiated with the values passed during function invocation. -- **Local Variables**: Created using an assignment statement inside the function and cease to exist when the function execution ends. -- **Nested Scopes**: In functions within functions (closures), non-local variables - those defined in the enclosing function - are accessible but not modifiable by the inner function, without using the `nonlocal` keyword. +```python +def outer(): + count = 0 + def inner(): + nonlocal count # Accesses enclosing scope + count += 1 + inner() +``` ### Global Visibility -If a variable is not defined within a function, the Python runtime will look for it in the global scope. This behavior enables functions to access and even modify global variables. +If a variable is not found in the local scope, Python looks in the **global scope**. Functions can read global variables, but modifying a global reference requires the `global` keyword. Without it, an assignment creates a new local variable, leaving the global one untouched. ### Avoiding Side Effects -Functions offer a level of encapsulation, potentially reducing side effects by ensuring that data and variables are managed within a controlled environment. Such containment can help enhance the robustness and predictability of a codebase. As a best practice, minimizing the reliance on global variables can lead to more maintainable, reusable, and testable code. +Functions provide encapsulation, ensuring internal variables do not leak into the global scope. Minimizing reliance on global state reduces **side effects**, resulting in "pure functions" that are easier to test, debug, and parallelize.
## 12. What is a _lambda function_, and where would you use it? -A **Lambda function**, or **lambda**, for short, is a small anonymous function defined using the `lambda` keyword in Python. - -While you can certainly use named functions when you need a function for something in Python, there are places where a lambda expression is more suitable. +A **Lambda function** is a small, anonymous function defined using the `lambda` keyword in Python. Unlike functions defined with `def`, lambdas are expressions used to create function objects on the fly. ### Distinctive Features -- **Anonymity**: Lambdas are not given a name in the traditional sense, making them suited for one-off uses in your codebase. -- **Single Expression Body**: Their body is limited to a single expression. This can be an advantage for brevity but a restriction for larger, more complex functions. -- **Implicit Return**: There's no need for an explicit `return` statement. -- **Conciseness**: Lambdas streamline the definition of straightforward functions. +- **Anonymity**: Lambdas are not bound to a specific name in the local namespace. +- **Single Expression Body**: The body is restricted to a single expression. Statements (like `return`, `pass`, `assert`) are not allowed. +- **Implicit Return**: The result of the expression is automatically returned; no explicit `return` keyword is needed. +- **Conciseness**: Designed for short, simple operations where a full function definition would be verbose. ### Common Use Cases -- **Map, Filter, and Reduce**: Functions like `map` can take a lambda as a parameter, allowing you to define simple transformations on the fly. For example, doubling each element of a list can be achieved with `list(map(lambda x: x*2, my_list))`. -- **List Comprehensions**: They are a more Pythonic way of running the same `map` or `filter` operations, often seen as an alternative to lambdas and `map`. -- **Sorting**: Lambdas can serve as a custom key function, offering flexibility in sort orders. -- **Callbacks**: Often used in events where a function is needed to be executed when an action occurs (e.g., button click). -- **Simple Functions**: For functions that are so basic that giving them a name, especially in more procedural code, would be overkill. +- **Key Functions**: The most prevalent use in modern Python is defining the `key` argument for sorting or finding extremes. +- **Functional Constructs**: Used as arguments for higher-order functions like `map()`, `filter()`, and `functools.reduce()`. +- **Callbacks**: effective for delayed execution, such as UI event handlers (e.g., a button click in Tkinter) that require a callable. + +#### Code Example + +```python +students = [{'name': 'Alice', 'grade': 88}, {'name': 'Bob', 'grade': 95}] + +# 1. Using lambda as a key for sorting by grade +sorted_students = sorted(students, key=lambda x: x['grade'], reverse=True) + +# 2. Using lambda with filter to get even numbers +evens = list(filter(lambda x: x % 2 == 0, range(10))) +``` ### Notable Limitations -- **Lack of Verbose Readability**: Named functions are generally preferred when their intended use is obvious from the name. Lambdas can make code harder to understand if they're complex or not used in a recognizable pattern. -- **No Formal Documentation**: While the function's purpose should be apparent from its content, a named function makes it easier to provide direct documentation. Lambdas would need a separate verbal explanation, typically in the code or comments. +- **Readability**: Named functions are preferred for complex logic. Overusing lambdas, especially nested ones, creates "write-only" code. +- **Debugging**: Tracebacks refer to the function simply as ``, making it harder to identify the source of an error compared to a named function. +- **PEP 8 Style**: Assigning a lambda to a variable (e.g., `func = lambda x: x`) is discouraged. Use `def func(x): return x` instead to ensure the function has a useful `__name__` for debugging.
## 13. Explain _*args_ and _**kwargs_ in _Python_. -In Python, `*args` and `**kwargs` are often used to pass a variable number of arguments to a function. - -`*args` collects a variable number of positional arguments into a **tuple**, while `**kwargs` does the same for keyword arguments into a **dictionary**. - -Here are the key features, use-cases, and their respective code examples. +In Python, `*args` and `**kwargs` enable functions to accept a variable number of arguments. -### **\*args**: Variable Number of Positional Arguments +`*args` collects excess positional arguments into a **tuple**, while `**kwargs` gathers excess keyword arguments into a **dictionary**. Note that in a function signature, `*args` must strictly precede `**kwargs`. -- **How it Works**: The name `*args` is a convention. The asterisk (*) tells Python to put any remaining positional arguments it receives into a tuple. +### **\*args**: Variable Positional Arguments -- **Use-Case**: When the number of arguments needed is uncertain. +- **Mechanism**: The single asterisk (`*`) operator packs any remaining positional arguments into a tuple. The name `args` is a convention; the syntax depends only on the asterisk. +- **Use-Case**: When the specific number of positional inputs is unknown or flexible. -#### Code Example: "*args" +#### Code Example: `*args` with Type Hints ```python -def sum_all(*args): - result = 0 - for num in args: - result += num - return result +def sum_all(*args: int) -> int: + # args is treated as a tuple of integers + return sum(args) print(sum_all(1, 2, 3, 4)) # Output: 10 ``` -### **\*\*kwargs**: Variable Number of Keyword Arguments +### **\*\*kwargs**: Variable Keyword Arguments -- **How it Works**: The double asterisk (**) is used to capture keyword arguments and their values into a dictionary. +- **Mechanism**: The double asterisk (`**`) operator captures named arguments that do not match specific parameters and stores them as key-value pairs in a dictionary. +- **Use-Case**: When a function requires flexibility to accept arbitrary configuration options or data. -- **Use-Case**: When a function should accept an arbitrary number of keyword arguments. - -#### Code Example: "**kwargs" +#### Code Example: `**kwargs` with Type Hints ```python -def print_values(**kwargs): +from typing import Any + +def print_values(**kwargs: Any) -> None: + # kwargs is a dictionary for key, value in kwargs.items(): print(f"{key}: {value}") -# Keyword arguments are captured as a dictionary print_values(name="John", age=30, city="New York") # Output: # name: John @@ -713,94 +727,115 @@ print_values(name="John", age=30, city="New York") ## 14. What are _decorators_ in _Python_? -In Python, a **decorator** is a design pattern and a feature that allows you to modify functions and methods dynamically. This is done primarily to keep the code clean, maintainable, and DRY (Don't Repeat Yourself). +In Python, a **decorator** is a design pattern that allows for the dynamic modification of functions, methods, or classes. It enables the extension of behavior without explicitly modifying the source code of the wrapped object, adhering to the **Open/Closed Principle**. -### How Decorators Work +### Core Mechanism -- Decorators wrap a target function, allowing you to execute custom code before and after that function. -- They are typically **higher-order functions** that take a function as an argument and return a new function. -- This paradigm of "functions that modify functions" is often referred to as **metaprogramming**. +- **Higher-Order Functions**: Decorators are functions that accept another function as an argument and return a new function (the wrapper). +- **Closures**: The returned wrapper function usually retains access to the scope of the outer decorator function, allowing it to manipulate the environment or state. +- **Metaprogramming**: This technique treats functions as data, allowing code to inspect and modify other code at runtime. ### Common Use Cases -- **Authorization and Authentication**: Control user access. -- **Logging**: Record function calls and their parameters. -- **Caching**: Store previous function results for quick access. -- **Validation**: Verify input parameters or function output. -- **Task Scheduling**: Execute a function at a specific time or on an event. -- **Counting and Profiling**: Keep track of the number of function calls and their execution time. +- **Cross-Cutting Concerns**: Logging, execution time profiling, and error handling. +- **Access Control**: Authorization and authentication (e.g., checking user roles before execution). +- **Caching**: Storing results of expensive function calls (Memoization). +- **Input Validation**: Enforcing type checks or value constraints on arguments. +- **Built-in Decorators**: Python includes standard decorators like `@staticmethod`, `@classmethod`, `@property`, and `@dataclass`. -### Using Decorators in Code +### Implementation Examples -Here is the Python code: +#### 1. Basic Function Decorator +This example demonstrates a decorator that adds logging before and after the target function runs. ```python from functools import wraps -# 1. Basic Decorator -def my_decorator(func): - @wraps(func) # Ensures the original function's metadata is preserved +def simple_logger(func): + @wraps(func) # Preserves metadata (name, docstring) def wrapper(*args, **kwargs): - print('Something is happening before the function is called.') + print(f"LOG: Starting '{func.__name__}'") result = func(*args, **kwargs) - print('Something is happening after the function is called.') + print(f"LOG: Finished '{func.__name__}'") return result return wrapper -@my_decorator -def say_hello(): - print('Hello!') +@simple_logger +def greet(name): + """Greets the user.""" + print(f"Hello, {name}!") + +greet("Alice") +# Output: +# LOG: Starting 'greet' +# Hello, Alice! +# LOG: Finished 'greet' +``` -say_hello() +#### 2. Decorators Accepting Arguments +To pass arguments to the decorator itself, three levels of nested functions are required. -# 2. Decorators with Arguments -def decorator_with_args(arg1, arg2): - def actual_decorator(func): +```python +def repeat(times): + def decorator_repeat(func): @wraps(func) def wrapper(*args, **kwargs): - print(f'Arguments passed to decorator: {arg1}, {arg2}') - result = func(*args, **kwargs) + for _ in range(times): + result = func(*args, **kwargs) return result return wrapper - return actual_decorator + return decorator_repeat -@decorator_with_args('arg1', 'arg2') -def my_function(): - print('I am decorated!') +@repeat(times=3) +def say_hi(): + print("Hi!") -my_function() +say_hi() +# Output: +# Hi! +# Hi! +# Hi! ``` -### Decorator Syntax in Python +### Syntactic Sugar -The `@decorator` syntax is a convenient shortcut for: +The `@decorator_name` syntax is syntactic sugar for passing the function into the decorator and reassigning the result to the original function name. ```python -def say_hello(): - print('Hello!') -say_hello = my_decorator(say_hello) +# The @ syntax: +@simple_logger +def do_work(): + pass + +# Is equivalent to: +def do_work(): + pass +do_work = simple_logger(do_work) ``` -### Role of **functools.wraps** +### Best Practices: `functools.wraps` -When defining decorators, particularly those that return functions, it is good practice to use `@wraps(func)` from the `functools` module. This ensures that the original function's metadata, such as its name and docstring, is preserved. +When writing custom decorators, always decorate the wrapper function with `@wraps(func)` from the `functools` module. Without this, the decorated function loses its original `__name__`, `__doc__`, and other metadata, which confuses debugging tools and introspection APIs.
## 15. How can you create a _module_ in _Python_? -You can **create** a Python module through one of two methods: +### Creating a Module in Python -- **Define**: Begin with saving a Python file with `.py` extension. This file will automatically function as a module. +A **module** in Python is simply a file containing Python definitions (functions, classes, variables) and statements. The file name becomes the module name, and it must have the `.py` extension. -- **Create a Blank Module**: Start an empty file with no extension. Name the file using the accepted module syntax, e.g., `__init__ `, for it to act as a module. +To **create** a module: +1. **Create a File**: Save a text file with a `.py` extension (e.g., `math_operations.py`). +2. **Define Content**: Write your Python code inside this file. +3. **Import**: Use the `import` command in another script to access the code. -Next, use **import** to access the module and its functionality. +*(Note: To organize multiple modules into a directory structure, you create a **package**, which typically involves adding an `__init__.py` file to the directory.)* ### Code Example: Creating a `math_operations` Module #### Module Definition -Save the below `math_operations.py` file : +Save the code below in a file named `math_operations.py`: ```python def add(x, y): @@ -813,44 +848,50 @@ def multiply(x, y): return x * y def divide(x, y): + if y == 0: + raise ValueError("Cannot divide by zero") return x / y ``` #### Module Usage -You can use `math_operations` module by using import as shown below: +You can use the `math_operations` module in another script (in the same directory) using `import`: ```python import math_operations result = math_operations.add(4, 5) -print(result) +print(result) # Output: 9 result = math_operations.divide(10, 5) -print(result) +print(result) # Output: 2.0 ``` -Even though it is not required in the later **versions of Python**, you can also use statement `from math_operations import *` to import all the members such as functions and classes at once: +You can also import specific members directly into the namespace, or use the wildcard `*` (though `*` is generally discouraged to avoid name collisions): ```python -from math_operations import * # Not recommended generally due to name collisions and readability concerns +from math_operations import add, subtract -result = add(3, 2) -print(result) +# No need to use the module prefix +print(add(3, 2)) ``` -### Best Practice -Before submitting the code, let's make sure to follow the **Best Practice**: +### Best Practices + +Ensure your modules are robust and reusable by following these practices: -- **Avoid Global Variables**: Use a `main()` function. -- **Guard Against Code Execution on Import**: To avoid unintended side effects, use: +* **Guard Against Side Effects**: Code at the top level of a module executes immediately upon import. To prevent test code or scripts from running when the module is imported, wrap execution logic in the `if __name__ == "__main__":` block. ```python if __name__ == "__main__": - main() + # This runs only when executed as 'python math_operations.py' + # It does NOT run when imported + print("Running manual tests...") + print(add(10, 5)) ``` -This makes sure that the block of code following `if __name__ == "__main__":` is only executed when the module is run directly and not when imported as a module in another program. +* **Avoid Global Variables**: Encapsulate state within classes or functions rather than relying on global module variables. +* **Naming**: Module names should be short, all-lowercase, and may use underscores for readability (e.g., `my_module.py`).
From f979c79da3ba5f0e785f2b3cf55a41bf1bee66b9 Mon Sep 17 00:00:00 2001 From: Devinterview-io <76989322+Devinterview-io@users.noreply.github.com> Date: Sun, 25 Jan 2026 17:23:56 -0500 Subject: [PATCH 5/7] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9f3899a..4009067 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Top 100 Python Interview Questions +# 100 Core Python Interview Questions in 2026

From 78aaaf67b9a2fe78f852ce7cef3ee0909cd345ab Mon Sep 17 00:00:00 2001 From: Devinterview-io <76989322+Devinterview-io@users.noreply.github.com> Date: Sun, 25 Jan 2026 23:24:06 -0500 Subject: [PATCH 6/7] Update python interview questions --- README.md | 863 ++++++++++++++++++++++++++---------------------------- 1 file changed, 417 insertions(+), 446 deletions(-) diff --git a/README.md b/README.md index 4009067..14babc9 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# 100 Core Python Interview Questions in 2026 +# Top 100 Python Interview Questions

@@ -13,505 +13,529 @@ ## 1. What are the _key features_ of _Python_? -**Python** is a versatile and popular high-level programming language known for its simplicity, **elegant syntax**, and a vast ecosystem of libraries. Let's look at the key features that make Python stand out. +**Python** is a versatile and popular programming language known for its simplicity, **elegant syntax**, and a vast ecosystem of libraries. Let's look at some of the key features that make Python stand out. ### Key Features of Python #### 1. Interpreted and Interactive -Python executes code **line-by-line** via an interpreter, making it ideal for rapid prototyping and debugging. Modern versions also incorporate Just-In-Time (JIT) compilation for improved performance. + +Python uses an interpreter, allowing developers to run code **line-by-line**, making it ideal for rapid prototyping and debugging. #### 2. Easy to Learn and Read -Python's **clean, readable syntax**, often resembling plain English, reduces the cognitive load for beginners and experienced developers alike by using indentation to define code blocks. + +Python's **clean, readable syntax**, often resembling plain English, reduces the cognitive load for beginners and experienced developers alike. #### 3. Cross-Platform Compatibility -Python is portable, running on various platforms such as Windows, Linux, and macOS without requiring platform-specific modifications. + +Python is versatile, running on various platforms, such as Windows, Linux, and macOS, without requiring platform-specific modifications. #### 4. Modular and Scalable -Developers can organize their code into modular packages and **reusable** functions, promoting better structure and maintainability for large applications. + +Developers can organize their code into modular packages and reusabale functions. #### 5. Rich Library Ecosystem -The Python Package Index (PyPI) hosts **hundreds of thousands** of libraries, providing robust solutions for tasks ranging from web development to artificial intelligence. + +The Python Package Index (PyPI) hosts over 260,000 libraries, providing solutions for tasks ranging from web development to data analytics. #### 6. Exceptionally Versatile -From web applications to scientific computing and automation, Python is a general-purpose language equally proficient in diverse domains. + +From web applications to scientific computing, Python is equally proficient in diverse domains. #### 7. Memory Management -Python automatically allocates and manages memory using a private heap and **garbage collection**, shielding developers from low-level tasks like manual deallocation. + +Python seamlessly allocates and manages memory, shielding developers from low-level tasks, such as memory deallocation. #### 8. Dynamically Typed -Python infers the data type of a variable during execution, easing the **declaration** and manipulation of variables, while supporting optional static type hinting. + +Python infers the data type of a variable during execution, easing the declartion and manipulation of variables. #### 9. Object-Oriented -Python supports object-oriented paradigms where everything is an **object**, offering attributes and methods to encapsulate and manipulate data efficiently. + +Python supports object-oriented paradigms, where everything is an **object**, offering attributes and methods to manipulate data. #### 10. Extensible -With its C-language API, developers can integrate performance-critical tasks and existing C/C++ modules directly with Python code. + +With its C-language API, developers can integrate performance-critical tasks and existing C modules with Python.
## 2. How is _Python_ executed? -**Python** source code undergoes a multi-stage process involving compilation, interpretation, and dynamic optimization before execution. Below are the technical details of this workflow as of 2026. +**Python** source code is processed through various steps before it can be executed. Let's explore the key stages in this process. ### Compilation & Interpretation -Python (specifically the standard **CPython** implementation) utilizes a hybrid execution model: - -- **Bytecode Compilation**: The source code is first compiled into **Bytecode**, a low-level, platform-independent intermediate representation. This bytecode is a sequence of instructions for the Python Virtual Machine (PVM), often cached in `.pyc` files. -- **The PVM Loop**: The PVM is a stack-based interpreter that iterates through bytecode instructions and executes them sequentially. +Python code goes through both **compilation** and **interpretation**. -### The Compilation Pipeline +- **Bytecode Compilation**: High-level Python code is transformed into low-level bytecode by the Python interpreter with the help of a compiler. Bytecode is a set of instructions that Python's virtual machine (PVM) can understand and execute. + +- **On-the-fly Interpretation**: The PVM reads and executes bytecode instructions in a step-by-step manner. + +This dual approach known as "compile and then interpret" is what sets Python (and certain other languages) apart. -The transformation from source code to executable bytecode involves strict steps: +### Bytecode versus Machine Code Execution -1. **Lexical Analysis**: The tokenizer breaks the source text into individual **tokens** (identifiers, keywords, operators). -2. **Parsing**: Tokens are structured into an **Abstract Syntax Tree (AST)**, which validates the grammar and syntax. -3. **Bytecode Generation**: The compiler traverses the AST, generates a Control Flow Graph (CFG), and emits the final bytecode instructions. +While some programming languages compile directly to machine code, Python compiles to bytecode. This bytecode is then executed by the Python virtual machine. This extra step of bytecode execution **can make Python slower** in certain use-cases when compared to languages that compile directly to machine code. -### Adaptive Execution and JIT +The advantage, however, is that bytecode is platform-independent. A Python program can be run on any machine with a compatible PVM, ensuring cross-platform support. -Modern Python (post-3.11) employs **Tiered Execution** to optimize performance dynamically: +### Source Code to Bytecode: Compilation Steps -- **Tier 1: Adaptive Interpretation**: The PVM monitors code as it runs. If specific instructions are executed frequently with consistent types, the interpreter "quickens" them by replacing generic bytecode with **specialized instructions** optimized for those specific types. -- **Tier 2: Just-In-Time (JIT) Compilation**: For highly active code paths ("hot spots"), the JIT compiler (utilizing a copy-and-patch architecture) translates specialized bytecode into native machine code. This bypasses the interpreter loop entirely for those segments, significantly reducing overhead. +1. **Lexical Analysis**: The source code is broken down into tokens, identifying characters and symbols for Python to understand. +2. **Syntax Parsing**: Tokens are structured into a parse tree to establish the code's syntax and grammar. +3. **Semantic Analysis**: Code is analyzed for its meaning and context, ensuring it's logically sound. +4. **Bytecode Generation**: Based on the previous steps, bytecode instructions are created. -### Bytecode versus Machine Code +### Just-In-Time (JIT) Compilation -Unlike languages like C++ that compile directly to hardware-specific **Machine Code**, Python compiles to **Bytecode**. -- **Portability**: Bytecode allows Python programs to run on any device with a PVM, regardless of the underlying hardware. -- **Performance**: While the extra layer of interpretation historically caused latency, the modern **Adaptive Interpreter** and **JIT** bridge the gap by compiling hot code to machine code at runtime. +While Python typically uses a combination of interpretation and compilation, **JIT** boosts efficiency by selectively compiling parts of the program that are frequently used or could benefit from optimization. -### Code Example: Disassembly and Optimization +JIT compiles sections of the program to machine code on-the-fly. This direct machine code generation for frequently executed parts can significantly speed up those segments, blurring the line between traditional interpreters and compilers. -We can inspect the bytecode using the `dis` module. The Python compiler performs **Peephole Optimization** during the compilation phase, such as pre-calculating constant expressions. +### Code Example: Disassembly of Bytecode ```python import dis def example_func(): - # The compiler evaluates 15 * 20 at compile-time (Constant Folding) return 15 * 20 # Disassemble to view bytecode instructions dis.dis(example_func) ``` -**Output**: -The output reveals that the multiplication does not happen at runtime; the result (`300`) is loaded directly. +Disassembling code using Python's `dis` module can reveal the underlying bytecode instructions that the PVM executes. Here's the disassembled output for the above code: ```plaintext - 4 0 LOAD_CONST 1 (300) + 4 0 LOAD_CONST 2 (300) 2 RETURN_VALUE ```
## 3. What is _PEP 8_ and why is it important? -### What is PEP 8? +**PEP 8** is a style guide for Python code that promotes code consistency, readability, and maintainability. It's named after Python Enhancement Proposal (PEP), the mechanism used to propose and standardize changes to the Python language. -**PEP 8** (Python Enhancement Proposal 8) is the standard style guide for writing Python code. Authored by Guido van Rossum, Barry Warsaw, and Nick Coghlan, it provides guidelines to ensure that Python code is formatted consistently across the entire ecosystem. +PEP 8 is not a set-in-stone rule book, but it provides general guidelines that help developers across the Python community write code that's visually consistent and thus easier to understand. -### Why It Is Important +### Key Design Principles -- **Readability**: Code is read much more often than it is written. PEP 8 ensures that code written by one developer appears familiar to another, reducing the cognitive load required to understand the logic. -- **Consistency**: Adhering to a uniform style makes codebases easier to maintain and review. -- **Tooling Standard**: Modern linters (e.g., `ruff`, `flake8`) and auto-formatters (e.g., `black`) rely on PEP 8 definitions to automate code quality checks. +PEP 8 emphasizes: + +- **Readability**: Code should be easy to read and understand, even by someone who didn't write it. +- **Consistency**: Codebase should adhere to a predictable style so there's little cognitive load in reading or making changes. +- **One Way to Do It**: Instead of offering multiple ways to write the same construct, PEP 8 advocates for a single, idiomatic style. ### Base Rules -- **Indentation**: Use **4 spaces** per indentation level. Do not use tabs. -- **Line Length**: The official guideline is **79 characters** to facilitate side-by-side editing, though many modern teams adjust this limit (e.g., to 88 or 100 characters). -- **Blank Lines**: Surround top-level function and class definitions with two blank lines. Method definitions inside a class are separated by a single blank line. +- **Indentation**: Use 4 spaces for each level of logical indentation. +- **Line Length**: Keep lines of code limited to 79 characters. This number is a guideline; longer lines are acceptable in certain contexts. +- **Blank Lines**: Use them to separate logical sections but not excessively. ### Naming Styles -- **Class Names**: Use `CapWords` (also known as PascalCase). -- **Functions & Variables**: Use `snake_case` (lowercase with underscores). -- **Constants**: Use `UPPER_CASE_WITH_UNDERSCORES`. -- **Non-public Attributes**: Use a leading underscore (e.g., `_internal_var`) to indicate internal use. +- **Class Names**: Prefer `CamelCase`. +- **Function and Variable Names**: Use `lowercase_with_underscores`. +- **Module Names**: Keep them short and in `lowercase`. -### Documentation & Whitespace +### Documentation -- **Docstrings**: Use triple double quotes (`"""`) for documentation strings. -- **Whitespace**: Avoid extraneous whitespace immediately inside parentheses, brackets, or braces. Always surround binary operators (like assignment `=` or addition `+`) with a single space. -- **Imports**: Place imports at the top of the file, on separate lines, grouped in order: standard library, third-party, and local application. +- Use triple quotes for documentation strings. +- Comments should be on their own line and explain the reason for the following code block. -### Example: Modern Compliant Code +### Whitespace Usage -The following example demonstrates PEP 8 spacing, naming, type hinting, and standard library usage (using `pathlib` instead of the older `os` module): +- **Operators**: Surround them with a single space. +- **Commas**: Follow them with a space. -```python -from pathlib import Path +### Example: Directory Walker -def list_files(base_path: str) -> None: - """ - Recursively print all file paths in the given directory. - """ - directory = Path(base_path) +Here is the `PEP8` compliant code: - # rglob is the modern, idiomatic way to walk directories - for file_path in directory.rglob('*'): - if file_path.is_file(): +```python +import os + +def walk_directory(path): + for dirpath, dirnames, filenames in os.walk(path): + for filename in filenames: + file_path = os.path.join(dirpath, filename) print(file_path) -if __name__ == "__main__": - list_files('/var/log') +walk_directory('/path/to/directory') ```
## 4. How is memory allocation and garbage collection handled in _Python_? -In Python, memory management is automated via a **private heap** and a hybrid **garbage collection** mechanism. +In Python, **both memory allocation** and **garbage collection** are handled discretely. ### Memory Allocation -* **Private Heap**: The Python Memory Manager ensures all objects and data structures are stored in a private heap, inaccessible to the programmer directly. -* **Pymalloc Strategy**: CPython utilizes a specialized allocator (optimized for objects smaller than 512 bytes) to reduce fragmentation and system calls. It organizes memory hierarchically: - 1. **Arenas**: Large, 256KB chunks mapped from the OS. - 2. **Pools**: 4KB subdivisions within arenas. - 3. **Blocks**: Fixed-size units within pools used to store data. -* **Raw Allocators**: For larger objects, Python bypasses the specialized allocator and interacts directly with the system's standard C allocator (e.g., `malloc`). +- The "heap" is the pool of memory for storing objects. The Python memory manager allocates and deallocates this space as needed. + +- In latest Python versions, the `obmalloc` system is responsible for small object allocations. This system preallocates small and medium-sized memory blocks to manage frequently created small objects. + +- The `allocator` abstracts the system-level memory management, employing memory management libraries like `Glibc` to interact with the operating system. + +- Larger blocks of memory are primarily obtained directly from the operating system. + +- **Stack** and **Heap** separation is joined by "Pool Allocator" for internal use. ### Garbage Collection -Python primarily uses **Reference Counting**, supplemented by a **Generational Garbage Collector**. +Python employs a method called **reference counting** along with a **cycle-detecting garbage collector**. #### Reference Counting -* Every internal object carries a reference count (`ob_refcnt`). When this count drops to $0$, the memory is **immediately deallocated**. -* This mechanism is highly efficient for the majority of objects, releasing resources without waiting for a GC cycle. -* **Immortal Objects**: In modern Python (PEP 683), core objects (like `None`, `True`, and small integers) are marked as "Immortal." Their reference counts are not tracked, which improves performance and thread safety. +- Every object has a reference count. When an object's count drops to zero, it is immediately deallocated. -```python -import sys -x = [] -# Output is typically 2: one for variable 'x', one for the getrefcount argument -print(sys.getrefcount(x)) -``` +- This mechanism is swift, often releasing objects instantly without the need for garbage collection. + +- However, it can be insufficient in handling **circular references**. #### Cycle-Detecting Garbage Collector -* Reference counting cannot resolve **circular references** (e.g., List A contains List B, and List B contains List A). -* A separate Cycle-Detecting GC runs periodically to identify these unreachable isolated subgraphs. -* **Generational Hypothesis**: The GC classifies objects into three generations (0, 1, 2). New objects start in Generation 0. If they survive a collection sweep, they are promoted to older generations. Older generations are scanned less frequently, minimizing runtime overhead. +- Python has a separate garbage collector that periodically identifies and deals with circular references. + +- This is, however, a more time-consuming process and is invoked less frequently than reference counting. ### Memory Management in Python vs. C -* **Abstraction**: Python developers are freed from manual `malloc`/`free` calls, eliminating classes of bugs like memory leaks and dangling pointers common in C. -* **Overhead**: Python objects are C structures containing metadata (headers, reference counts), resulting in higher memory usage than equivalent raw C data types. -* **Performance**: While Python's allocator is optimized, the overhead of maintaining reference counts and running the cyclic GC makes it less memory-efficient and generally slower than C's manual management. +Python handles memory management quite differently from languages like C or C++: + +- In Python, the developer isn't directly responsible for memory allocations or deallocations, reducing the likelihood of memory-related bugs. + +- The memory manager in Python is what's known as a **"general-purpose memory manager"** that can be slower than the dedicated memory managers of C or C++ in certain contexts. + +- Python, especially due to the existence of a garbage collector, might have memory overhead compared to C or C++ where manual memory management often results in minimal overhead is one of the factors that might contribute to Python's sometimes slower performance. + +- The level of memory efficiency isn't as high as that of C or C++. This is because Python is designed to be convenient and easy to use, often at the expense of some performance optimization.
## 5. What are the _built-in data types_ in _Python_? -Python provides a rich set of **built-in data types** generally categorized by their mutability—whether their value can be changed after creation. +Python offers numerous **built-in data types** that provide varying functionalities and utilities. ### Immutable Data Types -These objects cannot be modified once created. #### 1. int -Represents arbitrary-precision integers. -```python -x = 42 -``` + Represents a whole number, such as 42 or -10. #### 2. float -Represents double-precision floating-point numbers. -```python -pi = 3.14159 -``` + Represents a decimal number, like 3.14 or -0.01. #### 3. complex -Comprises a real and an imaginary part, denoted with a `j` suffix. -```python -c = 3 + 4j -``` + Comprises a real and an imaginary part, like 3 + 4j. #### 4. bool -A subtype of `int` representing boolean truth values: `True` (1) and `False` (0). + Represents a boolean value, True or False. #### 5. str -An immutable sequence of Unicode code points (text). -```python -s = "Python 2026" -``` + A sequence of unicode characters enclosed within quotes. #### 6. tuple -An ordered, immutable collection of items, often heterogeneous. -```python -t = (1, "apple", 3.14) -``` + An ordered collection of items, often heterogeneous, enclosed within parentheses. #### 7. frozenset -An immutable version of a `set`. It is hashable and can be used as a dictionary key. -```python -fs = frozenset([1, 2, 3]) -``` + A set of unique, immutable objects, similar to sets, enclosed within curly braces. #### 8. bytes -An immutable sequence of integers in the range $0 \le x < 256$, used for binary data. -```python -b = b'binary data' -``` + Represents a group of 8-bit bytes, often used with binary data, enclosed within brackets. -#### 9. range -Represents an immutable sequence of numbers, typically used in loops. -```python -r = range(0, 10, 2) -``` +#### 9. bytearray + Resembles the 'bytes' type but allows mutable changes. #### 10. NoneType -Represents the null value or the absence of a value, accessed via the `None` keyword. + Indicates the absence of a value. ### Mutable Data Types -These objects support in-place modifications. #### 1. list -A dynamic, ordered sequence that can contain mixed data types. -```python -nums = [1, 2, 3] -nums.append(4) -``` + A versatile ordered collection that can contain different data types and offers dynamic sizing, enclosed within square brackets. #### 2. set -An unordered collection of unique, hashable objects. -```python -unique_ids = {101, 102, 103} -``` + Represents a unique set of objects and is characterized by curly braces. #### 3. dict -A mapping of hashable **keys** to arbitrary **values**. -```python -user = {'name': 'Alice', 'id': 42} -``` + A versatile key-value paired collection enclosed within braces. -#### 4. bytearray -A mutable sequence of integers in the range $0 \le x < 256$. It allows in-place modification of binary data. -```python -ba = bytearray(b'hello') -ba[0] = 87 # Changes 'h' to 'W' -``` +#### 4. memoryview + Points to the memory used by another object, aiding efficient viewing and manipulation of data. -#### 5. memoryview -Allows Python code to access the internal data of an object (like `bytes` or `bytearray`) without copying, supporting the buffer protocol. -```python -mv = memoryview(b'abc') -``` +#### 5. array + Offers storage for a specified type of data, similar to lists but with dedicated built-in functionalities. + +#### 6. deque + A double-ended queue distinguished by optimized insertion and removal operations from both its ends. + +#### 7. object + The base object from which all classes inherit. + +#### 8. types.SimpleNamespace + Grants the capability to assign attributes to it. + +#### 9. types.ModuleType + Represents a module body containing attributes. + +#### 10. types.FunctionType + Defines a particular kind of function.
## 6. Explain the difference between a _mutable_ and _immutable_ object. -### Key Distinctions +Let's look at the difference between **mutable** and **immutable** objects. -In Python, every variable holds a reference to an object instance. The distinction lies in whether that object's value can change after it is created. +### Key Distinctions -- **Mutable Objects**: The internal state can be modified in place. The object's memory address (identity) remains the same across modifications. -- **Immutable Objects**: The internal state cannot be changed. Any operation that appears to modify the value actually creates a **new object** with a new memory address. +- **Mutable Objects**: Can be modified after creation. +- **Immutable Objects**: Cannot be modified after creation. ### Common Examples -- **Mutable**: `list`, `dict`, `set`, `bytearray` -- **Immutable**: `int`, `float`, `complex`, `str`, `tuple`, `frozenset`, `bytes` +- **Mutable**: Lists, Sets, Dictionaries +- **Immutable**: Tuples, Strings, Numbers ### Code Example: Immutability in Python -The following code demonstrates that modifying a **mutable** list keeps the same ID, whereas modifying an **immutable** integer changes the ID (rebinding). It also shows the `TypeError` raised when attempting structural changes on immutable types. +Here is the Python code: ```python -# --- Mutable Object (List) --- -my_list = [1, 2] -print(f"Original List ID: {id(my_list)}") +# Immutable objects (int, str, tuple) +num = 42 +text = "Hello, World!" +my_tuple = (1, 2, 3) -my_list.append(3) # Modifies in-place -print(f"Modified List ID: {id(my_list)}") # ID remains the same +# Trying to modify will raise an error +try: + num += 10 + text[0] = 'M' # This will raise a TypeError + my_tuple[0] = 100 # This will also raise a TypeError +except TypeError as e: + print(f"Error: {e}") -# --- Immutable Object (Integer & Tuple) --- -num = 10 -print(f"Original Int ID: {id(num)}") +# Mutable objects (list, set, dict) +my_list = [1, 2, 3] +my_dict = {'a': 1, 'b': 2} -num += 1 # Rebinds variable to a NEW object -print(f"New Int ID: {id(num)}") # ID changes +# Can be modified without issues +my_list.append(4) +del my_dict['a'] -# Structural modification raises TypeError -my_tuple = (1, 2) -try: - my_tuple[0] = 100 -except TypeError as e: - print(f"Error: {e}") # Tuples do not support item assignment +# Checking the changes +print(my_list) # Output: [1, 2, 3, 4] +print(my_dict) # Output: {'b': 2} ``` ### Benefits & Trade-Offs -**Immutability** ensures **hashability**, meaning immutable objects can serve as dictionary keys or set elements. It also provides inherent **thread safety**, as the state cannot change unexpectedly during concurrent execution. +**Immutability** offers benefits such as **safety** in concurrent environments and facilitating **predictable behavior**. -**Mutability** provides better **performance** for large collections. Modifying a large list in place is $O(1)$ or amortized $O(1)$, whereas modifying an immutable sequence (like a string) often requires copying the entire structure, resulting in $O(N)$ complexity. +**Mutability**, on the other hand, often improves **performance** by avoiding copy overhead and redundant computations. ### Impact on Operations -- **Parameter Passing**: Python passes references by assignment. If a function modifies a **mutable** argument (e.g., appending to a list), the change is reflected globally. If it modifies an **immutable** argument, it only changes the local reference, leaving the external variable untouched. +- **Reading and Writing**: Immutable objects typically favor **reading** over **writing**, promoting a more straightforward and predictable code flow. + +- **Memory and Performance**: Mutability can be more efficient in terms of memory usage and performance, especially concerning large datasets, thanks to in-place updates. -- **Memory Efficiency**: **Mutable** objects allow in-place updates, reducing memory churn. However, Python caches small **immutable** objects (like small integers and interned strings) to optimize memory usage. +Choosing between the two depends on the program's needs, such as the required data integrity and the trade-offs between predictability and performance.
## 7. How do you _handle exceptions_ in _Python_? -**Exception handling** is a fundamental mechanism in Python that safeguards code against runtime errors, ensuring robust execution flows. The core syntax relies on the `try`, `except`, `else`, and `finally` blocks. +**Exception handling** is a fundamental aspect of Python, and it safeguards your code against unexpected errors or conditions. Key components of exception handling in Python include: -### Core Components +### Components -- **`try`**: The block containing code that might raise an exception. -- **`except`**: catche and handles exceptions raised within the `try` block. -- **`else`**: Executes only if the `try` block completes **without** raising an exception. -- **`finally`**: Executes unconditionally after the `try` (and `except`/`else`) blocks. It is primarily used for cleanup operations like closing connections. +- **Try**: The section of code where exceptions might occur is placed within a `try` block. -### Handling Strategies: Specific, Generic, and Groups +- **Except**: Any possible exceptions that are `raised` by the `try` block are caught and handled in the `except` block. -Best practice dictates handling **specific** exceptions first. However, a catch-all is sometimes necessary at the end. Since Python 3.11+, handling multiple unrelated exceptions (often from asynchronous tasks) is supported via `ExceptionGroup` and the `except*` syntax. +- **Finally**: This block ensures a piece of code always executes, regardless of whether an exception occurred. It's commonly used for cleanup operations, such as closing files or database connections. + +### Generic Exception Handling vs. Handling Specific Exceptions + +It's good practice to **handle** specific exceptions. However, a more **general** approach can also be taken. When doing the latter, ensure the general exception handling is at the end of the chain, as shown here: ```python try: risky_operation() -except (IndexError, KeyError): # Handle specific standard exceptions - handle_lookup_error() -except* ValueError: # Handle part of an ExceptionGroup (Python 3.11+) - handle_async_validation_error() -except Exception as e: # General catch-all (Must be last) - log_generic_error(e) +except IndexError: # Handle specific exception types first. + handle_index_error() +except Exception as e: # More general exception must come last. + handle_generic_error() finally: - cleanup_resources() + cleanup() ``` ### Raising Exceptions -Use the `raise` keyword to trigger exceptions manually when specific conditions are not met, or to re-propagate caught exceptions. +Use this mechanism to **trigger and manage** exceptions under specific circumstances. This can be particularly useful when building custom classes or functions where specific conditions should be met. **Raise** a specific exception: + ```python def divide(a, b): if b == 0: raise ZeroDivisionError("Divisor cannot be zero") return a / b + +try: + result = divide(4, 0) +except ZeroDivisionError as e: + print(e) ``` -**Re-raise** to propagate an error up the stack: +**Raise a general exception**: + ```python -try: - divide(10, 0) -except ZeroDivisionError: - logger.error("Calculation failed") - raise # Re-raises the active ZeroDivisionError +def some_risky_operation(): + if condition: + raise Exception("Some generic error occurred") ``` -### Resource Management with `with` +### Using `with` for Resource Management -The `with` keyword is the standard for managing resources. It utilizes **Context Managers** (objects implementing `__enter__` and `__exit__`) to ensure resources are automatically released, regardless of whether the operation succeeds or fails. +The `with` keyword provides a more efficient and clean way to handle resources, like files, ensuring their proper closure when operations are complete or in case of any exceptions. The resource should implement a `context manager`, typically by having `__enter__` and `__exit__` methods. + +Here's an example using a file: ```python with open("example.txt", "r") as file: data = file.read() -# File is automatically closed here, even if read() raises an error. +# File is automatically closed when the block is exited. ``` -### Control Flow: `pass`, `continue`, and `else` +### Silence with `pass`, `continue`, or `else` + +There are times when not raising an exception is appropriate. You can use `pass` or `continue` in an exception block when you want to essentially ignore an exception and proceed with the rest of your code. -Control flow keywords allow you to fine-tune how the program proceeds after handling (or avoiding) an error. +- **`pass`**: Simply does nothing. It acts as a placeholder. -- **`pass`**: Silently ignores the exception. ```python - except IgnorableError: + try: + risky_operation() + except SomeSpecificException: pass ``` -- **`continue`**: Used in loops to skip the rest of the current iteration and proceed to the next one. + +- **`continue`**: This keyword is generally used in loops. It moves to the next iteration without executing the code that follows it within the block. + ```python - for item in data: + for item in my_list: try: - process(item) - except InvalidItemError: + perform_something(item) + except ExceptionType: continue + ``` + +- **`else` with `try-except` blocks**: The `else` block after a `try-except` block will only be executed if no exceptions are raised within the `try` block + + ```python + try: + some_function() + except SpecificException: + handle_specific_exception() + else: + no_exception_raised() ``` -- **`else`**: Placing code in the `else` block prevents the `try` block from becoming too broad. It ensures that exceptions raised by the "success logic" are not caught by the `except` block intended for the "risky logic". -### Global Callback: `sys.excepthook` +### Callback Function: `ExceptionHook` + +Python 3 introduced the better handling of uncaught exceptions by providing an optional function for printing stack traces. The `sys.excepthook` can be set to match any exception in the module as long as it has a `hook` attribute. -To handle **uncaught exceptions** globally (e.g., logging fatal errors before the program terminates), you can override `sys.excepthook`. +Here's an example for this test module: ```python +# test.py import sys -def global_exception_handler(exc_type, value, traceback): - print(f"Unhandled critical error: {value}") - # Call the default hook to ensure standard behavior (printing stack trace) - sys.__excepthook__(exc_type, value, traceback) +def excepthook(type, value, traceback): + print("Unhandled exception:", type, value) + # Call the default exception hook + sys.__excepthook__(type, value, traceback) -sys.excepthook = global_exception_handler +sys.excepthook = excepthook + +def test_exception_hook(): + throw_some_exception() ``` -*Note*: `sys.excepthook` does not capture `SystemExit` or interactive session errors like `SyntaxError`. +When run, calling `test_exception_hook` will print "Unhandled exception: ..." + +_Note_: `sys.excepthook` will not capture exceptions raised as the result of interactive prompt commands, such as SyntaxError or KeyboardInterrupt.
## 8. What is the difference between _list_ and _tuple_? -**Lists** and **Tuples** are both sequence data types in Python that support indexing and slicing. However, they serve different operational purposes. +**Lists** and **Tuples** in Python share many similarities, such as being sequences and supporting indexing. + +However, these data structures differ in key ways: ### Key Distinctions -- **Mutability**: **Lists** are mutable, allowing dynamic modification (addition, removal, or item assignment). **Tuples** are immutable; once created, their structure cannot be changed. -- **Performance**: **Tuples** are more memory-efficient and faster to iterate over than lists due to fixed memory allocation. -- **Syntax**: **Lists** use square brackets `[]`. **Tuples** use parentheses `()` (though the comma `,` is the primary defining operator). +- **Mutability**: Lists are mutable, allowing you to add, remove, or modify elements after creation. Tuples, once created, are immutable. + +- **Performance**: Lists are generally slower than tuples, most apparent in tasks like iteration and function calls. + +- **Syntax**: Lists are defined with square brackets `[]`, whereas tuples use parentheses `()`. ### When to Use Each -- **Lists**: Ideal for **homogeneous** collections of data where the size or content may change (e.g., a stack of files). -- **Tuples**: Ideal for **heterogeneous** data structures representing a single logical record (e.g., coordinates $(x, y)$ or database rows), or when a hashable key is required for a dictionary. +- **Lists** are ideal for collections that may change in size and content. They are the preferred choice for storing data elements. + +- **Tuples**, due to their immutability and enhanced performance, are a good choice for representing fixed sets of related data. ### Syntax -#### List: Mutable Sequence +#### List: Example ```python my_list = ["apple", "banana", "cherry"] -my_list.append("date") # Modification allowed -my_list[1] = "blackberry" # Item assignment allowed +my_list.append("date") +my_list[1] = "blackberry" ``` -#### Tuple: Immutable Record +#### Tuple: Example ```python my_tuple = (1, 2, 3, 4) -# my_tuple[0] = 5 # Raises TypeError -a, b, *rest = my_tuple # Tuple unpacking +# Unpacking a tuple +a, b, c, d = my_tuple ```
## 9. How do you create a _dictionary_ in _Python_? -Here is the updated answer regarding creating dictionaries in Python, tailored for technical accuracy in 2026. - -**Python dictionaries** are mutable, ordered (since Python 3.7+), and versatile data structures that map unique keys to values for efficient data retrieval. +**Python dictionaries** are versatile data structures, offering key-based access for rapid lookups. Let's explore various data within dictionaries and techniques to create and manipulate them. ### Key Concepts -- A **dictionary** stores data in `key: value` pairs. -- **Keys** must be unique and **hashable** (immutable types like strings, numbers, or tuples). -- **Values** can be of any data type and can be duplicated. +- A **dictionary** in Python contains a collection of `key:value` pairs. +- **Keys** must be unique and are typically immutable, such as strings, numbers, or tuples. +- **Values** can be of any type, and they can be duplicated. + +### Creating a Dictionary + +You can use several methods to create a dictionary: + +1. **Literal Definition**: Define key-value pairs within curly braces { }. + +2. **From Key-Value Pairs**: Use the `dict()` constructor or the `{key: value}` shorthand. -### Creation Methods +3. **Using the `dict()` Constructor**: This can accept another dictionary, a sequence of key-value pairs, or named arguments. -There are several standard ways to instantiate a dictionary: +4. **Comprehensions**: This is a concise way to create dictionaries using a single line of code. -1. **Literal Definition**: The most common method using curly braces `{}`. -2. **`dict()` Constructor**: Creates a dictionary from keyword arguments or an iterable of pairs. -3. **Dictionary Comprehension**: A concise, functional way to construct dictionaries dynamically. -4. **`zip()` Function**: Combines two iterables (keys and values) into a dictionary. -5. **`fromkeys()` Method**: Creates a new dictionary with specified keys and a default value. +5. **`zip()` Function**: This creates a dictionary by zipping two lists, where the first list corresponds to the keys, and the second to the values. ### Examples #### Dictionary Literal Definition -The most direct syntax using curly braces. +Here is a Python code: ```python -# Literal definition +# Dictionary literal definition student = { "name": "John Doe", "age": 21, @@ -519,204 +543,178 @@ student = { } ``` -#### The `dict()` Constructor +#### From Key-Value Pairs -You can pass keyword arguments or a list of tuples to the constructor. +Here is the Python code: ```python -# Using keyword arguments (keys must be valid identifiers) -student_kw = dict(name="Jane Doe", age=22, courses=["Biology"]) - -# Using a list of tuples (useful for keys that aren't valid identifiers) -student_tuples = dict([ +# Using the `dict()` constructor +student_dict = dict([ ("name", "John Doe"), ("age", 21), - (101, "Room Number") + ("courses", ["Math", "Physics"]) ]) + +# Using the shorthand syntax +student_dict_short = { + "name": "John Doe", + "age": 21, + "courses": ["Math", "Physics"] +} ``` -#### Dictionary Comprehensions +#### Using `zip()` -Similar to list comprehensions, this syntax generates a dictionary based on an expression. +Here is a Python code: ```python -numbers = [1, 2, 3, 4] +keys = ["a", "b", "c"] +values = [1, 2, 3] -# Create a dictionary mapping numbers to their squares -squares = {x: x**2 for x in numbers} -# Result: {1: 1, 2: 4, 3: 9, 4: 16} +zipped = zip(keys, values) +dict_from_zip = dict(zipped) # Result: {"a": 1, "b": 2, "c": 3} ``` -#### Using `zip()` +#### Using `dict()` Constructor -Useful when keys and values are in separate lists. +Here is a Python code: ```python -keys = ["a", "b", "c"] -values = [1, 2, 3] +# Sequence of key-value pairs +student_dict2 = dict(name="Jane Doe", age=22, courses=["Biology", "Chemistry"]) -# Zip combines them into pairs, dict converts them -dict_from_zip = dict(zip(keys, values)) -# Result: {"a": 1, "b": 2, "c": 3} +# From another dictionary +student_dict_combined = dict(student, **student_dict2) ```
## 10. What is the difference between _==_ and _is operator_ in _Python_? -Both the **`==`** and **`is`** operators in Python are used for comparison, but they distinguish between **value** and **identity**. +Both the **`==`** and **`is`** operators in Python are used for comparison, but they function differently. -- The **`==`** operator checks for **value equality**. It compares the data content of two objects to see if they are equivalent. -- The **`is`** operator checks for **object identity**. It validates whether two variables point to the exact same instance in memory (checking if their memory addresses are identical). +- The **`==`** operator checks for **value equality**. +- The **`is`** operator, on the other hand, validates **object identity**, -Internally, **`is`** is faster as it compares integer memory addresses (ids), whereas **`==`** generally invokes the object's `__eq__()` method, which can be arbitrarily complex. +In Python, every object is unique, identifiable by its memory address. The **`is`** operator uses this memory address to check if two objects are the same, indicating they both point to the exact same instance in memory. -### Code Illustration +- **`is`**: Compares the memory address or identity of two objects. +- **`==`**: Compares the content or value of two objects. -```python -# Two lists with the same content -list_a = [1, 2, 3] -list_b = [1, 2, 3] -list_c = list_a # Alias referring to the same object as list_a +While **`is`** is primarily used for **None** checks, it's generally advisable to use **`==`** for most other comparisons. -# Equality Check (==) -print(list_a == list_b) # True: The contents are the same. +### Tips for Using Operators -# Identity Check (is) -print(list_a is list_b) # False: They are two distinct objects in memory. -print(list_a is list_c) # True: They reference the exact same memory address. -``` - -### Usage Guidelines - -- **`==`**: Use this for standard comparisons (e.g., numbers, strings, lists, custom objects). -- **`is`**: Use this specifically for checking against singletons like **`None`**, **`True`**, or **`False`**. +- **`==`**: Use for equality comparisons, like when comparing numeric or string values. +- **`is`**: Use for comparing membership or when dealing with singletons like **None**.
## 11. How does a _Python function_ work? -**Python functions** are the fundamental units of code organization, facilitating reusability, modularity, and encapsulation. In modern Python, they are first-class objects that contain compiled bytecode. +**Python functions** are the building blocks of code organization, often serving predefined tasks within modules and scripts. They enable reusability, modularity, and encapsulation. ### Key Components -- **Function Signature**: Defined by `def`, including the name, parameters, and optional **type hints** (standard in 2026). -- **Function Body**: The indented block containing logic, loops, and conditional checks. -- **Return Statement**: Terminates execution and outputs a value. Returns `None` implicitly if omitted. -- **Local Namespace**: A discrete scope for variables defined within the function. - -```python -def process_data(value: int) -> int: # Signature with Type Hints - result = value * 2 # Local variable - return result # Return statement -``` +- **Function Signature**: Denoted by the `def` keyword, it includes the function name, parameters, and an optional return type. +- **Function Body**: This section carries the core logic, often comprising conditional checks, loops, and method invocations. +- **Return Statement**: The function's output is determined by this statement. When None is specified, the function returns by default. +- **Local Variables**: These variables are scoped to the function and are only accessible within it. ### Execution Process -When a function is called, the Python interpreter performs the following: +When a function is called: -1. **Frame Allocation**: A **stack frame** (activation record) is created on the call stack. This object manages the function's local variables, arguments, and the **instruction pointer**. -2. **Parameter Binding**: Arguments passed by the caller are bound to the parameter names in the local namespace. -3. **Function Execution**: Control transfers to the function body. The interpreter executes the underlying **bytecode** sequentially. -4. **Return**: Upon encountering `return`, the expression is evaluated, and the value is passed back to the caller. The stack frame is popped and discarded. -5. **Post Execution**: If the end of the body is reached without a return statement, `None` is returned automatically. +1. **Stack Allocation**: A stack frame, also known as an activation record, is created to manage the function's execution. This frame contains details like the function's parameters, local variables, and **instruction pointer**. + +2. **Parameter Binding**: The arguments passed during the function call are bound to the respective parameters defined in the function header. -### Local Variable Scope +3. **Function Execution**: Control is transferred to the function body. The statements in the body are executed in a sequential manner until the function hits a return statement or the end of the function body. -Python resolves names using the **LEGB** rule (Local, Enclosing, Global, Built-in). +4. **Return**: If a return statement is encountered, the function evaluates the expression following the `return` and hands the value back to the caller. The stack frame of the function is then popped from the call stack. -- **Function Parameters**: Initialized with values passed during invocation, acting as local variables. -- **Local Variables**: Created via assignment inside the function; they exist only during execution. -- **Nested Scopes**: Inner functions (closures) can read variables from enclosing functions. Modification requires the `nonlocal` keyword. +5. **Post Execution**: If there's no `return` statement, or if the function ends without evaluating any return statement, `None` is implicitly returned. -```python -def outer(): - count = 0 - def inner(): - nonlocal count # Accesses enclosing scope - count += 1 - inner() -``` +### Local Variable Scope + +- **Function Parameters**: These are a precursor to local variables and are instantiated with the values passed during function invocation. +- **Local Variables**: Created using an assignment statement inside the function and cease to exist when the function execution ends. +- **Nested Scopes**: In functions within functions (closures), non-local variables - those defined in the enclosing function - are accessible but not modifiable by the inner function, without using the `nonlocal` keyword. ### Global Visibility -If a variable is not found in the local scope, Python looks in the **global scope**. Functions can read global variables, but modifying a global reference requires the `global` keyword. Without it, an assignment creates a new local variable, leaving the global one untouched. +If a variable is not defined within a function, the Python runtime will look for it in the global scope. This behavior enables functions to access and even modify global variables. ### Avoiding Side Effects -Functions provide encapsulation, ensuring internal variables do not leak into the global scope. Minimizing reliance on global state reduces **side effects**, resulting in "pure functions" that are easier to test, debug, and parallelize. +Functions offer a level of encapsulation, potentially reducing side effects by ensuring that data and variables are managed within a controlled environment. Such containment can help enhance the robustness and predictability of a codebase. As a best practice, minimizing the reliance on global variables can lead to more maintainable, reusable, and testable code.
## 12. What is a _lambda function_, and where would you use it? -A **Lambda function** is a small, anonymous function defined using the `lambda` keyword in Python. Unlike functions defined with `def`, lambdas are expressions used to create function objects on the fly. +A **Lambda function**, or **lambda**, for short, is a small anonymous function defined using the `lambda` keyword in Python. + +While you can certainly use named functions when you need a function for something in Python, there are places where a lambda expression is more suitable. ### Distinctive Features -- **Anonymity**: Lambdas are not bound to a specific name in the local namespace. -- **Single Expression Body**: The body is restricted to a single expression. Statements (like `return`, `pass`, `assert`) are not allowed. -- **Implicit Return**: The result of the expression is automatically returned; no explicit `return` keyword is needed. -- **Conciseness**: Designed for short, simple operations where a full function definition would be verbose. +- **Anonymity**: Lambdas are not given a name in the traditional sense, making them suited for one-off uses in your codebase. +- **Single Expression Body**: Their body is limited to a single expression. This can be an advantage for brevity but a restriction for larger, more complex functions. +- **Implicit Return**: There's no need for an explicit `return` statement. +- **Conciseness**: Lambdas streamline the definition of straightforward functions. ### Common Use Cases -- **Key Functions**: The most prevalent use in modern Python is defining the `key` argument for sorting or finding extremes. -- **Functional Constructs**: Used as arguments for higher-order functions like `map()`, `filter()`, and `functools.reduce()`. -- **Callbacks**: effective for delayed execution, such as UI event handlers (e.g., a button click in Tkinter) that require a callable. - -#### Code Example - -```python -students = [{'name': 'Alice', 'grade': 88}, {'name': 'Bob', 'grade': 95}] - -# 1. Using lambda as a key for sorting by grade -sorted_students = sorted(students, key=lambda x: x['grade'], reverse=True) - -# 2. Using lambda with filter to get even numbers -evens = list(filter(lambda x: x % 2 == 0, range(10))) -``` +- **Map, Filter, and Reduce**: Functions like `map` can take a lambda as a parameter, allowing you to define simple transformations on the fly. For example, doubling each element of a list can be achieved with `list(map(lambda x: x*2, my_list))`. +- **List Comprehensions**: They are a more Pythonic way of running the same `map` or `filter` operations, often seen as an alternative to lambdas and `map`. +- **Sorting**: Lambdas can serve as a custom key function, offering flexibility in sort orders. +- **Callbacks**: Often used in events where a function is needed to be executed when an action occurs (e.g., button click). +- **Simple Functions**: For functions that are so basic that giving them a name, especially in more procedural code, would be overkill. ### Notable Limitations -- **Readability**: Named functions are preferred for complex logic. Overusing lambdas, especially nested ones, creates "write-only" code. -- **Debugging**: Tracebacks refer to the function simply as ``, making it harder to identify the source of an error compared to a named function. -- **PEP 8 Style**: Assigning a lambda to a variable (e.g., `func = lambda x: x`) is discouraged. Use `def func(x): return x` instead to ensure the function has a useful `__name__` for debugging. +- **Lack of Verbose Readability**: Named functions are generally preferred when their intended use is obvious from the name. Lambdas can make code harder to understand if they're complex or not used in a recognizable pattern. +- **No Formal Documentation**: While the function's purpose should be apparent from its content, a named function makes it easier to provide direct documentation. Lambdas would need a separate verbal explanation, typically in the code or comments.
## 13. Explain _*args_ and _**kwargs_ in _Python_. -In Python, `*args` and `**kwargs` enable functions to accept a variable number of arguments. +In Python, `*args` and `**kwargs` are often used to pass a variable number of arguments to a function. + +`*args` collects a variable number of positional arguments into a **tuple**, while `**kwargs` does the same for keyword arguments into a **dictionary**. + +Here are the key features, use-cases, and their respective code examples. -`*args` collects excess positional arguments into a **tuple**, while `**kwargs` gathers excess keyword arguments into a **dictionary**. Note that in a function signature, `*args` must strictly precede `**kwargs`. +### **\*args**: Variable Number of Positional Arguments -### **\*args**: Variable Positional Arguments +- **How it Works**: The name `*args` is a convention. The asterisk (*) tells Python to put any remaining positional arguments it receives into a tuple. -- **Mechanism**: The single asterisk (`*`) operator packs any remaining positional arguments into a tuple. The name `args` is a convention; the syntax depends only on the asterisk. -- **Use-Case**: When the specific number of positional inputs is unknown or flexible. +- **Use-Case**: When the number of arguments needed is uncertain. -#### Code Example: `*args` with Type Hints +#### Code Example: "*args" ```python -def sum_all(*args: int) -> int: - # args is treated as a tuple of integers - return sum(args) +def sum_all(*args): + result = 0 + for num in args: + result += num + return result print(sum_all(1, 2, 3, 4)) # Output: 10 ``` -### **\*\*kwargs**: Variable Keyword Arguments +### **\*\*kwargs**: Variable Number of Keyword Arguments -- **Mechanism**: The double asterisk (`**`) operator captures named arguments that do not match specific parameters and stores them as key-value pairs in a dictionary. -- **Use-Case**: When a function requires flexibility to accept arbitrary configuration options or data. +- **How it Works**: The double asterisk (**) is used to capture keyword arguments and their values into a dictionary. -#### Code Example: `**kwargs` with Type Hints +- **Use-Case**: When a function should accept an arbitrary number of keyword arguments. -```python -from typing import Any +#### Code Example: "**kwargs" -def print_values(**kwargs: Any) -> None: - # kwargs is a dictionary +```python +def print_values(**kwargs): for key, value in kwargs.items(): print(f"{key}: {value}") +# Keyword arguments are captured as a dictionary print_values(name="John", age=30, city="New York") # Output: # name: John @@ -727,115 +725,94 @@ print_values(name="John", age=30, city="New York") ## 14. What are _decorators_ in _Python_? -In Python, a **decorator** is a design pattern that allows for the dynamic modification of functions, methods, or classes. It enables the extension of behavior without explicitly modifying the source code of the wrapped object, adhering to the **Open/Closed Principle**. +In Python, a **decorator** is a design pattern and a feature that allows you to modify functions and methods dynamically. This is done primarily to keep the code clean, maintainable, and DRY (Don't Repeat Yourself). -### Core Mechanism +### How Decorators Work -- **Higher-Order Functions**: Decorators are functions that accept another function as an argument and return a new function (the wrapper). -- **Closures**: The returned wrapper function usually retains access to the scope of the outer decorator function, allowing it to manipulate the environment or state. -- **Metaprogramming**: This technique treats functions as data, allowing code to inspect and modify other code at runtime. +- Decorators wrap a target function, allowing you to execute custom code before and after that function. +- They are typically **higher-order functions** that take a function as an argument and return a new function. +- This paradigm of "functions that modify functions" is often referred to as **metaprogramming**. ### Common Use Cases -- **Cross-Cutting Concerns**: Logging, execution time profiling, and error handling. -- **Access Control**: Authorization and authentication (e.g., checking user roles before execution). -- **Caching**: Storing results of expensive function calls (Memoization). -- **Input Validation**: Enforcing type checks or value constraints on arguments. -- **Built-in Decorators**: Python includes standard decorators like `@staticmethod`, `@classmethod`, `@property`, and `@dataclass`. +- **Authorization and Authentication**: Control user access. +- **Logging**: Record function calls and their parameters. +- **Caching**: Store previous function results for quick access. +- **Validation**: Verify input parameters or function output. +- **Task Scheduling**: Execute a function at a specific time or on an event. +- **Counting and Profiling**: Keep track of the number of function calls and their execution time. -### Implementation Examples +### Using Decorators in Code -#### 1. Basic Function Decorator -This example demonstrates a decorator that adds logging before and after the target function runs. +Here is the Python code: ```python from functools import wraps -def simple_logger(func): - @wraps(func) # Preserves metadata (name, docstring) +# 1. Basic Decorator +def my_decorator(func): + @wraps(func) # Ensures the original function's metadata is preserved def wrapper(*args, **kwargs): - print(f"LOG: Starting '{func.__name__}'") + print('Something is happening before the function is called.') result = func(*args, **kwargs) - print(f"LOG: Finished '{func.__name__}'") + print('Something is happening after the function is called.') return result return wrapper -@simple_logger -def greet(name): - """Greets the user.""" - print(f"Hello, {name}!") - -greet("Alice") -# Output: -# LOG: Starting 'greet' -# Hello, Alice! -# LOG: Finished 'greet' -``` +@my_decorator +def say_hello(): + print('Hello!') -#### 2. Decorators Accepting Arguments -To pass arguments to the decorator itself, three levels of nested functions are required. +say_hello() -```python -def repeat(times): - def decorator_repeat(func): +# 2. Decorators with Arguments +def decorator_with_args(arg1, arg2): + def actual_decorator(func): @wraps(func) def wrapper(*args, **kwargs): - for _ in range(times): - result = func(*args, **kwargs) + print(f'Arguments passed to decorator: {arg1}, {arg2}') + result = func(*args, **kwargs) return result return wrapper - return decorator_repeat + return actual_decorator -@repeat(times=3) -def say_hi(): - print("Hi!") +@decorator_with_args('arg1', 'arg2') +def my_function(): + print('I am decorated!') -say_hi() -# Output: -# Hi! -# Hi! -# Hi! +my_function() ``` -### Syntactic Sugar +### Decorator Syntax in Python -The `@decorator_name` syntax is syntactic sugar for passing the function into the decorator and reassigning the result to the original function name. +The `@decorator` syntax is a convenient shortcut for: ```python -# The @ syntax: -@simple_logger -def do_work(): - pass - -# Is equivalent to: -def do_work(): - pass -do_work = simple_logger(do_work) +def say_hello(): + print('Hello!') +say_hello = my_decorator(say_hello) ``` -### Best Practices: `functools.wraps` +### Role of **functools.wraps** -When writing custom decorators, always decorate the wrapper function with `@wraps(func)` from the `functools` module. Without this, the decorated function loses its original `__name__`, `__doc__`, and other metadata, which confuses debugging tools and introspection APIs. +When defining decorators, particularly those that return functions, it is good practice to use `@wraps(func)` from the `functools` module. This ensures that the original function's metadata, such as its name and docstring, is preserved.
## 15. How can you create a _module_ in _Python_? -### Creating a Module in Python +You can **create** a Python module through one of two methods: -A **module** in Python is simply a file containing Python definitions (functions, classes, variables) and statements. The file name becomes the module name, and it must have the `.py` extension. +- **Define**: Begin with saving a Python file with `.py` extension. This file will automatically function as a module. -To **create** a module: -1. **Create a File**: Save a text file with a `.py` extension (e.g., `math_operations.py`). -2. **Define Content**: Write your Python code inside this file. -3. **Import**: Use the `import` command in another script to access the code. +- **Create a Blank Module**: Start an empty file with no extension. Name the file using the accepted module syntax, e.g., `__init__ `, for it to act as a module. -*(Note: To organize multiple modules into a directory structure, you create a **package**, which typically involves adding an `__init__.py` file to the directory.)* +Next, use **import** to access the module and its functionality. ### Code Example: Creating a `math_operations` Module #### Module Definition -Save the code below in a file named `math_operations.py`: +Save the below `math_operations.py` file : ```python def add(x, y): @@ -848,50 +825,44 @@ def multiply(x, y): return x * y def divide(x, y): - if y == 0: - raise ValueError("Cannot divide by zero") return x / y ``` #### Module Usage -You can use the `math_operations` module in another script (in the same directory) using `import`: +You can use `math_operations` module by using import as shown below: ```python import math_operations result = math_operations.add(4, 5) -print(result) # Output: 9 +print(result) result = math_operations.divide(10, 5) -print(result) # Output: 2.0 +print(result) ``` -You can also import specific members directly into the namespace, or use the wildcard `*` (though `*` is generally discouraged to avoid name collisions): +Even though it is not required in the later **versions of Python**, you can also use statement `from math_operations import *` to import all the members such as functions and classes at once: ```python -from math_operations import add, subtract +from math_operations import * # Not recommended generally due to name collisions and readability concerns -# No need to use the module prefix -print(add(3, 2)) +result = add(3, 2) +print(result) ``` -### Best Practices - -Ensure your modules are robust and reusable by following these practices: +### Best Practice +Before submitting the code, let's make sure to follow the **Best Practice**: -* **Guard Against Side Effects**: Code at the top level of a module executes immediately upon import. To prevent test code or scripts from running when the module is imported, wrap execution logic in the `if __name__ == "__main__":` block. +- **Avoid Global Variables**: Use a `main()` function. +- **Guard Against Code Execution on Import**: To avoid unintended side effects, use: ```python if __name__ == "__main__": - # This runs only when executed as 'python math_operations.py' - # It does NOT run when imported - print("Running manual tests...") - print(add(10, 5)) + main() ``` -* **Avoid Global Variables**: Encapsulate state within classes or functions rather than relying on global module variables. -* **Naming**: Module names should be short, all-lowercase, and may use underscores for readability (e.g., `my_module.py`). +This makes sure that the block of code following `if __name__ == "__main__":` is only executed when the module is run directly and not when imported as a module in another program.
From 6d3845d8cb8e712be1ac67d7a65705cd8c5ff551 Mon Sep 17 00:00:00 2001 From: Devinterview-io <76989322+Devinterview-io@users.noreply.github.com> Date: Mon, 26 Jan 2026 00:36:13 -0500 Subject: [PATCH 7/7] Update python interview questions --- README.md | 857 ++++++++++++++++++++++++++++-------------------------- 1 file changed, 448 insertions(+), 409 deletions(-) diff --git a/README.md b/README.md index 14babc9..6c0bcd0 100644 --- a/README.md +++ b/README.md @@ -13,481 +13,464 @@ ## 1. What are the _key features_ of _Python_? -**Python** is a versatile and popular programming language known for its simplicity, **elegant syntax**, and a vast ecosystem of libraries. Let's look at some of the key features that make Python stand out. +### Core Features of Python -### Key Features of Python +Python is a high-level, interpreted programming language with several key features: -#### 1. Interpreted and Interactive +1. **Easy to Learn and Read**: Python's syntax emphasizes **readability** and uses indentation for scoping. +2. **Dynamically Typed**: No need to declare variable types explicitly; types are resolved at **runtime**. +3. **Object-Oriented**: Supports **classes and objects** while also enabling functional and procedural paradigms. +4. **Extensive Libraries**: Rich **Standard Library** and massive third-party ecosystems (PyPI) for AI/ML. +5. **Cross-Platform**: Runs on Windows, macOS, and Linux, now featuring enhanced **JIT compilation** performance. -Python uses an interpreter, allowing developers to run code **line-by-line**, making it ideal for rapid prototyping and debugging. - -#### 2. Easy to Learn and Read - -Python's **clean, readable syntax**, often resembling plain English, reduces the cognitive load for beginners and experienced developers alike. - -#### 3. Cross-Platform Compatibility - -Python is versatile, running on various platforms, such as Windows, Linux, and macOS, without requiring platform-specific modifications. - -#### 4. Modular and Scalable - -Developers can organize their code into modular packages and reusabale functions. - -#### 5. Rich Library Ecosystem - -The Python Package Index (PyPI) hosts over 260,000 libraries, providing solutions for tasks ranging from web development to data analytics. - -#### 6. Exceptionally Versatile - -From web applications to scientific computing, Python is equally proficient in diverse domains. - -#### 7. Memory Management - -Python seamlessly allocates and manages memory, shielding developers from low-level tasks, such as memory deallocation. - -#### 8. Dynamically Typed - -Python infers the data type of a variable during execution, easing the declartion and manipulation of variables. - -#### 9. Object-Oriented +#### Code Illustration +```python +# Dynamic typing with modern type hinting +version: str = "Python 3.13+" +version = 2026 # Re-assignment is valid at runtime -Python supports object-oriented paradigms, where everything is an **object**, offering attributes and methods to manipulate data. +# Readability: Indentation defines the block +def display_feature(name): + if name: + print(f"Key Feature: {name}") -#### 10. Extensible +display_feature("Portability") +``` -With its C-language API, developers can integrate performance-critical tasks and existing C modules with Python. +[BATCH-UPDATED: 2026-01-26]
## 2. How is _Python_ executed? -**Python** source code is processed through various steps before it can be executed. Let's explore the key stages in this process. +**Python** follows a multi-stage execution process that combines compilation and interpretation. While often called an "interpreted" language, it actually compiles source code into an intermediate format before execution. -### Compilation & Interpretation +### Compilation and the PVM -Python code goes through both **compilation** and **interpretation**. +The execution flow consists of two primary stages: -- **Bytecode Compilation**: High-level Python code is transformed into low-level bytecode by the Python interpreter with the help of a compiler. Bytecode is a set of instructions that Python's virtual machine (PVM) can understand and execute. - -- **On-the-fly Interpretation**: The PVM reads and executes bytecode instructions in a step-by-step manner. - -This dual approach known as "compile and then interpret" is what sets Python (and certain other languages) apart. +1. **Bytecode Compilation**: When a script is run, the Python compiler translates the high-level source code (`.py`) into **Bytecode**. Bytecode is a low-level, platform-independent representation of the code, often stored in `__pycache__` folders as `.pyc` files. +2. **The Python Virtual Machine (PVM)**: The **PVM** is the runtime engine of Python. It iterates through the bytecode instructions and maps them to the corresponding machine-specific instructions. -### Bytecode versus Machine Code Execution +This hybrid "compile-then-interpret" model allows Python to maintain **cross-platform compatibility** while optimizing the execution loop. -While some programming languages compile directly to machine code, Python compiles to bytecode. This bytecode is then executed by the Python virtual machine. This extra step of bytecode execution **can make Python slower** in certain use-cases when compared to languages that compile directly to machine code. +### The Execution Pipeline -The advantage, however, is that bytecode is platform-independent. A Python program can be run on any machine with a compatible PVM, ensuring cross-platform support. +The transition from source code to execution involves several discrete steps: -### Source Code to Bytecode: Compilation Steps +#### 1. Lexical Analysis +The **Lexer** breaks the source code into **tokens** (keywords, identifiers, operators, and literals), removing whitespace and comments. -1. **Lexical Analysis**: The source code is broken down into tokens, identifying characters and symbols for Python to understand. -2. **Syntax Parsing**: Tokens are structured into a parse tree to establish the code's syntax and grammar. -3. **Semantic Analysis**: Code is analyzed for its meaning and context, ensuring it's logically sound. -4. **Bytecode Generation**: Based on the previous steps, bytecode instructions are created. +#### 2. Parsing and AST Generation +The **Parser** organizes tokens into an **Abstract Syntax Tree (AST)**. The AST represents the logical structure of the program based on Python's grammar rules. + +#### 3. Compilation to Bytecode +The compiler traverses the AST and generates **Bytecode**. This stage includes optimizations like **Constant Folding**, where simple expressions (e.g., $15 \times 20$) are pre-calculated. + +#### 4. Interpretation and Execution +The **PVM** executes the bytecode. Modern versions of Python (3.11+) utilize a **Specializing Adaptive Interpreter**, which identifies "hot" code paths and optimizes them for faster execution during runtime. ### Just-In-Time (JIT) Compilation -While Python typically uses a combination of interpretation and compilation, **JIT** boosts efficiency by selectively compiling parts of the program that are frequently used or could benefit from optimization. +In contemporary Python (specifically starting with versions like 3.13), a **JIT Compiler** has been introduced as an experimental feature. Unlike the standard interpreter that processes bytecode line-by-line, a **JIT** compiles frequently executed bytecode directly into **Machine Code** at runtime. + +By bypassing the PVM's interpretation overhead for performance-critical segments, JIT compilation significantly improves execution speed, bringing Python's performance closer to statically compiled languages. -JIT compiles sections of the program to machine code on-the-fly. This direct machine code generation for frequently executed parts can significantly speed up those segments, blurring the line between traditional interpreters and compilers. +### Code Example: Disassembling Bytecode -### Code Example: Disassembly of Bytecode +The `dis` module allows developers to inspect the bytecode generated by the compiler. ```python import dis -def example_func(): +def calculate_area(): + # The compiler performs constant folding: 15 * 20 = 300 return 15 * 20 -# Disassemble to view bytecode instructions -dis.dis(example_func) +# Disassemble the function to view bytecode +dis.dis(calculate_area) ``` -Disassembling code using Python's `dis` module can reveal the underlying bytecode instructions that the PVM executes. Here's the disassembled output for the above code: - +**Output (Python 3.11+):** ```plaintext - 4 0 LOAD_CONST 2 (300) - 2 RETURN_VALUE + 1 0 RESUME 0 + 3 2 LOAD_CONST 1 (300) + 4 RETURN_VALUE ``` + +In this output, `LOAD_CONST` demonstrates that the multiplication was handled during the **Compilation** phase, while `RETURN_VALUE` is the instruction handled by the **PVM** at runtime.
## 3. What is _PEP 8_ and why is it important? -**PEP 8** is a style guide for Python code that promotes code consistency, readability, and maintainability. It's named after Python Enhancement Proposal (PEP), the mechanism used to propose and standardize changes to the Python language. +**PEP 8** is the primary style guide for **Python** code, prioritizing **readability**, **consistency**, and **maintainability**. It is a **Python Enhancement Proposal (PEP)**, the formal mechanism for proposing new features or processes to the Python community. -PEP 8 is not a set-in-stone rule book, but it provides general guidelines that help developers across the Python community write code that's visually consistent and thus easier to understand. +In 2026, while PEP 8 remains the foundational standard, it is increasingly enforced by high-performance automated tools like **Ruff** and **Black**, which ensure codebases adhere to these conventions with minimal manual effort. ### Key Design Principles -PEP 8 emphasizes: +PEP 8 is governed by the philosophy that "code is read much more often than it is written": -- **Readability**: Code should be easy to read and understand, even by someone who didn't write it. -- **Consistency**: Codebase should adhere to a predictable style so there's little cognitive load in reading or making changes. -- **One Way to Do It**: Instead of offering multiple ways to write the same construct, PEP 8 advocates for a single, idiomatic style. +- **Readability**: Code should be transparent and easy to understand for any developer, not just the author. +- **Consistency**: A uniform style across a project reduces cognitive load and simplifies code reviews. +- **The Zen of Python**: It advocates for "one—and preferably only one—obvious way to do it," discouraging fragmented coding styles. ### Base Rules -- **Indentation**: Use 4 spaces for each level of logical indentation. -- **Line Length**: Keep lines of code limited to 79 characters. This number is a guideline; longer lines are acceptable in certain contexts. -- **Blank Lines**: Use them to separate logical sections but not excessively. +- **Indentation**: Use **4 spaces** per indentation level. Avoid using tabs. +- **Line Length**: Limit code lines to **79 characters**. For long blocks of text (docstrings or comments), a limit of **72 characters** is recommended. +- **Blank Lines**: Surround top-level function and class definitions with two blank lines. Method definitions inside a class are surrounded by a single blank line. ### Naming Styles -- **Class Names**: Prefer `CamelCase`. -- **Function and Variable Names**: Use `lowercase_with_underscores`. +- **Class Names**: Use **CapWords** (also known as `CamelCase` or `PascalCase`). +- **Function and Variable Names**: Use `lowercase_with_underscores` (snake_case). +- **Constants**: Use `UPPER_CASE_WITH_UNDERSCORES` to denote values that do not change. - **Module Names**: Keep them short and in `lowercase`. ### Documentation -- Use triple quotes for documentation strings. -- Comments should be on their own line and explain the reason for the following code block. +- **Docstrings**: Use **triple double quotes** (`"""`) for all public modules, functions, classes, and methods. +- **Comments**: Should be complete sentences. Inline comments should be separated by at least two spaces from the statement they describe. ### Whitespace Usage -- **Operators**: Surround them with a single space. -- **Commas**: Follow them with a space. +- **Operators**: Surround binary operators (assignment, comparisons, Booleans) with a single space (e.g., $x = y + 1$). +- **Punctuation**: Avoid extraneous whitespace immediately inside parentheses, brackets, or braces, and immediately before a comma. ### Example: Directory Walker -Here is the `PEP8` compliant code: +The following code demonstrates a **PEP 8** compliant structure: ```python import os +from typing import List -def walk_directory(path): +def walk_directory(path: str) -> List[str]: + """Iterate through a directory and return a list of file paths.""" + file_list = [] for dirpath, dirnames, filenames in os.walk(path): for filename in filenames: file_path = os.path.join(dirpath, filename) - print(file_path) + file_list.append(file_path) + return file_list -walk_directory('/path/to/directory') +if __name__ == "__main__": + # Example execution + files = walk_directory('/path/to/directory') + for f in files: + print(f) ```
## 4. How is memory allocation and garbage collection handled in _Python_? -In Python, **both memory allocation** and **garbage collection** are handled discretely. +In Python, **memory allocation** and **garbage collection** are handled automatically by the interpreter, abstracting complex memory management away from the developer. ### Memory Allocation -- The "heap" is the pool of memory for storing objects. The Python memory manager allocates and deallocates this space as needed. +Python manages a **private heap** that contains all Python objects and data structures. The **Python Memory Manager** ensures that the heap is efficiently utilized through several layers of abstraction. -- In latest Python versions, the `obmalloc` system is responsible for small object allocations. This system preallocates small and medium-sized memory blocks to manage frequently created small objects. +- **PyMalloc (obmalloc):** For small objects (typically $\leq 512$ bytes), Python uses a specialized allocator. It organizes memory into **Arenas** (256 KB), which are carved into **Pools** (4 KB), and then into **Blocks**. This minimizes the overhead of frequent system-level `malloc` calls. +- **Raw Allocator:** For large objects, Python bypasses its internal pool system and requests memory directly from the operating system’s heap using standard C library functions (e.g., `malloc`). +- **Object-Specific Allocators:** Python also employs specific allocators for frequently used types like integers and strings to further enhance performance and reduce fragmentation. -- The `allocator` abstracts the system-level memory management, employing memory management libraries like `Glibc` to interact with the operating system. +```python +import sys -- Larger blocks of memory are primarily obtained directly from the operating system. +# Small object (handled by PyMalloc) +x = 100 +print(f"Size of x: {sys.getsizeof(x)} bytes") -- **Stack** and **Heap** separation is joined by "Pool Allocator" for internal use. +# Large object (handled by Raw Allocator) +y = "A" * 10**6 +print(f"Size of y: {sys.getsizeof(y)} bytes") +``` ### Garbage Collection -Python employs a method called **reference counting** along with a **cycle-detecting garbage collector**. +Python uses a dual approach to garbage collection: **Reference Counting** and a **Generational Garbage Collector**. #### Reference Counting -- Every object has a reference count. When an object's count drops to zero, it is immediately deallocated. +- Every object has an internal `ob_refcnt` field. When an object is referenced, this count increases; when a reference is deleted or goes out of scope, the count decreases. +- If the count reaches zero, the object is immediately deallocated. This is the primary and most efficient way Python reclaims memory. -- This mechanism is swift, often releasing objects instantly without the need for garbage collection. +#### Generational Garbage Collector -- However, it can be insufficient in handling **circular references**. +- Reference counting cannot handle **circular references** (e.g., two objects pointing to each other). To solve this, Python's `gc` module periodically scans for cycles. +- It uses a **generational approach** with three generations ($G_0$, $G_1$, and $G_2$). New objects start in $G_0$. If they survive a collection, they move to an older generation. +- Older generations are scanned less frequently than younger ones, based on the heuristic that most objects have short lifespans. -#### Cycle-Detecting Garbage Collector +```python +import gc -- Python has a separate garbage collector that periodically identifies and deals with circular references. +# Check current thresholds for G0, G1, G2 +print(f"GC Thresholds: {gc.get_threshold()}") -- This is, however, a more time-consuming process and is invoked less frequently than reference counting. +# Manually trigger a collection +gc.collect() +``` ### Memory Management in Python vs. C -Python handles memory management quite differently from languages like C or C++: - -- In Python, the developer isn't directly responsible for memory allocations or deallocations, reducing the likelihood of memory-related bugs. - -- The memory manager in Python is what's known as a **"general-purpose memory manager"** that can be slower than the dedicated memory managers of C or C++ in certain contexts. +Python's memory management model offers significant trade-offs compared to manual management in C: -- Python, especially due to the existence of a garbage collector, might have memory overhead compared to C or C++ where manual memory management often results in minimal overhead is one of the factors that might contribute to Python's sometimes slower performance. - -- The level of memory efficiency isn't as high as that of C or C++. This is because Python is designed to be convenient and easy to use, often at the expense of some performance optimization. +- **Safety and Convenience:** Python prevents **memory leaks**, **buffer overflows**, and **use-after-free** errors, which are common in C/C++. +- **Memory Overhead:** Every Python object carries metadata (type and reference count), making Python more memory-intensive. For example, a 28-byte integer in Python requires significantly more space than a 4-byte `int` in C. +- **Deterministic vs. Non-deterministic:** While reference counting is deterministic (immediate), the generational GC is non-deterministic, potentially causing small, unpredictable pauses during execution. +- **Modern Optimizations:** Recent Python versions (3.11+) have integrated improvements like **mimalloc** and refined GC algorithms to close the performance gap with lower-level languages.
## 5. What are the _built-in data types_ in _Python_? -Python offers numerous **built-in data types** that provide varying functionalities and utilities. +Python offers a robust set of **built-in data types** categorized primarily by their **mutability**. Understanding these is essential for memory management and performance optimization. ### Immutable Data Types #### 1. int - Represents a whole number, such as 42 or -10. +Represents arbitrary-precision integers. Unlike many languages, Python's `int` can handle safely sized numbers limited only by available memory. #### 2. float - Represents a decimal number, like 3.14 or -0.01. +Represents double-precision floating-point numbers (standard IEEE 754). For higher precision, the `decimal` module is preferred. #### 3. complex - Comprises a real and an imaginary part, like 3 + 4j. +Used for mathematical computations involving a real and an imaginary part, represented as $a + bj$. #### 4. bool - Represents a boolean value, True or False. +A subclass of `int` representing truth values: `True` (1) and `False` (0). #### 5. str - A sequence of unicode characters enclosed within quotes. +An immutable sequence of **Unicode characters**. Strings support extensive slicing and formatting methods. #### 6. tuple - An ordered collection of items, often heterogeneous, enclosed within parentheses. +An ordered, immutable collection. Tuples are often used to store heterogeneous data and can be used as keys in dictionaries if they contain only hashable elements. #### 7. frozenset - A set of unique, immutable objects, similar to sets, enclosed within curly braces. +A hashable, immutable version of a `set`. It is defined using the `frozenset()` constructor and is useful for set operations where the collection must remain constant. #### 8. bytes - Represents a group of 8-bit bytes, often used with binary data, enclosed within brackets. +An immutable sequence of single bytes ($0 \le x < 256$), typically used for handling binary data and encoded text. -#### 9. bytearray - Resembles the 'bytes' type but allows mutable changes. +#### 9. range +Represents an immutable sequence of numbers, commonly used for looping a specific number of times in `for` loops without storing the entire sequence in memory. #### 10. NoneType - Indicates the absence of a value. +The type for the `None` singleton, used to signify the absence of a value or a default return value for functions. ### Mutable Data Types #### 1. list - A versatile ordered collection that can contain different data types and offers dynamic sizing, enclosed within square brackets. +A dynamic, ordered array that can contain heterogeneous elements. It is optimized for fast fixed-index access and end-of-list operations. #### 2. set - Represents a unique set of objects and is characterized by curly braces. +An unordered collection of unique, hashable objects. It provides $O(1)$ average-time complexity for membership testing. #### 3. dict - A versatile key-value paired collection enclosed within braces. +A collection of key-value pairs. Since Python 3.7+, `dict` maintains **insertion order** as a language feature. -#### 4. memoryview - Points to the memory used by another object, aiding efficient viewing and manipulation of data. +#### 4. bytearray +A mutable counterpart to the `bytes` type. It allows in-place modification of binary data. -#### 5. array - Offers storage for a specified type of data, similar to lists but with dedicated built-in functionalities. +#### 5. memoryview +A generalized interface for accessing the internal data of an object that supports the **buffer protocol** (like `bytes` or `bytearray`) without copying it. -#### 6. deque - A double-ended queue distinguished by optimized insertion and removal operations from both its ends. +#### 6. array (array.array) +Part of the `array` module, this provides space-efficient storage of basic values (integers, floats) constrained to a single C-style data type. -#### 7. object - The base object from which all classes inherit. +#### 7. deque (collections.deque) +A double-ended queue optimized for $O(1)$ additions and removals from both the beginning and the end. -#### 8. types.SimpleNamespace - Grants the capability to assign attributes to it. +#### 8. object +The most fundamental base class in Python. All other types, including classes and functions, inherit from `object`. -#### 9. types.ModuleType - Represents a module body containing attributes. +#### 9. types.SimpleNamespace +A simple object subclass that provides attribute access to its namespace, essentially a `dict` with dot-notation access. #### 10. types.FunctionType - Defines a particular kind of function. +The internal type for user-defined functions, allowing for the inspection and dynamic creation of function objects. + +```python +# Examples of modern type usage and literals +integer_val = 1_000_000 # int with underscores for readability +binary_data = b"Hello" # bytes +mutable_bytes = bytearray(b"Hi") # bytearray +unique_items = {1, 2, 3} # set +mapping = {"key": "value"} # dict (ordered by insertion) + +# Efficient data manipulation +view = memoryview(mutable_bytes) +view[0] = 72 # Modifies mutable_bytes to b"Hi" -> b"Hi" is now b"Hi" +```
## 6. Explain the difference between a _mutable_ and _immutable_ object. -Let's look at the difference between **mutable** and **immutable** objects. +In Python, every object has an **identity**, a **type**, and a **value**. The distinction between mutable and immutable objects is fundamental to how Python manages memory and handles data assignments. ### Key Distinctions -- **Mutable Objects**: Can be modified after creation. -- **Immutable Objects**: Cannot be modified after creation. +- **Mutable Objects**: Their state or contents can be changed in-place after creation without changing the object's unique **identity** (`id()`). +- **Immutable Objects**: Their state cannot be modified once created. Any operation that appears to modify an immutable object actually creates a **new object** in memory with a new identity. ### Common Examples -- **Mutable**: Lists, Sets, Dictionaries -- **Immutable**: Tuples, Strings, Numbers +- **Mutable**: `list`, `dict`, `set`, `bytearray`. +- **Immutable**: `int`, `float`, `complex`, `str`, `tuple`, `bool`, `frozenset`, `bytes`. ### Code Example: Immutability in Python -Here is the Python code: - ```python -# Immutable objects (int, str, tuple) -num = 42 -text = "Hello, World!" +# Immutable objects: (int, str, tuple) +text = "Python" my_tuple = (1, 2, 3) -# Trying to modify will raise an error +# Modifying contents directly raises a TypeError try: - num += 10 - text[0] = 'M' # This will raise a TypeError - my_tuple[0] = 100 # This will also raise a TypeError + text[0] = 'p' # Raises TypeError + my_tuple[0] = 100 # Raises TypeError except TypeError as e: print(f"Error: {e}") -# Mutable objects (list, set, dict) -my_list = [1, 2, 3] -my_dict = {'a': 1, 'b': 2} +# Re-assignment creates a new object (Name Rebinding) +x = 10 +initial_id = id(x) +x += 1 +print(id(x) == initial_id) # Output: False (New memory address) -# Can be modified without issues -my_list.append(4) -del my_dict['a'] - -# Checking the changes -print(my_list) # Output: [1, 2, 3, 4] -print(my_dict) # Output: {'b': 2} +# Mutable objects: (list, dict) +my_list = [1, 2, 3] +list_id = id(my_list) +my_list.append(4) # Modified in-place +print(id(my_list) == list_id) # Output: True (Same memory address) ``` ### Benefits & Trade-Offs -**Immutability** offers benefits such as **safety** in concurrent environments and facilitating **predictable behavior**. +#### Immutability +- **Thread-Safety**: Immutable objects are inherently thread-safe because their state cannot change, preventing race conditions. +- **Hashability**: Only immutable objects (or tuples containing only immutable objects) are **hashable**. This allows them to serve as **dictionary keys** or elements in a **set**. +- **Predictability**: Functions cannot inadvertently modify immutable arguments, reducing side effects. -**Mutability**, on the other hand, often improves **performance** by avoiding copy overhead and redundant computations. +#### Mutability +- **Performance**: In-place updates are more efficient for large datasets. Appending to a list is generally $O(1)$, whereas "appending" to a string or tuple requires a full copy, which is $O(n)$. +- **Memory Efficiency**: Mutability avoids the overhead of creating redundant temporary objects during frequent updates. ### Impact on Operations -- **Reading and Writing**: Immutable objects typically favor **reading** over **writing**, promoting a more straightforward and predictable code flow. - -- **Memory and Performance**: Mutability can be more efficient in terms of memory usage and performance, especially concerning large datasets, thanks to in-place updates. +- **Reading and Writing**: Immutable objects favor **read-heavy** workloads and functional programming patterns. Mutability is preferred for **write-heavy** data structures. +- **Hash Functions**: Python requires that a key's hash remains constant throughout its lifetime. This is expressed as: +$$hash(object) = \text{constant}$$ +If an object were mutable and its value changed, its hash would change, breaking the lookup mechanism in hash tables (dictionaries). -Choosing between the two depends on the program's needs, such as the required data integrity and the trade-offs between predictability and performance. +Choosing between the two depends on the need for data integrity versus the requirement for high-performance, in-place data manipulation.
## 7. How do you _handle exceptions_ in _Python_? -**Exception handling** is a fundamental aspect of Python, and it safeguards your code against unexpected errors or conditions. Key components of exception handling in Python include: +**Exception handling** in Python is a robust mechanism used to manage runtime errors, ensuring the program's flow is not interrupted unexpectedly. As of 2026, the core syntax remains stable, with enhanced support for concurrent error handling. -### Components +### Core Components -- **Try**: The section of code where exceptions might occur is placed within a `try` block. +- **`try`**: Encapsulates code that might throw an exception. +- **`except`**: Catches and handles specific exceptions. Multiple `except` blocks can follow a single `try`. +- **`else`**: Executes only if the code in the `try` block did **not** raise an exception. +- **`finally`**: Executes regardless of whether an exception occurred. It is essential for **resource cleanup**, such as closing file streams or network sockets. -- **Except**: Any possible exceptions that are `raised` by the `try` block are caught and handled in the `except` block. +### Handling Specific vs. General Exceptions -- **Finally**: This block ensures a piece of code always executes, regardless of whether an exception occurred. It's commonly used for cleanup operations, such as closing files or database connections. - -### Generic Exception Handling vs. Handling Specific Exceptions - -It's good practice to **handle** specific exceptions. However, a more **general** approach can also be taken. When doing the latter, ensure the general exception handling is at the end of the chain, as shown here: +It is a best practice to catch specific exceptions to avoid masking unrelated bugs. Specific handlers should always precede general ones. ```python try: - risky_operation() -except IndexError: # Handle specific exception types first. - handle_index_error() -except Exception as e: # More general exception must come last. - handle_generic_error() + process_data() +except FileNotFoundError: # Specific exception + handle_missing_file() +except Exception as e: # General catch-all + log_error(e) finally: - cleanup() + release_resources() ``` -### Raising Exceptions - -Use this mechanism to **trigger and manage** exceptions under specific circumstances. This can be particularly useful when building custom classes or functions where specific conditions should be met. +### Raising and Chaining Exceptions -**Raise** a specific exception: +The `raise` keyword triggers an exception manually. In modern Python, **exception chaining** using `from` is preferred when re-raising to preserve the original traceback (the "cause"). ```python -def divide(a, b): - if b == 0: - raise ZeroDivisionError("Divisor cannot be zero") - return a / b - -try: - result = divide(4, 0) -except ZeroDivisionError as e: - print(e) +def get_config(path): + try: + with open(path, 'r') as f: + return f.read() + except OSError as e: + # Chaining allows debugging the original 'e' while raising a custom error + raise RuntimeError("Configuration load failed") from e ``` -**Raise a general exception**: +### Modern Handling: Exception Groups + +Introduced in Python 3.11 and standard by 2026, `ExceptionGroup` and the `except*` syntax allow handling multiple exceptions simultaneously, which is common in **asynchronous programming** and `TaskGroups`. ```python -def some_risky_operation(): - if condition: - raise Exception("Some generic error occurred") +try: + async with asyncio.TaskGroup() as tg: + tg.create_task(task_one()) + tg.create_task(task_two()) +except* (ValueError, TypeError) as eg: + for e in eg.exceptions: + print(f"Caught part of group: {e}") ``` -### Using `with` for Resource Management - -The `with` keyword provides a more efficient and clean way to handle resources, like files, ensuring their proper closure when operations are complete or in case of any exceptions. The resource should implement a `context manager`, typically by having `__enter__` and `__exit__` methods. +### Resource Management with `with` -Here's an example using a file: +The `with` statement utilizes **Context Managers** (`__enter__` and `__exit__`) to handle setup and teardown automatically. This is the most "Pythonic" way to prevent resource leaks. ```python -with open("example.txt", "r") as file: - data = file.read() -# File is automatically closed when the block is exited. +with open("data.json", "w") as file: + file.write(json_string) +# File is closed automatically even if an exception occurs during write. ``` -### Silence with `pass`, `continue`, or `else` - -There are times when not raising an exception is appropriate. You can use `pass` or `continue` in an exception block when you want to essentially ignore an exception and proceed with the rest of your code. +### Control Flow: `pass`, `continue`, and `else` -- **`pass`**: Simply does nothing. It acts as a placeholder. +- **`pass`**: Used within an `except` block to suppress an error silently. +- **`continue`**: Used within a loop's `except` block to skip the current iteration and move to the next. +- **`else`**: Useful for logic that should only run if the `try` block succeeded, separating the "happy path" from the error handling. - ```python - try: - risky_operation() - except SomeSpecificException: - pass - ``` +### Global Exception Hook: `sys.excepthook` -- **`continue`**: This keyword is generally used in loops. It moves to the next iteration without executing the code that follows it within the block. - - ```python - for item in my_list: - try: - perform_something(item) - except ExceptionType: - continue - ``` - -- **`else` with `try-except` blocks**: The `else` block after a `try-except` block will only be executed if no exceptions are raised within the `try` block - - ```python - try: - some_function() - except SpecificException: - handle_specific_exception() - else: - no_exception_raised() - ``` - -### Callback Function: `ExceptionHook` - -Python 3 introduced the better handling of uncaught exceptions by providing an optional function for printing stack traces. The `sys.excepthook` can be set to match any exception in the module as long as it has a `hook` attribute. - -Here's an example for this test module: +For top-level error logging or custom crash reporting, you can override the global exception handler. This captures any exception that is not caught by a `try-except` block. ```python -# test.py import sys -def excepthook(type, value, traceback): - print("Unhandled exception:", type, value) - # Call the default exception hook - sys.__excepthook__(type, value, traceback) - -sys.excepthook = excepthook +def custom_hook(type, value, traceback): + print(f"Uncaught global exception: {value}") + # Optional: sys.__excepthook__(type, value, traceback) -def test_exception_hook(): - throw_some_exception() +sys.excepthook = custom_hook ``` -When run, calling `test_exception_hook` will print "Unhandled exception: ..." - -_Note_: `sys.excepthook` will not capture exceptions raised as the result of interactive prompt commands, such as SyntaxError or KeyboardInterrupt. +*Note: `sys.excepthook` does not trigger for `SystemExit` or exceptions raised in the interactive REPL.*
## 8. What is the difference between _list_ and _tuple_? -**Lists** and **Tuples** in Python share many similarities, such as being sequences and supporting indexing. - -However, these data structures differ in key ways: +**Lists** and **Tuples** in Python are sequence types that support indexing and slicing, yet they differ in several fundamental ways: ### Key Distinctions -- **Mutability**: Lists are mutable, allowing you to add, remove, or modify elements after creation. Tuples, once created, are immutable. - -- **Performance**: Lists are generally slower than tuples, most apparent in tasks like iteration and function calls. - +- **Mutability**: Lists are **mutable**, allowing you to add, remove, or modify elements in place. Tuples are **immutable**; once defined, their elements and length cannot be changed. +- **Memory & Performance**: Tuples are more **memory-efficient** and faster to instantiate. Lists require extra memory overhead (over-allocation) to support dynamic resizing during `append` operations. +- **Hashability**: Because they are immutable, tuples are **hashable** (provided their elements are also hashable). This allows them to be used as **dictionary keys** or stored in **sets**, whereas lists cannot. - **Syntax**: Lists are defined with square brackets `[]`, whereas tuples use parentheses `()`. ### When to Use Each -- **Lists** are ideal for collections that may change in size and content. They are the preferred choice for storing data elements. - -- **Tuples**, due to their immutability and enhanced performance, are a good choice for representing fixed sets of related data. +- **Lists** are ideal for collections of **homogeneous** data that may change in size or content during the program's lifecycle. +- **Tuples** are the preferred choice for **heterogeneous** data structures or fixed records, such as a coordinate pair $(x, y)$ or a database row. -### Syntax +### Syntax Examples -#### List: Example +#### List: Mutable Sequence ```python my_list = ["apple", "banana", "cherry"] @@ -495,44 +478,42 @@ my_list.append("date") my_list[1] = "blackberry" ``` -#### Tuple: Example +#### Tuple: Immutable Sequence ```python my_tuple = (1, 2, 3, 4) # Unpacking a tuple a, b, c, d = my_tuple +# my_tuple[0] = 10 # This would raise a TypeError ```
## 9. How do you create a _dictionary_ in _Python_? -**Python dictionaries** are versatile data structures, offering key-based access for rapid lookups. Let's explore various data within dictionaries and techniques to create and manipulate them. +**Python dictionaries** are versatile data structures that map unique keys to values, providing $O(1)$ average time complexity for lookups. In modern Python (3.7+), dictionaries also maintain **insertion order**. ### Key Concepts -- A **dictionary** in Python contains a collection of `key:value` pairs. -- **Keys** must be unique and are typically immutable, such as strings, numbers, or tuples. -- **Values** can be of any type, and they can be duplicated. +- A **dictionary** consists of a collection of `key:value` pairs. +- **Keys** must be **hashable** (immutable), such as strings, numbers, or tuples. +- **Values** can be of any data type, including lists or other dictionaries, and can be duplicated. +- Dictionaries are mutable, allowing for dynamic updates to their content. ### Creating a Dictionary -You can use several methods to create a dictionary: - -1. **Literal Definition**: Define key-value pairs within curly braces { }. +You can use several methods to create a dictionary depending on the data source: -2. **From Key-Value Pairs**: Use the `dict()` constructor or the `{key: value}` shorthand. - -3. **Using the `dict()` Constructor**: This can accept another dictionary, a sequence of key-value pairs, or named arguments. - -4. **Comprehensions**: This is a concise way to create dictionaries using a single line of code. - -5. **`zip()` Function**: This creates a dictionary by zipping two lists, where the first list corresponds to the keys, and the second to the values. +1. **Literal Definition**: Defining key-value pairs within curly braces `{}`. +2. **`dict()` Constructor**: Converting sequences of key-value pairs or using named arguments. +3. **Dictionary Comprehensions**: A concise, programmatic way to generate dictionaries. +4. **`zip()` Function**: Mapping two separate sequences (keys and values) into a single dictionary. +5. **`dict.fromkeys()`**: Creating a dictionary with a predefined set of keys and a single default value. ### Examples #### Dictionary Literal Definition -Here is a Python code: +This is the most common and readable way to initialize a dictionary. ```python # Dictionary literal definition @@ -543,249 +524,300 @@ student = { } ``` -#### From Key-Value Pairs +#### Using the `dict()` Constructor -Here is the Python code: +The `dict()` constructor is flexible, accepting iterables of pairs or keyword arguments. ```python -# Using the `dict()` constructor +# From a sequence of tuples student_dict = dict([ ("name", "John Doe"), - ("age", 21), - ("courses", ["Math", "Physics"]) + ("age", 21) ]) -# Using the shorthand syntax -student_dict_short = { - "name": "John Doe", - "age": 21, - "courses": ["Math", "Physics"] -} +# Using keyword arguments (keys must be valid identifiers) +student_dict_short = dict(name="John Doe", age=21, courses=["Math", "Physics"]) ``` -#### Using `zip()` +#### Using `zip()` and `fromkeys()` -Here is a Python code: +`zip()` is ideal for merging two lists, while `fromkeys()` initializes dictionaries with default values. ```python keys = ["a", "b", "c"] values = [1, 2, 3] -zipped = zip(keys, values) -dict_from_zip = dict(zipped) # Result: {"a": 1, "b": 2, "c": 3} +# Mapping two lists together +dict_from_zip = dict(zip(keys, values)) # {"a": 1, "b": 2, "c": 3} + +# Initializing with a default value +default_dict = dict.fromkeys(keys, 0) # {"a": 0, "b": 0, "c": 0} ``` -#### Using `dict()` Constructor +#### Comprehensions and Merging -Here is a Python code: +Comprehensions allow for logic during creation, while the **union operator** (`|`) is the modern (Python 3.9+) way to merge dictionaries. ```python -# Sequence of key-value pairs -student_dict2 = dict(name="Jane Doe", age=22, courses=["Biology", "Chemistry"]) +# Dictionary comprehension +squares = {x: x**2 for x in range(5)} -# From another dictionary -student_dict_combined = dict(student, **student_dict2) +# Merging dictionaries using the union operator +dict_a = {"x": 1, "y": 2} +dict_b = {"z": 3} +combined = dict_a | dict_b # {"x": 1, "y": 2, "z": 3} ```
## 10. What is the difference between _==_ and _is operator_ in _Python_? -Both the **`==`** and **`is`** operators in Python are used for comparison, but they function differently. +### Equality vs. Identity in Python + +Both the **`==`** and **`is`** operators are used for comparison, but they evaluate fundamentally different properties of objects. -- The **`==`** operator checks for **value equality**. -- The **`is`** operator, on the other hand, validates **object identity**, +* The **`==`** operator checks for **Value Equality**. It determines if the data stored within the objects is the same by invoking the `__eq__` method. +* The **`is`** operator validates **Object Identity**. It determines if two variables point to the exact same memory location. -In Python, every object is unique, identifiable by its memory address. The **`is`** operator uses this memory address to check if two objects are the same, indicating they both point to the exact same instance in memory. +In Python, every object is identifiable by a unique memory address. The **`is`** operator compares these addresses ($id(a) == id(b)$). Even if two objects look identical, they are distinct if they reside in different memory slots. -- **`is`**: Compares the memory address or identity of two objects. -- **`==`**: Compares the content or value of two objects. +```python +list_a = [1, 2, 3] +list_b = [1, 2, 3] +list_c = list_a + +print(list_a == list_b) # True: The values are the same +print(list_a is list_b) # False: They are different objects in memory +print(list_a is list_c) # True: Both reference the same object +``` -While **`is`** is primarily used for **None** checks, it's generally advisable to use **`==`** for most other comparisons. +#### Best Practices for 2026 -### Tips for Using Operators +* **`==`**: Use this for almost all equality comparisons, such as comparing strings, numbers, or data structures. +* **`is`**: Use this specifically when comparing against **Singletons**. The most common use case is checking for **`None`** (e.g., `if val is None:`). -- **`==`**: Use for equality comparisons, like when comparing numeric or string values. -- **`is`**: Use for comparing membership or when dealing with singletons like **None**. +#### Implementation Note +Avoid using **`is`** with literals (like integers or strings). While Python performs **interning** (reusing memory for small integers and certain strings), this is an implementation detail. Using **`is`** for value comparison with literals is technically incorrect and often triggers a **SyntaxWarning** in modern Python versions.
## 11. How does a _Python function_ work? -**Python functions** are the building blocks of code organization, often serving predefined tasks within modules and scripts. They enable reusability, modularity, and encapsulation. +A **Python function** is a first-class object that encapsulates a reusable block of code. In Python, functions are defined using the `def` keyword and represent a mapping between inputs (arguments) and outputs (return values). ### Key Components -- **Function Signature**: Denoted by the `def` keyword, it includes the function name, parameters, and an optional return type. -- **Function Body**: This section carries the core logic, often comprising conditional checks, loops, and method invocations. -- **Return Statement**: The function's output is determined by this statement. When None is specified, the function returns by default. -- **Local Variables**: These variables are scoped to the function and are only accessible within it. +- **Function Signature**: Defined by the `def` keyword, it includes the function name, **parameters** (with optional **type hints**), and a return type annotation. +- **Function Body**: An indented block containing the logic. In modern Python, this is compiled into **bytecode** before execution. +- **Return Statement**: Specifies the result. If no `return` is explicitly defined, the function implicitly returns `None`. +- **Function Object**: In Python, functions are **first-class citizens**, meaning they can be assigned to variables, passed as arguments, and returned from other functions. ### Execution Process -When a function is called: +When a **Python function** is invoked: -1. **Stack Allocation**: A stack frame, also known as an activation record, is created to manage the function's execution. This frame contains details like the function's parameters, local variables, and **instruction pointer**. +1. **Stack Frame Creation**: The Python Interpreter creates a new **frame object** on the **call stack**. This frame stores local variables, the **instruction pointer**, and references to the global and built-in namespaces. -2. **Parameter Binding**: The arguments passed during the function call are bound to the respective parameters defined in the function header. +2. **Argument Binding**: Arguments are bound to the parameters defined in the signature. Python uses **pass-by-object-reference**; the local name in the function refers to the same object in memory as the caller’s argument. -3. **Function Execution**: Control is transferred to the function body. The statements in the body are executed in a sequential manner until the function hits a return statement or the end of the function body. +3. **Bytecode Execution**: The interpreter executes the function’s **bytecode** sequentially. If the function is a generator (uses `yield`), execution can be suspended and resumed. -4. **Return**: If a return statement is encountered, the function evaluates the expression following the `return` and hands the value back to the caller. The stack frame of the function is then popped from the call stack. +4. **Return and Cleanup**: When a `return` is reached, the result is pushed to the caller's stack. The function's **stack frame** is popped, and the local variables' reference counts are decremented, potentially triggering **Garbage Collection**. -5. **Post Execution**: If there's no `return` statement, or if the function ends without evaluating any return statement, `None` is implicitly returned. +```python +def calculate_power(base: int, exponent: int = 2) -> int: + """Calculates base raised to the exponent.""" + result = base ** exponent # 'result' is a local variable + return result + +# Function call +output = calculate_power(5, 3) +``` + +### Variable Scope and Resolution -### Local Variable Scope +Python follows the **LEGB Rule** for variable resolution: -- **Function Parameters**: These are a precursor to local variables and are instantiated with the values passed during function invocation. -- **Local Variables**: Created using an assignment statement inside the function and cease to exist when the function execution ends. -- **Nested Scopes**: In functions within functions (closures), non-local variables - those defined in the enclosing function - are accessible but not modifiable by the inner function, without using the `nonlocal` keyword. +- **Local (L)**: Names assigned within the function (and not declared global). +- **Enclosing (E)**: Names in the local scope of any enclosing functions (relevant for **closures**). +- **Global (G)**: Names assigned at the top level of the module or declared `global`. +- **Built-in (B)**: Names pre-assigned in the built-in names module (e.g., `len`, `range`). -### Global Visibility +#### Modifying Non-Local Variables +To modify a variable outside the local scope, the `global` or `nonlocal` keywords must be used. Without them, assigning to a variable of the same name creates a new local variable, shielding the outer one. -If a variable is not defined within a function, the Python runtime will look for it in the global scope. This behavior enables functions to access and even modify global variables. +### Memory Management and Optimization -### Avoiding Side Effects +Functions in Python 3.11+ benefit from the **Specializing Adaptive Interpreter**, which optimizes function calls by identifying "hot" code paths. +- **Local Variable Access**: Accessing local variables is faster than global variables because locals are stored in a fixed-size array within the **stack frame**, whereas globals require a dictionary lookup. +- **Recursion Limit**: Python imposes a maximum recursion depth to prevent stack overflows, which can be checked via `sys.getrecursionlimit()`. -Functions offer a level of encapsulation, potentially reducing side effects by ensuring that data and variables are managed within a controlled environment. Such containment can help enhance the robustness and predictability of a codebase. As a best practice, minimizing the reliance on global variables can lead to more maintainable, reusable, and testable code. +#### Mathematical Representation of Execution +The complexity of a simple function call in terms of time is generally $O(1)$ for the overhead, though the internal logic determines the overall complexity $O(f(n))$.
## 12. What is a _lambda function_, and where would you use it? -A **Lambda function**, or **lambda**, for short, is a small anonymous function defined using the `lambda` keyword in Python. - -While you can certainly use named functions when you need a function for something in Python, there are places where a lambda expression is more suitable. +A **Lambda function** is a small, anonymous function defined using the `lambda` keyword. In Python, its syntax follows the structure `lambda arguments: expression`. Unlike standard functions defined with `def`, lambdas are restricted to a single expression and are often used for short-lived operations where a formal function name would be unnecessary. ### Distinctive Features -- **Anonymity**: Lambdas are not given a name in the traditional sense, making them suited for one-off uses in your codebase. -- **Single Expression Body**: Their body is limited to a single expression. This can be an advantage for brevity but a restriction for larger, more complex functions. -- **Implicit Return**: There's no need for an explicit `return` statement. -- **Conciseness**: Lambdas streamline the definition of straightforward functions. +- **Anonymity**: Lambdas are not bound to a name by default, making them suitable for one-off tasks. +- **Single Expression Body**: Their body is limited to a single logical expression. It cannot contain statements (e.g., `pass`, `assert`, or assignments) or multiple lines. +- **Implicit Return**: The evaluated result of the expression is returned automatically; the `return` keyword is not used. +- **Conciseness**: They reduce boilerplate code by allowing inline function definitions. ### Common Use Cases -- **Map, Filter, and Reduce**: Functions like `map` can take a lambda as a parameter, allowing you to define simple transformations on the fly. For example, doubling each element of a list can be achieved with `list(map(lambda x: x*2, my_list))`. -- **List Comprehensions**: They are a more Pythonic way of running the same `map` or `filter` operations, often seen as an alternative to lambdas and `map`. -- **Sorting**: Lambdas can serve as a custom key function, offering flexibility in sort orders. -- **Callbacks**: Often used in events where a function is needed to be executed when an action occurs (e.g., button click). -- **Simple Functions**: For functions that are so basic that giving them a name, especially in more procedural code, would be overkill. +- **Functional Programming Tools**: Lambdas are frequently passed as arguments to higher-order functions like `map()`, `filter()`, and `reduce()`. + ```python + # Doubling elements in a list: f(x) = x \cdot 2 + numbers = [1, 2, 3, 4] + doubled = list(map(lambda x: x * 2, numbers)) + ``` +- **Custom Sorting and Min/Max**: They serve as the `key` argument for sorting complex data structures. + ```python + # Sorting a list of dictionaries by a specific key + data = [{'name': 'Alpha', 'id': 3}, {'name': 'Beta', 'id': 1}] + sorted_data = sorted(data, key=lambda x: x['id']) + ``` +- **Callbacks**: In event-driven programming or GUI frameworks (like Tkinter or PyQt), lambdas provide a quick way to define small callback actions. +- **Closures**: They can be used to generate specific functions within a wrapper. + ```python + def multiplier(n): + return lambda x: x * n + ``` ### Notable Limitations -- **Lack of Verbose Readability**: Named functions are generally preferred when their intended use is obvious from the name. Lambdas can make code harder to understand if they're complex or not used in a recognizable pattern. -- **No Formal Documentation**: While the function's purpose should be apparent from its content, a named function makes it easier to provide direct documentation. Lambdas would need a separate verbal explanation, typically in the code or comments. +- **Readability**: Overusing lambdas or using them for complex logic violates the "Zen of Python" principle that "readability counts." +- **Debugging Hurdles**: Because they are anonymous, tracebacks identify them only as ``, which can make pinpointing errors more difficult than with named functions. +- **No Documentation**: Lambdas do not support docstrings, making it impossible to provide built-in documentation for the logic they perform. +- **Alternative Preference**: In modern Python (up to 2026), **List Comprehensions** and **Generator Expressions** are often preferred over `map` and `filter` with lambdas for better performance and clarity. + - *Example*: `[x * 2 for x in numbers]` is generally preferred over `map(lambda x: x * 2, numbers)`.
## 13. Explain _*args_ and _**kwargs_ in _Python_. -In Python, `*args` and `**kwargs` are often used to pass a variable number of arguments to a function. +In Python, `*args` and `**kwargs` allow a function to accept a variable number of arguments, providing flexibility in function definitions. -`*args` collects a variable number of positional arguments into a **tuple**, while `**kwargs` does the same for keyword arguments into a **dictionary**. +`*args` collects positional arguments into a **tuple**, while `**kwargs` collects keyword arguments into a **dictionary**. -Here are the key features, use-cases, and their respective code examples. +### **\*args**: Variable Positional Arguments -### **\*args**: Variable Number of Positional Arguments +- **Mechanism**: The asterisk (`*`) operator packs any remaining positional arguments into a **tuple**. While `args` is the naming convention, any identifier can be used. +- **Use-Case**: When the number of input values is not predefined, such as in mathematical aggregations or list processing. -- **How it Works**: The name `*args` is a convention. The asterisk (*) tells Python to put any remaining positional arguments it receives into a tuple. - -- **Use-Case**: When the number of arguments needed is uncertain. - -#### Code Example: "*args" +#### Code Example: `*args` ```python def sum_all(*args): - result = 0 - for num in args: - result += num - return result + # args is treated as a tuple: (1, 2, 3, 4) + return sum(args) print(sum_all(1, 2, 3, 4)) # Output: 10 ``` -### **\*\*kwargs**: Variable Number of Keyword Arguments +### **\*\*kwargs**: Variable Keyword Arguments -- **How it Works**: The double asterisk (**) is used to capture keyword arguments and their values into a dictionary. +- **Mechanism**: The double asterisk (`**`) operator captures named arguments into a **dictionary**, where the parameter names are keys and their values are the dictionary values. +- **Use-Case**: For functions requiring flexible configuration, handling optional parameters, or forwarding arguments to other functions (decorators). -- **Use-Case**: When a function should accept an arbitrary number of keyword arguments. - -#### Code Example: "**kwargs" +#### Code Example: `**kwargs` ```python -def print_values(**kwargs): +def print_profile(**kwargs): + # kwargs is treated as a dictionary for key, value in kwargs.items(): print(f"{key}: {value}") -# Keyword arguments are captured as a dictionary -print_values(name="John", age=30, city="New York") +print_profile(name="John", age=30, city="New York") # Output: # name: John # age: 30 # city: New York ``` + +### **Argument Order and Unpacking** + +In Python, there is a strict order for function parameters to avoid ambiguity: +1. Formal positional arguments. +2. `*args`. +3. Keyword-only arguments. +4. `**kwargs`. + +Additionally, the `*` and `**` operators can be used for **unpacking** sequences and dictionaries into function calls: + +```python +numbers = [1, 2, 3] +config = {"color": "red", "size": "large"} + +# Unpacking into function call +some_function(*numbers, **config) +``` + +This syntax ensures that functions remain extensible and maintainable, especially when the specific requirements of the caller may change over time.
## 14. What are _decorators_ in _Python_? -In Python, a **decorator** is a design pattern and a feature that allows you to modify functions and methods dynamically. This is done primarily to keep the code clean, maintainable, and DRY (Don't Repeat Yourself). +In Python, a **decorator** is a design pattern and a language feature that allows you to modify or extend the behavior of functions, methods, or classes dynamically. This is primarily used to keep code clean, maintainable, and **DRY** (Don't Repeat Yourself) by separating cross-cutting concerns from core logic. ### How Decorators Work -- Decorators wrap a target function, allowing you to execute custom code before and after that function. -- They are typically **higher-order functions** that take a function as an argument and return a new function. -- This paradigm of "functions that modify functions" is often referred to as **metaprogramming**. +- **Wrappers**: Decorators wrap a target function, allowing you to execute custom code before and after the target executes. +- **Higher-Order Functions**: They are functions that take another function as an argument and return a new function. +- **Metaprogramming**: This paradigm of "code that manipulates code" allows for powerful abstractions at runtime. ### Common Use Cases -- **Authorization and Authentication**: Control user access. -- **Logging**: Record function calls and their parameters. -- **Caching**: Store previous function results for quick access. -- **Validation**: Verify input parameters or function output. -- **Task Scheduling**: Execute a function at a specific time or on an event. -- **Counting and Profiling**: Keep track of the number of function calls and their execution time. +- **Authorization and Authentication**: Validating user permissions before executing logic. +- **Logging**: Recording function calls, arguments, and execution states. +- **Caching/Memoization**: Storing expensive function results (e.g., `functools.lru_cache`). +- **Validation**: Enforcing constraints on input parameters or return values. +- **Profiling**: Measuring execution time: $T_{total} = T_{end} - T_{start}$. +- **Framework Routing**: Mapping functions to web routes in libraries like FastAPI or Flask. ### Using Decorators in Code -Here is the Python code: +In modern Python (2026), decorators frequently utilize **Type Hinting** for better static analysis and IDE support. ```python from functools import wraps +from typing import Callable, Any # 1. Basic Decorator -def my_decorator(func): - @wraps(func) # Ensures the original function's metadata is preserved - def wrapper(*args, **kwargs): - print('Something is happening before the function is called.') +def my_decorator(func: Callable) -> Callable: + @wraps(func) # Preserves original function metadata + def wrapper(*args: Any, **kwargs: Any) -> Any: + print('Executing logic before the function.') result = func(*args, **kwargs) - print('Something is happening after the function is called.') + print('Executing logic after the function.') return result return wrapper @my_decorator -def say_hello(): - print('Hello!') - -say_hello() +def say_hello(name: str) -> None: + print(f'Hello, {name}!') -# 2. Decorators with Arguments -def decorator_with_args(arg1, arg2): - def actual_decorator(func): +# 2. Decorators with Arguments (Decorator Factory) +def repeat(times: int) -> Callable: + def actual_decorator(func: Callable) -> Callable: @wraps(func) - def wrapper(*args, **kwargs): - print(f'Arguments passed to decorator: {arg1}, {arg2}') - result = func(*args, **kwargs) + def wrapper(*args: Any, **kwargs: Any) -> Any: + result = None + for _ in range(times): + result = func(*args, **kwargs) return result return wrapper return actual_decorator -@decorator_with_args('arg1', 'arg2') -def my_function(): - print('I am decorated!') - -my_function() +@repeat(times=3) +def greet() -> None: + print('Greetings!') ``` -### Decorator Syntax in Python +### Decorator Syntax and Metadata -The `@decorator` syntax is a convenient shortcut for: +The `@decorator` syntax is **syntactic sugar** for passing the function into the decorator and reassigning the identifier: ```python def say_hello(): @@ -793,26 +825,29 @@ def say_hello(): say_hello = my_decorator(say_hello) ``` -### Role of **functools.wraps** +#### Role of functools.wraps -When defining decorators, particularly those that return functions, it is good practice to use `@wraps(func)` from the `functools` module. This ensures that the original function's metadata, such as its name and docstring, is preserved. +When a function is decorated, its identity technically changes to the internal `wrapper`. Using `@wraps(func)` is a best practice that copies the original function’s **metadata** (such as `__name__`, `__doc__`, and type annotations) to the wrapper. This ensures that introspection tools and debuggers correctly identify the original function.
## 15. How can you create a _module_ in _Python_? -You can **create** a Python module through one of two methods: +A **module** in Python is a file containing Python definitions and statements. It allows you to logically organize your code and promotes reusability across different projects. + +### Methods of Creation -- **Define**: Begin with saving a Python file with `.py` extension. This file will automatically function as a module. +You can **create** a Python module through the following standard methods: -- **Create a Blank Module**: Start an empty file with no extension. Name the file using the accepted module syntax, e.g., `__init__ `, for it to act as a module. +- **Define a .py File**: Save a Python file with a `.py` extension. This file automatically functions as a module, where the filename is the module's name. +- **Initialize a Package**: To group multiple modules, create a directory containing an `__init__.py` file. This tells Python to treat the directory as a **package**. -Next, use **import** to access the module and its functionality. +Next, use the **import** statement to access the module and its functionality within other scripts. ### Code Example: Creating a `math_operations` Module #### Module Definition -Save the below `math_operations.py` file : +Save the following code as `math_operations.py`: ```python def add(x, y): @@ -825,12 +860,12 @@ def multiply(x, y): return x * y def divide(x, y): - return x / y + return x / y if y != 0 else "Error: Division by zero" ``` #### Module Usage -You can use `math_operations` module by using import as shown below: +You can utilize the `math_operations` module by using **import** as shown below: ```python import math_operations @@ -842,27 +877,31 @@ result = math_operations.divide(10, 5) print(result) ``` -Even though it is not required in the later **versions of Python**, you can also use statement `from math_operations import *` to import all the members such as functions and classes at once: +You can also use the statement `from math_operations import *` to import all members (functions and classes) at once, though this is generally discouraged to avoid **namespace pollution**: ```python -from math_operations import * # Not recommended generally due to name collisions and readability concerns +from math_operations import add, subtract result = add(3, 2) print(result) ``` ### Best Practice -Before submitting the code, let's make sure to follow the **Best Practice**: +To ensure high-quality and maintainable code, follow these **Best Practices**: -- **Avoid Global Variables**: Use a `main()` function. -- **Guard Against Code Execution on Import**: To avoid unintended side effects, use: +- **Avoid Global Variables**: Encapsulate logic within functions or classes. +- **Guard Against Code Execution**: To prevent code from running automatically when the module is imported, use the following block: ```python +def main(): + # Primary logic or tests go here + print("Module executed directly") + if __name__ == "__main__": main() ``` -This makes sure that the block of code following `if __name__ == "__main__":` is only executed when the module is run directly and not when imported as a module in another program. +This ensures that the block following `if __name__ == "__main__":` is only executed when the file is run as a **standalone script**, and not when it is imported as a dependency in another module.