5. Chapter 5: Python Functions¶
5.1. Python Functions¶
5.1.1. 1. Defining a Basic Function¶
A function is defined using the def keyword followed by the function name and parentheses:
def greet():
print("Hello, Python!")
greet()
5.1.2. 2. Function with Parameters¶
Parameters allow functions to accept external input:
def greet_user(name):
print(f"Hello, {name}!")
greet_user("Alice")
5.1.3. 3. Function with Return Value¶
The return statement sends a value back to the caller:
def add(a, b):
return a + b
result = add(5, 3)
print(result) # Output: 8
5.1.4. 4. Default Parameters¶
Default values are used when no argument is provided:
def greet(name="Guest"):
print(f"Hello, {name}!")
greet()
greet("Bob")
5.1.5. 5. Keyword Arguments¶
Arguments can be passed by name for clarity:
def student_info(name, age):
print(f"Name: {name}, Age: {age}")
student_info(age=20, name="Alice")
5.1.6. 6. *Variable-Length Arguments (args)¶
Allows passing multiple positional arguments:
def sum_numbers(*args):
return sum(args)
print(sum_numbers(1, 2, 3, 4)) # Output: 10
5.1.7. 7. **Keyword Variable-Length Arguments (kwargs)¶
Accepts arbitrary keyword arguments:
def display_profile(**kwargs):
for key, value in kwargs.items():
print(f"{key}: {value}")
display_profile(name="Alice", role="Engineer")
5.1.8. 8. Function with Both *args and kwargs¶
Supports flexible function signatures:
def complete_profile(*args, **kwargs):
print("Positional:", args)
print("Keyword:", kwargs)
complete_profile("Python", level="Advanced", year=2025)
5.1.9. 9. Lambda (Anonymous) Functions¶
Used for short, inline function definitions:
square = lambda x: x ** 2
print(square(5)) # Output: 25
5.1.10. 10. Recursive Functions¶
A function calling itself to solve repetitive problems:
def factorial(n):
if n == 0:
return 1
return n * factorial(n - 1)
print(factorial(5)) # Output: 120
5.2. Python Function Arguments¶
5.2.1. 1. Positional Arguments¶
Arguments are passed in the order defined by the function:
def display_info(name, age):
print(f"Name: {name}, Age: {age}")
display_info("Alice", 25)
5.2.2. 2. Keyword Arguments¶
Arguments are passed using parameter names, improving readability and flexibility:
def display_info(name, age):
print(f"Name: {name}, Age: {age}")
display_info(age=25, name="Alice")
5.2.3. 3. Default Arguments¶
Default values are used when arguments are not supplied:
def greet(name="Guest"):
print(f"Hello, {name}")
greet()
greet("Bob")
5.2.4. 4. Required Arguments¶
Arguments without defaults must be provided:
def login(username, password):
print(f"User: {username}")
login("admin", "1234")
# login("admin") # Raises TypeError
5.2.5. 5. *Variable-Length Positional Arguments (args)¶
*args collects extra positional arguments into a tuple:
def calculate_sum(*numbers):
return sum(numbers)
print(calculate_sum(1, 2, 3)) # 6
print(calculate_sum(5, 10, 15, 5)) # 35
5.2.6. 6. **Variable-Length Keyword Arguments (kwargs)¶
**kwargs collects extra keyword arguments into a dictionary:
def user_profile(**details):
for key, value in details.items():
print(f"{key}: {value}")
user_profile(name="Alice", role="Developer", level="Senior")
5.2.7. 7. Combining Normal, *args, and kwargs¶
Order of parameters must follow: normal → *args → kwargs
def full_profile(name, *skills, **info):
print("Name:", name)
print("Skills:", skills)
print("Details:", info)
full_profile("Alice", "Python", "ML", age=30, city="Toronto")
5.2.8. 8. Positional-Only Arguments (Python 3.8+)¶
Parameters before / must be positional-only:
def divide(a, b, /):
return a / b
print(divide(10, 2))
# divide(a=10, b=2) # Raises TypeError
5.2.9. 9. Keyword-Only Arguments¶
Parameters after * must be specified as keyword arguments:
def configure(*, mode="light"):
print(f"Mode: {mode}")
configure(mode="dark")
5.2.10. 10. Argument Unpacking¶
and ** can unpack tuples/lists and dictionaries directly into function arguments:
def multiply(a, b, c):
return a * b * c
values = (2, 3, 4)
print(multiply(*values)) # Output: 24
data = {"a": 1, "b": 2, "c": 3}
print(multiply(**data)) # Output: 6
5.3. Python Variable Scope¶
5.3.1. 1. Local Scope¶
Local variables exist only within the function where they are declared:
def show_value():
x = 10 # Local variable
print(x)
show_value()
# print(x) # NameError: x is not defined
5.3.2. 2. Global Scope¶
Global variables are accessible throughout the module:
x = 20 # Global variable
def display():
print(x)
display()
print(x)
5.3.3. 3. Local vs Global Variable Conflict¶
Local variables override global variables within function scope:
value = 50
def update():
value = 10 # Local variable shadows global
print("Inside function:", value)
update()
print("Outside function:", value)
5.3.4. 4. Using the global Keyword¶
The global keyword allows modification of global variables inside functions:
count = 0
def increment():
global count
count += 1
increment()
print(count) # Output: 1
5.3.5. 5. Enclosing Scope (Nested Functions)¶
Nested functions can access variables from their enclosing scope:
def outer():
message = "Hello"
def inner():
print(message) # Accessing enclosing variable
inner()
outer()
5.3.6. 6. Using the nonlocal Keyword¶
nonlocal allows modification of variables from the enclosing (but not global) scope:
def outer():
count = 0
def inner():
nonlocal count
count += 1
print(count)
inner()
inner()
outer()
5.3.7. 7. LEGB Rule (Scope Resolution Order)¶
Python resolves variables using the LEGB rule:
Local
Enclosing
Global
Built-in
x = "Global"
def outer():
x = "Enclosing"
def inner():
x = "Local"
print(x)
inner()
outer()
5.3.8. 8. Built-in Scope¶
Built-in functions and exceptions reside in the built-in scope:
print(len([1, 2, 3])) # Built-in function
5.3.9. 9. Scope Inside Loops¶
Unlike some languages, Python loop variables are not block-scoped:
for i in range(3):
loop_var = i
print(loop_var) # Accessible outside loop
5.3.10. 10. Practical Example of Scope Management¶
Demonstrates coordinated use of local, enclosing, and global scopes:
total = 100
def calculate():
total = 50
def adjust():
nonlocal total
total += 10
adjust()
return total
print(calculate()) # Output: 60
print(total) # Output: 100
5.4. Python Global Keyword¶
5.4.1. 1. Basic Use of Global Variables¶
Global variables are declared outside functions and are readable within functions by default:
count = 10
def display():
print(count)
display() # Output: 10
5.4.2. 2. Modifying Global Variable Without global (Error Case)¶
Assigning to a variable inside a function makes it local unless explicitly declared global:
value = 5
def update():
value = value + 1 # UnboundLocalError
5.4.3. 3. Using global to Modify Global Variables¶
The global keyword allows direct modification of a global variable:
counter = 0
def increment():
global counter
counter += 1
increment()
print(counter) # Output: 1
5.4.4. 4. Global Variable Across Multiple Functions¶
A global variable can be shared and updated across multiple functions:
status = "inactive"
def activate():
global status
status = "active"
def show_status():
print(status)
activate()
show_status() # Output: active
5.4.5. 5. Global Keyword Inside Nested Functions¶
global always refers to the module-level scope, even when used inside nested functions:
x = 100
def outer():
def inner():
global x
x = 200
inner()
outer()
print(x) # Output: 200
5.4.6. 6. Difference Between global and nonlocal¶
global → Refers to module-level variable
nonlocal → Refers to nearest enclosing function scope
x = 50
def outer():
x = 10
def inner():
global x
x = 99
inner()
print("Outer x:", x)
outer()
print("Global x:", x)
5.4.7. 7. Tracking State Using Global Variables¶
Used for maintaining application-wide counters or shared state:
requests = 0
def handle_request():
global requests
requests += 1
handle_request()
handle_request()
print(requests) # Output: 2
5.4.8. 8. Global Variable with Conditional Logic¶
Global variables often manage configuration state:
mode = "light"
def toggle_mode():
global mode
if mode == "light":
mode = "dark"
else:
mode = "light"
toggle_mode()
print(mode) # Output: dark
5.4.9. 9. Avoiding Global Abuse (Best Practice Warning)¶
Mutable objects can be modified without global, reducing global keyword dependency:
config = {"theme": "dark"}
def update_config(new_theme):
config["theme"] = new_theme # No global keyword needed
5.4.10. 10. Best Practice: Encapsulation Instead of Global¶
Encapsulation is preferred over global variables in production systems for maintainability and testability:
class Counter:
def __init__(self):
self.value = 0
def increment(self):
self.value += 1
counter = Counter()
counter.increment()
print(counter.value) # Output: 1
5.5. Python Recursion¶
5.5.1. 1. Basic Recursive Function¶
A recursive function calls itself to solve a smaller instance of the same problem:
def print_numbers(n):
if n == 0:
return
print(n)
print_numbers(n - 1)
print_numbers(5)
5.5.2. 2. Base Case and Recursive Case¶
Every recursive function must define a base case to prevent infinite calls:
def factorial(n):
if n == 0: # Base case
return 1
return n * factorial(n - 1) # Recursive case
print(factorial(5)) # Output: 120
5.5.3. 3. Recursive Sum of Numbers¶
Demonstrates problem decomposition through recursion:
def sum_n(n):
if n == 1:
return 1
return n + sum_n(n - 1)
print(sum_n(5)) # Output: 15
5.5.4. 4. Recursive Fibonacci Series¶
Classic example of overlapping subproblems in recursion:
def fibonacci(n):
if n <= 1:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
print(fibonacci(6)) # Output: 8
5.5.5. 5. Recursion vs Iteration Comparison¶
Iteration often provides better performance and memory efficiency than deep recursion:
def factorial_iterative(n):
result = 1
for i in range(1, n + 1):
result *= i
return result
print(factorial_iterative(5)) # 120
5.5.6. 6. Recursive Traversal of List¶
Recursion is useful for navigating hierarchical or nested data:
def print_list(lst):
if not lst:
return
print(lst[0])
print_list(lst[1:])
print_list([1, 2, 3, 4])
5.5.7. 7. Recursive String Reversal¶
Illustrates recursive manipulation of sequences:
def reverse_string(text):
if len(text) == 0:
return text
return reverse_string(text[1:]) + text[0]
print(reverse_string("Python")) # Output: nohtyP
5.5.8. 8. Recursion Limit Error¶
Python enforces a recursion limit to prevent stack overflow:
def infinite_recursion():
return infinite_recursion()
# infinite_recursion() # Raises RecursionError
5.5.9. 9. Checking Recursion Depth¶
System recursion limits can be queried and adjusted cautiously:
import sys
print(sys.getrecursionlimit()) # Default ~1000
5.5.10. 10. Optimizing Recursion with Memoization¶
Memoization dramatically improves performance by caching results:
from functools import lru_cache
@lru_cache(maxsize=None)
def fibonacci(n):
if n <= 1:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
print(fibonacci(30)) # Optimized recursion
5.6. Python Modules¶
5.6.1. 1. What is a Module¶
A module is a file containing Python code (functions, variables, classes) that can be imported and reused in other programs:
# file: math_utils.py
def add(a, b):
return a + b
5.6.2. 2. Importing a Module¶
The import keyword loads the entire module and accesses its content using dot notation:
import math
print(math.sqrt(25)) # Output: 5.0
5.6.3. 3. Importing Specific Functions from a Module¶
Imports only selected items, reducing namespace clutter:
from math import sqrt, pow
print(sqrt(16)) # Output: 4.0
print(pow(2, 3)) # Output: 8.0
5.6.4. 4. Renaming a Module using as¶
Useful for improving readability or avoiding name conflicts:
import math as m
print(m.pi) # Output: 3.141592653589793
5.6.5. 5. Creating and Using a Custom Module¶
Custom modules help organize reusable logic:
# file: calculator.py
def multiply(a, b):
return a * b
# main.py
import calculator
print(calculator.multiply(4, 5)) # Output: 20
5.6.6. 6. Using __name__ in Modules¶
Ensures code runs only when the file is executed directly, not when imported:
# file: demo.py
def main():
print("Main function executed")
if __name__ == "__main__":
main()
5.6.7. 7. Built-in Modules Example¶
Python provides rich built-in modules like os, sys, math, datetime, and random:
import datetime
today = datetime.date.today()
print(today)
5.6.8. 8. Exploring Module Content with dir()¶
Lists all available attributes and methods inside a module:
import math
print(dir(math))
5.6.9. 9. Using help() for Module Documentation¶
Displays detailed documentation of a module and its components:
import math
help(math)
5.6.10. 10. Module Search Path (sys.path)¶
Shows directories Python searches to locate modules, including:
Current directory
Standard library path
Site-packages
import sys
print(sys.path)
5.7. Python Package¶
5.7.1. 1. What is a Package¶
A package is a collection of Python modules organized in a directory hierarchy, enabling structured and scalable code organization:
my_package/
│── __init__.py
│── module1.py
│── module2.py
The presence of __init__.py indicates that the directory is a Python package.
5.7.2. 2. Creating a Basic Package¶
Packages allow logical grouping of related modules:
# my_package/module1.py
def greet():
return "Hello from module1"
# main.py
import my_package.module1
print(my_package.module1.greet())
5.7.3. 3. Importing from a Package¶
Enables direct access to specific functions or classes within a package:
from my_package.module1 import greet
print(greet())
5.7.4. 4. Using init.py for Initialization¶
__init__.py controls what gets exposed when the package is imported:
# my_package/__init__.py
from .module1 import greet
5.7.5. 5. Sub-packages Structure¶
Packages can contain nested sub-packages for large systems:
my_package/
│── __init__.py
│── analytics/
│ ├── __init__.py
│ └── stats.py
5.7.6. 6. Relative Imports Inside a Package¶
Relative imports use:
. current package
Ensures internal module cohesion:
from my_package.analytics.stats import calculate_mean
5.7.7. 7. Absolute Imports in Packages¶
Preferred in production for clarity and maintainability:
from my_package.analytics.stats import calculate_mean
5.7.8. 8. Installing External Packages with pip¶
External packages extend Python’s functionality:
pip install requests
import requests
response = requests.get("https://api.example.com")
print(response.status_code)
5.7.9. 9. Viewing Installed Packages¶
Displays all packages installed in the Python environment:
pip list
5.7.10. 10. Packaging a Custom Project (Setup File)¶
This enables distribution and installation of your package via PyPI or internal repositories:
# setup.py
from setuptools import setup, find_packages
setup(
name="mypackage",
version="1.0",
packages=find_packages()
)
5.8. Python Main Function¶
5.8.1. 1. Purpose of the Main Function¶
In Python, the “main function” is a conventional entry point that controls program execution flow:
def main():
print("Program started")
main()
This provides a clear structure and separation of execution logic.
5.8.2. 2. Using if __name__ == “__main__”¶
This ensures the main logic runs only when the file is executed directly, not when imported:
def main():
print("Main function executed")
if __name__ == "__main__":
main()
5.8.3. 3. Why __name__ Works¶
When run directly → __main__
When imported → module name
This enables safe reusability of scripts as modules:
print(__name__)
5.8.4. 4. Main Function with Parameters¶
The main function can accept arguments like any other function:
def main(name):
print(f"Welcome, {name}")
if __name__ == "__main__":
main("Alice")
5.8.5. 5. Main Function for Program Flow Control¶
The main function orchestrates program execution in a clean, readable order:
def process_data():
print("Processing data...")
def main():
print("Initializing system...")
process_data()
if __name__ == "__main__":
main()
5.8.6. 6. Using Main with Command-Line Arguments¶
Allows integration with command-line tools and automation workflows:
import sys
def main():
print("Arguments received:", sys.argv)
if __name__ == "__main__":
main()
5.8.7. 7. Separating Business Logic from Entry Point¶
Improves modularity and testability:
def calculate_total(a, b):
return a + b
def main():
result = calculate_total(10, 20)
print("Total:", result)
if __name__ == "__main__":
main()
5.8.8. 8. Main Function in Large Applications¶
Supports structured application life-cycle management:
def initialize():
print("System initialized")
def run():
print("Application running")
def shutdown():
print("System shutdown")
def main():
initialize()
run()
shutdown()
if __name__ == "__main__":
main()
5.8.9. 9. Main Function with Error Handling¶
Encapsulates exception handling at the entry level:
def main():
try:
print("Executing task...")
except Exception as e:
print("Error occurred:", e)
if __name__ == "__main__":
main()
5.8.10. 10. Production-Ready Main Template¶
Standard best practice for scalable, maintainable Python applications:
def main():
"""Application entry point"""
print("Application started")
if __name__ == "__main__":
main()