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

You've likely encountered situations where you need a function to "remember" some piece of data, but you don't want to commit to the overhead of a full-blown class. I've seen a lot of developers struggle with this, usually falling into the trap of using global variables or passing the same configuration argument into a function ten times a day. That's where closures come in.

The Global State Trap

Imagine you're building a system to calculate sales tax for different regions. A naive approach might look like this:

TAX_RATE = 0.05

def calculate_total(price):
    return price + (price * TAX_RATE)

# Now we need to calculate for a different region
TAX_RATE = 0.08
print(calculate_total(100)) # 108.0

This feels fine for a script with ten lines, but it's a nightmare in a real production environment. Why? Because your logic is now tied to a global variable. If you're running an asynchronous task or a multi-threaded server, one request might change the TAX_RATE while another request is halfway through a calculation. You've created a race condition. You can't have two different tax calculators existing at the same time without them fighting over that one global variable.

Over-Engineering with Classes

The immediate "correct" software engineering answer is usually to wrap this in a class. I'll admit, I've done this too many times when a closure would have been cleaner.

class TaxCalculator:
    def __init__(self, rate):
        self.rate = rate

    def calculate(self, price):
        return price + (price * self.rate)

ny_calc = TaxCalculator(0.08)
ca_calc = TaxCalculator(0.07)

This works perfectly. It's encapsulated and safe. But look at the boilerplate. We had to define an __init__, manage self, and instantiate an object just to use one single method. If the only thing your class does is hold one variable and provide one function, you're basically using a class as a glorified function. It's not "wrong," but it's noisy.

Capturing State with Closures

A closure allows you to associate data with a function without using a class. It happens when you define a function inside another function, and the inner function references a variable from the outer function's scope. Even after the outer function finishes executing, the inner function "closes over" that variable, keeping it alive.

def make_tax_calculator(rate):
    # This is the outer function
    def calculate(price):
        # This inner function 'remembers' the rate from above
        return price + (price * rate)
    
    return calculate

# Create specialized functions
ny_calc = make_tax_calculator(0.08)
ca_calc = make_tax_calculator(0.07)

print(ny_calc(100)) # 108.0
print(ca_calc(100)) # 107.0

Notice what happened here. make_tax_calculator didn't just return a value; it returned a function. The ny_calc variable is now a function that has its own private copy of rate tucked away in its memory. This is a closure.

Why this actually matters

The trade-off here is between explicitness and brevity. Closures are incredibly powerful for creating "function factories" or decorators (which we'll dive into later). They give you the data encapsulation of a class but with the lightweight syntax of a function.

I generally recommend closures when you have a "one-method class" scenario. If you find yourself writing a class where the only method besides __init__ is a single operation, stop and ask if a closure is a better fit. It reduces the cognitive load for whoever reads your code next because they don't have to track object state—they just have a function that does exactly one thing with a predefined configuration.




📋 Practical Task

Build a Persistent API Request Header Generator

In many professional projects, you have to make multiple API calls to the same service using the same authentication token and content type, but with different endpoints. Instead of passing the token into every single function call, use a closure to create a "request generator."

Your Task:

  • Create a function called make_request_formatter that accepts an api_token and a version string (e.g., "v1").
  • This function should return a nested function called format_url.
  • The format_url function should take an endpoint string as an argument and return a full formatted string in this exact format: "Endpoint: /v1/users | Token: abc123xyz | Path: /profile".
  • Instantiate two different formatters (one for "v1" and one for "v2") and use them to print the formatted strings for the "/settings" endpoint.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.