Functions

Parameters, return values, scope, and pass-by-value vs pass-by-reference — how code gets named, reused and reasoned about.

programmingfunctionsscopeabstraction

What a function is

A function is a named, reusable block of code: values go in (parameters), work happens, a value comes out (return value).

Python
# Python
def area_of_circle(radius):
    return 3.14159 * radius * radius

print(area_of_circle(2))      # 12.56636
print(area_of_circle(10))     # 314.159
Java
// Java
static double areaOfCircle(double radius) {
    return 3.14159 * radius * radius;
}
C++
// C++
double areaOfCircle(double radius) {
    return 3.14159 * radius * radius;
}

Vocabulary, precisely:

  • Define a function: write its name, parameters and body. Nothing runs yet.
  • Call (invoke) it: area_of_circle(2)now the body runs, with radius = 2.
  • Parameter — the variable in the definition (radius). Argument — the actual value passed in the call (2).
  • Return value — what the call turns into. print(area_of_circle(2)) first becomes print(12.56636).

A function call is a jump: execution leaps into the body, runs it, and comes back carrying the return value.

Why functions exist (it's not just "avoid repetition")

The shallow reason: write the circle formula once, use it everywhere; fix a bug once, it's fixed everywhere (DRY — Don't Repeat Yourself).

The deep reason: functions let you stop thinking. Once area_of_circle works, you never again think about π — you think "area," one level higher. This is abstraction, the same move as Level 0's APIs: a name in front of hidden machinery. Software is towers of functions calling functions; your entire growth as an engineer is learning to build good towers.

The kitchen analogy: a function is a recipe card. "Make the sauce (see card 12)" — the main recipe stays readable because details live elsewhere.

Python
# This reads like a sentence — each call hides honest work
def process_order(order):
    validate(order)
    charge(order.customer, total_price(order))
    schedule_delivery(order)
    send_receipt(order)

Return early, return often

A function ends the moment it hits return. Use that for guard clauses (met in Control Flow):

Python
def withdraw(account, amount):
    if amount <= 0:
        return "Invalid amount"          # bail out early
    if amount > account.balance:
        return "Insufficient funds"      # and again
    account.balance -= amount            # happy path, un-nested
    return "Success"

A function with no explicit return hands back "nothing" (None in Python, void functions in Java/C++) — fine for functions called for their effects (printing, saving) rather than their answer.

Scope: where a name exists

Scope is the region of code where a variable is visible. Variables created inside a function are local — born when the call starts, gone when it returns:

Python
def calculate():
    result = 42        # local to calculate()
    return result

calculate()
print(result)          # ❌ NameError — 'result' doesn't exist out here

Each call gets fresh locals: two simultaneous calls to the same function don't see each other's variables. That isolation is precisely why functions are safe to reuse.

Variables defined outside all functions are global — visible everywhere. Convenient, dangerous:

Why engineers avoid global variables

A global can be modified by any function, so to debug it you must suspect every function. Ten functions sharing globals aren't ten independent tools — they're one tangled machine. Pass data in as parameters and out as return values; keep the plumbing visible. (Code review will enforce this; so do interviewers in Level 5 design rounds.)

Pass-by-value vs pass-by-reference

The payoff of the references idea from Variables & Data Types: what does a function receive — the thing, or a way to reach the thing?

Python
def add_bonus(score):          # numbers: reassignment is invisible outside
    score = score + 10

def add_item(cart):            # lists: MUTATION is visible outside
    cart.append("gift")

my_score = 50
add_bonus(my_score)
print(my_score)                # 50 — unchanged

my_cart = ["bread"]
add_item(my_cart)
print(my_cart)                 # ['bread', 'gift'] — changed!

The model that explains both: arguments are copies of what's in the box. For my_score, the box holds the value 50 → the function gets a copy of 50; reassigning its copy touches nothing outside. For my_cart, the box holds a reference (an address) → the function gets a copy of the address — pointing at the same list. Reassigning cart inside would be invisible; mutating through it (append) changes the one shared list.

  • Python and Java both work exactly this way (copies of references for objects).
  • C++ is the language of explicit choice: f(int x) copies the value, f(int& x) passes a true reference (even reassignment shows outside), f(const std::string& x) passes a reference but forbids changes — the efficient read-only default you'll see everywhere in C++ code.

Lambdas & Custom Sorting in DSA

In DSA, we frequently need to write quick, one-off functions to define custom sorting logic (e.g., sorting meeting intervals by their end times). Instead of defining a full helper function with name and signature, we use Lambdas (anonymous functions).

Let's look at how to sort a list of intervals [[1, 3], [5, 6], [2, 4]] by their second element:

1. Python Lambdas

In Python, lambdas are declared with the lambda keyword: lambda arguments: expression.

Python
intervals = [[1, 3], [5, 6], [2, 4]]

