Python for Data Science & Automation · Module 1: Foundational Programming & Environment Setup · Lesson 5 of 34

1.5 Modular Code & Errors

Modular Code & Error Handling in Python

As Python programs become larger, placing all instructions in a single block makes the code difficult to understand, test, reuse, and maintain.

Python solves this problem through modular programming. Functions, modules, and packages allow a large program to be divided into smaller, manageable components.

Python also provides exception handling so that programs can respond gracefully to errors instead of terminating unexpectedly.

In this lesson, you will learn:
  • Why modular programming is important
  • Defining and calling functions
  • Parameters and arguments
  • Return values
  • Default and keyword arguments
  • Variable-length arguments
  • Scope of variables
  • Lambda expressions
  • Modules and imports
  • Creating custom modules
  • Packages
  • Errors and exceptions
  • try-except
  • else and finally
  • Raising exceptions
  • Practical Data Science and automation examples

1. What Is Modular Programming?

Modular programming is the practice of dividing a program into smaller, independent and reusable components called modules.

Each component can perform a specific task.

Example

An automation application could be divided into:

  • A module for reading files
  • A module for cleaning data
  • A module for generating reports
  • A module for sending emails
Benefits of Modular Programming:
  • Code reusability
  • Better organization
  • Easier testing
  • Easier debugging
  • Improved maintainability
  • Team collaboration
  • Reduced code duplication

2. Functions in Python

A function is a reusable block of code designed to perform a particular task.

Functions are defined using the def keyword.

Basic Syntax

def function_name():
    statements

Example

def greet():
    print("Hello, Python!")

greet()

The function is executed when it is called using greet().

3. Functions with Parameters

A function can accept information through parameters.

def greet(name):
    print(f"Hello, {name}!")

greet("Alex")
greet("Jordan")

Here, name is a parameter and the supplied strings are arguments.

Term Meaning
Parameter Variable defined in the function definition.
Argument Actual value supplied when calling the function.

4. Returning Values from Functions

A function can send a result back to the caller using the return statement.

def add(a, b):
    return a + b

result = add(10, 20)

print(result)

Output:

30

The return statement terminates the function and provides its result to the calling code.

5. Returning Multiple Values

Python can return multiple values from a function. They are typically packed into a tuple.

def calculate(a, b):

    total = a + b
    difference = a - b

    return total, difference


total, difference = calculate(20, 8)

print(total)
print(difference)

Output:

28
12

6. Default Arguments

A parameter can have a default value. The default is used when the caller does not provide an argument for that parameter.

def greet(name, message="Welcome"):
    print(f"{message}, {name}!")

greet("Alex")
greet("Jordan", "Good morning")

Default arguments make functions more flexible.

7. Keyword Arguments

Arguments can be passed using parameter names.

def student_info(name, age, course):
    print(name, age, course)

student_info(
    course="Data Science",
    name="Alex",
    age=20
)

Keyword arguments improve readability and allow arguments to be supplied by name.

8. Positional vs Keyword Arguments

Type Example Characteristic
Positional calculate(10, 20) Position determines the parameter.
Keyword calculate(a=10, b=20) Parameter name identifies the value.

9. Variable-Length Arguments

Sometimes a function needs to accept an unknown number of arguments.

*args

*args collects extra positional arguments into a tuple.

def total(*numbers):

    result = 0

    for number in numbers:
        result += number

    return result


print(total(10, 20))
print(total(10, 20, 30, 40))

**kwargs

**kwargs collects extra keyword arguments into a dictionary.

def display_info(**details):

    for key, value in details.items():
        print(f"{key}: {value}")


display_info(
    name="Alex",
    age=20,
    course="Python"
)

10. Variable Scope

Scope determines where a variable can be accessed in a program.

Local Variable

def calculate():

    value = 100

    print(value)


calculate()

The variable value exists within the function's local scope.

Global Variable

tax_rate = 0.18

def calculate_tax(amount):
    return amount * tax_rate

print(calculate_tax(1000))

A global variable can be read from inside a function.

Best Practice:

Prefer passing data into functions and returning results rather than relying heavily on global variables.

11. Lambda Expressions

A lambda is a small anonymous function written using the lambda keyword.

Syntax

lambda arguments: expression

Example

square = lambda x: x ** 2

print(square(5))

Output:

25

Lambda expressions are particularly useful when a short function is required temporarily.

12. Lambda with sorted()

Lambda expressions are frequently used as sorting keys.

students = [
    {"name": "Alex", "score": 82},
    {"name": "Jordan", "score": 95},
    {"name": "Taylor", "score": 76}
]

