Python: lesson 6 of 7
Python
Functions in Python
Write reusable, testable Python functions for cleaning, metrics, and feature transformations.
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:
- Input: What shape, type, or content does it expect?
- Output: What does it return?
- Assumptions: What must be true for the result to make sense?
- Side effects: Does it change anything outside itself or mutate an input?
- Failure: What should happen if the assumptions are violated?
For calculate_conversion_rate, the contract is:
- Input: two numeric counts, where
visits > 0and0 <= conversions <= visits - Output: a ratio from
0to1 - 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
- Write a function contract in plain language for
calculate_conversion_rate(conversions, visits). - Explain why returning a mean is easier to test than printing it.
- A function removes invalid values from a list in place. How could that surprise later code?
- Give one normal case, one boundary case, and one invalid case for a metric function.
Quick Quiz
- What is one part of a function contract besides input and output? Answer: assumptions, side effects, or failure behavior.
- What does a function return without an explicit
return? Answer:None. - Why is
except: return 0risky in data work? Answer: it can hide different failures behind a plausible wrong number. - 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.