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