Skip to main content

How to Apply Callback Techniques in Python

Callbacks are a fundamental concept in Python that allows for flexible, event-driven, and asynchronous programming. A callback is simply a function passed as an argument to another function, which is then "called back" at a later time to perform a task.

This guide explores how to implement callbacks in synchronous and asynchronous contexts, along with common design patterns like Observer and Strategy.

Understanding the Basics​

In Python, functions are first-class objects. This means they can be assigned to variables, passed as arguments, and returned from other functions. This capability is the foundation of callback logic.

Simple Function Callback​

Here, greet is passed into apply_operation and executed internally.

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

def apply_operation(func, value):
# 'func' is the callback
return func(value)

# Pass the function 'greet' (without parentheses) as an argument
result = apply_operation(greet, "Developer")
print(result)

Output:

Hello, Developer!

Lambda Callbacks​

For simple, one-off logic, you can use lambda (anonymous) functions as callbacks.

numbers = [1, 2, 3, 4, 5]
# 'lambda x: x**2' is the callback applied to every item
squared = list(map(lambda x: x**2, numbers))
print(squared)

Output:

[1, 4, 9, 16, 25]

Synchronous vs. Asynchronous Callbacks​

Callbacks function differently depending on whether the main task blocks program execution.

Synchronous Callbacks​

The callback executes immediately as part of the main function's flow. This is common in data processing pipelines.

def process_data(data, transform_callback):
results = []
for item in data:
# The callback is executed immediately for every item
results.append(transform_callback(item))
return results

def double(x):
return x * 2

nums = [10, 20, 30]
print(process_data(nums, double))

Output:

[20, 40, 60]

Asynchronous Callbacks​

These are used when a task takes a long time (like a network request or file I/O). The program continues running, and the callback triggers only when the task finishes.

import time
import threading

def async_task(on_complete):
def worker():
print("Starting heavy task...")
time.sleep(2) # Simulate delay
result = "Task Finished"
# Trigger the callback once work is done
on_complete(result)

# Run in a separate thread so the main program isn't blocked
thread = threading.Thread(target=worker)
thread.start()

def my_callback(message):
print(f"Callback received: {message}")

async_task(my_callback)
print("Main program continues running...")

Output:

Starting heavy task...
Main program continues running...
(2 second delay)
Callback received: Task Finished

Common Design Patterns​

Callbacks are the engine behind several popular software design patterns.

The Observer Pattern​

This allows an object (Subject) to notify a list of subscribers (Observers) when an event occurs.

class EventManager:
def __init__(self):
self._listeners = {}

def subscribe(self, event_type, callback):
if event_type not in self._listeners:
self._listeners[event_type] = []
self._listeners[event_type].append(callback)

def notify(self, event_type, data):
if event_type in self._listeners:
for callback in self._listeners[event_type]:
callback(data)

# Usage
events = EventManager()

def log_error(msg): print(f"[LOG]: {msg}")
def email_admin(msg): print(f"[EMAIL]: Sending alert for {msg}")

events.subscribe("error", log_error)
events.subscribe("error", email_admin)

events.notify("error", "Database Connection Failed")

Output:

[LOG]: Database Connection Failed
[EMAIL]: Sending alert for Database Connection Failed

The Strategy Pattern​

This allows you to select an algorithm at runtime by passing different callback functions.

class PaymentProcessor:
def __init__(self, strategy_callback):
self._strategy = strategy_callback

def process(self, amount):
return self._strategy(amount)

def pay_pal(amount): return f"Paid ${amount} via PayPal"
def credit_card(amount): return f"Paid ${amount} via Credit Card"

# Swap strategies easily
processor = PaymentProcessor(pay_pal)
print(processor.process(50))

processor = PaymentProcessor(credit_card)
print(processor.process(100))

Output:

Paid $50 via PayPal
Paid $100 via Credit Card

Advanced Techniques: Decorators and Error Handling​

Decorator-Based Callbacks​

Decorators are essentially wrappers around callbacks. They allow you to modify function behavior dynamically.

def retry_logic(func):
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except ValueError:
print("Error detected. Retrying...")
# Retry logic could go here
return None
return wrapper

@retry_logic
def unstable_function(x):
if x < 0:
raise ValueError("Negative number")
return x

unstable_function(-5)

Output:

Error detected. Retrying...

Safe Execution Wrapper​

When accepting callbacks from external sources, always wrap them in error handling to prevent them from crashing your main application.

def safe_execute(callback, *args):
try:
return callback(*args)
except Exception as e:
print(f"Callback failed: {e}")
return None

def risky_code(x):
return 10 / x

safe_execute(risky_code, 0) # Handles ZeroDivisionError gracefully

Output:

Callback failed: division by zero

Conclusion​

Callbacks provide the flexibility needed for modular and responsive Python applications.

  • Decoupling: They separate the "what" (the core logic) from the "how" (the specific implementation).
  • Asynchronicity: They are essential for non-blocking operations like UI events and network requests.
  • Patterns: They underpin powerful architectures like Observer and Strategy.

By keeping callbacks concise and handling errors robustly, you can write cleaner, more scalable code.