Imagine you are baking a cake. Every time a customer orders one, you don't invent a brand-new recipe from scratch ยท you follow the same recipe each time, maybe swapping the flavour depending on what the customer wants. That recipe is a function.
In Python, a function is a reusable block of code that you write once and run as many times as you need, with different values each time if required. Functions are the single most important building block of any real program. Without them, you would copy and paste the same code over and over ยท messy, error-prone, and hard to fix.
This lesson covers four closely connected topics from the W3Schools Python tutorials, merged into one smooth, progressive lesson:
| Topic | What You Will Learn |
|---|---|
| Functions | How to create and call functions |
| Arguments | How to pass values into functions |
| \args and \\*kwargs | How to handle an unknown number of arguments |
| Scope | Where variables live and who can see them |
By the end of this lesson you will be able to write your own functions, pass data into them flexibly, and fully understand why variables sometimes seem to "disappear" ยท and why that is actually a good thing.
Before we begin, here are the concepts you must already understand. If any of these feel unfamiliar, take a few minutes to review them.
- Variables ยท storing a value in a name (e.g.,
x = 5) print()ยท displaying output in the terminal- Basic data types ยท strings (
"hello"), integers (5), floats (3.14), lists ([1, 2, 3]) if/elseยท making decisions in codeforloops ยท repeating an action multiple times
No prior experience with functions or scope is needed.
1.1 What Is a Function?
A function is a named, reusable block of code. You define it once, and then you can call (run) it as many times as you like from anywhere in your program.
Real-world analogy: Think of a function like a vending machine button. The machine (your function) is built once. Every time someone pushes the button (calls the function), the machine does the same job and delivers a result.
Why do functions exist?
- DRY principle ยท "Don't Repeat Yourself." Write the logic once, use it everywhere.
- Readability ยท Code broken into named functions is far easier to read and understand.
- Maintenance ยท If a rule changes, you fix it in one place instead of hunting through hundreds of lines.
- Testing ยท Small, focused functions are much easier to test and debug.
1.2 Defining a Function
In Python, you create a function using the def keyword.
Syntax:
def function_name():
# code that runs when the function is calledBreaking down the syntax line by line:
| Part | What It Means |
|---|---|
def | Short for "define" ยท tells Python you are creating a function |
function_name | The name you give your function (use lowercase and underscores) |
() | Parentheses ยท this is where inputs (arguments) will go later |
: | The colon signals the start of the function body |
| Indented block | All code indented underneath belongs to this function |
1.3 Your First Function
def say_hello():
print("Hello from inside a function!")Important: Defining a function does not run it. It just stores the recipe. Nothing appears on screen yet.
1.4 Calling a Function
To actually run the function, you call it by writing its name followed by parentheses:
def say_hello():
print("Hello from inside a function!")
say_hello()Expected output:
Hello from inside a function!You can call the same function multiple times:
say_hello()
say_hello()
say_hello()Expected output:
Hello from inside a function!
Hello from inside a function!
Hello from inside a function!๐ก Think about it: What happens if you never call the function? Try removing
say_hello()from the code. What do you see?
1.5 Why Indentation Matters
Python uses indentation (spaces at the start of a line) to know which lines belong inside a function. This is not optional ยท it is the rule.
def greet():
print("Line 1 โ inside the function")
print("Line 2 โ also inside the function")
print("Line 3 โ OUTSIDE the function, runs immediately")
greet()Expected output:
Line 3 โ OUTSIDE the function, runs immediately
Line 1 โ inside the function
Line 2 โ also inside the function๐ง Notice:
Line 3runs first because it is outside the function and sits at the top level of the program.Line 1andLine 2only run whengreet()is called.
1.6 Common Beginner Mistake ยท Forgetting the Call
# โ WRONG โ defines the function but never calls it
def show_score():
print("Your score is 100")
# Nothing is printed because we forgot to call it!Corrected version:
# โ
CORRECT
def show_score():
print("Your score is 100")
show_score() # This line actually runs the functionExpected output:
Your score is 1001.7 Common Beginner Mistake ยท Indentation Error
# โ WRONG โ missing indentation inside function body
def greet():
print("Hello") # IndentationError!Corrected version:
# โ
CORRECT
def greet():
print("Hello") # 4 spaces of indentation1.8 The pass Statement
Sometimes you want to create a function skeleton now and fill in the logic later. An empty function body causes a syntax error ยท use pass as a placeholder:
def my_future_function():
pass # Does nothing, but Python is happy
my_future_function() # No output, no errorThis is common in professional development when planning out a project structure before writing all the logic.
2.1 What Are Arguments?
So far our functions do the exact same thing every time. But real functions need to work with different data each time ยท like a calculator that can add any two numbers, not just 2 + 2.
Parameters and arguments let you pass information into a function.
The difference:
| Word | Meaning | Where |
|---|---|---|
| Parameter | The variable name listed in the function definition | Inside def parentheses |
| Argument | The actual value you pass when calling the function | Inside the call parentheses |
Think of a parameter as an empty slot, and an argument as the value you drop into that slot.
2.2 A Function with One Parameter
def greet_person(name): # 'name' is the parameter
print("Hello, " + name + "!")
greet_person("Alice") # "Alice" is the argument
greet_person("Bob") # "Bob" is the argument
greet_person("Dr. Johnson") # "Dr. Johnson" is the argumentExpected output:
Hello, Alice!
Hello, Bob!
Hello, Dr. Johnson!๐ง What changed? The same function ran three times. Each time, the
nameparameter held a different value. The function code never changed ยท only the input did.
2.3 A Function with Multiple Parameters
You can define as many parameters as you need, separated by commas:
def describe_student(first_name, subject, grade):
print(first_name + " studies " + subject + " and scored " + str(grade) + "%.")
describe_student("Maria", "Maths", 94)
describe_student("James", "Biology", 78)Expected output:
Maria studies Maths and scored 94%.
James studies Biology and scored 78%.Note:
str(grade)converts the number94into the string"94"so it can be joined with the other strings using+. This is called type conversion.
2.4 Positional Arguments
When you pass arguments in order, Python matches them to parameters by position:
def show_order(item, quantity, price):
print(item + " ร " + str(quantity) + " = ยฃ" + str(price))
show_order("Apples", 6, 3.00)
# item = "Apples", quantity = 6, price = 3.00Expected output:
Apples ร 6 = ยฃ3.0โ ๏ธ Order matters! If you mix up the order, you get incorrect results ยท or an error:
show_order(6, "Apples", 3.00) # quantity and item are swapped!Incorrect output:
6 ร Apples = ยฃ3.02.5 Keyword Arguments
You can also pass arguments using the parameter name so order does not matter:
def show_order(item, quantity, price):
print(item + " ร " + str(quantity) + " = ยฃ" + str(price))
show_order(quantity=6, price=3.00, item="Apples")Expected output:
Apples ร 6 = ยฃ3.0Using keyword arguments makes your code self-documenting ยท anyone reading it knows exactly what each value means.
2.6 Default Parameter Values
You can give a parameter a default value. If the caller does not provide that argument, the default is used automatically:
def greet(name, language="English"):
if language == "English":
print("Hello, " + name + "!")
elif language == "Spanish":
print("Hola, " + name + "!")
elif language == "French":
print("Bonjour, " + name + "!")
greet("Alice") # uses default: English
greet("Carlos", "Spanish") # overrides default
greet("Marie", "French") # overrides defaultExpected output:
Hello, Alice!
Hola, Carlos!
Bonjour, Marie!๐ก Rule: Parameters with defaults must come after parameters without defaults in the function definition.
# โ WRONG
def greet(language="English", name): # SyntaxError
pass
# โ
CORRECT
def greet(name, language="English"):
pass2.7 Returning Values with return
So far our functions have printed things. But often you want a function to calculate a result and send it back so your program can use it elsewhere.
The return keyword sends a value back to wherever the function was called:
def add(a, b):
result = a + b
return result
total = add(10, 25)
print("The total is:", total)Expected output:
The total is: 35What happens line by line:
add(10, 25)is called ยทa = 10,b = 25- Inside:
result = 10 + 25โresult = 35 return resultsends35back to the callertotalreceives the returned value35print(...)displays it
๐ After
return, the function stops. Any code afterreturnin the same function body will never run.
def demo():
return "I am returned"
print("This line never runs!") # unreachable code
value = demo()
print(value)Expected output:
I am returned2.8 Practical Example ยท Temperature Converter
def celsius_to_fahrenheit(celsius):
fahrenheit = (celsius * 9 / 5) + 32
return fahrenheit
temp_c = 100
temp_f = celsius_to_fahrenheit(temp_c)
print(str(temp_c) + "ยฐC = " + str(temp_f) + "ยฐF")
print(str(0) + "ยฐC = " + str(celsius_to_fahrenheit(0)) + "ยฐF")
print(str(37) + "ยฐC = " + str(celsius_to_fahrenheit(37)) + "ยฐF")Expected output:
100ยฐC = 212.0ยฐF
0ยฐC = 32.0ยฐF
37ยฐC = 98.6ยฐF๐ก Real-world use: Weather apps, scientific instruments, and cooking sites use this exact type of function. Write it once, use it everywhere.
2.9 Passing a List as an Argument
Any data type can be passed as an argument, including lists:
def show_fruits(fruits):
for fruit in fruits:
print("๐ " + fruit)
my_fruits = ["Apple", "Banana", "Mango", "Grape"]
show_fruits(my_fruits)Expected output:
๐ Apple
๐ Banana
๐ Mango
๐ Grape3.1 The Problem: What If You Don't Know How Many Arguments There Will Be?
Imagine writing a function that adds numbers. How many numbers should it accept? Two? Five? Twenty?
If you hard-code three parameters, a caller who wants to sum six numbers is stuck. Python solves this with arbitrary arguments.
3.2 \*args ยท Arbitrary Positional Arguments
Adding a single * before a parameter name tells Python: "collect all the extra positional arguments into a tuple."
By convention, the name used is args, but any name after works.
def add_all(*numbers):
total = 0
for n in numbers:
total = total + n
return total
print(add_all(1, 2))
print(add_all(1, 2, 3, 4, 5))
print(add_all(10, 20, 30, 40, 50, 60))Expected output:
3
15
210What is numbers here? It is a tuple ยท an immutable, ordered collection of all the values passed in:
def inspect_args(*items):
print(type(items)) # <class 'tuple'>
print(items)
inspect_args("cat", "dog", "fish")Expected output:
<class 'tuple'>
('cat', 'dog', 'fish')3.3 \*args With Other Parameters
args can be combined with regular parameters. Regular parameters must come before args:
def show_menu(restaurant_name, *dishes):
print("Restaurant: " + restaurant_name)
print("Today's menu:")
for dish in dishes:
print(" - " + dish)
show_menu("La Piazza", "Pasta", "Pizza", "Tiramisu", "Risotto")Expected output:
Restaurant: La Piazza
Today's menu:
- Pasta
- Pizza
- Tiramisu
- Risotto๐ง How it works:
"La Piazza"fills therestaurant_nameparameter. Everything else ยท"Pasta","Pizza","Tiramisu","Risotto"ยท is collected into thedishestuple.
3.4 \\kwargs ยท Arbitrary Keyword Arguments
kwargs does the same thing as *args, but for keyword arguments (name=value pairs). The double collects them into a dictionary.
By convention the name is kwargs (keyword arguments), but you can use any name after .
def describe_person(**details):
print("Person Profile:")
for key, value in details.items():
print(" " + key + ": " + str(value))
describe_person(name="Sarah", age=28, city="London", job="Engineer")Expected output:
Person Profile:
name: Sarah
age: 28
city: London
job: EngineerWhat is details here? It is a dictionary:
def inspect_kwargs(**info):
print(type(info)) # <class 'dict'>
print(info)
inspect_kwargs(colour="blue", size=12)Expected output:
<class 'dict'>
{'colour': 'blue', 'size': 12}3.5 \\kwargs ยท Practical Example: Product Builder
def create_product(**specs):
print("=== New Product ===")
for spec, value in specs.items():
print(spec.capitalize() + ": " + str(value))
print()
create_product(name="Laptop", brand="TechPro", ram="16GB", price=999.99)
create_product(name="Headphones", brand="SoundWave", colour="Black", wireless=True)Expected output:
=== New Product ===
Name: Laptop
Brand: TechPro
Ram: 16GB
Price: 999.99
=== New Product ===
Name: Headphones
Brand: SoundWave
Colour: Black
Wireless: True๐ก Real-world use: Web frameworks like Flask and Django use
**kwargseverywhere. When you create a database model or an HTML element with attributes, the framework accepts them as keyword arguments.
3.6 Combining Regular, \args, and \\*kwargs
You can use all three together in one function. The order must be:
def function(regular_params, *args, **kwargs):def full_order(table_number, *items, **extras):
print("Table " + str(table_number) + " ordered:")
for item in items:
print(" - " + item)
if extras:
print(" Special requests:")
for key, val in extras.items():
print(" " + key + ": " + str(val))
full_order(7, "Pizza", "Salad", "Water", sauce="extra", gluten_free=True)Expected output:
Table 7 ordered:
- Pizza
- Salad
- Water
Special requests:
sauce: extra
gluten_free: True3.7 The \ and \\* Unpacking Operators in Function Calls
The and can also be used when calling a function, to unpack* a list or dictionary into separate arguments:
def add(a, b, c):
return a + b + c
numbers = [1, 2, 3]
print(add(*numbers)) # unpacks list into: add(1, 2, 3)Expected output:
6def greet(name, language):
print("Hello " + name + " in " + language)
settings = {"name": "Aiko", "language": "Japanese"}
greet(**settings) # unpacks dict into: greet(name="Aiko", language="Japanese")Expected output:
Hello Aiko in Japanese3.8 Common Beginner Mistakes with \args and \\*kwargs
Mistake 1 ยท Wrong order:
# โ WRONG โ **kwargs before *args
def bad_func(**kwargs, *args): # SyntaxError
pass
# โ
CORRECT โ *args always before **kwargs
def good_func(*args, **kwargs):
passMistake 2 ยท Treating \*args as a list (it's a tuple):
def my_func(*numbers):
numbers.append(99) # AttributeError! Tuples don't have append
# โ
CORRECT โ convert to list if you need to modify it
def my_func(*numbers):
nums_list = list(numbers)
nums_list.append(99)
print(nums_list)
my_func(1, 2, 3)Expected output:
[1, 2, 3, 99]4.1 What Is Scope?
Scope describes where in your code a variable is visible and accessible. Not every variable can be seen from every part of a program ยท and this is intentional.
Why does scope exist? Imagine a huge program with hundreds of functions. If every variable was visible everywhere, any function could accidentally overwrite another function's total or count. Scope creates walls between different parts of code, preventing accidental interference.
Analogy: Think of scope like rooms in a house. A lamp in your bedroom (local scope) only lights your bedroom. It does not light the kitchen. The main electricity supply (global scope) powers the entire house.
Python has four levels of scope, remembered with the acronym LEGB:
| Letter | Name | Where |
|---|---|---|
| L | Local | Inside the current function |
| E | Enclosing | Inside an outer function (for nested functions) |
| G | Global | At the top level of the file |
| B | Built-in | Python's built-in names (e.g., print, len, range) |
We will focus on Local and Global ยท the two you will encounter most.
4.2 Local Scope
A variable created inside a function is local ยท it only exists while the function is running, and it is invisible outside.
def my_function():
local_variable = "I only exist inside this function"
print(local_variable)
my_function()
print(local_variable) # NameError! local_variable doesn't exist hereExpected output:
I only exist inside this function
Traceback (most recent call last):
...
NameError: name 'local_variable' is not defined๐ง Why? When
my_function()finishes, Python destroys all local variables. They are temporary ยท like writing on a whiteboard that gets erased when the meeting ends.
4.3 Local Variables in Different Functions Don't Interfere
Each function has its own private workspace. Two functions can both use a variable called x without conflict:
def function_a():
x = 100
print("Inside function_a, x =", x)
def function_b():
x = 999
print("Inside function_b, x =", x)
function_a()
function_b()Expected output:
Inside function_a, x = 100
Inside function_b, x = 999Both functions have their own x. They never interfere with each other. This is the power of local scope.
4.4 Global Scope
A variable created outside all functions is global ยท it is visible to all code in the file:
greeting = "Good morning" # global variable
def say_it():
print(greeting) # can READ the global variable
def shout_it():
print(greeting.upper()) # can also READ it
say_it()
shout_it()Expected output:
Good morning
GOOD MORNING4.5 Can a Function Modify a Global Variable?
By default, reading a global variable inside a function is fine. But modifying it is blocked ยท Python creates a new local variable instead:
counter = 0 # global
def increment():
counter = counter + 1 # โ UnboundLocalError!
increment()Why the error? Python sees counter = on the left side of = inside the function, assumes you mean a new local variable, then tries to read the (not-yet-created local) counter on the right side ยท and fails.
4.6 The global Keyword
To tell Python "I want to modify the actual global variable, not create a local one," use the global keyword:
counter = 0 # global
def increment():
global counter # declare intent to use the global
counter = counter + 1
print("Counter:", counter)
increment()
increment()
increment()
print("Final counter:", counter)Expected output:
Counter: 1
Counter: 2
Counter: 3
Final counter: 3โ ๏ธ Use
globalsparingly. Overusingglobalis considered bad practice because it makes code hard to trace and debug. The professional approach is to pass values as arguments and return results instead.
Professional alternative to global:
# โ Using global (not ideal)
total = 0
def add_to_total(n):
global total
total += n
# โ
Better approach โ pass and return
def add_to_total(current_total, n):
return current_total + n
total = 0
total = add_to_total(total, 5)
total = add_to_total(total, 10)
print(total) # 154.7 Local Takes Priority Over Global
If a local variable and a global variable share the same name, the local one wins inside the function:
name = "Global Name"
def show_name():
name = "Local Name" # this is a NEW local variable
print(name)
show_name()
print(name) # global variable unchangedExpected output:
Local Name
Global Name๐ง Notice: Assigning inside the function did NOT change the global
name. The global is completely untouched.
4.8 The nonlocal Keyword (Bonus: Enclosing Scope)
When you have a function inside another function (a nested function), the inner function can access the outer function's variables as enclosing scope. To modify them, use nonlocal:
def outer():
message = "Hello from outer"
def inner():
nonlocal message
message = "Modified by inner"
print("Inner says:", message)
inner()
print("Outer says:", message)
outer()Expected output:
Inner says: Modified by inner
Outer says: Modified by innerThis is an advanced pattern. You will encounter it when learning about closures and decorators ยท but
nonlocalfollows exactly the same logic asglobal, just one level in.
4.9 Built-in Scope
Python has many built-in names that are always available ยท like print, len, range, type, int, str, list. These live in the built-in scope, the outermost layer:
print(len("Hello")) # 'len' and 'print' are built-in
print(type(42)) # 'type' is built-inExpected output:
5
<class 'int'>โ ๏ธ Never shadow built-in names! Don't create variables called
list,input,id, etc. ยท this overwrites the built-in and causes confusing errors.
# โ WRONG โ never do this
list = [1, 2, 3] # You've now overwritten the built-in 'list'
print(list([1,2,3])) # TypeError โ list is now just a list object, not a function
# โ
CORRECT โ use a descriptive name
my_list = [1, 2, 3]Exercise 1 ยท Grading Function
Objective: Write a function that takes a student's score and returns a letter grade.
Scenario: A teacher needs a quick tool to convert any numeric score to a grade.
Steps:
- Define a function called
get_gradethat accepts one parameter:score - Inside, use
if / elif / elseto determine the grade:
- 90 ยท 100 โ "A" - 80 ยท 89 โ "B" - 70 ยท 79 โ "C" - 60 ยท 69 โ "D" - Below 60 โ "F"
- Return the grade letter
- Call the function with at least five different scores and print each result
Hint: Use return to send the grade back, then print() outside the function.
Solution:
def get_grade(score):
if score >= 90:
return "A"
elif score >= 80:
return "B"
elif score >= 70:
return "C"
elif score >= 60:
return "D"
else:
return "F"
scores = [95, 83, 72, 61, 45, 100, 59]
for s in scores:
print("Score:", s, "โ Grade:", get_grade(s))Expected output:
Score: 95 โ Grade: A
Score: 83 โ Grade: B
Score: 72 โ Grade: C
Score: 61 โ Grade: D
Score: 45 โ Grade: F
Score: 100 โ Grade: A
Score: 59 โ Grade: FSelf-check questions:
- What grade does
get_grade(60)return? Why? - What happens if you pass
get_grade(105)? What should you do to handle that?
Exercise 2 ยท Shopping Bill with \*args
Objective: Use *args to calculate a total from any number of prices.
Scenario: A checkout system needs to add up a variable number of item prices.
def calculate_bill(customer_name, *prices):
total = sum(prices)
print("Customer: " + customer_name)
print("Items: " + str(len(prices)))
print("Total: ยฃ" + str(round(total, 2)))
print()
calculate_bill("Alice", 4.99, 1.49, 7.25)
calculate_bill("Bob", 12.99, 3.49, 8.00, 2.50, 1.99)
calculate_bill("Carol", 35.00)Expected output:
Customer: Alice
Items: 3
Total: ยฃ13.73
Customer: Bob
Items: 5
Total: ยฃ28.97
Customer: Carol
Items: 1
Total: ยฃ35.0What-if challenge: What happens if you call calculate_bill("Dave") with no prices? Try it. Is the result sensible? How would you handle an empty bill?
Exercise 3 ยท User Profile with \\kwargs
Objective: Use **kwargs to build flexible user profiles.
def create_profile(username, **attributes):
print("=== Profile: " + username + " ===")
for key, value in attributes.items():
print(" " + key.replace("_", " ").title() + ": " + str(value))
print()
create_profile("coder99", age=22, city="Berlin", language="Python", years_experience=3)
create_profile("data_queen", age=30, field="Data Science", tools=["pandas", "numpy", "sklearn"])Expected output:
=== Profile: coder99 ===
Age: 22
City: Berlin
Language: Python
Years Experience: 3
=== Profile: data_queen ===
Age: 30
Field: Data Science
Tools: ['pandas', 'numpy', 'sklearn']Exercise 4 ยท Scope Experiment
Objective: Understand what happens when local and global variables share a name.
x = "I am global"
def test_local():
x = "I am local"
print("Inside function:", x)
test_local()
print("Outside function:", x)Expected output:
Inside function: I am local
Outside function: I am globalQuestions:
- Did the function change the global
x? Why not? - What would you add to the function to actually change the global
x?
| Mistake | Example | Fix |
|---|---|---|
| Defining but not calling a function | def greet(): print("Hi") with no call | Add greet() after the definition |
| Wrong indentation | def f(): then print(...) at column 0 | Indent the body with 4 spaces |
| Passing wrong number of arguments | def add(a, b): ... called as add(1) | Match the number of arguments to parameters |
Confusing print with return | Function prints but you try to store the result | Use return for values you need to use later |
| Wrong order: \\kwargs before \*args | def f(*kw, ar) | Must be def f(ar, *kw) |
Modifying a global without global keyword | count = 0 โ def f(): count += 1 | Add global count inside the function |
| Shadowing built-in names | list = [1,2,3] | Use descriptive names like my_list |
| Treating \*args as a list | args.append(x) | args is a tuple; convert with list(args) first |
| Default parameter before non-default | def f(x=1, y) | Non-defaults must come first: def f(y, x=1) |
This project combines functions, arguments, args, *kwargs, and scope into one realistic application.
Project Goal
Build a Student Report Generator that:
- Accepts student data (name, scores for multiple subjects, personal notes)
- Calculates the average score
- Assigns a letter grade
- Prints a formatted report
Stage 1 ยท Helper Functions (Setup)
def calculate_average(*scores):
"""Calculate the average of any number of scores."""
if len(scores) == 0:
return 0
return round(sum(scores) / len(scores), 1)
def get_grade(average):
"""Convert an average score to a letter grade."""
if average >= 90:
return "A โ Excellent"
elif average >= 80:
return "B โ Good"
elif average >= 70:
return "C โ Satisfactory"
elif average >= 60:
return "D โ Needs Improvement"
else:
return "F โ Unsatisfactory"
# Test Stage 1
avg = calculate_average(85, 90, 78, 92)
print("Average:", avg)
print("Grade:", get_grade(avg))Lesson 15 complete! ๐
You covered:
- โ Lesson Introduction
- โ Prerequisite Concepts
- โ Part 1 ยท Python Functions
- โ Part 2 ยท Arguments and Parameters
- โ Part 3 ยท \*args and \*\*kwargs
- โ Part 4 ยท Python Scope
- โ Guided Practice Exercises
- โ Common Beginner Mistakes ยท Full Summary