DATA ANALYSIS  

Functions: Different aspects

📥
You can download the original Jupyter Notebook for this lesson: functions.ipynb

Table of Contents

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.

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.

1. Defining and Calling Functions

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.

def greet(): """This is a simple greeting function.""" print("Hello! Welcome to the Python Functions lesson.") # Calling the function greet()

Tasks

  • 1. Define a function show_current_date that prints today's date (you can just print a hardcoded string for now).
  • 2. Create a function display_message that prints a short message about why you are learning Python.
  • 3. Define a function empty_function using the pass keyword and call it.
  • 4. Write a function greet_user(name) that takes a name as a parameter and prints a personalized greeting.

2. Function Parameters and Arguments

Parameters are the names listed in the function's definition. Arguments are the real values passed to the function when it is called.

Positional Arguments

The most common way to pass arguments; their order matters.

def describe_pet(animal_type, pet_name): print(f"I have a {animal_type} named {pet_name.title()}.") describe_pet('hamster', 'harry') describe_pet('dog', 'willie')

Keyword Arguments

You can specify the parameter name when passing an argument, so the order no longer matters.

describe_pet(pet_name='harry', animal_type='hamster')

Default Values

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.

def describe_pet(pet_name, animal_type='dog'): print(f"I have a {animal_type} named {pet_name.title()}.") describe_pet(pet_name='willie') # animal_type defaults to 'dog' describe_pet('mimi', 'cat') # animal_type is overridden

Note on Passing Lists:
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.

Tasks

  • 1. Modify describe_pet to include a third parameter age and print it.
  • 2. Create a function make_shirt(size, message) and call it using positional arguments.
  • 3. Call make_shirt using keyword arguments.
  • 4. Define make_shirt with a default size of 'L' and call it with only the message.

3. Returning Values

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.

def add_numbers(num1, num2): """Return the sum of two numbers.""" return num1 + num2 result = add_numbers(5, 3) print(f"The result of 5 + 3 is: {result}")
def get_formatted_name(first_name, last_name): """Return a full name, neatly formatted.""" full_name = f"{first_name} {last_name}" return full_name.title() musician = get_formatted_name('jimi', 'hendrix') print(musician)

Tasks

  • 1. Write a function multiply_numbers(a, b) that returns their product.
  • 2. Create a function is_even(number) that returns True if a number is even, and False otherwise.
  • 3. Define a function get_circle_area(radius) that calculates and returns the area (use 3.14 for pi).
  • 4. Create a function format_city_country(city, country) that returns a string like "Santiago, Chile".

4. Arbitrary Number of Arguments (*args and **kwargs)

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.

def make_pizza(size, *toppings): """Summarize the pizza we are about to make.""" print(f"\nMaking a {size}-inch pizza with the following toppings:") for topping in toppings: print(f"- {topping}") make_pizza(12, 'mushrooms', 'green peppers', 'extra cheese') def build_profile(first, last, **user_info): """Build a dictionary containing everything we know about a user.""" # user_info is a dictionary containing all extra keyword arguments user_info['first_name'] = first user_info['last_name'] = last return user_info user_profile = build_profile('albert', 'einstein', location='princeton', field='physics') print(user_profile)

Important Note on Parameter Order:
When using multiple types of parameters, they must follow a specific order:
1. Standard positional arguments
2. *args
3. **kwargs

You cannot put **kwargs before other parameters. For example, def build_profile(**user_info, first, last) is invalid and will cause a SyntaxError.

Unpacking Arguments

You can use the same operators: * and ** to unpack lists/tuples and dictionaries when calling a function

# Unpacking a list with * my_toppings = ['pepperoni', 'extra cheese', 'onions'] make_pizza(16, *my_toppings) # Unpacking a dictionary with ** my_profile = {'location': 'paris', 'field': 'art'} print(build_profile('marie', 'curie', **my_profile))

Tasks

  • 1. Create a function sum_all(*args) that takes any number of integers and returns their sum.
  • 2. Define a function show_user_details(**kwargs) that prints all keys and values passed to it.
  • 3. Create a list of your favorite foods and pass them to a function using *.
  • 4. Create a dictionary with information about a car (make, model, year) and pass it to a function using **.

5. Scope: Local vs. Global Variables

Variables defined inside a function are local to that function. Variables defined outside are global.

To modify a global variable from within a function, you must use the global keyword.

x = 10 # Global variable def my_func(): x = 20 # Local variable (shadows the global x) print(f"Inside function (local), x = {x}") my_func() print(f"Outside function (global), x = {x}") # --- Modifying Global Variables --- counter = 0 def increment(): global counter counter += 1 print(f"Inside increment (using global keyword), counter is {counter}") increment() print(f"Outside function, final counter is {counter}")

Tasks

  • 1. Define a global variable name = "Global", then a function that defines a local name = "Local". Print both.
  • 2. Try to change a global string variable inside a function without using global and observe what happens.
  • 3. Create a function that uses the global keyword to modify a global score variable.
  • 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.

6. Functions as First-Class Objects

