Python Functions and Modules: Complete Guide with Worked Examples

Build a reliable mental model for Python functions, scope, imports, and packages, then trace a two-module score report from call to printed result.

KnowledgeGate Team

Exam prep & CS education

Updated 17 Aug 20266 min read

A short Python program works until print, return, local names, defaults, and imports interact. Read four marks, add a three-mark bonus, average them across two files, and the answer is 80.5 only if you can say what summarise received, what it copied, and what app.py actually printed. That tracing is what coding tests and interviews measure. The Coding & Skills category holds the wider path.

1. Function, method, module, package: build the right mental model

A function is a reusable block that receives inputs and may return a value.

def add_bonus(mark, bonus=3):
    return min(100, mark + bonus)

In add_bonus(72), add_bonus is the name, mark and bonus are parameters, 72 is an argument, and 3 is the default. The body returns 75. def creates the object; its body waits for a call.

Keep the terms separate. scores.append(75) calls a list method. score_tools.py is a module. A directory such as exam_tools/ holding modules is a package. Library is an informal label.

A 20-line script that reads four marks, adds a bonus, averages, and prints mixes four jobs. Splitting it into normalise, summarise, and display_report makes each job testable. Docstrings and type hints communicate intent but do not enforce types.

2. Parameters, arguments, return, and flexible calls

Calls usually use positional and keyword arguments. Python also offers tighter controls:

def scaled_score(mark: float, /, maximum: float = 100, *, digits: int = 1) -> float:
    return round(mark / maximum * 100, digits)

scaled_score(42, 50, digits=1) gives 42 / 50 * 100 = 84.0. The slash makes mark positional-only, so scaled_score(mark=42) fails. The star makes digits keyword-only, so scaled_score(42, 50, 2) fails.

Variable-length parameters collect values:

def total_attempts(*scores):
    return sum(scores)

def student_record(name, **details):
    return {"name": name, **details}

total_attempts(12, 18, 20) returns 50; scores is a tuple. student_record("Asha", city="Pune", semester=3) returns {"name": "Asha", "city": "Pune", "semester": 3}; details is a dictionary.

result = add_bonus(72) binds 75; print(result) displays it but returns None. Falling off a function also returns None. summarise_basic([72, 81, 65, 92]) can return (310, 77.5, 92), unpacked as total, average, highest.

3. Scope, state, recursion, and functions as values

Python searches local, enclosing, global, then built-in scope, called LEGB.

points = 10
def outer():
    points = 20
    def inner():
        nonlocal points
        points += 5
        return points
    return inner()

outer() returns 25; module-level points stays 10. Global state complicates testing.

With def append_score(score, scores=[]): ..., calls produce [72] then [72, 81] because they share one list. Use scores=None and create a list inside; separate calls return [72] and [81]. The same trap, plus *args and **kwargs at greater length, is worked through in Python Functions Deep Dive.

Recursion needs a base case: factorial(5) is 5 * 4 * 3 * 2 * 1 = 120, with factorial(0) = 1. Functions are values: apply_twice(lambda n: n + 3, 4) returns 10. Sorting Asha 72, Ravi 92, and Meera 81 with key=lambda row: row["score"] gives Asha, Meera, Ravi. Study sorting algorithms and complexity next; dynamic programming later optimises repeated recursion.

LEGB scope diagram: outer() returns 25 while global points stays 10, next to a factorial(5) call stack returning 120.

4. Worked example: build a score report across two Python modules

Create score_tools.py:

PASS_MARK = 40

def normalise(mark, maximum=100):
    if maximum <= 0:
        raise ValueError("maximum must be positive")
    return round(mark / maximum * 100, 2)

def summarise(scores, *, bonus=0):
    if not scores:
        raise ValueError("scores cannot be empty")
    adjusted = [min(100, score + bonus) for score in scores]
    total = sum(adjusted)
    return {
        "adjusted": adjusted,
        "total": total,
        "average": round(total / len(adjusted), 2),
        "highest": max(adjusted),
        "passed": sum(score >= PASS_MARK for score in adjusted),
    }

Now create app.py:

from score_tools import summarise

raw_scores = [72, 81, 65, 92]
report = summarise(raw_scores, bonus=3)
print(report)

