TechLead
Lesson 4 of 25
5 min read
Python

Functions and Modules

Learn to write reusable functions, work with arguments, and organize code into modules and packages

Defining Functions

Functions are the primary building blocks for code reuse in Python. Defined with the def keyword, they encapsulate logic into callable units. Python functions are first-class objects, meaning they can be assigned to variables, passed as arguments, and returned from other functions.

# Basic function definition
def greet(name):
    """Return a greeting message."""
    return f"Hello, {name}!"

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

# Function with multiple return values (returns a tuple)
def divide(a, b):
    quotient = a // b
    remainder = a % b
    return quotient, remainder

q, r = divide(17, 5)
print(f"17 / 5 = {q} remainder {r}")  # 17 / 5 = 3 remainder 2

# Functions without return statement return None
def say_hello(name):
    print(f"Hello, {name}!")

result = say_hello("Bob")
print(result)  # None

Function Arguments

Python supports several types of function arguments: positional, keyword, default, variable-length positional (*args), and variable-length keyword (**kwargs). Understanding these is essential for writing flexible and clean APIs.

# Default arguments
def power(base, exponent=2):
    return base ** exponent

print(power(3))      # 9 (uses default exponent=2)
print(power(3, 3))   # 27

# Keyword arguments
def create_user(name, age, email=""):
    return {"name": name, "age": age, "email": email}

user = create_user(age=30, name="Alice", email="alice@example.com")

# *args - variable positional arguments
def sum_all(*args):
    total = 0
    for num in args:
        total += num
    return total

print(sum_all(1, 2, 3, 4, 5))  # 15

# **kwargs - variable keyword arguments
def build_profile(**kwargs):
    return kwargs

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

# Combining all argument types
def complex_function(required, *args, default="value", **kwargs):
    print(f"Required: {required}")
    print(f"Args: {args}")
    print(f"Default: {default}")
    print(f"Kwargs: {kwargs}")

complex_function("hello", 1, 2, 3, default="custom", extra="data")

# Positional-only and keyword-only arguments (Python 3.8+)
def strict_function(pos_only, /, normal, *, kw_only):
    print(pos_only, normal, kw_only)

strict_function(1, 2, kw_only=3)         # Works
strict_function(1, normal=2, kw_only=3)  # Works
# strict_function(pos_only=1, normal=2, kw_only=3)  # Error!

Mutable Default Arguments Trap

One of the most common Python gotchas is using a mutable object as a default argument. The default is created once when the function is defined, not each time it is called.

# BAD - mutable default argument
def add_item_bad(item, items=[]):
    items.append(item)
    return items

print(add_item_bad("a"))  # ['a']
print(add_item_bad("b"))  # ['a', 'b'] - Unexpected!

# GOOD - use None as default
def add_item_good(item, items=None):
    if items is None:
        items = []
    items.append(item)
    return items

print(add_item_good("a"))  # ['a']
print(add_item_good("b"))  # ['b'] - Correct!

Lambda Functions

Lambda functions are small, anonymous functions defined with the lambda keyword. They are limited to a single expression and are commonly used as arguments to higher-order functions like sorted(), map(), and filter().

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

# Lambdas are most useful as arguments
students = [
    {"name": "Alice", "grade": 92},
    {"name": "Bob", "grade": 85},
    {"name": "Charlie", "grade": 98},
]

# Sort by grade
sorted_students = sorted(students, key=lambda s: s["grade"], reverse=True)
print(sorted_students[0]["name"])  # "Charlie"

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

# Note: list comprehensions are usually preferred over map/filter
squares = [x ** 2 for x in numbers]
evens = [x for x in numbers if x % 2 == 0]

Modules and Imports

A module is simply a Python file. A package is a directory containing an __init__.py file. Python's import system lets you organize code into reusable units and access the vast standard library and third-party packages.

# Importing modules
import math
print(math.sqrt(16))  # 4.0
print(math.pi)        # 3.141592653589793

# Import specific items
from datetime import datetime, timedelta
now = datetime.now()
tomorrow = now + timedelta(days=1)

# Import with alias
import numpy as np
import pandas as pd

# Import everything (avoid in production code)
from math import *

# Creating your own module
# utils.py
def sanitize(text):
    return text.strip().lower()

def validate_email(email):
    return "@" in email and "." in email

# main.py
# from utils import sanitize, validate_email

Creating Packages

Packages let you organize related modules into a directory hierarchy. Each package directory needs an __init__.py file (which can be empty). This file runs when the package is imported and can be used to define the package's public API.

# Package structure:
# mypackage/
#   __init__.py
#   math_utils.py
#   string_utils.py
#   subpackage/
#     __init__.py
#     helpers.py

# mypackage/__init__.py
from .math_utils import add, multiply
from .string_utils import capitalize_words

# mypackage/math_utils.py
def add(a, b):
    return a + b

def multiply(a, b):
    return a * b

# Usage:
# from mypackage import add, multiply
# from mypackage.string_utils import capitalize_words
# from mypackage.subpackage.helpers import some_function

# Relative imports (within a package)
# from . import math_utils          # same package
# from .. import other_package      # parent package
# from .subpackage import helpers   # subpackage

Key Takeaways

  • First-class functions: Functions can be passed around like any other object
  • Avoid mutable defaults: Use None instead of [] or {} as default arguments
  • *args and **kwargs: Provide flexibility for variable numbers of arguments
  • Prefer comprehensions: Use list comprehensions over map/filter for readability
  • Organize with packages: Group related modules in directories with __init__.py

Continue Learning