> ## Documentation Index
> Fetch the complete documentation index at: https://rajanand.org/llms.txt
> Use this file to discover all available pages before exploring further.

# Python Functions

### Functions

A function is a reusable block of code designed to perform a specific task.  Using functions helps avoid repetition and makes code easier to manage.

* **Declaration**: You declare a function using the `def` keyword, followed by the function's name and parentheses.
  * `def calculate_tax(bill, tax_rate):`
* **Parameters**: You can pass data into a function through parameters (also called arguments) inside the parentheses.  This makes the function dynamic.
* **Returning Data**: A function can send data back using the `return` keyword.
  * `return bill * tax_rate / 100`
* **Calling a Function**: A function only runs when it is "called" by its name.
  * `total = calculate_tax(175.00, 15)`

### Variable Scope

Scope determines the accessibility of a variable.  It's a way to protect variables from being changed unintentionally by other parts of the code.  The four types of scope follow the LEGB rule.

* **Local (L)**: The variable is declared inside a function and is only accessible within that function.
* **Enclosing (E)**: This applies to nested functions. The inner function can access variables from the outer (enclosing) function.
* **Global (G)**: The variable is declared outside of any function and can be accessed from anywhere in the code.  Using global variables is generally discouraged because it increases the chance of errors.
* **Built-in (B)**: These are the reserved keywords in Python, like `print` and `def`.  They are always accessible.

The general rule is that inner scopes can access variables from outer scopes, but the reverse is not true.

## Data Structures

### Lists

A list is a flexible, ordered sequence that can hold multiple items of different data types.  Think of it as a dynamic array.

* **Declaration**: Use square brackets `[]` with items separated by commas.
  * `my_list = ["a", 1, True, 3.14]`
* **Accessing Items**: Items are accessed by their index, which always starts at 0.
  * `my_list[0]` would return `"a"`.
* **Key Methods**:
  * **Adding**: `.append(item)` adds to the end , `.insert(index, item)` adds at a specific position , and `.extend([items])` adds multiple items to the end.
  * **Removing**: `.pop(index)` removes an item at a specific index , and `del my_list[index]` does the same.
* **Iteration**: You can loop through a list using a simple `for` loop.

### Tuples

A tuple is an ordered sequence used to store data, similar to a list.

* **Declaration**: Use parentheses `()`.
  * `my_tuple = (1, "string", 4.5, True)`
* **Key Difference**: Tuples are **immutable**, which means their contents cannot be changed after they are created.  Any attempt to change an item will result in a `TypeError`.
* **Key Methods**:
  * `.count(value)`: Returns the number of times a value appears in the tuple.
  * `.index(value)`: Returns the index of a given value.

### Sets

A set is a collection of items with two main properties: it is **unordered** and **does not allow duplicate values**.

* **Declaration**: Use curly braces `{}`.
  * `my_set = {1, 2, 3, 4, 5}`
* **Key Properties**:
  * If you add a duplicate item, it is ignored.
  * Since sets are unordered, you cannot access items using an index.
* **Set Operations**: Sets are useful for mathematical operations.
  * `.union()` or `|`: Combines two sets.
  * `.intersection()` or `&`: Finds items that exist in both sets.
  * `.difference()` or `-`: Finds items present in the first set but not the second.
  * `.symmetric_difference()` or `^`: Finds items present in either set, but not both.

### Dictionaries

A dictionary is a collection of `key-value` pairs.  They are optimized for retrieving values quickly when you know the key.

* **Declaration**: Use curly braces `{}` with `key: value` pairs.
  * `my_dict = {"name": "Jim", 1: "test"}`
* **Key Properties**:
  * Keys must be unique; a duplicate key will overwrite the previous value.
  * Values are mutable and can be changed.
* **Accessing and Modifying**:
  * Access a value using its key in square brackets: `my_dict["name"]`.
  * Add or update a value: `my_dict[key] = new_value`.
  * Delete an item: `del my_dict[key]`.
* **Iteration**:
  * A simple `for` loop iterates over the keys by default.
  * To get both the key and the value, use the `.items()` method: `for key, value in my_dict.items():`.

## Advanced Function Arguments

### `*args` and `**kwargs`

These are used to allow a function to accept a variable number of arguments.

* **`*args`**: Allows you to pass any number of non-keyword (positional) arguments.  Inside the function, `args` acts as a tuple containing all the passed arguments.
  * **Usage**: `def my_function(*args):`
* **`**kwargs`**: Allows you to pass any number of keyword arguments (e.g., `bill=10.53`).  Inside the function, `kwargs` acts as a dictionary containing the passed arguments.
  * **Usage**: `def my_function(**kwargs):`

## Error and Exception Handling

### Types of Errors

* **Syntax Errors**: Mistakes made by the developer, such as typos or breaking Python's grammar rules (like a missing colon).  Most code editors will help you find these.
* **Exceptions**: Errors that occur while the code is running, even if the syntax is correct.  An example is the `ZeroDivisionError` that occurs when trying to divide by zero.  Exceptions must be "handled" to prevent the program from crashing.

### The `try...except` Block

This is how you handle exceptions in Python.