The comprehension creates [75, 84, 68, 95]. Thus 75 + 84 + 68 + 95 = 322, average 322 / 4 = 80.5, highest 95, and passed 4. It prints {'adjusted': [75, 84, 68, 95], 'total': 322, 'average': 80.5, 'highest': 95, 'passed': 4}.

The list reference enters summarise, but the comprehension creates a new list, leaving raw_scores as [72, 81, 65, 92]. Keyword-only bonus receives 3; the dictionary binds to report; only app.py prints. Separately, normalise(42, 50) gives 42 / 50 * 100 = 84.0.

Value-flow trace from app.py into score_tools.py: summarise returns total 322, average 80.5, highest 95, passed 4; raw_scores unchanged.

5. Imports, module execution, and package boundaries

import score_tools keeps the namespace visible: call score_tools.summarise(...). from score_tools import summarise imports the name directly. import score_tools as st creates an alias. Avoid from score_tools import * because origins and collisions become unclear.

Add this guard to score_tools.py:

if __name__ == "__main__":
    print(summarise([40, 55], bonus=5))

Running the file prints {'adjusted': [45, 60], 'total': 105, 'average': 52.5, 'highest': 60, 'passed': 2}. Importing defines names without running the guard. Top-level code runs on first import in a process, then Python caches the module.

A tree can contain exam_tools/__init__.py, exam_tools/scores.py, and top-level app.py. The app uses from exam_tools.scores import summarise; package code may use from .scores import summarise. Local random.py can shadow the standard library. Mutual top-level imports can expose partially initialised names.

6. Common failures, tracebacks, and small tests

Six repairs matter: return instead of only printing; use None for a list default; rename sum = 0; import explicitly; guard side effects; move circularly shared definitions into a third module.

Read tracebacks from the last line upward. ModuleNotFoundError: score_tool means the name or path is wrong. After import score_tools, NameError: summarise calls for score_tools.summarise. Positional digits causes TypeError. The two ValueError checks reject maximum=0 and an empty list.

Put these checks inside the main guard in score_tools.py, where both names are already in scope and importers stay unaffected:

assert normalise(42, 50) == 84.0
assert summarise([72, 81, 65, 92], bonus=3)["average"] == 80.5
try:
    summarise([], bonus=3)
except ValueError as error:
    assert str(error) == "scores cannot be empty"
else:
    raise AssertionError("ValueError was not raised")

Return values test directly; printed output usually needs capture.

7. How GATE-style tracing and Python interviews test the ideas

GATE CS does not carry Python modules as a named syllabus area, so the part that transfers is the tracing itself: calls, recursion, parameters, scope, and shared state, which the paper examines in C and pseudocode. Imports, packages, and Python error messages earn their keep in coding assessments and interviews instead.

Try three rapid traces, with one sentence of reasoning each:

  1. def f(a, b=2): return a * b, then print(f(3), f(3, 4)), prints 6 12 because the first call uses the default and the second replaces it.

  2. With global x = 5, def g(): x = 8; return x, then print(g(), x), prints 8 5 because the local binding does not replace the global one.

  3. Two flawed append_score calls end with [72, 81] because the default list persists.

Interview extensions: return instead of print, add median without changing app.py, place validation, and refactor without circular imports. Coding for Placements drills the same round formats in C, C++, Java, and Python.

8. Python functions and modules in one minute, then practise

  • Define once and call many times, as with add_bonus.

  • Parameters receive arguments, such as mark=72.

  • return hands a value back, so 72 becomes 75.

  • Defaults are evaluated when the function is defined.

  • Names follow local, enclosing, global, and built-in lookup.

  • Recursion needs a base case.

  • A module is an importable file.

  • The main guard separates reusable definitions from direct execution.

KnowledgeGate's question bank carries over 90 questions on Python functions and modules. In 20 minutes, predict the output, type both files, change bonus from 3 to 8, then check [80, 89, 73, 100], total 342, average 85.5, highest 100, passed 4.

Use the Python course for structured concepts, MCQs, and coding practice. Trace each call, keep state visible, test returns, and remember that 322 / 4 = 80.5 only because the comprehension built [75, 84, 68, 95] first.