Programming Fundamentals

Chapter 5
Functions, Methods, Scope, and Modular Design

Certificate in Java-Python-Programming Languages
Programming Fundamentals   •   Certificate

Functions, Methods, Scope, and Modular Design

Introduction

Most useful programs are too large and too changeable to be written as one continuous sequence of statements. Functions and methods allow programmers to divide a problem into named, reusable units. Scope determines where names can be accessed and how long their associated values remain available. Modular design organizes related units into coherent components that can be developed, tested, maintained, and reused independently.

This chapter compares the implementation of these ideas in Python and Java. The languages use different terminology and rules in some areas. Python commonly defines functions with def, while Java places methods inside classes. Python supports flexible argument passing and default values; Java provides compile-time method overloading but does not provide Python-style default parameters. Understanding both the common principles and the language-specific differences is essential for effective programming.

Learning Objectives

By the end of this chapter, you should be able to:

  • define and call functions and methods in Python and Java;
  • distinguish parameters from arguments and explain return values;
  • use local, global, class, and instance scope appropriately;
  • explain pass-by-object-reference behavior in Python and pass-by-value behavior in Java;
  • implement method overloading in Java and describe its limitations in Python;
  • design and trace simple recursive functions;
  • decompose a problem into cohesive, loosely coupled modules;
  • apply testing, documentation, and error-handling practices to reusable code.

1. Functions and Methods as Abstractions

A function is a named block of code that performs a focused operation. It may receive input, perform computation, produce output, or change program state. A method is a function associated with a class or object. In Java, all ordinary methods are declared inside classes. In Python, a function becomes a method when it is defined within a class and accessed through an object or class.

A useful abstraction hides implementation details behind a meaningful interface. For example, a program can call calculate_total(items) without knowing whether the function uses a loop, a database query, or a library routine internally. This separation reduces cognitive load and makes implementation changes safer.

A well-designed function generally has:

  1. a clear name;
  2. a focused responsibility;
  3. explicit inputs;
  4. a predictable result or effect;
  5. limited dependence on hidden state;
  6. documentation describing its contract.

The function’s contract states what it expects, what it guarantees, and what errors may occur. For example, a function that calculates an average should specify whether an empty collection is valid and whether it raises an exception when no values are supplied.

Parameters and Arguments

A parameter is a variable appearing in a function definition. An argument is the value supplied when the function is called.

def rectangle_area(width, height):
    return width * height

area = rectangle_area(5, 3)

Here, width and height are parameters; 5 and 3 are arguments. The result is 15.

In Java, the equivalent method is:

static int rectangleArea(int width, int height) {
    return width * height;
}

int area = rectangleArea(5, 3);

The Java declaration includes types for both parameters and the return value. static means that the method belongs to the class rather than to a particular object.

Return Values

The return statement ends a function’s current execution and optionally sends a value to the caller. Returning a value is different from printing a value. Printing displays information immediately; returning makes a result available for further computation.

def discounted_price(price, rate):
    return price * (1 - rate)

final_price = discounted_price(100, 0.20)
print(final_price)

The function returns 80. The caller decides whether to print, store, compare, or pass that result to another function.

A function that reaches the end without a Python return statement returns None. In Java, a method declared with a non-void return type must return a compatible value on every possible execution path. A void method performs an action without returning a value.

Python Argument Features

Python supports positional arguments, keyword arguments, default parameters, and variable-length arguments.

def greet(name, punctuation="!"):
    return "Hello, " + name + punctuation

greet("Mina")
greet(name="Mina", punctuation=".")

Default parameters must be used carefully. Mutable objects such as lists should generally not be used as default values because the same object may be reused across calls.

# Safer pattern
def add_item(item, items=None):
    if items is None:
        items = []
    items.append(item)
    return items

Python also supports args for additional positional arguments and *kwargs for additional keyword arguments. These features are useful for flexible interfaces, but excessive flexibility can make a function difficult to understand and validate.

2. Methods and Object-Oriented Context

A method operates in the context of a class or object. Consider a bank-account example:

class BankAccount:
    def __init__(self, owner, balance=0):
        self.owner = owner
        self.balance = balance

    def deposit(self, amount):
        if amount <= 0:
            raise ValueError("Amount must be positive")
        self.balance += amount
        return self.balance

