🎉 75% of content is free forever — Unlock Premium from $10/mo →
CW
NEWSLIVESearch All Content
💼 Servicesℹ️ About✉️ ContactView Pricing Plansfrom $10

Python Functions — Complete Guide to Defining and Using Functions

Python BasicsFunctions🟢 Free Lesson

Advertisement

Python Functions — Complete Guide to Defining and Using Functions

Functions are the building blocks of Python programs. They let you organize code into reusable pieces, avoid repetition, and write programs that are easier to read, test, and maintain.


Learning Objectives

By the end of this tutorial, you will be able to:

  • Define functions using def with proper naming conventions
  • Use parameters, default values, *args, and **kwargs
  • Return values including multiple values via tuples
  • Understand variable scope and the LEGB rule
  • Write closures and understand late binding
  • Create lambda functions and higher-order functions
  • Implement basic recursion with proper base cases
  • Write effective docstrings and type hints
  • Avoid common function-related mistakes

Defining Functions

The def Keyword

python
def greet(name):
    """Return a greeting string."""
    return f"Hello, {name}!"

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

Naming Conventions (PEP 8)

ConventionExampleDescription
lowercase + underscorescalculate_averageStandard snake_case
Verbs firstget_user, compute_totalAction verb prefix
No built-in namesDon't use list, dict, sumAvoid shadowing

Docstrings

python
def calculate_bmi(weight_kg, height_m):
    """Calculate Body Mass Index (BMI).

    Args:
        weight_kg: Weight in kilograms.
        height_m: Height in meters.

    Returns:
        BMI value as a float.
    """
    return weight_kg / (height_m ** 2)

Parameters and Arguments

Parameters are variable names in the function definition. Arguments are actual values passed when calling.

Positional and Keyword Arguments

python
def power(base, exponent):
    return base ** exponent

print(power(2, 3))                    # 8 (positional)
print(power(exponent=3, base=2))      # 8 (keyword)

Default Parameter Values

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

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

Mutable Default Argument Gotcha: Never use mutable objects (lists, dicts) as default values. They are created once and shared across calls.

python
# WRONG — shared list across calls
def add_item(item, items=[]):
    items.append(item)
    return items

print(add_item("apple"))   # ['apple']
print(add_item("banana"))  # ['apple', 'banana'] — unexpected!

# CORRECT — new list each time
def add_item(item, items=None):
    if items is None:
        items = []
    items.append(item)
    return items

print(add_item("apple"))   # ['apple']
print(add_item("banana"))  # ['banana'] — correct!

*args and **kwargs

python
def total(*numbers):
    """Sum any number of arguments."""
    return sum(numbers)

print(total(1, 2, 3))        # 6
print(total(1, 2, 3, 4, 5))  # 15

def build_profile(**info):
    """Build a profile dictionary."""
    return info

profile = build_profile(name="Alice", age=30, city="NYC")
print(profile)  # {'name': 'Alice', 'age': 30, 'city': 'NYC'}

Parameter Ordering Rules

Parameters must be declared in this order:

  1. Regular parameters
  2. Default parameters
  3. *args
  4. Keyword-only parameters (after *)
  5. **kwargs
python
def func(positional, keyword_default, *args, key_only, **kwargs):
    print(f"positional: {positional}")
    print(f"keyword_default: {keyword_default}")
    print(f"*args: {args}")
    print(f"key_only: {key_only}")
    print(f"**kwargs: {kwargs}")

func(1, 2, 3, 4, 5, key_only="mandatory", extra="optional")

Positional-Only and Keyword-Only (Python 3.8+)

python
def pos_only(a, b, /, c=10):
    return a + b + c

print(pos_only(1, 2))    # 13
# pos_only(a=1, b=2)  # TypeError

def kw_only(*, x, y=0):
    return x + y

print(kw_only(x=5))      # 5
# kw_only(5)  # TypeError

Return Values

Returning Multiple Values (Tuples)

python
def min_max(numbers):
    return min(numbers), max(numbers)

low, high = min_max([3, 1, 7, 2, 9])
print(f"Min: {low}, Max: {high}")  # Min: 1, Max: 9

Implicit None Return

python
def say_hello(name):
    print(f"Hello, {name}!")

print(say_hello("Alice"))  # None

Early Returns

python
def process(value):
    if value is None:
        return None
    if value <= 0:
        return 0
    return value * 2

Variable Scope

LEGB Rule

Python searches for variables in this order:

Built-in (B)Global (G)Enclosing (E)Local (L)Local → Enclosing → Global → Built-in

Local and Global Scope

python
x = "global"

def my_function():
    x = "local"
    print(f"Inside: {x}")

my_function()             # Inside: local
print(f"Outside: {x}")    # Outside: global

The global and nonlocal Keywords

