0%

27- Defining Functions

Create your own functions. Give them names, parameters, and return values. The complete guide to writing reusable code blocks.

You understand what functions are. Now you need to write your own. Defining functions is one of the most important skills in programming. It transforms you from someone who writes scripts to someone who designs systems. A function definition creates a reusable block of code. You use the def keyword. You choose a name. You list parameters in parentheses. You write the body with proper indentation. Optionally, you return a value. This lesson covers everything about defining functions: syntax, naming conventions, parameters, return values, docstrings, and best practices. By the end, you will be comfortable creating functions for any task.

🕯️ 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.

Basic Function Definition Syntax
The def keyword, followed by the function name, parentheses, a colon, and an indented body.

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)

💡 The function body must be indented. PEP 8 recommends 4 spaces. Do not mix tabs and spaces.
Function Naming Conventions
Good function names are descriptive and follow Python’s naming conventions.
  • 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():

Parameters vs Arguments (Review)
Parameters are placeholders in the function definition. Arguments are the actual values passed when calling.

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.

Types of Parameters
Python functions can have different kinds of parameters. Order matters when defining them.
Parameter TypeSyntaxDescription
Positionaldef func(a, b)Most common. Order matters.
Defaultdef func(a, b=10)Parameter with default value. Optional when calling.
Keyword-onlydef func(*, a, b)Must be called with keyword syntax.
Var-positionaldef func(*args)Collects extra positional arguments.
Var-keyworddef 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)

Positional Parameters
The simplest and most common. The order of arguments must match the order of parameters.

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

Default Parameters
Parameters with default values are optional when calling the function. They must come after all non-default parameters.

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

⚠️ Default parameter values are evaluated once when the function is defined, not each time it is called. Never use mutable defaults like lists, dictionaries, or sets.
Keyword-Only Parameters
Parameters that must be called using their name. Defined after a * in the parameter list.

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.

The Return Statement
The return statement exits the function and optionally sends a value back to the caller.

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

Early Return (Guard Clauses)
You can have multiple return statements. This pattern is called a guard clause.

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

💡 Guard clauses make functions easier to read. Handle error cases first, then the happy path. This avoids deep nesting.
Docstrings (Documentation)
Always document your functions with docstrings. A docstring is a multi-line string right after the def line.

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.

Type Hints (Type Annotations)
Type hints indicate what types parameters and return values should be. They are optional but helpful for readability and IDE support.

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)

💡 Type hints make your code self-documenting and help IDEs provide better autocomplete. Use them for non-trivial functions, especially when working in a team.
Nested Functions (Functions Inside Functions)
You can define functions inside other functions. They are only visible inside the outer function.

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

Recursive Functions
A function that calls itself is recursive. Useful for problems that can be broken into smaller similar subproblems.

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

⚠️ Recursive functions must have a base case that stops the recursion. Without it, you get infinite recursion and a RecursionError. Python has a recursion limit (usually 1000) to prevent stack overflow.
Function Composition
Functions can call other functions. This is called composition and is a fundamental design pattern.

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)

Common Mistakes When Defining Functions
  • 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
Check Your Understanding
  • 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.

Related posts