deposit is an instance method. Its first parameter, conventionally named self, refers to the object on which the method is called. The expression account.deposit(50) is conceptually equivalent to passing account as the first argument.

Java expresses the same idea explicitly through this:

class BankAccount {
    private String owner;
    private double balance;

    BankAccount(String owner, double balance) {
        this.owner = owner;
        this.balance = balance;
    }

    public double deposit(double amount) {
        if (amount <= 0) {
            throw new IllegalArgumentException("Amount must be positive");
        }
        balance += amount;
        return balance;
    }
}

The fields are private, so external code cannot modify them directly. The method controls valid state changes. This is an example of encapsulation: data and the operations that govern it are placed behind a controlled interface.

Java also supports static methods, which do not require an object:

class Temperature {
    static double celsiusToFahrenheit(double celsius) {
        return celsius * 9.0 / 5.0 + 32;
    }
}

A static method should not be used merely because it is convenient. If an operation depends on object state, an instance method usually communicates the design more accurately.

3. Scope, Lifetime, and Name Resolution

Scope is the region of a program in which a name can be referenced. Lifetime is the period during which the associated object or variable exists. These concepts are related but not identical.

Python Scope

Python name resolution commonly follows the LEGB order:

  • Local: names inside the current function;
  • Enclosing: names in an enclosing function when nested functions are used;
  • Global: names defined at module level;
  • Built-in: names supplied by Python, such as len and print.
tax_rate = 0.15

def tax(amount):
    return amount * tax_rate

The function reads the module-level tax_rate. However, assigning to a name inside a function normally creates a local name:

count = 0

def increment():
    count = 1       # local assignment; does not change global count

To rebind a global name, Python requires global, but this should be used sparingly:

count = 0

def increment():
    global count
    count += 1

Nested functions can use nonlocal to rebind a name in an enclosing function. Both global and nonlocal increase coupling and can make testing more difficult. Passing values explicitly is usually clearer.

Java Scope

Java scope is determined by declarations and blocks. A local variable declared inside a method or block is accessible only there. A parameter is local to its method. An instance field is accessible throughout the object’s methods, subject to access modifiers. A static field belongs to the class.

class Counter {
    private int value;          // instance field
    private static int total;   // class field

    void increase(int amount) { // amount is a parameter
        int change = amount;    // local variable
        value += change;
        total += change;
    }
}

The access modifiers private, package-private, protected, and public control visibility across classes and packages. Restricting visibility is a design tool: it prevents unrelated code from depending on implementation details.

Avoiding Global State

Global state can be appropriate for genuinely universal, immutable configuration, but mutable global state creates risks:

  • functions become dependent on hidden inputs;
  • tests may influence one another through shared state;
  • execution order becomes important;
  • concurrent programs may experience race conditions;
  • changes become difficult to trace.

Prefer constants, parameters, returned values, and objects whose state is explicitly managed. In Java, constants are commonly declared with static final; in Python, uppercase names communicate a convention, but do not enforce immutability.

4. Parameter Passing and Mutability

A common source of confusion is whether a language uses “pass by reference.” A precise explanation is preferable.

Python passes object references by value. The function receives a new parameter binding containing a reference to the same object. If the object is mutable, the function may change that object; rebinding the parameter does not rebind the caller’s variable.

def change(items):
    items.append("new")      # mutates the shared list
    items = ["different"]    # rebinds only the local parameter

After the call, the caller’s original list contains "new", but it is not replaced by ['different'].

Java always passes arguments by value. For an object argument, the value copied is a reference to the object. Consequently, a method can mutate the object but cannot replace the caller’s reference by assigning a new object to its parameter.

static void change(List<String> items) {
    items.add("new");
    items = new ArrayList<>();
}

The caller observes the added element, not the reassignment. Primitive values such as int are copied directly.

Good interfaces document whether a function mutates an argument. Avoid surprising mutation unless it is part of the documented contract.

5. Overloading and Flexible Interfaces

Method overloading means defining multiple methods with the same name but different parameter lists. Java resolves an overloaded call primarily at compile time using the method name and parameter types, number, or order.

class Printer {
    void print(String text) {
        System.out.println(text);
    }

    void print(int number) {
        System.out.println(number);
    }

    void print(String text, int copies) {
        for (int i = 0; i < copies; i++) {
            System.out.println(text);
        }
    }
}