* **How it Works**:
  * You place the code that might cause an error inside the `try` block.
  * If an error occurs, the code inside the `except` block is executed.
* **Catching Specific Exceptions**:
  * You can make your handling more precise by catching a specific error type, like `except ZeroDivisionError:`.
  * You can access the error information itself by using `as e`: `except ZeroDivisionError as e:`.
  * You can chain multiple `except` blocks to handle different potential errors.

## File Handling

### Opening and Closing Files

Python has built-in functions to create, read, and write files.

* **The `open()` function**: Takes two main arguments: the file name and the mode.
  * **Modes**: `'r'` for read, `'w'` for write (overwrites the file), and `'a'` for append (adds to the end).  Adding `b` (e.g., `'rb'`) opens the file in binary mode, which is more compact but not human-readable.
* **Reading**: After opening a file, you can use `.readline()` to read one line or `.readlines()` to read all lines into a list.
* **Closing**: It is important to close the file connection with `file.close()` when you are done.

### The `with open()` Statement

This is the recommended way to work with files.

* **Usage**: `with open('test.txt', 'r') as my_file:`
* **Advantage**: It automatically closes the file for you, even if errors occur.  This makes your code safer and cleaner.

## Creating and Writing to Files

When you're working with code, any data stored in variables is held in your computer's RAM (Random Access Memory), which is **temporary**. To store data permanently, you need to write it to a file. Python makes this easy using the `open()` function combined with a specific **mode**.

The best practice is to use a `with` statement, as it automatically handles closing the file for you.

### Write vs. Append Modes

You primarily use two modes for writing to files:

* **Write Mode (`'w'`)**: This mode creates a new file. **Be careful**, because if the file already exists, this mode will **completely overwrite** its contents every time you run the script.
* **Append Mode (`'a'`)**: This mode adds new content to the **end** of an existing file. If the file doesn't exist, it will be created. This is useful when you want to add to a file without deleting its previous content.

### How to Write Content

You have two main methods for writing your data:

* **`.write()`**: Use this to write a single string of text to the file.
* **`.writelines()`**: This method takes a **list of strings** and writes each one to the file.

It's important to remember that Python doesn't automatically add new lines. You have to explicitly tell it where to break a line by adding the **newline character** (`\n`) in your strings.

#### Example: Creating a File and Writing Lines

Here’s how you can create a file called `shopping_list.txt` and add some items to it.

```python theme={"system"}
# A list of items to add to our file
items_to_buy = [
    "Milk\n",
    "Bread\n",
    "Eggs\n"
]

try:
    # 'w' mode will create shopping_list.txt and let us write to it
    with open('shopping_list.txt', 'w') as file:
        file.write("My Shopping List:\n") # Using .write() for the title
        file.writelines(items_to_buy)    # Using .writelines() for the list
    print("File created successfully!")
except Exception as e:
    print(f"An error occurred: {e}")

```

If you run this code, it will create a new file named `shopping_list.txt`. If you run it again, it will overwrite the old one with a fresh copy.

#### Example: Appending to the File

Now, let's add another item to the list without deleting what's already there. We just need to change the mode to `'a'`.

```python theme={"system"}
try:
    # 'a' mode will open the existing file and let us add to the end
    with open('shopping_list.txt', 'a') as file:
        file.write("Cheese\n")
    print("Item appended successfully!")
except Exception as e:
    print(f"An error occurred: {e}")
```

Running this will add "Cheese" to the bottom of your shopping list.

***

## Reading From Files

Once you have data in a file, you'll need to read it back into your program. Python provides a few straightforward methods for this.

### Methods for Reading Files

There are three key methods you can use to read content:

* **`.read()`**: This reads the **entire contents** of the file and returns it as a **single string**. You can also give it a number, like `.read(10)`, to read only the first 10 characters.
* **`.readline()`**: This reads just **one line** from the file at a time, returning it as a string.
* **`.readlines()`**: This reads the **entire contents** of the file and returns it as a **list of strings**, where each item in the list is a line from the file. This is very useful because you can easily loop over the list.

#### Example: Reading a File

Let's read the `shopping_list.txt` file we created earlier. The most efficient way is often to loop directly over the file object itself.

```python theme={"system"}
try:
    with open('shopping_list.txt', 'r') as file:
        print("--- Here is your shopping list ---")
        for line in file:
            # The .strip() removes any extra whitespace or newline characters
            print(line.strip())
except FileNotFoundError:
    print("Error: The file was not found.")
except Exception as e:
    print(f"An error occurred: {e}")
```

This code opens the file in `'r'` (read) mode and prints each line neatly, demonstrating the most common and practical way to read a file's content line by line.

### File Paths: Absolute vs. Relative

When you open a file, you need to tell Python where to find it.

* **Relative Path**: This is a path from your **current working directory**. For example, `shopping_list.txt` works if the file is in the same folder as your Python script. It's simple, but your code might break if you move the files around.
* **Absolute Path**: This is the **full path** from the root of your file system, like `C:\Users\YourUser\Documents\shopping_list.txt`. It's more specific and will always find the file, no matter where your script is running from.

***

[Source](https://www.coursera.org/learn/programming-in-python)
