Functions are Python's building blocks. Here's everything you need to know.

Basic Functions

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

Arguments

Positional and Keyword

def create_user(name, email, admin=False):
    return {"name": name, "email": email, "admin": admin}
 
# Positional
create_user("Owen", "owen@example.com")
 
# Keyword
create_user(name="Owen", email="owen@example.com")
 
# Mixed
create_user("Owen", email="owen@example.com", admin=True)

*args and **kwargs

def func(*args, **kwargs):
    print(f"args: {args}")      # Tuple of positional args
    print(f"kwargs: {kwargs}")  # Dict of keyword args
 
func(1, 2, 3, name="Owen", age=25)
# args: (1, 2, 3)
# kwargs: {'name': 'Owen', 'age': 25}

Unpacking Arguments

args = [1, 2, 3]
kwargs = {"name": "Owen"}
 
func(*args, **kwargs)
# Same as: func(1, 2, 3, name="Owen")

Keyword-Only Arguments

def func(a, b, *, keyword_only):
    pass
 
func(1, 2, keyword_only=3)  # OK
func(1, 2, 3)               # TypeError

Positional-Only Arguments (3.8+)

def func(positional_only, /, normal):
    pass
 
func(1, 2)           # OK
func(1, normal=2)    # OK
func(positional_only=1, normal=2)  # TypeError

Return Values

# Single value
def square(x):
    return x ** 2
 
# Multiple values (tuple)
def divide(a, b):
    return a // b, a % b
 
quotient, remainder = divide(10, 3)
 
# No return = None
def no_return():
    pass
 
result = no_return()  # None

Default Arguments

def greet(name, greeting="Hello"):
    return f"{greeting}, {name}!"
 
# Warning: mutable defaults are shared!
def bad(items=[]):  # Don't do this
    items.append(1)
    return items
 
bad()  # [1]
bad()  # [1, 1] - same list!
 
# Use None instead
def good(items=None):
    if items is None:
        items = []
    items.append(1)
    return items

Lambda Functions

# Anonymous functions
square = lambda x: x ** 2
add = lambda x, y: x + y
 
# Common use: sorting
users = [{"name": "Bob"}, {"name": "Alice"}]
sorted(users, key=lambda u: u["name"])
 
# With filter/map
numbers = [1, 2, 3, 4, 5]
evens = list(filter(lambda x: x % 2 == 0, numbers))
squares = list(map(lambda x: x ** 2, numbers))

Use lambda for simple, one-time functions. Otherwise define a regular function.

First-Class Functions

Functions are objects:

def greet(name):
    return f"Hello, {name}!"
 
# Assign to variable
say_hello = greet
say_hello("Owen")  # "Hello, Owen!"
 
# Pass as argument
def apply(func, value):
    return func(value)
 
apply(greet, "Owen")
 
# Return from function
def make_greeter(greeting):
    def greeter(name):
        return f"{greeting}, {name}!"
    return greeter
 
hi = make_greeter("Hi")
hi("Owen")  # "Hi, Owen!"

Closures

Inner functions that remember outer scope:

def counter():
    count = 0
    def increment():
        nonlocal count  # Access outer variable
        count += 1
        return count
    return increment
 
c = counter()
c()  # 1
c()  # 2
c()  # 3

Decorators

Functions that modify functions:

def log(func):
    def wrapper(*args, **kwargs):
        print(f"Calling {func.__name__}")
        return func(*args, **kwargs)
    return wrapper
 
@log
def greet(name):
    return f"Hello, {name}!"
 
greet("Owen")
# Calling greet
# Hello, Owen!

Docstrings

def calculate_area(radius):
    """Calculate the area of a circle.
    
    Args:
        radius: The radius of the circle.
        
    Returns:
        The area of the circle.
        
    Raises:
        ValueError: If radius is negative.
    """
    if radius < 0:
        raise ValueError("Radius must be non-negative")
    return 3.14159 * radius ** 2
 
# Access docstring
print(calculate_area.__doc__)

Type Hints

def greet(name: str) -> str:
    return f"Hello, {name}!"
 
def process(items: list[int]) -> dict[str, int]:
    return {"sum": sum(items), "count": len(items)}

Recursion

def factorial(n):
    if n <= 1:
        return 1
    return n * factorial(n - 1)
 
# With memoization
from functools import lru_cache
 
@lru_cache
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n-1) + fibonacci(n-2)

Best Practices

Keep functions small:

# Do one thing well
def validate_email(email: str) -> bool:
    return "@" in email and "." in email

Use descriptive names:

# Bad
def f(x): ...
 
# Good
def calculate_tax(amount): ...

Avoid side effects:

# Bad - modifies global state
total = 0
def add(x):
    global total
    total += x
 
# Good - pure function
def add(a, b):
    return a + b

Return early:

def process(data):
    if not data:
        return None
    if not valid(data):
        return None
    # Main logic here
    return result

Functions are the heart of Python. Master them.

React to this post: