🕯️ Magic Note
In Python, function definitions are executable statements. When Python encounters def, it creates a function object in memory and assigns it to the function name. This means you can define functions conditionally, inside other functions, or even at runtime.
Python
# Simplest function
def say_hello():
print(“Hello!”)
# Function with parameters
def greet(name):
print(f”Hello, {name}!”)
# Function with return value
def add(a, b):
return a + b
# Function with multiple statements
def calculate_bmi(weight, height):
bmi = weight / (height ** 2)
return round(bmi, 2)
- Use snake_case: lowercase with underscores
- Use verbs or verb phrases: get_user(), calculate_total(), is_valid()
- Be descriptive but not overly long: find_average() not f()
- Boolean functions often start with is, has, can: is_empty(), has_permission()
Python
# Good function names
def calculate_area(radius):
return 3.14159 * radius ** 2
def is_palindrome(word):
return word == word[::-1]
def get_formatted_date(year, month, day):
return f”{year}-{month:02d}-{day:02d}”
# Poor function names (avoid)
# def a():
# def do_stuff():
# def temp():
Python
# a and b are parameters
def multiply(a, b):
return a * b
# 5 and 6 are arguments
result = multiply(5, 6)
print(result) # 30
🕯️ Magic Note
The terms are often used interchangeably, but the distinction matters when reading error messages. “Missing 1 required positional argument” means you need to pass a value for a parameter.
| Parameter Type | Syntax | Description |
|---|---|---|
| Positional | def func(a, b) | Most common. Order matters. |
| Default | def func(a, b=10) | Parameter with default value. Optional when calling. |
| Keyword-only | def func(*, a, b) | Must be called with keyword syntax. |
| Var-positional | def func(*args) | Collects extra positional arguments. |
| Var-keyword | def func(**kwargs) | Collects extra keyword arguments. |
Python
# Different parameter types in one function
def example(a, b=10, *args, c=20, **kwargs):
print(f”a: {a}”)
print(f”b: {b}”)
print(f”args: {args}”)
print(f”c: {c}”)
print(f”kwargs: {kwargs}”)
example(1, 2, 3, 4, 5, d=30, e=40)
Python
def describe_pet(animal, name):
print(f”I have a {animal} named {name}”)
# Order matters
describe_pet(“dog”, “Max”) # I have a dog named Max
describe_pet(“cat”, “Luna”) # I have a cat named Luna
# describe_pet(“Max”, “dog”) # Wrong order! Would be confusing
Python
def greet(name, greeting=”Hello”):
print(f”{greeting}, {name}!”)
greet(“Ali”) # Hello, Ali!
greet(“Sara”, “Hi”) # Hi, Sara!
greet(“Reza”, greeting=”Hey”) # Hey, Reza!
# Default parameters must come after non-default
# def bad(a=1, b): # SyntaxError! non-default argument follows default argument
Python
# Parameters after * are keyword-only
def create_user(name, *, age, city):
return {“name”: name, “age”: age, “city”: city}
# Must use keyword arguments for age and city
user = create_user(“Ali”, age=25, city=”Tehran”)
# user = create_user(“Ali”, 25, “Tehran”) # TypeError!
# Another example
def configure(host, port, *, ssl=True, timeout=30):
print(f”Connecting to {host}:{port} (ssl={ssl}, timeout={timeout})”)
configure(“localhost”, 8080, ssl=False, timeout=60)
🕯️ Magic Note
Keyword-only parameters force clarity. When you see ssl=True in a function call, you know exactly what that value means. Positional arguments would require you to remember the order.
Python
# Function that returns a value
def square(x):
return x * x
result = square(5)
print(result) # 25
# Function that returns multiple values (tuple)
def get_min_max(numbers):
return min(numbers), max(numbers)
minimum, maximum = get_min_max([3, 1, 4, 1, 5])
print(minimum, maximum) # 1 5
# Function that returns nothing (implicitly returns None)
def print_message(msg):
print(msg)
result = print_message(“Hello”)
print(result) # None
Python
def divide(a, b):
# Guard clause
if b == 0:
return None # Cannot divide by zero
return a / b
def classify_age(age):
if age < 0:
return “Invalid”
if age < 13:
return “Child”
if age < 18:
return “Teenager”
if age < 65:
return “Adult”
return “Senior”
print(classify_age(-5)) # Invalid
print(classify_age(10)) # Child
print(classify_age(30)) # Adult
Python
def calculate_area(radius, pi=3.14159):
“””
Calculate the area of a circle.
Parameters:
radius (float): The radius of the circle
pi (float, optional): Value of pi. Defaults to 3.14159
Returns:
float: The area of the circle
Example:
>>> calculate_area(5)
78.53975
“””
return pi * radius ** 2
# View the docstring
print(calculate_area.__doc__)
# Or help(calculate_area) in the interpreter
🕯️ Magic Note
Docstrings are not comments. They are stored as the .__doc__ attribute of the function. Tools like Sphinx, pydoc, and IDEs use them to generate documentation automatically.
Python
# Function with type hints
def greet(name: str) -> str:
return f”Hello, {name}!”
def add(a: int, b: int) -> int:
return a + b
def process_list(items: list[int]) -> list[int]:
return [x * 2 for x in items]
def find_user(user_id: int, database: dict) -> dict | None:
return database.get(user_id)
# Type hints are ignored at runtime
print(greet(“Ali”)) # Works even if you pass an int (but tools will warn)
Python
def outer_function(x):
def inner_function(y):
return y * 2
return inner_function(x) + 10
print(outer_function(5)) # (5 * 2) + 10 = 20
# inner_function is not defined outside
# print(inner_function(5)) # NameError!
🕯️ Magic Note
Nested functions are useful for: – Helper functions that are only needed inside one function – Creating closures (functions that remember variables from their outer scope) – Organizing code without polluting global namespace
Python
# Factorial using recursion
def factorial(n):
“””Return n! (n factorial)”””
if n <= 1:
return 1 # Base case
return n * factorial(n – 1) # Recursive case
print(factorial(5)) # 120 (5 * 4 * 3 * 2 * 1)
# Sum of numbers 1 to n
def sum_to_n(n):
if n <= 0:
return 0
return n + sum_to_n(n – 1)
print(sum_to_n(10)) # 55
Python
def double(x):
return x * 2
def square(x):
return x ** 2
def double_then_square(x):
return square(double(x))
print(double_then_square(3)) # square(6) = 36
# Real-world example
def get_input():
return input(“Enter a number: “)
def to_int(value):
return int(value)
def double_value(value):
return value * 2
def display_result(value):
print(f”Double is: {value}”)
# Compose functions
user_input = get_input()
number = to_int(user_input)
doubled = double_value(number)
display_result(doubled)
- Forgetting the colon : at the end of the def line
- Indentation errors (function body not indented properly)
- Mutable default parameters (def func(items=[]))
- Defining default parameters before non-default parameters
- Returning nothing when something is expected
- Code after return that never executes
- Not handling the base case in recursive functions
- Write a function called is_even that returns True if a number is even.
- What is the difference between a parameter and an argument?
- How do you write a function with a default parameter?
- What is the purpose of a docstring?
- Write a recursive function that calculates the sum of digits of a number.
- What is wrong with def add(a=1, b)?
⚡ Whisper
Defining a function is an act of creation. You give a name to a block of logic. You decide what it needs to do its job (parameters) and what it will give back (return). This act transforms a script into a system. Small, focused functions are like well-crafted tools. A hammer that drives nails. A saw that cuts wood. A ruler that measures length. Each does one thing, does it well, and has a clear name. When you read code full of such functions, you do not need to understand the details of every hammer and saw. You see calculate_total() and validate_input() and save_to_file(). You understand the story without the details. That is the magic of functions. They hide complexity behind names. They let you think at higher levels. Define your functions carefully. Name them well. Write docstrings. Your future self will thank you.