Skip to content
Python Functions Explained — Complete Step-by-Step Guide with Examples

Python Functions Explained — Complete Step-by-Step Guide with Examples

DodaTech Updated Jun 4, 2026 9 min read

Functions are reusable blocks of code that take inputs, perform actions, and return outputs. Think of a function like a vending machine — you put in money and a selection (inputs), the machine processes your request, and out comes a snack (output).

What You’ll Learn

  • How to define and call functions with def
  • How parameters, arguments, and return values work
  • The difference between *args and **kwargs
  • What variable scope means and how Python resolves names
  • Lambda functions, type hints, and docstrings explained simply
  • Common mistakes that trip up beginners and how to fix them

Why Functions Matter

Without functions, every program would be a single massive block of code — impossible to read, test, or reuse. Imagine if DodaZIP had to rewrite its file compression logic every time it needed to zip a file. Instead, it calls a compression function. Durga Antivirus Pro uses functions for signature checking, behavioral analysis, and quarantine — each is a separate, testable unit. Functions let you write once, use anywhere.

    flowchart LR
    A["Python Basics"] --> B["Control Flow"]
    B --> C["Functions"]
    C --> D["Lists & Dicts"]
    D --> E["Modules & Packages"]
    E --> F["File I/O & Errors"]
    A:::done --> B:::done --> C:::current --> D
    style A fill:#2563eb,stroke:#2563eb,color:#fff
    style B fill:#2563eb,stroke:#2563eb,color:#fff
    style C fill:#2563eb,stroke:#2563eb,color:#fff
    style D fill:#dbeafe,stroke:#2563eb,color:#1e40af
    style E fill:#dbeafe,stroke:#2563eb,color:#1e40af
    style F fill:#f1f5f9,stroke:#94a3b8,color:#64748b
  
Prerequisite: You should understand variables, data types, and control flow. If you haven’t covered those yet, review https://tutorials.dodatech.com/programming-languages/python/py-basics/ and https://tutorials.dodatech.com/programming-languages/python/py-control-flow/ first.

Defining Functions

You define a function with the def keyword. Here’s the anatomy:

def greet(name):
    return f"Hello, {name}!"

print(greet("Alice"))  # "Hello, Alice!"
print(greet("Bob"))    # "Hello, Bob!"

Let’s break this down:

  • def — Python keyword that says “I’m defining a function”
  • greet — the function name (use lowercase with underscores, like calculate_total)
  • (name) — parameters the function accepts (like slots for inputs)
  • : — colon marks the start of the function body
  • return — sends a value back to wherever the function was called
  • The indented block — the code that runs when the function is called

When Python runs greet("Alice"), it: (1) assigns "Alice" to the parameter name, (2) runs the body, (3) returns the result, (4) the print() function displays it.

PartMeaning
defKeyword that starts a function definition
greetFunction name
nameParameter — receives the input value
returnSends value back to caller. Without it, returns None
bodyThe indented block that runs when called

Parameters

Parameters are the inputs your function accepts. They’re like the settings on a coffee machine — you can customize what goes in.

Default Values

def greet(name="Guest", greeting="Hello"):
    return f"{greeting}, {name}!"

print(greet())              # "Hello, Guest!"
print(greet("Alice"))       # "Hello, Alice!"
print(greet("Bob", "Hi"))   # "Hi, Bob!"
Default arguments are evaluated once when the function is defined, not each call. Never use a mutable default like [] or {} — use None instead.

Positional vs Keyword Arguments

def describe(name, age, role):
    return f"{name} is {age} and works as a {role}"

# Positional (order matters)
print(describe("Alice", 25, "Dev"))

# Keyword (order doesn't matter — more readable)
print(describe(role="Dev", age=25, name="Alice"))

Both calls produce the same output. Keyword arguments make your code self-documenting — you can see what each value means.

*args and **kwargs

Sometimes you don’t know how many arguments you’ll receive. *args packs extra positional arguments into a tuple. **kwargs packs extra keyword arguments into a dict.

# *args — any number of positional args
def sum_all(*numbers):
    return sum(numbers)

print(sum_all(1, 2, 3, 4))  # 10

