Python: lesson 6 of 7

Python

PATH 01MODULE 02LESSON 06 OF 07Next: Lists, Tuples, Sets, and Dictionaries

Functions in Python

Write reusable, testable Python functions for cleaning, metrics, and feature transformations.

Beginner14 min readpythonfunctionsreusable-codedata-cleaning

Concept

A function is a named, reusable block of code. Define one with def, accept inputs through parameters, and send a result back with return. In data work, a useful function does more than run once: it makes its assumptions visible so you can reason about what it expects, returns, and does when something is wrong.

Why It Matters

Data projects commonly repeat cleaning rules, metric calculations, and feature transformations. Copying the same code into many cells creates inconsistent behavior. A function gives one rule one home, which makes it easier to reuse, inspect, and test.

Intuition

Think of a function as a small, well-labeled tool. You supply inputs, it performs one clear job, and it returns an output you can use elsewhere.

Def, Parameters, Arguments, and Return

def calculate_conversion_rate(conversions, visits):
    if visits <= 0:
        raise ValueError("visits must be positive")
    if conversions < 0 or conversions > visits:
        raise ValueError("conversions must be between 0 and visits")
    return conversions / visits


rate = calculate_conversion_rate(84, 1200)
print(rate)

Expected output: 0.07. conversions and visits are parameters in the definition; 84 and 1200 are arguments in the call. return gives the calculated value back to the caller.

A Lightweight Function Contract

You do not need a formal contract language, but you should be able to answer five questions about a function:

  1. Input: What shape, type, or content does it expect?
  2. Output: What does it return?
  3. Assumptions: What must be true for the result to make sense?
  4. Side effects: Does it change anything outside itself or mutate an input?
  5. Failure: What should happen if the assumptions are violated?

For calculate_conversion_rate, the contract is:

  • Input: two numeric counts, where visits > 0 and 0 <= conversions <= visits
  • Output: a ratio from 0 to 1
  • Assumptions: conversions are part of the recorded visits
  • Side effects: none; the inputs are not modified
  • Failure: raise a clear error when the counts violate the assumptions

Return Values Versus Printing

print() is useful for showing a value to a person or inspecting it while debugging. return is how a function passes a result to other code.

def show_mean(values):
    print(sum(values) / len(values))


def calculate_mean(values):
    return sum(values) / len(values)


scores = [0.81, 0.90, 0.87]
show_mean(scores)
mean_score = calculate_mean(scores)
print(mean_score > 0.85)

Expected output:

0.86
True

The returned mean can be stored, tested, compared, or used in another calculation. print() is not bad; it serves a different job.

Local Variables

Names created inside a function are local to that function.

def add_tax(amount):
    tax_rate = 0.18
    return amount * (1 + tax_rate)

tax_rate is available while add_tax runs, but it is not a general variable outside the function. Passing values as parameters is usually clearer than relying on hidden notebook variables from elsewhere in an analysis.

Side Effects and Mutation

A function can transform an input into a new result or change a mutable input in place. Neither choice is automatically right or wrong, but mutation should be intentional and visible to the caller.

def add_tax(prices):
    return [price * 1.1 for price in prices]


def add_tax_in_place(prices):
    for index, price in enumerate(prices):
        prices[index] = price * 1.1
    return prices

The first function leaves the caller's original list unchanged. The second changes that list itself. A later calculation may therefore see different data even though the surprise happened in an earlier step.

One Python-specific warning: do not use a mutable object as a default argument when you expect a fresh value each call.

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

Using items=[] instead can reuse the same list across calls, creating hidden shared state.

Boundary Validation, Tiny Tests, and Sanity Checks

Validate assumptions at boundaries where bad input would otherwise produce a misleading result or a cryptic failure. You do not need to validate everything everywhere.

For a metric function, test a tiny normal case, a boundary case, and an invalid case before using full data:

normal_rate = calculate_conversion_rate(20, 100)
zero_conversion_rate = calculate_conversion_rate(0, 100)

print(normal_rate)
print(zero_conversion_rate)

try:
    calculate_conversion_rate(5, 0)