python
counter = 0

def increment():
    global counter
    counter += 1

increment()
increment()
print(counter)  # 2

def make_counter():
    count = 0

    def increment():
        nonlocal count
        count += 1
        return count

    return increment

counter = make_counter()
print(counter())  # 1
print(counter())  # 2

First-Class Functions

In Python, functions are first-class objects — they can be assigned to variables, passed as arguments, and returned from other functions.

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

plus = add
print(plus(3, 4))  # 7

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

def square(x):
    return x ** 2

print(apply(square, 5))   # 25
print(apply(len, "hello")) # 5

def make_multiplier(factor):
    def multiplier(x):
        return x * factor
    return multiplier

double = make_multiplier(2)
print(double(5))  # 10

Closures

A closure is a function that remembers variables from its enclosing scope.

python
def make_greeting(prefix):
    def greet(name):
        return f"{prefix}, {name}!"
    return greet

hello = make_greeting("Hello")
goodbye = make_greeting("Goodbye")
print(hello("Alice"))  # Hello, Alice!
print(goodbye("Bob"))  # Goodbye, Bob!

Late Binding Gotcha

Closures capture variables by reference, not by value:

python
# WRONG — classic gotcha
functions = []
for i in range(5):
    functions.append(lambda: i)
print([f() for f in functions])  # [4, 4, 4, 4, 4]

# CORRECT — capture the current value
functions = []
for i in range(5):
    functions.append(lambda i=i: i)
print([f() for f in functions])  # [0, 1, 2, 3, 4]

Practical Closure Uses

python
def make_counter(start=0):
    count = [start]
    def counter():
        count[0] += 1
        return count[0]
    return counter

c = make_counter()
print(c())  # 1
print(c())  # 2

def make_cache():
    cache = {}
    def cached_func(key, value=None):
        if value is not None:
            cache[key] = value
        return cache.get(key)
    return cached_func

settings = make_cache()
settings("theme", "dark")
print(settings("theme"))  # dark

Lambda Functions

Lambda functions are anonymous, single-expression functions:

python
square = lambda x: x ** 2
print(square(5))  # 25

When to Use Lambdas

Best as arguments to higher-order functions:

python
students = [("Alice", 90), ("Bob", 80), ("Charlie", 95)]
students.sort(key=lambda student: student[1])
print(students)  # [('Bob', 80), ('Alice', 90), ('Charlie', 95)]

numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9]
evens = list(filter(lambda x: x % 2 == 0, numbers))
print(evens)  # [2, 4, 6, 8]

Limitations

  • Only a single expression (no statements)
  • No docstrings or type hints
  • Harder to debug

Higher-Order Functions

A higher-order function takes a function as argument or returns a function.

Built-in Higher-Order Functions

python
numbers = [1, 2, 3, 4, 5]

doubled = list(map(lambda x: x * 2, numbers))
print(doubled)  # [2, 4, 6, 8, 10]

evens = list(filter(lambda x: x % 2 == 0, numbers))
print(evens)  # [2, 4]

words = ["banana", "apple", "cherry"]
print(sorted(words, key=len))  # ['apple', 'banana', 'cherry']

Decorators — Higher-Order Functions in Practice

A decorator wraps a function to add behavior:

python
import time

def timer(func):
    """Measure execution time of a function."""
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        end = time.time()
        print(f"{func.__name__} took {end - start:.4f} seconds")
        return result
    return wrapper

@timer
def slow_function():
    time.sleep(1)
    return "Done"

result = slow_function()
# slow_function took 1.0012 seconds

Recursion

A function that calls itself is recursive. It requires a base case to stop.

Base Case and Recursive Case

python
def factorial(n):
    if n <= 1:       # Base case
        return 1
    return n * factorial(n - 1)  # Recursive case

print(factorial(5))  # 120
factorial(5)
  -> 5 * factorial(4)
    -> 4 * factorial(3)
      -> 3 * factorial(2)
        -> 2 * factorial(1)
          -> 1  (base case)
        -> 2
      -> 6
    -> 24
  -> 120

Fibonacci Sequence

python
def fibonacci(n):
    if n <= 0:
        return 0
    if n == 1:
        return 1
    return fibonacci(n - 1) + fibonacci(n - 2)

for i in range(10):
    print(fibonacci(i), end=" ")
# 0 1 1 2 3 5 8 13 21 34

Recursion Limit

python
import sys
print(sys.getrecursionlimit())  # 1000 (default)

When to Use Recursion vs Iteration

Use Recursion WhenUse Iteration When
Problem has recursive structure (trees, graphs)Simple loops over sequences
Depth is guaranteed smallDepth could be large
Code clarity matters mostPerformance is critical

Docstrings and Type Hints

Writing Good Docstrings

