TutorChase logo
Login
AQA A-Level Computer Science

1.1.7 Subroutines: Definition, Use, and Parameters

What is a subroutine?

A subroutine is a named sequence of instructions that performs a specific, self-contained task within a larger program. Subroutines are central to procedural programming and offer an effective way to simplify code, reduce repetition, and improve the organisation of software.

When writing a program, certain operations may need to be performed multiple times. Rather than writing the same block of code repeatedly, a subroutine can be defined once and called whenever that operation is needed.

There are two main types of subroutines:

  • Procedure: A subroutine that performs a specific task but does not return a value to the part of the program that called it.

  • Function: A subroutine that performs a specific task and returns a value (or values) to the calling code.

Once a subroutine has completed its execution, control is passed back to the point in the program where it was invoked.

Structure of a subroutine

Subroutines follow a consistent structure that typically includes:

  • Subroutine name: A descriptive identifier that indicates what the subroutine does.

  • Parameters (optional): Inputs passed into the subroutine to customise its behaviour.

  • Body: The main set of instructions that perform the subroutine’s task.

  • Return value (functions only): The result or output that is passed back to the calling code.

Example of a procedure

python

def greet_user(name):
    print("Welcome, " + name)

This procedure accepts a single parameter name and prints a welcome message. It does not return a value.

Example of a function

python

def square(number):
    return number * number

This function accepts a number and returns its square. For example, square(5) returns 25.

Subroutines promote abstraction, meaning programmers can use a subroutine without needing to understand how it works internally. They only need to understand its interface (how to use it) and the expected result.

Advantages of using subroutines

Subroutines provide several key benefits that make them essential in programming:

Modularity

Modularity involves dividing a program into distinct, manageable components. Each subroutine focuses on a single task and can be developed, tested, and updated independently.

Benefits of modularity include:

  • Focusing on smaller parts of a problem at a time.

  • Easier testing and debugging of specific units.

  • Organised code that reflects logical sections.

Reusability

Subroutines can be reused in different parts of a program, or even across multiple programs. Once written and tested, they can be used repeatedly without rewriting the logic.

Advantages include:

  • Less duplicated code.

  • Lower likelihood of bugs due to reuse of tested code.

  • Faster development by building on pre-existing subroutines.

Improved clarity and readability

Subroutines with clear names like calculate_tax or display_error allow readers to immediately understand their purpose. This improves overall code readability and supports easier maintenance and collaboration.

Simplified testing and debugging

Subroutines allow for unit testing, where individual components can be tested in isolation. If a bug occurs, it's easier to trace the error to a specific subroutine.

Team collaboration

In team projects, subroutines enable developers to work independently on different parts of the program. Each subroutine can be assigned to a different team member, with integration happening through well-defined interfaces.

Parameters and arguments

To perform meaningful work, subroutines often require inputs. These inputs are passed via parameters and arguments.

Definitions

  • Parameter: A variable declared in the subroutine definition, representing a value to be provided when the subroutine is called.

  • Argument: The actual value or expression passed into the subroutine when it is called.

Example

python

def add(x, y):  # x and y are parameters
    return x + y

result = add(4, 7)  # 4 and 7 are arguments

Types of parameter passing

Different programming languages handle parameter passing in different ways. The two main methods are:

Pass-by-value

  • A copy of the argument is passed into the subroutine.

  • Any changes made to the parameter do not affect the original variable.

Example:

python

def double(n):
    n = n * 2

x = 5
double(x)
print(x)  # Output is 5

Pass-by-reference

  • A reference to the actual memory location is passed.

  • Changes made to the parameter do affect the original variable.

In Python, this behaviour applies to mutable types like lists and dictionaries.

Example:

python

def append_item(lst):
    lst.append(10)

my_list = [1, 2]
append_item(my_list)
print(my_list)  # Output is [1, 2, 10]

Subroutine interfaces

A subroutine interface defines how a subroutine interacts with the rest of the program. It includes:

  • The subroutine’s name.

  • The number and type of parameters.

  • Whether a return value is provided, and its type.

Importance of interfaces

Clear and well-defined interfaces ensure:

  • Subroutines are used correctly and consistently.

  • Collaboration among team members is more efficient.

  • Future maintenance and documentation are easier.

