Skip to Content
Course content

186: Functions as First-Class Objects Revisited

Click on the "Edit" button in the top corner of the screen to edit your slide content.

We've touched on first-class functions before, but usually in the context of simple map() or filter() calls. In a real production codebase, treating functions as first-class objects isn't just a neat trick; it's how we build flexible, decoupled systems. I want to show you how this looks when you're building a system that needs to change its behavior dynamically without a giant if/elif/else block.

Let's build a simple Notification Dispatcher. Imagine we have different ways to alert a user—email, SMS, or a push notification—and we want to decide which one to use based on the severity of the event.

Defining our handler logic

First, I'll write a few standalone functions. Each one does one thing and takes the same set of arguments. This consistency is key; if the signatures don't match, passing them around becomes a nightmare of *args and **kwargs.

def send_email(message):
    print(f"Sending Email: {message}")

def send_sms(message):
    print(f"Sending SMS: {message}")

def send_slack(message):
    print(f"Posting to Slack: {message}")

Creating a function registry

Instead of writing a long conditional block every time I want to send a notification, I'm going to store these functions in a dictionary. Because functions are first-class objects, I can use the function name (without parentheses) as a value in the dictionary. I'm essentially creating a "lookup table" for behavior.

# Mapping severity levels to specific functions
NOTIFICATION_STRATEGY = {
    "low": send_slack,
    "medium": send_email,
    "high": send_sms
}

The mistake: Executing too early

Now, here is where I usually trip up when I'm rushing. I'll try to write the dispatcher function, but I might accidentally call the function while trying to assign it or retrieve it. Look at this mistake I just made:

def notify(severity, message):
    # BUG: I'm calling the function immediately here by adding ()
    # This will try to call send_slack() and assign its RETURN value to handler
    handler = NOTIFICATION_STRATEGY.get(severity, send_slack)() 
    handler(message)

If you ran this, it would crash. Why? Because NOTIFICATION_STRATEGY.get(severity, send_slack)() executes the function right then and there. Since send_slack returns None, the variable handler becomes None, and then I try to call None(message). Classic.

I need to retrieve the function object itself, and then call it on the next line.

Executing the retrieved function

Here is the corrected version. I grab the function reference first, then I invoke it. I'm also using .get() to provide a sensible default (Slack) in case a severity level is passed that I haven't defined.

def notify(severity, message):
    # Grab the function reference
    handler = NOTIFICATION_STRATEGY.get(severity, send_slack)
    
    # Now invoke the reference
    handler(message)

# Testing it out
notify("high", "The server is on fire!")  # Output: Sending SMS: The server is on fire!
notify("low", "Coffee is ready.")        # Output: Posting to Slack: Coffee is ready.
notify("unknown", "Something happened.") # Output: Posting to Slack: Something happened.

By treating the functions as objects, I've made the notify function completely agnostic about how the notification is sent. If we need to add a "PagerDuty" notification tomorrow, I don't have to touch the notify logic at all; I just add a new function and a new entry to the NOTIFICATION_STRATEGY dictionary.




📋 Practical Task

Build a Plugin-based Data Validator

You are building a data ingestion tool. You need to create a system that validates a string based on a "validation type" passed in as a key.

  • Create three functions: is_numeric (returns True if the string is all digits), is_email (returns True if '@' is in the string), and is_long (returns True if the string is longer than 10 characters).
  • Create a dictionary called VALIDATOR_REGISTRY that maps the strings "number", "email", and "length" to their respective functions.
  • Write a function called validate_data(data_type, value). This function should look up the validator in your registry and return the result of that validator when called with value.
  • If a data_type is provided that isn't in the registry, the function should return False.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.