Mastering Python Closures: A Beginner-Friendly Guide with Real-World Examples and Visuals

Learn how Python closures work with simple real-world analogies, beginner-friendly code examples, and visual breakdowns. Master nonlocal, scope levels, and practical use cases like function factories, private variables, and decorators.

The Key Idea

A closure happens when:

An inner function remembers the variables from the outer function even after the outer function has finished running.

Let’s walk through this with a real-world analogy

Code

def make_multiplier(n):
    def multiplier(x):
        return x * n
    return multiplier

times3 = make_multiplier(3)
print(times3(10))  # Output: 30

Think of it Like This:

Imagine:

You’re making a customized calculator.

  1. You call make_multiplier(3), asking: “Hey, can you build me a multiplier that always multiplies by 3?”
  2. Python says: “Sure! Here’s a custom function (called multiplier) that remembers the number 3 you gave me.”
  3. That custom multiplier gets saved into times3.
  4. Later, when you run times3(10): It remembers: “I was built with n = 3, so I’ll return 10 * 3.”

This memory of n = 3 is what we call a closure.

Visualization

times3 = make_multiplier(3)

→ make_multiplier builds:
   def multiplier(x):
       return x * 3    ← "remembers n = 3"

→ returns multiplier → assigned to times3

Then:
times3(10) → 10 * 3 → 30

Even though make_multiplier() has finished, its inner function multiplier() keeps a snapshot of its environment, including the value n = 3.

Closure = Inner Function + Remembered Values

So:

times3 = make_multiplier(3)  # returns multiplier(x): return x * 3

Even though make_multiplier is done executing, the times3 function it returned still remembers n = 3.

Summary

  • You create a function that returns another function.
  • That returned function remembers the context in which it was created.
  • This is closure.

Why we use closure-based functions

1. Preserve State Without Global Variables or Classes

Closures are great when you want a function to remember a value (like a configuration, multiplier, or counter) without using a global variable or creating a class.

Example: Create customized functions

def make_discount(percent):
    def apply(price):
        return price - (price * percent / 100)
    return apply

student_discount = make_discount(10)
print(student_discount(200))  # 180.0

2. Create Factory Functions

You can use closures to generate specialized functions at runtime. This is a common design in function factories or decorators.

Example: Customized HTML wrapper

def html_tag(tag):
    def wrap(text):
        return f"<{tag}>{text}</{tag}>"
    return wrap

h1 = html_tag("h1")
print(h1("Welcome"))  # <h1>Welcome</h1>

3. Encapsulate Data (like private variables)

Closures let you hide data inside a function — acting like private members of a class.

Example: A counter with internal state

def make_counter():
    count = 0
    def increment():
        nonlocal count
        count += 1
        return count
    return increment

counter = make_counter()
print(counter())  # 1
print(counter())  # 2

4. Cleaner Alternative to Classes (for small behavior)

If you only need to encapsulate behavior and state for a simple case, closures are often lighter and cleaner than defining a class.

5. Functional Programming Patterns

Closures are used a lot in:

  • Decorators
  • Callbacks
  • Event handling
  • Functional APIs (like filters or strategies)

Summary

Use CaseWhy Use Closure
Preserve configuration/stateWithout using globals or classes
Build specialized functionse.g., customized formatters
Encapsulate private variablesSecure internal state
Functional programmingClean and concise logic
Create decoratorsAdd behavior dynamically

So What Is nonlocal?

🔑 Definition:

nonlocal is a keyword that allows inner functions to modify variables from the enclosing (but non-global) scope.

Without nonlocal, this line:

count += 1

Would create a new local variable count inside the increment() function — completely separate from the count = 0 in make_counter().

This would result in:

UnboundLocalError: local variable 'count' referenced before assignment

Scope Levels in Python (important!)

Python has 4 main scopes:

LevelExample
LocalInside a function
EnclosingOuter function (like make_counter)
GlobalTop-level script/module variables
Built-inprint, len, etc.

In your code:

  • count = 0 is in the enclosing scope
  • nonlocal count lets the inner function increment() access and modify it

Visual Breakdown:

# EN CLOSING SCOPE
def make_counter():
    count = 0  # <--- Outer variable, remembered by the closure

    # INNER FUNCTION
    def increment():
        nonlocal count  # <--- Declare we're using/modifying outer 'count'
        count += 1
        return count

    return increment

Now each time you call counter(), it uses and updates the same count variable.

Without nonlocal

If you removed nonlocal:

def make_counter():
    count = 0
    def increment():
        count += 1  # ❌ Will raise UnboundLocalError
        return count

Python would think you’re trying to create a new local variable count, but it’s also being used before it’s assigned. That’s why nonlocal is needed to tell Python:

“Don’t create a new variable — use the one from the enclosing scope.”

When to Use nonlocal

  • You have a closure (a function inside another).
  • The outer function has a mutable or changing variable.
  • The inner function needs to update that variable over multiple calls.

Practice Creating Closure-Based Functions

See solved exercises here.

Let’s Connect!

If you enjoyed this and want more tutorials like it, follow me:

🎥 YouTube
👩‍💻 GitHub
💼 LinkedIn
📱 Instagram
📘 Facebook

Thanks so much for dropping by.

Leave a Reply

Your email address will not be published. Required fields are marked *