# Sort by the second element (index 1) of each sublist
intervals.sort(key=lambda x: x[1])
# intervals is now [[1, 3], [2, 4], [5, 6]]

# Sort by first element descending, then second element ascending (tie-breaker)
intervals.sort(key=lambda x: (-x[0], x[1]))

2. Java Lambda Comparators

In Java, lambdas use the arrow syntax (parameters) -> expression. We pass a comparator lambda to Arrays.sort or Collections.sort:

Java
int[][] intervals = {{1, 3}, {5, 6}, {2, 4}};

// Sort by the second element (index 1)
Arrays.sort(intervals, (a, b) -> Integer.compare(a[1], b[1]));

// Note: (a, b) -> a[1] - b[1] is also common, but can cause integer overflow 
// if values are large or negative. Integer.compare is always safe.

3. C++ Lambdas

In C++, lambdas have a capture clause [], parameters, and body: [](parameters) { return expression; }.

C++
std::vector<std::vector<int>> intervals = {{1, 3}, {5, 6}, {2, 4}};

// Sort by the second element
std::sort(intervals.begin(), intervals.end(), [](const std::vector<int>& a, const std::vector<int>& b) {
    return a[1] < b[1]; // returns true if 'a' should come before 'b'
});

Writing functions people thank you for

Four rules, learnable now, judged in every interview and code review forever:

  1. One job. "Calculate the total" — yes. "Calculate the total and email the user and update the log" — three functions wearing a trenchcoat. (Formally the Single Responsibility Principle — Level 5.)
  2. Name = verb phrase that tells the truth. send_receipt, is_valid_email (booleans read as questions). If an honest name needs "and" in it, see rule 1.
  3. Few parameters. More than three is a smell — bundle related ones into an object (Order), or your calls become f(true, false, 7, null, true).
  4. Prefer returning over mutating. clean(text) → new_text is easier to reason about than silently editing shared data — see the QA above for why.

Common beginner mistakes

  • Defining but never calling — then wondering why nothing printed. A definition is a recipe card; you still have to cook.
  • Confusing print with return. print shows a value to a human; return hands it to code. A function that prints its answer instead of returning it can't be composed: total(prices) + tax breaks.
  • Capturing nothing: calling s.upper() in Python and ignoring that it returns the new string (strings are immutable — it can't edit in place): name = name.upper().
  • One mega-function. A 300-line main() works until it doesn't. If you can't summarize a function in one sentence, split it.
  • Same name, different things — shadowing a global or built-in (list = [1,2] in Python kills the list() built-in for the rest of the program).

Think it through

The references model has one infamous trap that bites every Python beginner exactly once. Predict the output before revealing — getting surprised here is how it sticks.

Think it through: The mutable default argumentBeginner — predict the output0/5 stages

PROBLEMWhat does this print, and why? def add_item(item, basket=[]): basket.append(item); return basket — then print(add_item('a')) followed by print(add_item('b')).

  1. 1

    Make your prediction

    Write down what you expect the two prints to be.

  2. 2

    Why it happens

    When is the default value [] actually created — every call, or once?

    unlocks after the stage above
  3. 3

    Connect to references

    Tie it back to the box-and-reference model.

    unlocks after the stage above
  4. 4

    Fix it

    How do you get a fresh list each call without losing the 'optional argument' feature?

    unlocks after the stage above
  5. 5

    The rule

    State the takeaway in one sentence.

    unlocks after the stage above

Check yourself

Check yourself0/4 answered

1. A function mutates a list you passed in, but can't change a number you passed in. Why?

2. What makes a function 'pure'?

3. print vs return — which statement is correct?

4. Why is `def add_item(item, basket=[])` a classic bug?

Interview perspective

Practice

Beginner

  1. Write celsius_to_fahrenheit(c) (formula: c × 9/5 + 32), then use it in a loop printing a conversion table for 0–40°C in steps of 5.
  2. Write is_even(n) returning a boolean — then rewrite FizzBuzz from the previous page using helper functions is_multiple_of(n, k).

Intermediate

  1. Write largest(numbers) without using built-in max — accumulator pattern inside a function. Then write second_largest(numbers); the edge cases (duplicates, short lists) are the actual exercise.
  2. Predict the output, then run:
    Python
    def mystery(items):
        items.append(99)
        items = [0]
        items.append(1)
        return items
    
    data = [1, 2]
    result = mystery(data)
    print(data, result)
    
    Explain every printed value using the box-and-reference model.

Advanced

  1. Write apply_twice(f, x) that returns f(f(x)), and use it with a double(n) function. Congratulations — functions are values too (higher-order functions), the gateway to callbacks (Level 8) and functional programming.
  2. Refactor: take your guess-the-number game from the previous page and split it into read_guess(), give_hint(guess, secret) and play(). Notice how testable each piece became.

Next: Recursion — functions that call themselves, and the call stack earning its keep.