Example

python

def find_max(a: int, b: int) -> int:
    return max(a, b)

This tells us:

  • The subroutine is named find_max.

  • It expects two integer inputs.

  • It returns a single integer value.

Returning values from subroutines

Only functions return values. Returning values allows a subroutine to compute a result that can be stored or used in further calculations.

Returning a single value

python

def triple(x):
    return x * 3

y = triple(4)  # y becomes 12

The return keyword sends the result back to the caller.

Returning multiple values

Some languages, including Python, allow multiple values to be returned using tuples or other structures.

python

def divide_and_mod(x, y):
    return x // y, x % y

q, r = divide_and_mod(10, 3)  # q = 3, r = 1

This is especially useful when a single task produces more than one result.

When to use a subroutine

Good software design involves knowing when to create a subroutine. Common triggers include:

  • A block of code is repeated in multiple places.

  • A task performs a clear, separate function.

  • A section of code would benefit from a descriptive name.

  • The program is increasing in size and needs structure.

  • A block of code may need to be tested on its own.

Examples of use cases

  • Calculating formulas (e.g. area, interest).

  • Formatting inputs and outputs.

  • Performing validation.

  • Handling recurring game mechanics or UI updates.

By identifying these patterns, developers can design cleaner, more efficient code.

Designing effective subroutines

Effective subroutines follow several best practices to ensure clarity and maintainability.

1. Single responsibility principle

Each subroutine should do one thing. Avoid mixing unrelated operations.

Example:

  • Good: calculate_total_price()

  • Bad: calculate_price_and_display_invoice()

2. Descriptive naming

Use names that clearly explain the subroutine’s purpose.

  • Good: is_prime, get_user_input

  • Bad: do_it, stuff

This helps others understand the code without needing to read its internal logic.

3. Keep parameter lists manageable

Too many parameters can be confusing. If necessary, group related parameters into a structure like a dictionary or object.

4. Use local variables

Use variables declared inside the subroutine whenever possible. This reduces reliance on global state and improves encapsulation.

5. Minimise side effects

Avoid changing global variables or external state unless necessary. Subroutines should ideally operate like black boxes—taking input and returning output without affecting the rest of the program.

Unintended side effects can lead to hard-to-find bugs.

6. Include error handling

Anticipate potential errors such as:

  • Division by zero.

  • Invalid inputs.

  • Empty data structures.

Use appropriate control structures or exception handling mechanisms (like try/except) to manage runtime errors gracefully.

Subroutine patterns and techniques

Different subroutines serve different purposes. Some common categories include:

Utility functions

These perform simple, general-purpose tasks. Examples include:

  • Formatting strings.

  • Converting between data types.

  • Performing mathematical calculations.

They are typically stateless and widely reused.

Handlers

Handlers manage interaction, file input/output, or control flow. They may coordinate several lower-level subroutines.

Example:

python

def process_login():
    username = input("Username: ")
    password = input("Password: ")
    if authenticate(username, password):
        show_dashboard()

Recursive subroutines

A recursive subroutine is one that calls itself to solve a problem in smaller parts. It must include a base case to stop recursion.

Example:

python

def factorial(n):
    if n <= 1:
        return 1
    return n * factorial(n - 1)

Recursion is useful for problems that naturally break into smaller versions of themselves (e.g. tree traversal, factorials, Fibonacci sequences), but it requires careful planning to avoid infinite loops or stack overflow errors.

FAQ

Yes, a subroutine can call another subroutine, and this is a common and powerful programming practice. When subroutines call other subroutines, it enables a layered and structured approach to problem-solving. This technique is useful for decomposing complex tasks into smaller, manageable steps, each handled by a separate subroutine. For example, a process_order() subroutine might internally call validate_payment(), update_inventory(), and send_confirmation_email(). Each of these subtasks is isolated, making the code easier to understand, debug, and test. This also promotes code reuse, as the same helper subroutines can be used in different contexts. It encourages cleaner code and better separation of concerns—each subroutine performs a specific job and delegates more detailed work to other subroutines as needed. This type of hierarchical design mirrors how real-world systems are constructed and is especially useful in team environments where different developers may work on different layers of the system.

