Functions let you organize code into reusable blocks. In this lesson, you’ll learn how to define functions, pass arguments in different ways, return values, understand scope, use small anonymous functions (lambdas), and see a friendly introduction to recursion.
What is a function?
A function is a named, reusable block of code that performs a specific task. You can call it whenever you need that task done, optionally give it inputs (called arguments), and it can send a result back with return.
Why use functions?
- Reuse: write once, call many times.
- Clarity: break big problems into small, readable pieces.
- Testing: easier to test and debug small units of logic.
1) Defining a Function
In Python, you define a function with the def keyword, a name, an optional parameter list, and an indented body (after a colon). You can optionally include a docstring to describe the function and a return statement to send a value back. A function definition creates a reusable block of code and binds it to a name.
General Syntax
def function_name(param1, param2=default, *args, **kwargs):
"""Optional docstring describing what the function does."""
# function body (indented)
return result
function_name: the name you will call later.param1: a regular (positional/keyword) parameter.param2=default: a parameter with a default value.*args: collects extra positional arguments as a tuple.**kwargs: collects extra keyword arguments as a dict.return: sends a value back to the caller (omit to returnNone).
Definition vs Call
# Define (create) a function
def greet(name):
print(f"Hello, {name}!")
# Call (use) the function
greet("Ali")
Simple Examples
# 1) No return value (prints a greeting)
def greet(name):
print(f"Hello, {name}!")
greet("Ali") # Hello, Ali!
# 2) Returning a value
def add(a, b):
return a + b
result = add(2, 3)
print(result) # 5
Docstrings (recommended): describe what the function does.
def area_of_circle(r):
"""Return the area of a circle of radius r."""
PI = 3.14159
return PI * (r ** 2)
2) Arguments & Parameters
Functions receive input values through parameters. When you call a function, you supply those values as arguments. You can pass them by position or by name (keywords).
a) Positional arguments
Arguments are matched to parameters by their order: first to first, second to second, etc.
# Parameters: base, exponent
def power(base, exponent):
return base ** exponent
# Arguments positional: 2 → base, 3 → exponent
print(power(2, 3)) # 8
b) Keyword arguments
Arguments are matched by the parameter name, so order doesn’t matter—as long as the names are correct.
# Keyword: matched by name (order can change)
print(power(exponent=3, base=2)) # 8
Mixing rule: positional arguments must come first, then keyword arguments.
print(power(2, exponent=3)) # ✅ OK
# print(power(base=2, 3)) # ❌ Error: positional after keyword is not allowed
Common mistakes:
- Passing the same parameter twice (once positionally and once by keyword).
- Misspelling a parameter name when using keywords.
# power(2, base=2) # ❌ TypeError: multiple values for argument 'base'
# power(b=2, e=3) # ❌ TypeError: unexpected keyword argument
c) Default values
Parameters can define a default. If you don’t pass a value, the default is used; you can override it by passing an argument (often by keyword for clarity).
def welcome(name, greeting="Hello"):
return f"{greeting}, {name}!"
print(welcome("Ali")) # Hello, Ali!
print(welcome("Ali", greeting="Hi")) # Hi, Ali!
d) Variable arguments: *args and **kwargs
*args collects extra positional arguments as a tuple; **kwargs collects extra keyword arguments as a dict.
def summarize(title, *items, **options):
line = f"{title}: " + ", ".join(str(x) for x in items)
if options.get("uppercase"):
line = line.upper()
return line
print(summarize("Fruits", "apple", "banana", "mango"))
print(summarize("Fruits", "apple", uppercase=True))
Parameter order tip: def f(positional, /, standard, *, keyword_only): is advanced syntax. For now, remember: normal params → *args → **kwargs.
3) Multiple Returns & Unpacking
Functions can return a tuple; unpack it on the left.
def min_max(values):
return min(values), max(values)
lo, hi = min_max([3, 7, 2, 9])
print(lo, hi) # 2 9
4) Scope: Local, Enclosing, Global
Python looks up names using the LEGB rule: Local → Enclosing (in nested functions) → Global → Built-in.
a) Local vs Global
x = 10 # global
def show():
x = 5 # local (different from global x)
print("inside:", x)
show()
print("outside:", x)
# inside: 5
# outside: 10
b) global (modify a global — use sparingly)
count = 0
def increment():
global count
count += 1
increment()
print(count) # 1
c) nonlocal (modify the nearest enclosing scope)
def outer():
total = 0
def inner():
nonlocal total
total += 1
return total
print(inner()) # 1
print(inner()) # 2
outer()
5) Functions as First-Class Objects
You can store functions in variables, pass them to other functions, and return them.
def square(x):
return x * x
def apply(fn, value):
return fn(value)
op = square
print(apply(op, 5)) # 25
6) Lambdas (Small Anonymous Functions)
Use lambdas for tiny throwaway functions.
# sort by length
words = ["kiwi", "banana", "fig", "apple"]
print(sorted(words, key=lambda w: len(w)))
# map & filter
nums = [1, 2, 3, 4, 5]
doubles = list(map(lambda n: n * 2, nums))
evens = list(filter(lambda n: n % 2 == 0, nums))
print(doubles) # [2, 4, 6, 8, 10]
print(evens) # [2, 4]
Tip: Prefer def for anything non-trivial; lambdas are limited to a single expression.
7) Recursion (Optional)
A recursive function calls itself. Always define a base case to stop.
a) Factorial
def factorial(n):
if n <= 1: # base case
return 1
return n * factorial(n - 1)
print(factorial(5)) # 120
b) Fibonacci (illustrative)
def fib(n):
if n <= 1:
return n
return fib(n - 1) + fib(n - 2)
print(fib(6)) # 8
Note: Recursion can be elegant, but in Python, it’s often slower than loops and lacks tail-call optimization. Each recursive call adds stack overhead, and deep recursion will hit the default recursion limit (around ~1000 frames), raising a RecursionError. For large inputs or simple repetition, prefer iterative loops; use recursion when it naturally matches the problem and stays shallow.
8) Practical Mini-Examples
a) Safe average
def average(values):
if not values:
return 0.0
return sum(values) / len(values)
print(average([10, 20, 30])) # 20.0
print(average([])) # 0.0
b) Flexible greeting with *args / **kwargs
def make_greeting(*names, prefix="Hello"):
joined = ", ".join(names) if names else "friend"
return f"{prefix}, {joined}!"
print(make_greeting("Ali", "Ammar"))
print(make_greeting(prefix="Hi"))
c) Using a function as a key
def last_char(s):
return s[-1]
cities = ["Lahore", "Karachi", "Quetta", "Multan"]
print(sorted(cities, key=last_char)) # sort by last letter
Key Takeaways
- Define functions with
def; return values withreturn. - Arguments: positional, keyword, default values; expand with
*argsand**kwargs. - Return multiple values via tuples and unpacking.
- Understand scope (LEGB): use
global/nonlocalcarefully. - Functions are first-class: pass, store, and return them.
- Use lambdas for tiny one-liners; prefer
defotherwise. - Recursion needs a base case; loops are often simpler and faster.
Ready to continue? Dive into the next lesson, Python Data Structures — lists, tuples, sets, dictionaries, and comprehensions , where you’ll learn how to store, access, and transform data efficiently in Python.
Leave a comment
Your email address will not be published. Required fields are marked *
