2025, Dec 10 01:00

Avoiding Python's Default Argument Gotcha: Use Instance State Instead of Globals

Learn why Python default arguments capture stale values, how globals and class calls worsen it, and how to fix with instance state and a None sentinel override.

When a function in Python takes a default argument tied to a variable that you later change, it’s easy to assume calls will pick up the new value. They won’t. This subtle trap, combined with globals and methods that aren’t actual instance methods, is exactly why a circle stubbornly renders black even after you “change” the color.

Reproducing the issue

Consider two files that set a color, “change” it, and then draw using a default parameter that is supposed to reflect the current color.

# file_a.py
shade = (0, 0, 0)

class Sketch:
    def setShade(new_shade):
        global shade
        shade = (new_shade)
    def drawDisk(x, y, radius, col=shade):
        pass  # imagine drawing code here that uses col
# file_b.py
from file_a import *

obj = Sketch()
Sketch.setShade((255, 255, 255))
Sketch.drawDisk(0, 0, 5)

Despite the attempted color change, the drawing still uses black. Printing the variable after the “change” shows the original value as well.

What’s really happening and why it breaks

Default arguments in Python are evaluated once, at the moment the function is defined. In the example above, the default for col in drawDisk is bound to the value of shade at definition time, which is (0, 0, 0). Later updates to shade don’t affect that already-bound default. On top of that, shade is handled as a global while methods are being called via the class and not an instance, which makes state management confusing and brittle. The function that claims to “change” color isn’t defined as an instance method either, so it doesn’t manage per-object state and adds to the confusion.

A robust fix: instance state instead of globals and frozen defaults

Move color into instance state and use proper instance methods. This sidesteps stale defaults and avoids the pitfalls of global state.

# file_core.py
class Painter:
    def __init__(self):
        self.tint = (0, 0, 0)

    def setTint(self, new_tint):
        self.tint = new_tint

    def drawCircle(self, x, y, r):
        # use self.tint for drawing
        # e.g., pygame draw call would consume self.tint
        pass
# run_paint.py
from file_core import Painter

c = Painter()
c.setTint((255, 255, 255))
c.drawCircle(0, 0, 5)

This version stores the color on the instance and uses it at call time, so there’s no stale default to fight with.

If you want an optional override color

Sometimes a draw method should use the current instance color by default but allow an explicit override. To do that without falling into the default-argument trap, defer the choice until the function runs, not when it’s defined.

# file_core.py
class Painter:
    def __init__(self):
        self.tint = (0, 0, 0)

    def setTint(self, new_tint):
        self.tint = new_tint

    def drawCircle(self, x, y, r, col=None):
        if col is None:
            col = self.tint
        # draw using col
        pass
# run_paint.py
from file_core import Painter

c = Painter()

# Uses instance tint by default
c.setTint((255, 255, 255))
c.drawCircle(0, 0, 5)

# Overrides color for this call only
c.drawCircle(10, 10, 8, col=(128, 128, 128))

Here, col is evaluated at call time. If nothing is provided, the method uses the current instance color. If you pass an explicit color, that single call uses the override.

Why this matters

Default argument binding is a classic footgun: it locks in values at definition time, which is almost never what you want when the default should reflect mutable or late-bound state. Mixing that with globals and non-instance methods creates hard-to-trace behavior. Shifting to instance attributes makes state explicit and predictable, and choosing defaults at call time keeps behavior consistent.

Takeaways

When a default depends on the current state, don’t bind it as a default argument. Defer the decision to runtime, typically by using a sentinel like None and falling back to instance state. Avoid globals for shared state in object-oriented code, and define methods as proper instance methods so the flow of data is clear. This keeps drawing code—whether for circles or anything else—honest about the color it’s actually using.