# **kwargs — any number of keyword args
def print_info(**info):
    for key, value in info.items():
        print(f"{key}: {value}")

print_info(name="Alice", age=25, role="Dev")

The parameter order must be: normal params → *args → default params → **kwargs.

Return Values

A function can return one value, multiple values, or nothing.

# Single value
def square(x):
    return x * x

# Multiple values (packed as a tuple)
def min_max(lst):
    return min(lst), max(lst)

low, high = min_max([3, 1, 7, 2, 9])
print(low, high)  # 1 9

# No return → None
def log(msg):
    print(msg)
    # implicit: return None

Scope — Where Variables Live

Think of scope like rooms in a house. A variable defined inside a function (a room) can’t be seen from outside that room.

global_var = "I'm global"

def demo():
    local_var = "I'm local"
    print(global_var)  # accessible (read-only)
    print(local_var)   # accessible

demo()
print(global_var)       # accessible
# print(local_var)      # NameError! — local_var doesn't exist here

To modify a global variable inside a function, use global:

counter = 0

def increment():
    global counter
    counter += 1

increment()
print(counter)  # 1
Avoid global when possible. Pass values as parameters and return results instead — it makes your code easier to test and reason about.

Lambda Functions

A lambda is a tiny anonymous function — useful for short operations where writing a full def feels like overkill:

# Regular function
def double(x):
    return x * 2

# Lambda (one expression only, no return keyword needed)
double = lambda x: x * 2

print(double(5))  # 10

Common real use: sorting a list of dicts:

students = [
    {"name": "Alice", "grade": 85},
    {"name": "Bob", "grade": 92},
    {"name": "Charlie", "grade": 78},
]
students.sort(key=lambda s: s["grade"], reverse=True)
print(students)
# Bob (92), Alice (85), Charlie (78)

Type Hints (Python 3.5+)

Type hints document what types a function expects and returns. They’re optional and not enforced at runtime, but tools like mypy and IDE autocompletion use them:

def greet(name: str, age: int) -> str:
    return f"{name} is {age} years old"

Docstrings

A docstring describes what a function does. Triple-quoted strings right after the def line:

def calculate(a: float, b: float, operation: str) -> float:
    """Perform basic arithmetic operations.

    Args:
        a: First number
        b: Second number
        operation: 'add', 'subtract', 'multiply', or 'divide'

    Returns:
        Result of the operation
    """
    operations = {
        "add": a + b,
        "subtract": a - b,
        "multiply": a * b,
        "divide": a / b if b != 0 else float("inf"),
    }
    return operations.get(operation, 0)

You can view any function’s docstring with help(function_name).

Common Mistakes

1. Forgetting the Return Statement

def square(x):
    result = x * x
    # missing return!

print(square(5))  # None — not 25!

Fix: Always end value-producing functions with an explicit return.

2. Using Mutable Default Arguments

def add_item(item, items=[]):  # BAD
    items.append(item)
    return items

print(add_item(1))  # [1]
print(add_item(2))  # [1, 2] — same list reused!

Fix: Use None and create a new list inside:

def add_item(item, items=None):
    if items is None:
        items = []
    items.append(item)
    return items

3. Wrong *args / **kwargs Order

def func(**kwargs, *args):  # SyntaxError!
    pass

Fix: normal params → *args → default params → **kwargs.

4. Modifying a Global Without global

count = 0
def increment():
    count += 1  # UnboundLocalError!

Fix: Use global count inside, or (better) pass it as a parameter and return the updated value.

5. Forgetting Parentheses When Calling

def greet():
    return "Hello"

print(greet)   # <function greet at 0x...> — the object, not the result!
print(greet()) # "Hello" — correct

Fix: Always add () to call a function.

6. Confusing Parameters and Arguments

Parameters are the variables in the function definition. Arguments are the values you pass when calling. def greet(name):name is a parameter. greet("Alice")"Alice" is an argument.

Practice Questions

1. What does this code return?

def add(a, b):
    return a + b

result = add(3, 4)

7. The function takes a=3, b=4, returns their sum.

2. Why does this print None?

def greet(name):
    print(f"Hello, {name}")

print(greet("Alice"))