Return type alone cannot distinguish overloaded Java methods. For example, two methods with identical names and parameters but different return types are not valid overloads.

Python does not support signature-based method overloading in the same way. A later definition replaces an earlier definition with the same name. Flexible behavior can instead be implemented with default arguments, keyword arguments, type checks, or functools.singledispatch, although these approaches have different complexity and maintenance costs.

def print_value(value, copies=1):
    for _ in range(copies):
        print(value)

Use overloading or flexible interfaces when the operations are conceptually one operation. Do not overload merely to combine unrelated behaviors under one name.

6. Recursion Basics

Recursion occurs when a function directly or indirectly calls itself. A correct recursive solution requires:

  1. a base case, which stops recursion;
  2. a recursive case, which reduces the problem toward the base case;
  3. progress toward termination.

A mathematical definition of factorial is:

n! = 1 when n = 0
n! = n × (n − 1)! when n > 0

For non-negative integer n, the recursive implementation is:

def factorial(n):
    if n < 0:
        raise ValueError("n must be non-negative")
    if n == 0:
        return 1
    return n * factorial(n - 1)

For example, factorial(4) computes 4 × factorial(3), then 3 × factorial(2), then 2 × factorial(1), then 1 × factorial(0). The base case returns 1, and the results then return upward, producing 24.

A recursive solution is not automatically superior to an iterative one. Each recursive call uses stack space. Deep recursion can cause a stack overflow, and Python does not generally optimize tail recursion. For simple counting or accumulation, iteration is often more efficient and easier to trace.

Recursion is particularly natural for hierarchical data, tree traversal, directory processing, and divide-and-conquer algorithms. Before implementing recursion, identify the smallest valid input and prove that each call moves closer to it.

7. Modular Decomposition

Modular decomposition divides a system into modules with clear responsibilities. A module may be a Python file, a Java class, a package, or a larger service. Effective modular design balances two properties:

  • Cohesion: how strongly the elements of a module belong together;
  • Coupling: how much one module depends on other modules.

High cohesion and low coupling are desirable. A module that validates orders, sends emails, formats reports, and connects to a database has low cohesion. A module that performs only order validation has stronger cohesion.

A practical decomposition process is:

  1. state the overall problem and desired outputs;
  2. identify major responsibilities or domain concepts;
  3. define the data each responsibility requires;
  4. assign one focused function or class to each responsibility;
  5. specify interfaces between components;
  6. implement and test units independently;
  7. integrate components and test their interactions.

A Python project might use this structure:

shop/
    main.py
    pricing.py
    validation.py
    repository.py
    tests/

pricing.py should not need to know how a user interface displays a result. repository.py should isolate storage details. Such boundaries make replacement and testing easier.

Functional Decomposition Example

A monolithic procedure might read input, validate it, calculate a total, save a record, and print a receipt. A decomposed design separates these tasks:

def validate_order(order):
    if not order.items:
        raise ValueError("Order must contain an item")


def calculate_total(order, tax_rate):
    subtotal = sum(item.price * item.quantity for item in order.items)
    return subtotal * (1 + tax_rate)


def create_receipt(order, total):
    return f"Order {order.number}: {total:.2f}"

Each function has a small contract and can be tested with controlled inputs. The orchestration code can call them in sequence without duplicating their internal logic.

8. Worked Application: A Library Loan Service

The following is an illustrative case, not a description of a particular organization.

Background

A small community library initially uses one long program to register members, check books out, calculate overdue charges, and print messages. Staff members report inconsistent charges and difficulty changing the rules.

Problem Analysis

The program contains duplicated charge calculations, global variables for member data, and input handling mixed with business rules. A change to the overdue policy requires edits in several places. Testing is difficult because the program must be run interactively.

Design Approach

The system is decomposed into four responsibilities:

  • member validation;
  • loan-period and charge calculation;
  • loan-record management;
  • presentation of messages.

The charge calculation is defined by a simple policy: a loan is free when it is not overdue; otherwise, the charge is the number of overdue days multiplied by a daily rate. If d is the number of overdue days and r is the daily rate, the charge is:

charge = d × r, where d ≥ 0 and r ≥ 0

The formula applies only after overdue days and the rate have been validated as non-negative. Currency rounding should be handled according to the organization’s defined monetary policy rather than by casually rounding intermediate calculations.

