PYTHON PROGRAMMING FOR DATA SCIENCE
TUTORIAL: FUNCTIONS
Page 1
1. Why Functions?
Let's start with a real problem: you need to calculate the average score for many students.
1.1. WITHOUT functions
Without functions — Repetitive, error-prone
# Student 1
math_1, phys_1, chem_1 = 8.5, 7.0, 9.0
avg_1 = (math_1 + phys_1 + chem_1) / 3
print(f"S1: {avg_1:.2f}")
# Student 2 - copy-paste again...
math_2, phys_2, chem_2 = 7.0, 8.5, 6.5
avg_2 = (math_2 + phys_2 + chem_2) / 3
print(f"S2: {avg_2:.2f}")
# Student 3 - copy-paste again...
math_3, phys_3, chem_3 = 9.0, 9.5, 8.5
avg_3 = (math_3 + phys_3 + chem_3) / 3
print(f"S3: {avg_3:.2f}")
# 47 more students...
1.2. WITH functions
With functions — Write once, use forever
def calc_average(math, phys, chem):
"""Calculate the average of 3 subjects"""
return (math + phys + chem) / 3
# Clean, readable, no repetition!
print(f"S1: {calc_average(8.5, 7.0, 9.0):.2f}")
print(f"S2: {calc_average(7.0, 8.5, 6.5):.2f}")
print(f"S3: {calc_average(9.0, 9.5, 8.5):.2f}")
# 47 more students - just 1 line each
Why do we need functions?
• Reusability: Write once, call many times
• Readability: Code has clear, descriptive names
• Maintainability: Fix in one place, applied everywhere
• Abstraction: Hide details, expose only what matters
• Testability: Easy to test each part independently
Page 2
2. Creating and calling functions
2.1. Basic Syntax
Function structure
def function_name(param_1, param_2):
"""Docstring: describes what the function does"""
# Function body: processing statements
result = param_1 + param_2
return result # Return the result
• def: keyword to start a function definition
• function_name: use snake_case, describe what the function does
• parameters: input data (can have 0 or many)
• """Docstring""": describes the function (optional but recommended)
• return: sends result back to the caller (optional)
2.2. Functions without return
Functions without return implicitly return None. Used when you only need to perform an action
(print, save file...).
No return
def greet(name):
"""Print a greeting — no return needed"""
print(f"Hello {name}! Happy learning.")
greet("Minh")
greet("Lan")
# Check return value
result = greet("An")
print(f"Return value: {result}") # None
Output:
Hello Minh! Happy learning.
Hello Lan! Happy learning.
Hello An! Happy learning.
Return value: None
2.3. Functions with return
With return
def absolute_value(num):
Page 3
"""Return the absolute value"""
if num < 0:
return -num
return num
a = absolute_value(-7)
b = absolute_value(3)
print(f"|{-7}| = {a}") # 7
print(f"|{3}| = {b}") # 3
print(f"Sum = {a + b}") # 10
Notes
• return ends the function immediately — code after return won't execute.
• A function can have multiple return statements (in different if/else branches).
• A function can return multiple values (as a tuple): return a, b
3. Parameter types
3.1. Positional arguments
Arguments matched by position — order must match the definition.
Positional
def introduce(name, age, major):
print(f"{name}, {age} years old, studying {major}")
introduce("An", 20, "CS") # Correct order
introduce(20, "An", "CS") # WRONG: 20 assigned to name!
3.2. Keyword arguments
Call by parameter name — order doesn't matter.
Keyword arguments
def msg(id, name):
print(f"ID: {id}, Name: {name}")
msg(id=100, name="Hoang") # Clear
msg(name="Hoang", id=101) # Reversed order - also OK!
3.3. Default parameters
Default parameters
Page 4
def hello(name, loud=False):
"""loud is optional, defaults to False"""
if loud:
print(f"HELLO, {[Link]()}!")
else:
print(f"Hello, {name}")
hello("Hoang") # No loud arg -> False
hello("Hoang", loud=True) # Explicit loud = True
Output:
Hello, Hoang
HELLO, HOANG!
3.4. Returning multiple values
Return multiple values
def statistics(numbers):
"""Return min, max, average of a list"""
smallest = min(numbers)
largest = max(numbers)
average = sum(numbers) / len(numbers)
return smallest, largest, average
# Call and unpack multiple values
scores = [8.5, 7.0, 9.2, 6.5, 8.0]
mn, mx, avg = statistics(scores)
print(f"Min: {mn}, Max: {mx}, Avg: {avg:.2f}")
Output:
Min: 6.5, Max: 9.2, Avg: 7.84
4. Variable scope
Scope determines where a variable "lives".
4.1. Local vs Global
Local vs Global
x = 20 # GLOBAL variable
def my_func():
x = 10 # LOCAL variable (different from global x!)
print("Inside:", x)
my_func() # Prints: 10
Page 5
print("Outside:", x) # Prints: 20 (unaffected)
Output:
Inside: 10
Outside: 20
4.2. Pass by Value vs Reference
Numbers/Strings: COPY Lists/Dicts: REFERENCE
def change(x): def add_item(lst):
x = 6 [Link](9)
x = 5 my_list = [5, 6, 7]
change(x) add_item(my_list)
print(x) # Still 5! print(my_list) # [5,6,7,9]
Remember
• Numbers, strings, tuples: function receives a COPY → changes inside don't affect outside.
• Lists, dicts: function receives a REFERENCE → changes inside DO affect outside!
• This is a common source of bugs. Be careful when passing lists/dicts to functions.
5. Use cases
Use case 1: Data validation
Validation
def validate_score(score):
if not isinstance(score, (int, float)):
return False, "Score must be a number"
if score < 0 or score > 10:
return False, "Score must be 0-10"
return True, "Valid"
print(validate_score(8.5)) # (True, "Valid")
print(validate_score(15)) # (False, "Score must be 0-10")
print(validate_score("abc")) # (False, "Score must be a number")
Use case 2: Data processing
Data processing
def clean_name(raw):
return [Link]().title()
Page 6
def make_email(full_name, domain="[Link]"):
parts = clean_name(full_name).lower().split()
if len(parts) < 2: return None
given = parts[-1]
family = "".join(parts[:-1])
return f"{given}.{family}@{domain}"
names = [" nguyen van AN ", "TRAN binh", " le thi chi "]
for raw in names:
print(f"{clean_name(raw):20s} -> {make_email(raw)}")
Output:
Nguyen Van An -> [Link]@[Link]
Tran Binh -> [Link]@[Link]
Le Thi Chi -> [Link]@[Link]
Use case 3: Function composition
Composition
def classify(score):
if score >= 8.5: return "Excellent"
if score >= 7.0: return "Good"
if score >= 5.5: return "Average"
return "Weak"
def process_student(name, scores):
avg = sum(scores) / len(scores)
grade = classify(avg) # Calling another function!
return {"name": name, "avg": avg, "grade": grade}
sv = process_student("An", [8.5, 9.0, 7.5])
print(f"{sv[\"name\"]}: {sv[\"avg\"]:.1f} - {sv[\"grade\"]}")
Output:
An: 8.3 - Good
6. Lambda — Anonymous functions
6.1. What is Lambda?
Lambda is a "one-line" function without a name. Used when you need a small, quick function
that's only used once or twice.
Page 7
Regular function (def) Lambda equivalent
def square(x): square = lambda x: x ** 2
return x ** 2
print(square(5)) # 25 print(square(5)) # 25
When to use Lambda?
Simple functions with a single expression, used 1–2 times
As a key for sorted(), max(), min()
As an argument for map(), filter()
NOT for complex logic (multiple lines, nested if/else)
6.2. Lambda with sorted()
This is the most common use case for lambda — sorting data by a custom criterion.
Lambda + sorted()
students = [("An", 8.5), ("Binh", 7.0), ("Chi", 9.2), ("Dung", 6.8)]
# Sort by score (2nd element of tuple)
by_score = sorted(students, key=lambda s: s[1])
print("Ascending:", by_score)
# Sort descending
by_score_desc = sorted(students, key=lambda s: s[1], reverse=True)
print("Descending:", by_score_desc)
# Sort by name length
by_len = sorted(students, key=lambda s: len(s[0]))
print("By name length:", by_len)
Output:
Ascending: [('Dung', 6.8), ('Binh', 7.0), ('An', 8.5), ('Chi', 9.2)]
Descending: [('Chi', 9.2), ('An', 8.5), ('Binh', 7.0), ('Dung', 6.8)]
By name length: [('An', 8.5), ('Chi', 9.2), ('Binh', 7.0), ('Dung', 6.8)]
6.3. Lambda with map() and filter()
map() applies a function to every element. filter() keeps elements that satisfy a condition.
map() & filter()
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
# map: apply function to each element
squares = list(map(lambda x: x ** 2, numbers))
Page 8
print("Squares:", squares)
# filter: keep elements matching condition
evens = list(filter(lambda x: x % 2 == 0, numbers))
print("Even numbers:", evens)
# Combine: square of even numbers
even_sq = list(map(lambda x: x**2, filter(lambda x: x%2==0, numbers)))
print("Squared evens:", even_sq)
Output:
Squares: [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
Even numbers: [2, 4, 6, 8, 10]
Squared evens: [4, 16, 36, 64, 100]
Lambda+map vs List comprehension
Both produce the same result:
• list(map(lambda x: x**2, nums)) → lambda style
• [x**2 for x in nums] → list comprehension
List comprehension is generally preferred for readability.
Lambda+map is useful when you already have a function to pass in.
7. Practice with lambda
7.1. Filtering product data
Filter products
products = [
{"name": "Laptop", "price": 15000000, "stock": 5},
{"name": "Mouse", "price": 200000, "stock": 0},
{"name": "Monitor", "price": 5000000, "stock": 12},
{"name": "Keyboard","price": 500000, "stock": 0},
{"name": "Headset", "price": 800000, "stock": 8},
]
# In-stock products
in_stock = list(filter(lambda p: p["stock"] > 0, products))
print("In stock:", [p["name"] for p in in_stock])
# Cheap + available
cheap = list(filter(
lambda p: p["price"] < 1000000 and p["stock"] > 0, products))
print("Cheap + available:", [p["name"] for p in cheap])
Page 9
Output:
In stock: ['Laptop', 'Monitor', 'Headset']
Cheap + available: ['Headset']
7.2. Currency conversion with map()
map() for conversion
prices_usd = [10.5, 25.0, 7.99, 42.0, 15.75]
prices_vnd = list(map(lambda usd: usd * 25000, prices_usd))
formatted = list(map(lambda v: f"{v:,.0f} VND", prices_vnd))
print(formatted)
Output:
['262,500 VND', '625,000 VND', '199,750 VND', '1,050,000 VND', '393,750 VND']
7.3. Scale function
scale() + lambda + map()
def scale(val, src, dst=(-1, 1)):
"""Map value from source range to destination range"""
return (val - src[0]) / (src[1] - src[0]) \
* (dst[1] - dst[0]) + dst[0]
print(scale(49, (-100, 100), (-50, 50))) # 24.5
print(scale(49, (-100, 100))) # 0.49
# Application: normalize scores to 0-1
scores = [65, 80, 45, 92, 73]
normalized = list(map(lambda s: scale(s, (0, 100), (0, 1)), scores))
print([f"{x:.2f}" for x in normalized])
Output:
24.5
0.49
['0.65', '0.80', '0.45', '0.92', '0.73']
Page 10
8. Exercises
Exercise 1:
Requirements
Write and test the following functions:
a) is_even(n) — returns True if n is even
b) count_vowels(text) — counts vowels (a,e,i,o,u) in text
c) celsius_to_all(c) — returns tuple (Fahrenheit, Kelvin)
Exercise 2:
Requirements
Write format_price(amount, currency='VND', decimals=0):
• format_price(1500000) → '1,500,000 VND'
• format_price(49.99, 'USD', 2) → '49.99 USD'
Exercise 3: Sorting
Requirements
products = [("Laptop",15000000), ("Mouse",200000), ("Monitor",5000000)]
Sort using lambda:
a) By price ascending b) By name length descending c) Alphabetically
Exercise 4: Data pipeline
Requirements
Build a mini pipeline with these functions:
1. load_data() — return list of 10 student dicts
2. calculate_averages(data) — add 'avg' key
3. classify_all(data) — add 'grade' key
4. filter_by_grade(data, grade) — filter by classification
5. generate_report(data) — print formatted report
Page 11