result = sorted(
    students,
    key=lambda student: student["score"],
    reverse=True
)

print(result)

This sorts the students from highest to lowest score.

13. Lambda with map()

map() applies a function to each item of an iterable.

numbers = [1, 2, 3, 4, 5]

squares = list(
    map(lambda x: x ** 2, numbers)
)

print(squares)

Output:

[1, 4, 9, 16, 25]

14. Lambda with filter()

filter() selects elements for which a condition evaluates to true.

numbers = [10, 15, 20, 25, 30]

even_numbers = list(
    filter(lambda x: x % 2 == 0, numbers)
)

print(even_numbers)

Output:

[10, 20, 30]
Modern Python Tip:

For many straightforward transformations and filters, comprehensions are often easier to read than nested map() and filter() expressions.

15. What Is a Module?

A module is a Python file containing reusable code such as functions, classes, and variables.

A module normally has a .py extension.

Modules allow related functionality to be organized separately.

Example

math_tools.py

def add(a, b):
    return a + b

def multiply(a, b):
    return a * b

16. Importing a Module

Use import to load a module.

import math

print(math.sqrt(25))
print(math.pi)

The module name is used to access its members.

17. from ... import

Specific members can be imported directly.

from math import sqrt

print(sqrt(36))

Multiple members can also be imported.

from math import sqrt, pi

print(sqrt(49))
print(pi)

18. Importing with an Alias

The as keyword creates a shorter or alternative name for a module.

import math as m

print(m.sqrt(64))

Aliases are widely used in Data Science.

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
Data Science Convention:

The aliases np, pd, and plt are widely recognized conventions for NumPy, pandas, and Matplotlib.

19. Creating Your Own Module

Suppose a file named calculator.py contains:

def add(a, b):
    return a + b


def subtract(a, b):
    return a - b

Another Python file can import it:

import calculator

print(calculator.add(10, 5))
print(calculator.subtract(10, 5))

This allows reusable functionality to be maintained separately from the main program.

20. if __name__ == "__main__"

Python modules can contain code that should run only when the file is executed directly, not when it is imported.

def greet():
    print("Hello")


if __name__ == "__main__":
    greet()

This pattern is commonly used in reusable Python modules.

21. What Is a Package?

A package is a way of organizing related Python modules into a directory structure.

A project might be organized as:

analytics/
    data.py
    cleaning.py
    reports.py

Packages help organize larger applications and libraries.

22. Importing from a Package

from analytics import cleaning

cleaning.remove_duplicates()

Packages make it easier to structure large projects into logical components.

23. Python Standard Library

Python includes a large collection of modules in its standard library.

Module Common Purpose
math Mathematical functions.
random Random number generation.
datetime Date and time operations.
os Operating-system interaction.
pathlib Object-oriented filesystem paths.
json JSON encoding and decoding.
re Regular expressions.
statistics Basic statistical calculations.

24. Errors and Exceptions

Programs can encounter problems during execution. Python reports many runtime problems using exceptions.

Example

number = 10
result = number / 0

This raises a ZeroDivisionError.

Exception handling allows a program to detect and respond to such conditions gracefully.

25. Common Python Exceptions

Exception Typical Cause
ValueError Correct type but inappropriate value.
TypeError Operation applied to an inappropriate type.
ZeroDivisionError Division by zero.
IndexError Sequence index is out of range.
KeyError Dictionary key does not exist.
FileNotFoundError Requested file does not exist.
NameError Name or variable is not defined.
AttributeError Object does not have the requested attribute.

26. try-except

The try block contains code that may raise an exception. The except block handles the exception.

Syntax

try:
    risky_code

except ExceptionType:
    handling_code

Example

try:

    number = int(input("Enter a number: "))
    print(100 / number)

except ValueError:

    print("Please enter a valid integer.")

except ZeroDivisionError:

    print("Zero cannot be used as the divisor.")

Different exception types can have different handling logic.

27. Catching an Exception Object

The exception object can be stored using as.

try:

    number = int("abc")

except ValueError as error:

    print("Error:", error)

This can be useful for logging or displaying diagnostic information.

Best Practice:

Prefer catching specific exceptions instead of using a broad except: whenever practical.

28. try-except-else

The else block runs only when no exception occurs in the try block.

try:

    number = int(input("Enter a number: "))

except ValueError:

    print("Invalid input.")

else:

    print("Valid number:", number)

This keeps successful execution separate from error handling.

29. The finally Block