def overdue_charge(due_date, return_date, daily_rate):
    if daily_rate < 0:
        raise ValueError("Daily rate cannot be negative")

    overdue_days = max(0, (return_date - due_date).days)
    return overdue_days * daily_rate

The function receives dates and a rate rather than reading global configuration. It returns a value rather than printing a message. A separate presentation function can format the amount for a user.

Outcome

The library can test the charge policy with ordinary dates, same-day returns, and overdue returns. A revised policy can be implemented by replacing the policy function without changing member registration or message formatting. The design also makes it possible to use the same calculation in a web interface and a command-line program.

Learning Points

The case demonstrates that modularity is not merely splitting a long file into smaller files. The important work is assigning responsibilities, defining interfaces, reducing hidden state, and keeping business rules independent of presentation and storage.

9. Testing, Documentation, and Error Handling

Reusable units require predictable behavior. Test normal cases, boundary cases, invalid inputs, and interactions between modules.

For a function calculating an overdue charge, useful tests include:

  • return on the due date produces zero overdue days;
  • return after three days produces three times the daily rate;
  • a negative rate is rejected;
  • dates are interpreted consistently;
  • a valid result has the expected numeric type and unit.

A docstring or Java documentation comment should explain purpose, parameters, return value, exceptions, side effects, and important assumptions.

def square(value):
    """Return value multiplied by itself."""
    return value * value

Exceptions should communicate invalid conditions clearly. Avoid catching broad exceptions merely to conceal errors. Validate at system boundaries, then keep internal functions’ assumptions explicit. A function that accepts only non-negative integers should either validate that contract or clearly state that callers are responsible for it.

10. Professional Design Principles and Limitations

The following practices improve maintainability:

  • use names that describe intent;
  • keep functions short enough to understand, but do not split code mechanically;
  • prefer one responsibility per function;
  • minimize mutable global state;
  • pass dependencies explicitly where practical;
  • avoid unnecessary parameter lists;
  • document mutation and exceptions;
  • use type hints in Python and appropriate Java types;
  • test public behavior rather than private implementation details;
  • refactor duplication only when the shared abstraction is genuinely stable.

There are trade-offs. Excessive decomposition can produce many trivial functions and obscure the main workflow. Excessive abstraction can make simple code difficult for beginners to follow. Overloaded methods can create ambiguous calls, and recursive solutions can consume substantial stack space. Good design is therefore contextual: clarity, correctness, testability, and changeability matter more than following a rule mechanically.

Security and governance also matter. A function that processes personal data should collect only what is necessary and should not expose sensitive values through logs or error messages. Shared modules should validate untrusted input at appropriate boundaries. In collaborative projects, stable interfaces, code review, version control, and automated tests reduce the risk that a local change silently breaks other components.

11. Contemporary Relevance: Libraries, APIs, and AI-Assisted Coding

Modern applications are assembled from functions and methods supplied by standard libraries, third-party packages, and web APIs. A developer must understand an API’s contract, including types, exceptions, side effects, authentication requirements, and compatibility guarantees. Modular design makes it possible to replace an external service behind an internal interface.

AI-assisted programming tools can suggest functions, tests, and refactorings, but generated code must be reviewed. A suggestion may use mutable defaults incorrectly, expose global state, mishandle exceptions, or misunderstand a parameter’s unit and meaning. The programmer remains responsible for testing, security, licensing compliance, and correctness. Small, well-defined functions make such review easier because their behavior can be checked independently.

Chapter Summary

Functions and methods provide reusable abstractions. Parameters communicate inputs, while return values communicate results and support composition. Python and Java both support methods associated with objects, but they differ in syntax, typing, scope rules, argument features, and overloading. Python resolves names through local, enclosing, global, and built-in scopes; Java uses method, block, class, and package visibility. Both languages pass object references in a way that permits mutation while preventing reassignment of the caller’s variable through a parameter.

Recursion requires a base case, a reducing recursive case, and guaranteed progress toward termination. Modular decomposition organizes a system around cohesive responsibilities and explicit interfaces. High cohesion, low coupling, limited global state, useful documentation, validation, and automated testing support maintainable software. The strongest designs are not those with the most functions, but those whose boundaries make behavior clear and change safe.