In Python, functions are first-class objects. This means they can be treated like any other variable. You can:

  • Assign a function to a variable.
  • Pass a function as an argument to another function.
  • Return a function from another function.

This fundamental feature is what allows for powerful concepts like Lambda functions, decorators, and closures.

def shout(text): return text.upper() # 1. Assign to a variable yell = shout print(yell("hello")) # 2. Pass as an argument def greet_with_style(func, message): greeting = func(message) print(greeting) greet_with_style(shout, "welcome to python")

Tasks

  • 1. Create a function square(x) and assign it to a variable f. Call f(5).
  • 2. Write a function apply_operation(func, val) that applies func to val and returns the result.

7. Lambda Functions

Lambda functions are small, anonymous functions defined with the lambda keyword. They can have any number of arguments but only one expression.

Where they are common and useful:

  • Lambda functions are ideal for short-lived, simple logic that is passed as an argument to other functions (like map(), filter(), and sort()).
  • They are frequently used in data processing and functional programming patterns.

Creation and Limitations:

  • Syntax: lambda arguments: expression
  • 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).
# Simple lambda functions square = lambda x: x * x print(f"Square of 5: {square(5)}") add = lambda x, y: x + y print(f"Sum of 3 and 4: {add(3, 4)}") # --- Practical Examples --- numbers = [1, 5, 4, 6, 8, 11, 3, 12] # Using lambda with filter() to get even numbers evens = list(filter(lambda x: x % 2 == 0, numbers)) print(f"Even numbers: {evens}") # Using lambda with map() to square each number squared_numbers = list(map(lambda x: x**2, numbers)) print(f"Squared numbers: {squared_numbers}") # Using lambda with sort() to sort a list of tuples by the second value pairs = [(1, 'one'), (4, 'four'), (2, 'two'), (3, 'three')] pairs.sort(key=lambda pair: pair[1]) print(f"Sorted pairs by name: {pairs}")

Tasks

  • 1. Create a lambda function that calculates the cube of a number.
  • 2. Use a lambda function with filter() to get only numbers greater than 10 from a list.
  • 3. Use a lambda function with map() to double every number in a list.
  • 4. Sort a list of tuples [(1, 'b'), (2, 'a')] by the second element using a lambda function as the key.

8. Type Hinting

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.

For more complex types, we use the standard typing library.

from typing import List, Dict, Optional, Union # Basic hints def multiply(a: int, b: int) -> int: return a * b # Advanced hints using typing library def process_names(names: List[str]) -> None: for name in names: print(f"Hello, {name.title()}") def get_user_data(user_id: int) -> Dict[str, Union[str, int]]: # Returns a dictionary with string keys and values that can be str or int return {"id": user_id, "name": "John Doe", "age": 30} def find_item(item_id: int) -> Optional[str]: # Returns a string or None items = {1: "Apple", 2: "Banana"} return items.get(item_id) print(multiply(10, 5)) process_names(["alice", "bob"]) print(get_user_data(123)) print(f"Found item: {find_item(1)}") print(f"Missing item: {find_item(99)}")

Tasks

  • 1. Add type hints to a function that takes a string and returns its length as an integer.
  • 2. Use List from the typing module to hint a function that takes a list of integers.
  • 3. Define a function that can return either a string or None using Optional.
  • 4. Create a function that takes a dictionary where keys are strings and values are integers, using Dict for type hinting.

9. Nested Functions and Closures

Functions can be defined inside other functions. A closure occurs when a nested function references a variable from its even-closing scope.

def outer_function(text): def inner_function(): print(text) return inner_function my_func = outer_function("Hello from the closure!") my_func()

Tasks

  • 1. Create a function make_multiplier(n) that returns a function that multiplies its input by n.
  • 2. Write a nested function where the inner function accesses a variable from the outer function's scope.

10. Recursion

A recursive function is a function that calls itself. It must have a base case to prevent infinite recursion.

def factorial(n): """Calculate the factorial of a number recursively.""" if n <= 1: # Base case return 1 else: return n * factorial(n - 1) # Recursive call print(factorial(5))

Tasks

  • 1. Write a recursive function sum_recursive(n) that calculates the sum of numbers from 1 to n.
  • 2. Create a recursive function to calculate the $n$-th Fibonacci number.
  • 3. Add a print statement to your factorial function to trace each recursive call.

11. Best Practices (PEP 8, PEP 257)

Following Python's official style guides ensures your code is readable and maintainable.

Some PEP 8 Rules for Functions:

  • Naming: Use snake_case for function and variable names to improve readability.
  • Indentation: Use 4 spaces per indentation level. Never mix tabs and spaces.
  • Whitespace: Surround top-level function definitions with two blank lines. Surround method definitions inside a class with a single blank line.
  • 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).
  • Docstrings (PEP 257): Always document your functions using triple quotes """. The first line should be a short summary of the function's effect.

You can find all PEP 8 documentation at the following link: https://peps.python.org/pep-0008/

Contact details:

+48 790-430-860

analysislessons@gmail.com