The finally block runs whether an exception occurs or not.

try:

    number = int(input("Enter a number: "))

except ValueError:

    print("Invalid input.")

finally:

    print("Program execution completed.")

finally is commonly used for cleanup operations.

30. Complete try-except-else-finally Structure

try:

    number = int(input("Enter a number: "))
    result = 100 / number

except ValueError:

    print("Invalid number.")

except ZeroDivisionError:

    print("Cannot divide by zero.")

else:

    print("Result:", result)

finally:

    print("Operation completed.")
Block Purpose
try Contains code that may raise an exception.
except Handles specified exceptions.
else Executes when no exception occurs.
finally Executes regardless of whether an exception occurs.

31. Raising Exceptions

Python allows a program to explicitly raise an exception using the raise statement.

age = -5

if age < 0:
    raise ValueError("Age cannot be negative")

Raising exceptions is useful for enforcing rules and validating program inputs.

32. Input Validation with Exceptions

def calculate_percentage(marks, total):

    if total <= 0:
        raise ValueError("Total marks must be positive.")

    return (marks / total) * 100


try:

    percentage = calculate_percentage(450, 500)
    print(f"Percentage: {percentage:.2f}%")

except ValueError as error:

    print("Error:", error)

This pattern is useful when building reliable data-processing functions.

33. Custom Exceptions

Python also allows developers to define their own exception classes for application-specific errors.

class InvalidScoreError(Exception):
    pass


score = 120

if score > 100:
    raise InvalidScoreError("Score cannot exceed 100.")

Custom exceptions are particularly useful in larger applications and reusable libraries.

34. Exception Handling with Files

File operations can fail when a file does not exist or cannot be accessed.

try:

    with open("data.csv", "r") as file:
        content = file.read()

except FileNotFoundError:

    print("The requested file was not found.")

else:

    print("File loaded successfully.")

finally:

    print("File operation completed.")

This pattern is useful in automation scripts that process files supplied by users or external systems.

35. Data Science Example — Statistical Function

Functions make repeated calculations reusable.

def average(values):

    if not values:
        raise ValueError("The collection cannot be empty.")

    return sum(values) / len(values)


scores = [82, 91, 76, 88, 95]

try:

    result = average(scores)
    print(f"Average: {result:.2f}")

except ValueError as error:

    print("Error:", error)

The function validates its input and returns a calculated result.

36. Automation Example — Safe Data Processing

Modular functions and exception handling can be combined to create safer automation workflows.

def convert_amount(value):

    try:
        return float(value)

    except ValueError:
        return None


data = ["1200", "850.50", "invalid", "2400"]

valid_amounts = []

for value in data:

    amount = convert_amount(value)

    if amount is not None:
        valid_amounts.append(amount)


print(valid_amounts)

Invalid input is handled without stopping the entire processing operation.

37. Mini Project — Data Utility Module

Suppose a file named data_utils.py contains:

def clean_numbers(values):

    return [
        float(value)
        for value in values
        if value is not None
    ]


def average(values):

    if not values:
        raise ValueError("No values available.")

    return sum(values) / len(values)

Another program can reuse these functions:

import data_utils

data = [10, 20, 30, 40]

cleaned = data_utils.clean_numbers(data)

print(data_utils.average(cleaned))
Design Principle:

Separate reusable logic from the main application whenever doing so improves clarity and maintainability.

38. Good Function Design

Well-designed functions generally have a clear responsibility.

Less Reusable

def process_data():

    # Read data
    # Clean data
    # Calculate statistics
    # Generate report
    # Send email

More Modular

def read_data():
    ...


def clean_data():
    ...


def calculate_statistics():
    ...


def generate_report():
    ...


def send_email():
    ...

Smaller functions can be tested and reused independently.

39. Function Documentation with Docstrings

A docstring documents what a function, class, or module does.

def calculate_average(values):
    """Return the arithmetic mean of a collection of numbers."""

    if not values:
        raise ValueError("Values cannot be empty.")

    return sum(values) / len(values)

Docstrings make reusable code easier to understand and maintain.

40. Common Mistakes

Mistake Problem Better Approach
Repeating the same code Increases maintenance effort. Extract reusable logic into functions.
Huge functions Difficult to test and understand. Divide responsibilities into smaller functions.
Using many global variables Creates hidden dependencies. Prefer parameters and return values.
Bare except: Can hide unexpected errors. Catch specific exceptions where possible.
Ignoring exception details Makes debugging difficult. Log or inspect the exception appropriately.
Putting everything in one module Large projects become difficult to maintain. Organize related functionality into modules/packages.