Key Terms

Function: A named unit of reusable procedural logic.

Method: A function associated with a class or object.

Parameter: A variable listed in a function or method definition.

Argument: A value supplied when a function or method is called.

Return value: Data sent from a function to its caller.

Contract: A description of expected inputs, guarantees, outputs, and errors.

Scope: The region in which a name can be referenced.

Lifetime: The period during which a variable or object exists.

Local variable: A name declared within a function or restricted block.

Global variable: A name defined at module or broad program scope.

Instance method: A method that operates on a particular object.

Static method: A method associated with a class rather than an object instance.

Overloading: Defining methods with the same name but different parameter lists.

Recursion: A technique in which a function calls itself directly or indirectly.

Base case: The condition that terminates a recursive computation.

Cohesion: The degree to which a module’s elements belong together.

Coupling: The degree of dependency between modules.

Encapsulation: Controlling access to data through a defined interface.

Mutable object: An object whose contents can be changed after creation.

Modular decomposition: Dividing a system into focused, interacting components.

Review Questions

Short Answer Questions

  1. Distinguish a parameter from an argument.
  2. Why is returning a value generally more reusable than printing it inside a function?
  3. State the LEGB name-resolution order used by Python.
  4. What two elements are required for a correct recursive function?
  5. Define cohesion and coupling.

Long Answer Questions

  1. Compare functions in Python with methods in Java, including syntax, typing, and object context.
  2. Explain local, global, instance, and static scope, and discuss why excessive mutable global state is risky.
  3. Explain Java method overloading and compare it with Python’s approach to flexible function interfaces.
  4. Describe Python and Java argument-passing behavior using mutable objects as an example.
  5. Explain a systematic process for decomposing a monolithic program into testable modules.

Analytical / Application Questions

  1. Design a set of functions or methods for an online order system that validates an order, calculates a total, saves the order, and formats a receipt. Identify each unit’s inputs, outputs, and responsibilities.
  2. A recursive function repeatedly calls itself with the same argument. Diagnose the defect and describe how you would correct and test it.
  3. A program uses a global list of users that is modified by several functions. Analyze two risks of this design and propose a modular alternative.

Multiple Choice Questions with Answers

Question 1

In the call rectangle_area(5, 3), what are 5 and 3?

A. Parameters

B. Arguments

C. Return types

D. Local scopes

Correct Answer: B. Arguments

Question 2

What does a Python function return when it reaches the end without a return statement?

A. 0

B. False

C. None

D. An exception automatically

Correct Answer: C. None

Question 3

Which Python scope is searched after local and enclosing scopes?

A. Global

B. Private

C. Class-only

D. Package

Correct Answer: A. Global

Question 4

Which statement about Java method overloading is correct?

A. Methods may differ only by return type

B. Methods must have different names

C. Methods can share a name with different parameter lists

D. Overloading occurs only at run time

Correct Answer: C. Methods can share a name with different parameter lists

Question 5

What is essential for a terminating recursive function?

A. A global variable

B. A base case

C. A static method

D. A default parameter

Correct Answer: B. A base case

Question 6

If an overdue loan is 3 days late and the daily rate is 2 monetary units, what is the charge under the chapter’s policy?

A. 0 monetary units

B. 2 monetary units

C. 5 monetary units

D. 6 monetary units

Correct Answer: D. 6 monetary units

Question 7

Which design combination is generally desirable for modules?

A. Low cohesion and high coupling

B. High cohesion and low coupling

C. High cohesion and high hidden state

D. Low cohesion and no interfaces

Correct Answer: B. High cohesion and low coupling

Question 8

In Python, what happens when a function appends to a list passed as an argument?

A. The original list may be mutated

B. The list becomes immutable

C. The caller’s variable is always rebound

D. The function receives no object reference

Correct Answer: A. The original list may be mutated

Question 9

Which Java modifier commonly restricts a field so it can be accessed only within its class?

A. public

B. protected

C. private

D. static

Correct Answer: C. private

Question 10

Why should a calculation function usually return a result instead of printing it?

A. Printing is illegal inside functions

B. A returned result can be stored, tested, or used by another function

C. Returned values cannot be tested

D. Printing always changes global scope

Correct Answer: B. A returned result can be stored, tested, or used by another function

Programming Fundamentals
Chapter 5 of 14