How to Use Static Variables in Python Functions

Python has no static keyword, so a plain local variable resets on every call. To create a static variable in Python function code, store the value on the function object itself as an attribute; it survives between calls:

def counter():
    counter.calls = getattr(counter, "calls", 0) + 1
    return counter.calls

I pasted each snippet into a .py file and ran it with Python 3.12.5 from Command Prompt; the screenshots are those runs.

We’ll cover function attributes, closures, mutable defaults, class attributes and lru_cache.

Static variables with function attributes

Functions are objects, so you can attach attributes to them. The hasattr() check sets starting values on the first call only:

def track_order(amount):
    if not hasattr(track_order, "count"):     # runs only on the first call
        track_order.count = 0
        track_order.total = 0.0
    track_order.count += 1
    track_order.total += amount
    print(f"Order #{track_order.count}: ${amount:.2f}  running total ${track_order.total:.2f}")

track_order(19.99)
track_order(45.50)
track_order(7.25)
print("Stored on the function:", track_order.count, "orders")

Output:

Order #1: $19.99  running total $19.99
Order #2: $45.50  running total $65.49
Order #3: $7.25  running total $72.74
Stored on the function: 3 orders
Command Prompt output of a Python function attribute keeping an order count and running total between calls
The count and total carry over from call to call because they live on the function, not in its local scope.

Why doesn’t a normal local work? Locals are created fresh each call and thrown away at the end. My guide to Python variable scope explains the LEGB lookup behind that.

The downside: anyone can reset track_order.count from outside, so it isn’t private.

How do closures with nonlocal keep a static variable?

A closure is an inner function that remembers variables from the function that built it. Declare the variable nonlocal so it gets updated, not shadowed:

def make_counter(store):
    visits = 0                    # lives on after make_counter returns
    def record():
        nonlocal visits           # rebind the enclosing variable, not a new local
        visits += 1
        return f"{store}: visit {visits}"
    return record

austin = make_counter("Austin")
denver = make_counter("Denver")
print(austin())
print(austin())
print(denver())
print(austin())

Output:

Austin: visit 1
Austin: visit 2
Denver: visit 1
Austin: visit 3
Command Prompt output of two Python closures using nonlocal to keep separate visit counters for Austin and Denver
Each counter keeps its own visits value; Austin reaches 3 while Denver stays at 1.

Each make_counter() call gives an independent, hidden counter. Forget nonlocal, though, and the first call fails:

def make_counter():
    visits = 0
    def record():
        visits += 1               # no nonlocal -> Python treats visits as local
        return visits
    return record

try:
    make_counter()()
except UnboundLocalError as err:
    print(type(err).__name__ + ":", err)

Output:

UnboundLocalError: cannot access local variable 'visits' where it is not associated with a value

The full story of that error is in fixing UnboundLocalError in Python.

Mutable default arguments as static storage

Python evaluates defaults once, when def runs, so a mutable default list is shared by every call:

def add_tag(tag, seen=[]):        # the list is built ONCE, when def runs
    seen.append(tag)
    return seen

print(add_tag("python"))
print(add_tag("tutorial"))
print(add_tag("new", seen=[]))    # a caller can swap the state out
print(add_tag("guide"))

Output:

['python']
['python', 'tutorial']
['new']
['python', 'tutorial', 'guide']
Command Prompt output of a Python function whose mutable default list keeps growing across calls
The default list keeps every tag from earlier calls, while the call that passes seen=[] starts fresh.

It works, but it reads like a bug, and callers can replace the state. I only use it in quick scripts.

Share a static counter with a class attribute

When several functions share state, put it on a class: a class attribute has one copy that a @staticmethod can update. The difference between method types is covered in Python class method vs static method:

class Invoice:
    issued = 0                    # class attribute: one copy shared by every call

    @staticmethod
    def next_number():
        Invoice.issued += 1
        return f"INV-2026-{Invoice.issued:04d}"

print(Invoice.next_number())
print(Invoice.next_number())
print(Invoice.next_number())
print("Invoice.issued =", Invoice.issued)

Output:

INV-2026-0001
INV-2026-0002
INV-2026-0003
Invoice.issued = 3

It’s the closest match to a C-style static counter. You can inspect what a class stores with the Python __dict__ attribute.

Remember function results with functools.lru_cache

Often a static variable is really a cache. functools.lru_cache keeps one for you, keyed by the arguments:

from functools import lru_cache

@lru_cache(maxsize=None)
def shipping_quote(state):
    print(f"  computing quote for {state}...")
    rates = {"CA": 12.50, "TX": 9.75, "NY": 11.00}
    return rates.get(state, 14.99)

for st in ["CA", "TX", "CA", "NY", "TX", "CA"]:
    print(st, f"${shipping_quote(st):.2f}")

print(shipping_quote.cache_info())
shipping_quote.cache_clear()
print("after clear:", shipping_quote.cache_info())

Output:

  computing quote for CA...
CA $12.50
  computing quote for TX...
TX $9.75
CA $12.50
  computing quote for NY...
NY $11.00
TX $9.75
CA $12.50
CacheInfo(hits=3, misses=3, maxsize=None, currsize=3)
after clear: CacheInfo(hits=0, misses=0, maxsize=None, currsize=0)
Command Prompt output of Python functools.lru_cache computing each shipping quote once and reporting cache hits and misses
Each state is computed only on its first request; cache_info() reports 3 hits and 3 misses, and cache_clear() empties it.

Use maxsize=None for an unbounded cache or a number to keep recent results. Arguments must be hashable.

Static variable or global variable: which should you use?

A module-level global also persists, but any function can change it, and reassigning needs global. See setting global variables in Python functions for that route.

I keep globals for configuration and use these patterns for one function’s state:

PatternState is private?Independent copies?Best for
Function attributeNoNoSimple counters
Closure + nonlocalYesYesCounters per user or store
Mutable defaultNoNoQuick scripts only
Class attributeNoNoSeveral related values
lru_cacheYesNoCaching results

If you want to keep going with variables and functions, these are good next reads:

Frequently asked questions

Does Python have static variables in functions?

Not as a keyword. You get the same effect with a function attribute, a closure with nonlocal, or a class attribute.

What is the simplest way to make a static variable in a Python function?

Use a function attribute: f.count = getattr(f, "count", 0) + 1 inside f.

Why do I get UnboundLocalError in my counter closure?

You assigned to the enclosing variable without declaring it nonlocal, so Python treats it as a new local.

Is the mutable default argument trick safe?

It works, but callers can replace the state by passing their own value, and readers often mistake it for a bug.

How do I reset a static variable in Python?

Assign the attribute again, for example track_order.count = 0, or call cache_clear() on an lru_cache function.

Where is functools.lru_cache documented?

See functools.lru_cache in the Python docs.