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

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

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']

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)

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:
| Pattern | State is private? | Independent copies? | Best for |
|---|---|---|---|
| Function attribute | No | No | Simple counters |
Closure + nonlocal | Yes | Yes | Counters per user or store |
| Mutable default | No | No | Quick scripts only |
| Class attribute | No | No | Several related values |
lru_cache | Yes | No | Caching results |
If you want to keep going with variables and functions, these are good next reads:
- Python variables explained
- Access variables outside a function in Python
- The Python classmethod() function
- Create attributes dynamically with setattr and getattr
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.

Bijay Kumar is a 13-time Microsoft MVP with more than 18 years in software development, and the founder of Python Guides and TSinfo Technologies. He started out building .NET and SharePoint solutions at HP, TCS and KPIT before moving into Python, machine learning and AI, and he also builds web apps with TypeScript and React. He writes the tutorials here himself, and every example is run before publishing so you see the real output. More about Bijay · Microsoft MVP profile · LinkedIn