The function prints “Hello, Alice” inside, but has no return statement, so it returns None. The print(greet(...)) then prints that None.

3. Fix this bug:

def append_to(item, list=[]):
    list.append(item)
    return list

The default list [] is shared across all calls. Fix with list=None and create a new list inside.

4. What’s the output?

x = 10
def change():
    x = 5
change()
print(x)

10. The x = 5 inside the function creates a local variable, it doesn’t modify the global x.

Challenge: Write a function is_palindrome(s) that returns True if a string reads the same forward and backward (ignoring case and spaces). Example: "racecar"True, "A man a plan a canal panama"True.

Solution
def is_palindrome(s: str) -> bool:
    cleaned = s.lower().replace(" ", "")
    return cleaned == cleaned[::-1]

FAQ

Why does my function return None?
When a function has no return statement, or a return without a value, it returns None. Make sure every code path ends with return <value>.
What's the difference between *args and **kwargs?
*args captures extra positional arguments as a tuple. **kwargs captures extra keyword arguments as a dict. The names args and kwargs are convention — you could use *params and **options.
Can I have both *args and **kwargs in the same function?
Yes. The order is: normal parameters → *args → default parameters → **kwargs.
Are type hints enforced?
No. Python ignores type hints at runtime. They’re documentation for humans and tools like mypy. Your code runs the same way with or without them.
Why shouldn't I use mutable default arguments?
Because default arguments are evaluated once at function definition time, not each call. If the default is a list or dict, all calls share the same object, causing surprising behaviour.

Try It Yourself

Run this to see functions in action:

def factorial(n):
    """Calculate n! recursively."""
    if n <= 1:
        return 1
    return n * factorial(n - 1)

def apply_twice(func, value):
    return func(func(value))

print(f"factorial(5) = {factorial(5)}")
print(f"apply_twice(lambda x: x+5, 10) = {apply_twice(lambda x: x+5, 10)}")

Expected output:

factorial(5) = 120
apply_twice(lambda x: x+5, 10) = 20

Mini Project: Task Manager

Build a simple task manager using functions:

tasks = []

def add_task(name: str, priority: str = "medium") -> None:
    """Add a new task to the list."""
    tasks.append({"name": name, "priority": priority, "done": False})
    print(f"Added: {name}")

def list_tasks() -> None:
    """Display all tasks."""
    if not tasks:
        print("No tasks yet!")
        return
    for i, task in enumerate(tasks, 1):
        status = "✓" if task["done"] else " "
        print(f"{i}. [{status}] {task['name']} ({task['priority']})")

def complete_task(index: int) -> None:
    """Mark a task as done by its number."""
    try:
        tasks[index - 1]["done"] = True
        print(f"Task {index} completed!")
    except IndexError:
        print("Invalid task number!")

# Try it out
add_task("Learn Python functions", "high")
add_task("Build a mini project", "high")
add_task("Review common mistakes", "medium")
list_tasks()
print()
complete_task(1)
list_tasks()

Expected output:

Added: Learn Python functions
Added: Build a mini project
Added: Review common mistakes
1. [ ] Learn Python functions (high)
2. [ ] Build a mini project (high)
3. [ ] Review common mistakes (medium)

Task 1 completed!
1. [✓] Learn Python functions (high)
2. [ ] Build a mini project (high)
3. [ ] Review common mistakes (medium)

What’s Next

Now that you understand functions, learn how to work with collections of data.

TopicDescriptionLink
Python Lists & DictsWork with collectionshttps://tutorials.dodatech.com/programming-languages/python/py-lists-dicts/
Python Modules & PackagesOrganize and reuse codehttps://tutorials.dodatech.com/programming-languages/python/py-modules/
DjangoWeb framework that uses functions extensivelyDjango

Practice tip: Extend the task manager with a show_by_priority(level) function that filters tasks. The best way to learn functions is to write your own!

What’s Next

Congratulations on completing this Py Functions tutorial! Here’s where to go from here:

  • Practice daily — Consistency is more important than long study sessions
  • Build a project — Apply what you learned by building something real
  • Explore related topics — Check out other tutorials in the same category
  • Join the community — Discuss with other learners and share your progress

Remember: every expert was once a beginner. Keep coding!

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro