{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "dd98e2d7",
   "metadata": {},
   "source": [
    "# Functions: Different aspects\n",
    "\n",
    "Functions are the fundamental building blocks of Python. They allow us to wrap a block of code and give it a name, making our code modular, reusable, and much easier to maintain. \n",
    "\n",
    "The primary advantage of functions is **reusability**: you define the logic once and can call it many times throughout your program. This makes your code significantly shorter and easier to understand. Furthermore, thanks to **modularity**, you can focus on different functionalities separately, testing and debugging individual components without worrying about the entire system at once."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5ea96d92",
   "metadata": {},
   "source": [
    "## 1. Defining and Calling Functions\n",
    "\n",
    "To define a function, we use the `def` keyword, followed by the function name, parentheses `()`, and a colon `:`. The code block inside the function is indented."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "75672d22",
   "metadata": {},
   "outputs": [],
   "source": [
    "def greet():\n",
    "    \"\"\"This is a simple greeting function.\"\"\"\n",
    "    print(\"Hello! Welcome to the Python Functions lesson.\")\n",
    "\n",
    "# Calling the function\n",
    "greet()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "tasks1",
   "metadata": {},
   "source": [
    "### Tasks\n",
    "1. Define a function `show_current_date` that prints today's date (you can just print a hardcoded string for now).\n",
    "2. Create a function `display_message` that prints a short message about why you are learning Python.\n",
    "3. Define a function `empty_function` using the `pass` keyword and call it.\n",
    "4. Write a function `greet_user(name)` that takes a name as a parameter and prints a personalized greeting."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "05846df5",
   "metadata": {},
   "source": [
    "## 2. Function Parameters and Arguments\n",
    "\n",
    "Parameters are the names listed in the function's definition. Arguments are the real values passed to the function when it is called."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "734f94bf",
   "metadata": {},
   "source": [
    "### Positional Arguments\n",
    "The most common way to pass arguments; their order matters."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "01d09ebc",
   "metadata": {},
   "outputs": [],
   "source": [
    "def describe_pet(animal_type, pet_name):\n",
    "    print(f\"I have a {animal_type} named {pet_name.title()}.\")\n",
    "\n",
    "describe_pet('hamster', 'harry')\n",
    "describe_pet('dog', 'willie')"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "442cb681",
   "metadata": {},
   "source": [
    "### Keyword Arguments\n",
    "You can specify the parameter name when passing an argument, so the order no longer matters."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "6d4de3f7",
   "metadata": {},
   "outputs": [],
   "source": [
    "describe_pet(pet_name='harry', animal_type='hamster')"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a8669574",
   "metadata": {},
   "source": [
    "### Default Values\n",
    "You can provide a default value for a parameter. If the caller doesn't provide an argument for it, the default value will be used."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "6b8a481d",
   "metadata": {},
   "outputs": [],
   "source": [
    "def describe_pet(pet_name, animal_type='dog'):\n",
    "    print(f\"I have a {animal_type} named {pet_name.title()}.\")\n",
    "\n",
    "describe_pet(pet_name='willie') # animal_type defaults to 'dog'\n",
    "describe_pet('mimi', 'cat')      # animal_type is overridden"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "passing_lists",
   "metadata": {},
   "source": [
    "**Note on Passing Lists:**\n",
    "When you pass a list (or any mutable object like a dictionary or set) to a function, it is passed by **reference**, not by copy. This means if you modify the list inside the function, those changes will persist outside the function."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "tasks2",
   "metadata": {},
   "source": [
    "### Tasks\n",
    "1. Modify `describe_pet` to include a third parameter `age` and print it.\n",
    "2. Create a function `make_shirt(size, message)` and call it using positional arguments.\n",
    "3. Call `make_shirt` using keyword arguments.\n",
    "4. Define `make_shirt` with a default `size` of 'L' and call it with only the message."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b630f590",
   "metadata": {},
   "source": [
    "## 3. Returning Values\n",
    "\n",
    "The `return` statement sends a value back from the function to the caller. A function stops execution as soon as it hits a `return` statement."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "calc_example",
   "metadata": {},
   "outputs": [],
   "source": [
    "def add_numbers(num1, num2):\n",
    "    \"\"\"Return the sum of two numbers.\"\"\"\n",
    "    return num1 + num2\n",
    "\n",
    "result = add_numbers(5, 3)\n",
    "print(f\"The result of 5 + 3 is: {result}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "364e9f2c",
   "metadata": {},
   "outputs": [],
   "source": [
    "def get_formatted_name(first_name, last_name):\n",
    "    \"\"\"Return a full name, neatly formatted.\"\"\"\n",
    "    full_name = f\"{first_name} {last_name}\"\n",
    "    return full_name.title()\n",
    "\n",
    "musician = get_formatted_name('jimi', 'hendrix')\n",
    "print(musician)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "tasks3",
   "metadata": {},
   "source": [
    "### Tasks\n",
    "1. Write a function `multiply_numbers(a, b)` that returns their product.\n",
    "2. Create a function `is_even(number)` that returns `True` if a number is even, and `False` otherwise.\n",
    "3. Define a function `get_circle_area(radius)` that calculates and returns the area (use 3.14 for pi).\n",
    "4. Create a function `format_city_country(city, country)` that returns a string like \"Santiago, Chile\"."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a35fff2e",
   "metadata": {},
   "source": [
    "## 4. Arbitrary Number of Arguments (`*args` and `**kwargs`)\n",
    "\n",
    "Sometimes you don't know how many arguments will be passed to your function. Python provides `*args` for variable positional arguments and `**kwargs` for variable keyword arguments."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "f66bb86d",
   "metadata": {},
   "outputs": [],
   "source": [
    "def make_pizza(size, *toppings):\n",
    "    \"\"\"Summarize the pizza we are about to make.\"\"\"\n",
    "    print(f\"\\nMaking a {size}-inch pizza with the following toppings:\")\n",
    "    for topping in toppings:\n",
    "        print(f\"- {topping}\")\n",
    "\n",
    "make_pizza(12, 'mushrooms', 'green peppers', 'extra cheese')\n",
    "\n",
    "def build_profile(first, last, **user_info):\n",
    "    \"\"\"Build a dictionary containing everything we know about a user.\"\"\"\n",
    "    # user_info is a dictionary containing all extra keyword arguments\n",
    "    user_info['first_name'] = first\n",
    "    user_info['last_name'] = last\n",
    "    return user_info\n",
    "\n",
    "user_profile = build_profile('albert', 'einstein',\n",
    "                             location='princeton',\n",
    "                             field='physics')\n",
    "print(user_profile)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "57937465",
   "metadata": {},
   "source": [
    "**Important Note on Parameter Order:**\n",
    "When using multiple types of parameters, they must follow a specific order:\n",
    "1. Standard positional arguments\n",
    "2. `*args`\n",
    "3. `**kwargs`\n",
    "\n",
    "You **cannot** put `**kwargs` before other parameters. For example, `def build_profile(**user_info, first, last)` is **invalid** and will cause a SyntaxError."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "87464fd7",
   "metadata": {},
   "source": [
    "### Unpacking Arguments\n",
    "You can use the same operators: * and ** to unpack lists/tuples and dictionaries when calling a function\n",
    "\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "a739c0d6",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Unpacking a list with *\n",
    "my_toppings = ['pepperoni', 'extra cheese', 'onions']\n",
    "make_pizza(16, *my_toppings)\n",
    "\n",
    "# Unpacking a dictionary with **\n",
    "my_profile = {'location': 'paris', 'field': 'art'}\n",
    "print(build_profile('marie', 'curie', **my_profile))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "tasks4",
   "metadata": {},
   "source": [
    "### Tasks\n",
    "1. Create a function `sum_all(*args)` that takes any number of integers and returns their sum.\n",
    "2. Define a function `show_user_details(**kwargs)` that prints all keys and values passed to it.\n",
    "3. Create a list of your favorite foods and pass them to a function using `*`.\n",
    "4. Create a dictionary with information about a car (make, model, year) and pass it to a function using `**`."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "dbad4295",
   "metadata": {},
   "source": [
    "## 5. Scope: Local vs. Global Variables\n",
    "\n",
    "Variables defined inside a function are **local** to that function. Variables defined outside are **global**. \n",
    "\n",
    "To modify a global variable from within a function, you must use the `global` keyword."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "e55e9ee2",
   "metadata": {},
   "outputs": [],
   "source": [
    "x = 10 # Global variable\n",
    "\n",
    "def my_func():\n",
    "    x = 20 # Local variable (shadows the global x)\n",
    "    print(f\"Inside function (local), x = {x}\")\n",
    "\n",
    "my_func()\n",
    "print(f\"Outside function (global), x = {x}\")\n",
    "\n",
    "# --- Modifying Global Variables ---\n",
    "counter = 0\n",
    "\n",
    "def increment():\n",
    "    global counter\n",
    "    counter += 1\n",
    "    print(f\"Inside increment (using global keyword), counter is {counter}\")\n",
    "\n",
    "increment()\n",
    "print(f\"Outside function, final counter is {counter}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "tasks5",
   "metadata": {},
   "source": [
    "### Tasks\n",
    "1. Define a global variable `name = \"Global\"`, then a function that defines a local `name = \"Local\"`. Print both.\n",
    "2. Try to change a global string variable inside a function without using `global` and observe what happens.\n",
    "3. Create a function that uses the `global` keyword to modify a global `score` variable.\n",
    "4. Write a function that takes a list as an argument and appends an item to it. Observe if the change persists outside the function."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "first_class_objects",
   "metadata": {},
   "source": [
    "## 6. Functions as First-Class Objects\n",
    "\n",
    "In Python, functions are **first-class objects**. This means they can be treated like any other variable. You can:\n",
    "- Assign a function to a variable.\n",
    "- Pass a function as an argument to another function.\n",
    "- Return a function from another function.\n",
    "\n",
    "This fundamental feature is what allows for powerful concepts like Lambda functions, decorators, and closures."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "function_as_object_code",
   "metadata": {},
   "outputs": [],
   "source": [
    "def shout(text):\n",
    "    return text.upper()\n",
    "\n",
    "# 1. Assign to a variable\n",
    "yell = shout\n",
    "print(yell(\"hello\"))\n",
    "\n",
    "# 2. Pass as an argument\n",
    "def greet_with_style(func, message):\n",
    "    greeting = func(message)\n",
    "    print(greeting)\n",
    "\n",
    "greet_with_style(shout, \"welcome to python\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "tasks_first_class",
   "metadata": {},
   "source": [
    "### Tasks\n",
    "1. Create a function `square(x)` and assign it to a variable `f`. Call `f(5)`.\n",
    "2. Write a function `apply_operation(func, val)` that applies `func` to `val` and returns the result."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "12b2336a",
   "metadata": {},
   "source": [
    "## 7. Lambda Functions\n",
    "\n",
    "Lambda functions are small, anonymous functions defined with the `lambda` keyword. They can have any number of arguments but only one expression.\n",
    "\n",
    "**Where they are common and useful:**\n",
    "- Lambda functions are ideal for short-lived, simple logic that is passed as an argument to other functions (like `map()`, `filter()`, and `sort()`).\n",
    "- They are frequently used in data processing and functional programming patterns.\n",
    "\n",
    "**Creation and Limitations:**\n",
    "- **Syntax**: `lambda arguments: expression`\n",
    "- **Limitations**: They can only contain a single expression, not multiple statements (no `if-else` blocks, though ternary operators like `A if condition else B` are allowed). They cannot contain `return` statements (the expression itself is returned automatically)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "e0094f1c",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Simple lambda functions\n",
    "square = lambda x: x * x\n",
    "print(f\"Square of 5: {square(5)}\")\n",
    "\n",
    "add = lambda x, y: x + y\n",
    "print(f\"Sum of 3 and 4: {add(3, 4)}\")\n",
    "\n",
    "# --- Practical Examples ---\n",
    "numbers = [1, 5, 4, 6, 8, 11, 3, 12]\n",
    "\n",
    "# Using lambda with filter() to get even numbers\n",
    "evens = list(filter(lambda x: x % 2 == 0, numbers))\n",
    "print(f\"Even numbers: {evens}\")\n",
    "\n",
    "# Using lambda with map() to square each number\n",
    "squared_numbers = list(map(lambda x: x**2, numbers))\n",
    "print(f\"Squared numbers: {squared_numbers}\")\n",
    "\n",
    "# Using lambda with sort() to sort a list of tuples by the second value\n",
    "pairs = [(1, 'one'), (4, 'four'), (2, 'two'), (3, 'three')]\n",
    "pairs.sort(key=lambda pair: pair[1])\n",
    "print(f\"Sorted pairs by name: {pairs}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "tasks6",
   "metadata": {},
   "source": [
    "### Tasks\n",
    "1. Create a lambda function that calculates the cube of a number.\n",
    "2. Use a lambda function with `filter()` to get only numbers greater than 10 from a list.\n",
    "3. Use a lambda function with `map()` to double every number in a list.\n",
    "4. Sort a list of tuples `[(1, 'b'), (2, 'a')]` by the second element using a lambda function as the `key`."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a20db396",
   "metadata": {},
   "source": [
    "## 8. Type Hinting\n",
    "\n",
    "Introduced in Python 3.5, type hinting makes your code clearer and helps IDEs catch bugs. While Python remains dynamically typed, hints provide valuable documentation.\n",
    "\n",
    "For more complex types, we use the standard `typing` library."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "5e885513",
   "metadata": {},
   "outputs": [],
   "source": [
    "from typing import List, Dict, Optional, Union\n",
    "\n",
    "# Basic hints\n",
    "def multiply(a: int, b: int) -> int:\n",
    "    return a * b\n",
    "\n",
    "# Advanced hints using typing library\n",
    "def process_names(names: List[str]) -> None:\n",
    "    for name in names:\n",
    "        print(f\"Hello, {name.title()}\")\n",
    "\n",
    "def get_user_data(user_id: int) -> Dict[str, Union[str, int]]:\n",
    "    # Returns a dictionary with string keys and values that can be str or int\n",
    "    return {\"id\": user_id, \"name\": \"John Doe\", \"age\": 30}\n",
    "\n",
    "def find_item(item_id: int) -> Optional[str]:\n",
    "    # Returns a string or None\n",
    "    items = {1: \"Apple\", 2: \"Banana\"}\n",
    "    return items.get(item_id)\n",
    "\n",
    "print(multiply(10, 5))\n",
    "process_names([\"alice\", \"bob\"])\n",
    "print(get_user_data(123))\n",
    "print(f\"Found item: {find_item(1)}\")\n",
    "print(f\"Missing item: {find_item(99)}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "tasks7",
   "metadata": {},
   "source": [
    "### Tasks\n",
    "1. Add type hints to a function that takes a string and returns its length as an integer.\n",
    "2. Use `List` from the `typing` module to hint a function that takes a list of integers.\n",
    "3. Define a function that can return either a string or `None` using `Optional`.\n",
    "4. Create a function that takes a dictionary where keys are strings and values are integers, using `Dict` for type hinting."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f2a1b3c4",
   "metadata": {},
   "source": [
    "## 9. Nested Functions and Closures\n",
    "\n",
    "Functions can be defined inside other functions. A **closure** occurs when a nested function references a variable from its even-closing scope."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "d5e6f7a8",
   "metadata": {},
   "outputs": [],
   "source": [
    "def outer_function(text):\n",
    "    def inner_function():\n",
    "        print(text)\n",
    "    return inner_function\n",
    "\n",
    "my_func = outer_function(\"Hello from the closure!\")\n",
    "my_func()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "tasks8",
   "metadata": {},
   "source": [
    "### Tasks\n",
    "1. Create a function `make_multiplier(n)` that returns a function that multiplies its input by `n`.\n",
    "2. Write a nested function where the inner function accesses a variable from the outer function's scope."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b9c0d1e2",
   "metadata": {},
   "source": [
    "## 10. Recursion\n",
    "\n",
    "A recursive function is a function that calls itself. It must have a **base case** to prevent infinite recursion."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "a3b4c5d6",
   "metadata": {},
   "outputs": [],
   "source": [
    "def factorial(n):\n",
    "    \"\"\"Calculate the factorial of a number recursively.\"\"\"\n",
    "    if n <= 1: # Base case\n",
    "        return 1\n",
    "    else:\n",
    "        return n * factorial(n - 1) # Recursive call\n",
    "\n",
    "print(factorial(5))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "tasks9",
   "metadata": {},
   "source": [
    "### Tasks\n",
    "1. Write a recursive function `sum_recursive(n)` that calculates the sum of numbers from 1 to `n`.\n",
    "2. Create a recursive function to calculate the $n$-th Fibonacci number.\n",
    "3. Add a print statement to your `factorial` function to trace each recursive call."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "689a6a58",
   "metadata": {},
   "source": [
    "## 11. Best Practices (PEP 8, PEP 257)\n",
    "\n",
    "Following Python's official style guides ensures your code is readable and maintainable.\n",
    "\n",
    "**Some PEP 8 Rules for Functions:**\n",
    "- **Naming**: Use `snake_case` for function and variable names to improve readability.\n",
    "- **Indentation**: Use 4 spaces per indentation level. Never mix tabs and spaces.\n",
    "- **Whitespace**: Surround top-level function definitions with two blank lines. Surround method definitions inside a class with a single blank line.\n",
    "- **Arguments**: Do not use spaces around the `=` sign when used to indicate a keyword argument or a default parameter value (e.g., `def func(a=1):` is correct, `def func(a = 1):` is not).\n",
    "- **Docstrings (PEP 257)**: Always document your functions using triple quotes `\"\"\"`. The first line should be a short summary of the function's effect.\n",
    "\n",
    "You can find all PEP 8 documentation at the following link: https://peps.python.org/pep-0008/\n"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.12.0"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