python
def calculate_statistics(data):
    """Calculate basic statistics for a dataset.

    Args:
        data: A list of numerical values. Must not be empty.

    Returns:
        A dictionary with keys 'mean', 'median', and 'stdev'.

    Raises:
        ValueError: If data is empty.
    """
    if not data:
        raise ValueError("Data must not be empty")
    n = len(data)
    mean = sum(data) / n
    sorted_data = sorted(data)
    median = sorted_data[n // 2]
    variance = sum((x - mean) ** 2 for x in data) / n
    stdev = variance ** 0.5
    return {"mean": mean, "median": median, "stdev": stdev}

Type Annotations

python
def greet(name: str) -> str:
    return f"Hello, {name}!"

def add_numbers(a: float, b: float) -> float:
    return a + b

from typing import Optional

def find_item(items: list[str], target: str) -> Optional[str]:
    for item in items:
        if item == target:
            return item
    return None

Common Mistakes

1. Mutable Default Argument

python
# WRONG
def append_to(item, target=[]):
    target.append(item)
    return target

print(append_to(1))  # [1]
print(append_to(2))  # [1, 2] — unexpected!

# CORRECT
def append_to(item, target=None):
    if target is None:
        target = []
    target.append(item)
    return target

2. Modifying Global Without global

python
x = 10

def change_x():
    x = 20  # Creates local variable, doesn't modify global

change_x()
print(x)  # Still 10!

3. Forgetting to Return

python
def calculate_sum(a, b):
    total = a + b
    # Missing return!

print(calculate_sum(3, 4))  # None — not 7!

4. Late Binding in Closures

python
# WRONG
functions = [lambda: i for i in range(5)]
print([f() for f in functions])  # [4, 4, 4, 4, 4]

# CORRECT
functions = [lambda i=i: i for i in range(5)]
print([f() for f in functions])  # [0, 1, 2, 3, 4]

5. Too Many Parameters

python
# BAD
def create_user(name, age, email, phone, address, city, state, zip_code, country):
    pass

# BETTER
def create_user(**kwargs):
    required = {"name", "email"}
    if not required.issubset(kwargs.keys()):
        raise ValueError(f"Missing required: {required - kwargs.keys()}")
    return kwargs

Practice Exercises

Exercise 1: Function Composition

Write a function compose that takes two functions and returns a new function that applies them in sequence.

python
def compose(f, g):
    """Return a function that applies g then f."""
    def composed(x):
        return f(g(x))
    return composed

double = lambda x: x * 2
add_one = lambda x: x + 1

double_then_add = compose(add_one, double)
print(double_then_add(3))  # 7 (3 * 2 + 1)

add_then_double = compose(double, add_one)
print(add_then_double(3))  # 8 ((3 + 1) * 2)

Exercise 2: Memoization Decorator

Write a decorator memoize that caches function results.

python
import functools

def memoize(func):
    """Cache results of function calls."""
    cache = {}

    @functools.wraps(func)
    def wrapper(*args):
        if args not in cache:
            cache[args] = func(*args)
        return cache[args]

    wrapper.cache = cache
    return wrapper

@memoize
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n - 1) + fibonacci(n - 2)

print(fibonacci(30))   # 832040
print(fibonacci.cache) # {0: 0, 1: 1, 2: 1, ...}

Exercise 3: Curry Function

Write a function curry that transforms a multi-argument function into a chain of single-argument calls.

python
import inspect

def curry(func):
    """Curry a function — convert f(a, b, c) to f(a)(b)(c)."""
    params = inspect.signature(func).parameters

    def curried(*args):
        if len(args) >= len(params):
            return func(*args)
        def partial(*more_args):
            return curried(*(args + more_args))
        return partial

    return curried

@curry
def add(a, b, c):
    return a + b + c

print(add(1)(2)(3))   # 6
print(add(1, 2)(3))   # 6
print(add(1)(2, 3))   # 6

Key Takeaways

  1. Functions are reusable blocks of code that encapsulate logic
  2. Parameters define what a function accepts; arguments are actual values passed
  3. Default arguments make parameters optional; always use None for mutable defaults
  4. *args and **kwargs handle variable numbers of arguments flexibly
  5. Return values can be any type; multiple values are returned as tuples
  6. Scope follows the LEGB rule: Local -> Enclosing -> Global -> Built-in
  7. Closures remember variables from enclosing scope — watch out for late binding
  8. Functions are first-class objects — assign them, pass them, return them
  9. Decorators are higher-order functions for adding cross-cutting concerns
  10. Recursion needs a base case; Python doesn't optimize tail calls
  11. Type hints and docstrings make functions self-documenting
—
☆☆☆☆☆
0 ratings

Rate & Feedback

Need Expert Python Help?

Get personalized tutoring, project support, or professional consulting.

Advertisement