Intermediate

Functions in Python

Learn how to define and call functions in Python, including default and keyword arguments, scope, recursion, and higher-order functions with Docker-ready examples

Functions are how Python programs are organized into reusable, named blocks of logic. Instead of repeating the same code, you wrap it in a function, give it a name, and call it whenever you need it. This keeps programs readable and makes complex behavior easier to test and maintain.

Python is a multi-paradigm language, and its function model reflects that. Functions are first-class objects: you can assign them to variables, pass them as arguments to other functions, return them from functions, and store them in data structures. This blends procedural style with functional programming techniques like higher-order functions, closures, and lambdas—all without leaving idiomatic Python.

In this tutorial you’ll learn how to define and call functions, work with default and keyword arguments, handle variable scope, write recursive functions, and use functions as values. Every example is self-contained and runnable with Docker.

Defining and Calling Functions

A function is defined with the def keyword, a name, a parameter list in parentheses, and an indented body. The return statement sends a value back to the caller. A function that doesn’t explicitly return anything returns None.

Create a file named functions.py:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
# Defining and calling functions in Python

def greet(name):
    """Return a friendly greeting for the given name."""
    return f"Hello, {name}!"

def add(a, b):
    """Return the sum of two numbers."""
    return a + b

# Call the functions and use their return values
message = greet("Ada")
print(message)
print(add(3, 4))

# A function with no explicit return yields None
def announce(text):
    print(f">> {text}")

result = announce("Functions are first-class in Python")
print(result)

The triple-quoted string just below each def is a docstring—Python’s built-in way to document what a function does. The greet function uses an f-string to build its result, while announce only prints and therefore returns None.

Default and Keyword Arguments

Python lets you give parameters default values, call functions with keyword arguments (in any order), and accept an arbitrary number of arguments using *args and **kwargs. This flexibility is one of the reasons Python function signatures are so expressive.

Create a file named parameters.py:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# Default, keyword, and variadic parameters

def power(base, exponent=2):
    """Raise base to exponent; exponent defaults to 2 (square)."""
    return base ** exponent

print(power(5))                   # uses the default exponent of 2
print(power(2, 10))               # positional arguments
print(power(base=3, exponent=3))  # keyword arguments, explicit and clear

def describe(*args, **kwargs):
    """Accept any number of positional and keyword arguments."""
    print("positional:", args)
    print("keyword:", kwargs)

describe(1, 2, 3, name="Python", year=1991)

*args collects extra positional arguments into a tuple, and **kwargs collects extra keyword arguments into a dictionary. Since Python 3.7, dictionaries preserve insertion order, so kwargs prints its keys in the order they were passed.

Variable Scope

Variables created inside a function are local by default—they exist only while the function runs. To modify a variable defined at module level (global) from inside a function, you must declare it with the global keyword. Otherwise, assigning to a name inside a function creates a new local variable that shadows any global of the same name.

Create a file named scope.py:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# Local vs global scope

counter = 0  # module-level (global) variable

def increment():
    global counter      # rebind the global, not a new local
    counter += 1
    return counter

def local_demo():
    counter = 100       # a new local variable that shadows the global
    return counter

print(increment())   # 1
print(increment())   # 2
print(local_demo())  # 100
print(counter)       # 2 - local_demo never touched the global

This example shows why scope matters: local_demo assigns to counter, but because it didn’t declare global, it only changed its own local copy. The module-level counter still reflects the two calls to increment.

Recursion

A recursive function calls itself to solve a smaller version of a problem until it reaches a base case. Recursion is natural in Python and reads cleanly for problems like factorials and Fibonacci numbers.

Create a file named recursion.py:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# Recursion: functions that call themselves

def factorial(n):
    """Compute n! recursively."""
    if n <= 1:           # base case stops the recursion
        return 1
    return n * factorial(n - 1)

def fibonacci(n):
    """Return the nth Fibonacci number (0-indexed)."""
    if n < 2:
        return n
    return fibonacci(n - 1) + fibonacci(n - 2)

print(factorial(5))                       # 5 * 4 * 3 * 2 * 1
print([fibonacci(i) for i in range(8)])   # first 8 Fibonacci numbers

Note that Python has a default recursion limit (around 1000 frames) to guard against runaway recursion, so deep recursion may need an iterative approach instead. For the small inputs here, recursion is clear and correct.

Higher-Order Functions and Lambdas

Because functions are first-class objects, you can pass them around like any other value. A higher-order function takes a function as an argument or returns one. The lambda keyword creates small anonymous functions inline, and a closure is a function that remembers values from the scope in which it was created.

Create a file named higher_order.py:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# Higher-order functions, lambdas, and closures

def apply_twice(func, value):
    """Apply a function to a value two times."""
    return func(func(value))

double = lambda x: x * 2
print(apply_twice(double, 5))   # double(double(5))

# map and filter both take a function as their first argument
numbers = [1, 2, 3, 4, 5, 6]
squares = list(map(lambda n: n ** 2, numbers))
evens = list(filter(lambda n: n % 2 == 0, numbers))
print(squares)
print(evens)

# A closure: make_multiplier returns a function that remembers `factor`
def make_multiplier(factor):
    def multiplier(n):
        return n * factor
    return multiplier

triple = make_multiplier(3)
print(triple(10))

Here apply_twice accepts the double function as an argument, map and filter apply a lambda across a list, and make_multiplier returns a new multiplier function that captures the factor value—a closure. These patterns are at the heart of Python’s functional side.

Running with Docker

You can run each example without installing Python locally by using the official Alpine image.

1
2
3
4
5
6
7
8
9
# Pull the official Python Alpine image (lightweight)
docker pull python:3.13-alpine

# Run each example
docker run --rm -v $(pwd):/app -w /app python:3.13-alpine python functions.py
docker run --rm -v $(pwd):/app -w /app python:3.13-alpine python parameters.py
docker run --rm -v $(pwd):/app -w /app python:3.13-alpine python scope.py
docker run --rm -v $(pwd):/app -w /app python:3.13-alpine python recursion.py
docker run --rm -v $(pwd):/app -w /app python:3.13-alpine python higher_order.py

Expected Output

Running functions.py:

Hello, Ada!
7
>> Functions are first-class in Python
None

Running parameters.py:

25
1024
27
positional: (1, 2, 3)
keyword: {'name': 'Python', 'year': 1991}

Running scope.py:

1
2
100
2

Running recursion.py:

120
[0, 1, 1, 2, 3, 5, 8, 13]

Running higher_order.py:

20
[1, 4, 9, 16, 25, 36]
[2, 4, 6]
30

Key Concepts

  • def defines a function, and return sends a value back; a function with no return yields None.
  • Parameters are flexible: defaults, keyword arguments, and *args/**kwargs let one signature handle many calling styles.
  • Scope is local by default: assigning inside a function creates a local name; use global to rebind a module-level variable.
  • Docstrings document functions and are accessible at runtime via help() or the __doc__ attribute.
  • Recursion needs a base case, and Python’s recursion limit (~1000) means very deep recursion should be rewritten iteratively.
  • Functions are first-class objects: assign them, pass them, and return them just like any other value.
  • lambda creates anonymous functions for short, inline logic—ideal as arguments to map, filter, and sorted.
  • Closures capture their environment, letting an inner function remember values from where it was defined.

Running Today

All examples can be run using Docker:

docker pull python:3.13-alpine
Last updated:

Comments

Loading comments...

Leave a Comment

2000 characters remaining