Functions, Part 1: Writing and Calling Them
print() is one, and so is len(). Somebody wrote them, gave them a name, and now you use them without knowing what is inside. This lesson is about writing your own, and it is the point where you stop copying code and start building with it.After this lesson, you will be able to:
- Define functions with def, parameters, and return values
- Use default arguments to make function parameters optional
- Understand *args and **kwargs for flexible argument passing
- Write concise lambda functions for simple, throwaway operations
- Explain variable scope — why local variables do not leak outside their function
Before You Start
#What Is a Function?
A function is a named, reusable block of code that takes input, does something, and (optionally) returns output.
print(), len(), type(), range(), int(), input(). Now you will learn to create your own.# Defining a function
def greet(name):
"""Say hello to someone."""
return f"Hello, {name}! Welcome to Python."
# Calling (using) the function
message = greet("Alex")
print(message) # Hello, Alex! Welcome to Python.
print(greet("Meera")) # Hello, Meera! Welcome to Python.Let us break down the syntax:
def. Keyword that starts a function definitiongreet. The function's name (usesnake_case)(name)— the parameter (input variable)""" ... """. Docstring (documentation). Describes what the function doesreturn. Sends a value back to the caller. Without it, the function returnsNone
#Parameters and Arguments
def calculate_bmi(weight_kg, height_m):
"""Calculate Body Mass Index."""
bmi = weight_kg / (height_m ** 2)
return round(bmi, 1)
# Positional arguments (order matters)
result = calculate_bmi(70, 1.75)
print(f"BMI: {result}") # BMI: 22.9
# Keyword arguments (order does not matter)
result = calculate_bmi(height_m=1.75, weight_kg=70)
print(f"BMI: {result}") # BMI: 22.9Given `def calculate_bmi(weight_kg, height_m):`, which call computes the BMI for someone who weighs 70kg and is 1.75m tall?
#Multiple Return Values
def analyze_scores(scores):
"""Return statistics about a list of scores."""
avg = sum(scores) / len(scores)
highest = max(scores)
lowest = min(scores)
return avg, highest, lowest # returns a tuple
# Unpack the results
average, high, low = analyze_scores([85, 92, 78, 95, 88])
print(f"Average: {average}, High: {high}, Low: {low}")
# Average: 87.6, High: 95, Low: 78What does a function return if there is no return statement?
def say_hello(name):
print(f"Hello, {name}!") # this PRINTS but does not RETURN
result = say_hello("Alex")
print(result) # None -- because there is no return statement!#Now trace it yourself
greet, bind name and the default greeting, and pop the frame on return — every variable change, every print, live.Edit the code, then click Trace it. Python actually runs in your browser — every line, every variable, every print.
Click Trace it to capture the execution trace. The scrubber below will let you step through every variable change line-by-line.
#Default Arguments
You can give parameters default values, making them optional when calling the function:
def send_reminder(name, days=3, urgent=False, show_details=True):
"""Print a reminder. Only the name is required."""
if show_details:
print(f"Reminder for {name}")
print(f"Due in {days} days")
message = f"{name}, your book is due in {days} days."
if urgent:
message = "URGENT: " + message
return message
# Use all defaults
print(send_reminder("Priya"))
# Override some defaults
print(send_reminder("Ravi", days=1, urgent=True))
# Override just one
print(send_reminder("Sara", show_details=False))# WRONG
# def bad_func(x=10, y): # SyntaxError
# RIGHT
def good_func(y, x=10):
return x + yprint("a", "b", sep=", ") works because print has a sep parameter that defaults to a space — you only pass it when you want something else. Almost every function you will ever use is built this way: a couple of required things, and a long tail of options with sensible defaults.What does the second call print? def add_item(item, items=[]): items.append(item) return items print(add_item("a")) print(add_item("b"))
#*args and **kwargs
Sometimes you want a function to accept any number of arguments.
*args — Variable Positional Arguments
def average(*args):
"""Calculate the average of any number of values."""
if not args:
return 0
return sum(args) / len(args)
print(average(10, 20)) # 15.0
print(average(1, 2, 3, 4, 5)) # 3.0
print(average(100)) # 100.0*args collects all positional arguments into a tuple. The name args is a convention — you could call it *numbers or *values.**kwargs — Variable Keyword Arguments
def make_order(**kwargs):
"""Build a pizza order from whatever the customer specifies."""
order = {
"size": "medium", # default
"crust": "thin", # default
"cheese": True, # default
}
order.update(kwargs) # override defaults with provided values
return order
order1 = make_order()
print(order1)
# {'size': 'medium', 'crust': 'thin', 'cheese': True}
order2 = make_order(size="large", extra_toppings=["mushroom", "olive"])
print(order2)
# {'size': 'large', 'crust': 'thin', 'cheese': True,
# 'extra_toppings': ['mushroom', 'olive']}**kwargs collects all keyword arguments into a dictionaryPython DictionariesA dictionary maps keys to values for O(1) lookup. Created with {key: value} syntax, accessed with dict[key] or dict.get(key, default).Learn more →. Notice what it bought you: make_order never mentions extra_toppings, yet the caller could add it. That is why library functions use it — they accept options the author never had to list.#Combining Everything
def flexible_function(required, *args, default="hello", **kwargs):
print(f"Required: {required}")
print(f"Extra positional: {args}")
print(f"Default: {default}")
print(f"Extra keyword: {kwargs}")
flexible_function("yes", 1, 2, 3, default="world", color="blue", size=10)
# Required: yes
# Extra positional: (1, 2, 3)
# Default: world
# Extra keyword: {'color': 'blue', 'size': 10}*args, keyword-only parameters (with defaults), **kwargs.#Lambda Functions
# Regular function
def double(x):
return x * 2
# Same thing as a lambda
double = lambda x: x * 2
print(double(5)) # 10Lambdas are most useful when passed to other functions:
# Sort a list of tuples by the second element
students = [("Alice", 92), ("Bob", 78), ("Charlie", 95), ("Diana", 88)]
# Sort by grade (second element of each tuple)
students.sort(key=lambda student: student[1])
print(students)
# [('Bob', 78), ('Diana', 88), ('Alice', 92), ('Charlie', 95)]
# Sort by grade descending
students.sort(key=lambda student: student[1], reverse=True)
print(students)
# [('Charlie', 95), ('Alice', 92), ('Diana', 88), ('Bob', 78)]
# Filter with a lambda
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
evens = list(filter(lambda x: x % 2 == 0, numbers))
print(evens) # [2, 4, 6, 8, 10]
# Map with a lambda
squared = list(map(lambda x: x ** 2, numbers))
print(squared) # [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]def function instead.#Scope — Local vs Global
# Global variable -- accessible everywhere
shop_name = "Corner Bakery"
def open_shop():
# Local variable -- only exists inside this function
staff_today = 3
print(f"{shop_name} is open with {staff_today} staff") # can READ global
open_shop()
# print(staff_today) # NameError: 'staff_today' is not defined (it is local to open_shop)
print(shop_name) # "Corner Bakery" -- global variables persist#The Key Rules
- Functions can READ global variables but should not modify them
- Local variables exist only inside the function — they are created when the function runs and destroyed when it returns
- Local variables shadow global ones — if you create a variable with the same name as a global, the local one takes priority inside the function
x = 100 # global
def demo():
x = 999 # local x -- does NOT change the global x
print(f"Inside function: x = {x}") # 999
demo()
print(f"Outside function: x = {x}") # 100 -- unchanged!#Why Scope Matters
count. Without scope, they would overwrite each other's values. With scope, each function has its own private count that cannot interfere.What gets printed? count = 10 def bump(): count = count + 1 print(count) bump()
Scope prevents accidents like this, but the error message can confuse beginners — it says "local variable referenced before assignment," but the cause is the assignment LATER in the function.
def count_vowels(text):
count = 0 # local to this function
for char in text.lower():
if char in "aeiou":
count += 1
return count
def count_words(text):
count = len(text.split()) # different variable, local to THIS function
return count
# These do not interfere with each other
print(count_vowels("Hello World")) # 3
print(count_words("Hello World")) # 2HitUnboundLocalErrororTypeError: missing argument? Reading a name that's later assigned inside the same function triggersUnboundLocalError, and forgetting a positional arg triggersTypeError. See the error decoder for both.
#Putting Functions Together
#Example 1: Data Preprocessing Pipeline
def clean_text(text):
"""Lowercase, strip whitespace, remove punctuation."""
import string
text = text.lower().strip()
text = text.translate(str.maketrans("", "", string.punctuation))
return text
def tokenize(text):
"""Split text into individual words."""
return text.split()
def remove_stop_words(tokens, stop_words=None):
"""Remove common words that do not carry meaning."""
if stop_words is None:
stop_words = {"the", "a", "an", "is", "are", "was", "in", "on", "at", "to", "of"}
return [word for word in tokens if word not in stop_words]
def preprocess(text):
"""Full preprocessing pipeline."""
cleaned = clean_text(text)
tokens = tokenize(cleaned)
filtered = remove_stop_words(tokens)
return filtered
# Use it
result = preprocess("The Cat is sitting ON the Mat!")
print(result) # ['cat', 'sitting', 'mat']#Example 2: Scoring a Quiz
def accuracy(predictions, actual):
"""Calculate classification accuracy."""
correct = sum(p == a for p, a in zip(predictions, actual))
return correct / len(actual)
def precision(predictions, actual, positive_label="spam"):
"""Of all items we predicted as positive, how many were actually positive?"""
true_positives = sum(p == a == positive_label for p, a in zip(predictions, actual))
predicted_positive = sum(p == positive_label for p in predictions)
return true_positives / predicted_positive if predicted_positive > 0 else 0.0
def evaluate_model(predictions, actual):
"""Run full evaluation suite."""
acc = accuracy(predictions, actual)
prec = precision(predictions, actual)
return {
"accuracy": round(acc, 4),
"precision": round(prec, 4),
"total_samples": len(actual),
}
# Test it
preds = ["spam", "ham", "spam", "spam", "ham", "spam", "ham", "ham"]
truth = ["spam", "ham", "ham", "spam", "ham", "spam", "spam", "ham"]
results = evaluate_model(preds, truth)
for metric, value in results.items():
print(f"{metric}: {value}")#Code Playground
Tests · Write each function, then test it with print() to verify it works!
What does this function return? def add(a, b): c = a + b
This function should return a fresh list with the new item each time it's called. Instead, items keep accumulating across calls. Fix the function.
['apple'] ['banana'] ['cherry']