41. Python Interview Questions

Q1. What is a function in Python?

View Answer

A function is a reusable block of code designed to perform a particular task. It can accept parameters and return values.

Q2. What is the difference between a parameter and an argument?

View Answer

A parameter is a variable defined in a function definition, while an argument is the actual value passed to the function during a call.

Q3. What is a lambda function?

View Answer

A lambda is a small anonymous function defined using the lambda keyword, generally containing a single expression.

Q4. What is a module?

View Answer

A module is a Python file containing reusable code such as functions, classes, and variables.

Q5. What is the purpose of exception handling?

View Answer

Exception handling allows a program to detect and respond to runtime problems without necessarily terminating unexpectedly.

Q6. What is the difference between else and finally in exception handling?

View Answer

else executes when no exception occurs in the try block, while finally executes regardless of whether an exception occurred.

Q7. What does the raise statement do?

View Answer

raise explicitly triggers an exception, allowing a program to enforce validation rules or report application-specific errors.

Q8. What are *args and **kwargs?

View Answer

*args collects additional positional arguments into a tuple, while **kwargs collects additional keyword arguments into a dictionary.

42. Examination Questions

Multiple Choice Questions

Q1. Which keyword is used to define a function in Python?

  1. function
  2. def
  3. define
  4. func

Answer: B — def

Q2. Which statement sends a value back from a function?

  1. send
  2. output
  3. return
  4. yield-value

Answer: C — return

Q3. Which keyword is used to handle an exception?

  1. catch
  2. except
  3. error
  4. handle

Answer: B — except

Q4. Which block executes regardless of whether an exception occurs?

  1. try
  2. except
  3. else
  4. finally

Answer: D — finally

Q5. What is the purpose of a module?

  1. To delete variables
  2. To organize and reuse Python code
  3. To replace Python syntax
  4. To prevent functions from executing

Answer: B — To organize and reuse Python code

Short Answer Questions

  1. Define a function and state two advantages of using functions.
  2. Differentiate between positional and keyword arguments.
  3. What is a lambda expression? Give an example.
  4. Explain the difference between a module and a package.
  5. Explain the purpose of try, except, else, and finally.
  6. What is exception handling and why is it useful?
  7. Explain *args and **kwargs.
  8. What is the purpose of the raise statement?

43. Practical Challenge

Build a Modular Data Processing Utility

Create a small Python project that demonstrates functions, modules, and exception handling.

  1. Create a module named data_utils.py.
  2. Add a function that validates numeric input.
  3. Add a function that calculates an average.
  4. Add a function that identifies the maximum value.
  5. Raise a ValueError when an empty collection is supplied.
  6. Import the module into a separate main program.
  7. Use try-except to handle invalid input.
  8. Display a clear final report.

44. Concept Map

Concept Key Idea
Function Reusable block of code.
Parameter Input variable in a function definition.
Argument Value passed to a function.
return Sends a result back to the caller.
Lambda Small anonymous function.
Module Python file containing reusable code.
Package Organized collection of related modules.
try Code that may raise an exception.
except Handles an exception.
else Runs when no exception occurs.
finally Runs regardless of exception outcome.
raise Explicitly triggers an exception.

45. Quick Reference

Task Python Syntax
Define function def function():
Return value return value
Lambda lambda x: x * 2
Import module import module
Import specific member from module import function
Handle exception try ... except
Run after success else
Always execute cleanup finally
Raise exception raise ValueError(...)
Variable positional arguments *args
Variable keyword arguments **kwargs

46. Key Takeaways

  • Functions make Python programs reusable and modular.
  • Parameters define the inputs a function accepts.
  • Arguments are the actual values supplied to a function.
  • return sends a result back to the caller.
  • Default and keyword arguments improve function flexibility.
  • *args handles variable positional arguments.
  • **kwargs handles variable keyword arguments.
  • Lambda expressions are useful for short, temporary functions.
  • Modules organize reusable Python code.
  • Packages organize related modules into larger project structures.
  • Exception handling prevents expected runtime problems from unnecessarily terminating a program.
  • Catch specific exceptions whenever practical.
  • else executes after successful try execution.
  • finally is used for code that should execute regardless of the exception outcome.
  • raise allows programs to explicitly report invalid conditions.
  • Modular code and robust error handling are essential for production-quality Data Science and automation applications.
Golden Rule:

Divide large programs into reusable functions and modules, and handle expected failures explicitly so your Python applications remain readable, maintainable, and reliable.