except ValueError as error:
    print(error)

Expected output:

0.2
0.0
visits must be positive

These tests check behavior: did the function produce the expected result and fail clearly for invalid input? A sanity check asks whether a result is plausible. Here, a conversion rate should always be between 0 and 1; a value outside that range signals a data or logic problem even if the code runs.

assert can document or check an internal assumption during development, but it is not a replacement for explicit validation of external or user-provided data.

Function Responsibility

Functions are easier to reason about when each performs one coherent task. Small functions can form a readable analysis flow, such as load_orders() -> validate_orders() -> summarize_orders(), without turning one function into the whole workflow.

Worked Example: A Testable Feature Rule

Return a value so the same feature rule can be used in multiple parts of a project.

def is_long_tenure(tenure_months, minimum_months=12):
    if tenure_months < 0:
        raise ValueError("tenure_months cannot be negative")
    return tenure_months >= minimum_months


print(is_long_tenure(18))
print(is_long_tenure(6))

Expected output:

True
False

Because the function returns a Boolean, it can be tested directly and used later in conditional logic.

Decision Lab

Decision Lab: Shared-State Bug

An analyst writes:

def clean_orders(orders):
    orders.remove(None)
    return orders


raw_orders = [120, None, 85, 200]
clean_orders_list = clean_orders(raw_orders)
missing_count = raw_orders.count(None)

print(clean_orders_list)
print(missing_count)

Expected output:

[120, 85, 200]
0

What happened? The function mutated the original list in place. clean_orders_list and raw_orders now refer to the same changed data, so later code cannot measure missing values in the raw input.

Why is this difficult to notice? The later calculation looks innocent. The real cause is an earlier side effect that created a hidden dependency between steps.

Is mutation always wrong? No. It can be useful and efficient when callers expect it. A safer alternative here is to return a cleaned copy:

def clean_orders(orders):
    return [order for order in orders if order is not None]

A small test should check both the returned value and the original input after the call.

Failure Signals

Visible Failure Versus Silent Wrongness

This pattern is dangerous in data work:

def average_value(total, count):
    try:
        return total / count
    except:
        return 0

A zero denominator, wrong data type, or unexpected bug all become 0, which can look like a legitimate result. Broad exception handling can turn a real data problem into silent wrongness. Catch a specific exception only when you can handle it meaningfully; otherwise let the failure surface.

Compact Reliability Recap

  • State what a function expects, returns, and may change.
  • Return values when later code needs the result.
  • Make mutation intentional rather than surprising.
  • Test a small normal case, boundary case, and invalid case.
  • Prefer a clear failure to a plausible but misleading number.

Check Your Reasoning

Transfer Question

A churn-rate function worked on the current dataset. Next month, one segment has zero active customers, and the function now crashes or returns NaN.

Was the original function necessarily badly written? Not always. It may never have been tested on that boundary. Improve it by stating the zero-denominator behavior explicitly, validating that case, and adding a small test. The appropriate result may be undefined or skipped; it does not have to be zero.

Practice Questions

  1. Write a function contract in plain language for calculate_conversion_rate(conversions, visits).
  2. Explain why returning a mean is easier to test than printing it.
  3. A function removes invalid values from a list in place. How could that surprise later code?
  4. Give one normal case, one boundary case, and one invalid case for a metric function.

Quick Quiz

  1. What is one part of a function contract besides input and output? Answer: assumptions, side effects, or failure behavior.
  2. What does a function return without an explicit return? Answer: None.
  3. Why is except: return 0 risky in data work? Answer: it can hide different failures behind a plausible wrong number.
  4. What does a sanity check ask? Answer: whether a result is plausible given what you know about the data.

Key Takeaway

Key Takeaways

Functions make repeated data work consistent and easier to reason about. Reliable functions make assumptions visible, return useful values, use intentional mutation, validate important boundaries, and are checked on small examples before you trust them on full data.

Next Lesson

Next, learn the core Python collections used to store features, categories, and metrics.

Finish this lesson on your terms

Mark it complete when you have worked through the material and are ready to move on.