If a subroutine has the same name as a variable or another subroutine, the outcome depends on the programming language being used and the scope rules it follows. In most modern languages, naming conflicts are resolved using scope—the region of code where a name is valid and accessible. If a variable shares its name with a subroutine within the same scope, it can cause ambiguity or even a syntax error. For example, in Python, if you define a variable called calculate() and later attempt to call a subroutine with the same name, the interpreter may treat calculate as a variable rather than a callable function, resulting in a runtime error. Some languages allow method overloading or use namespaces and classes to distinguish names. Best practice is to always use descriptive and unique names for both variables and subroutines to avoid these issues. Following clear naming conventions and keeping consistent styles helps prevent such conflicts altogether.

No, subroutines are not necessarily executed in the order they are written in the code. The flow of execution in a program depends entirely on how and when subroutines are called. A subroutine defined at the top of a file may never execute unless it is explicitly called. Conversely, a subroutine defined later in the file might run immediately if called from the main program or from another subroutine. The actual execution order is determined by the logic of the main program and the call hierarchy of subroutines. For instance, a program might call main_menu() first, which then calls display_options(), which in turn calls get_user_input(). Although these subroutines may be defined in a different order within the code, the program flow follows the sequence in which the calls occur. Therefore, while the definition order affects readability and organisation, it does not influence the execution unless the language enforces top-down interpretation without pre-declaration.

Yes, in some programming languages like Python, JavaScript, and Swift, it is possible to define a subroutine inside another subroutine. These are called nested subroutines or local functions. A nested subroutine can only be accessed from within the outer subroutine where it is defined. This can be useful when the inner subroutine performs a task that is only relevant to the outer subroutine, helping to limit its visibility and reduce clutter in the global or module-level namespace. For example, an outer subroutine process_survey() might include a nested subroutine validate_answer() that is only used for checking user responses within that specific process. Using local subroutines improves encapsulation, as they are hidden from the rest of the program and cannot be misused elsewhere. It also improves readability by keeping related logic together. However, overusing nested subroutines can make the code harder to maintain if nesting becomes too deep or convoluted.

Using subroutines extensively can have both positive and negative performance implications, depending on the context. From a software engineering perspective, the benefits far outweigh the downsides: subroutines improve modularity, readability, reusability, and maintainability. However, from a performance standpoint, each subroutine call introduces a small overhead. This is because the program must push the current execution state onto a call stack, transfer control to the subroutine, and later pop that state to resume execution. For small, frequently called subroutines—especially those in tight loops—this overhead can accumulate and impact speed. Some languages and compilers apply optimisation techniques like inlining (replacing the call with the subroutine's code) to reduce this overhead. That said, in most real-world applications, the impact is negligible compared to the advantages subroutines offer. Performance issues due to subroutines are typically only a concern in highly optimised systems, real-time applications, or embedded environments where every millisecond matters.

Practice Questions

Describe two advantages of using subroutines in a large software project. Provide an example for each.

Using subroutines in large software projects improves modularity and reusability. Modularity allows developers to break the program into smaller, manageable units, each responsible for a single task. For example, a validate_input() subroutine can be tested independently and reused across multiple input forms. Reusability reduces duplication; once a subroutine like calculate_tax() is written and tested, it can be called wherever tax calculations are needed, ensuring consistent logic. These benefits lead to better organisation, easier debugging, and more efficient teamwork, as different developers can work on different subroutines without interfering with each other’s code.

Explain the difference between passing a parameter by value and by reference. Illustrate your answer with an example.

Passing by value sends a copy of the argument to the subroutine, so any changes made inside the subroutine do not affect the original variable. For instance, if a variable x = 5 is passed to a function that doubles it, x remains 5 after the function finishes. In contrast, passing by reference sends the actual memory address, so changes inside the subroutine affect the original variable. For example, if a list [1, 2] is passed to a subroutine that appends 3, the original list becomes [1, 2, 3], since the subroutine modifies the same memory location.

Hire a tutor

Please fill out the form and we'll find a tutor for you.

1/2
Your details
Alternatively contact us via
WhatsApp, Phone Call, or Email