Skip to content

Latest commit

 

History

History
76 lines (57 loc) · 2.24 KB

File metadata and controls

76 lines (57 loc) · 2.24 KB

Argument Defaults Are Evaluated When Function Is Defined

When you define a function with any arguments that have default values, those default values are evaluated and stored at the time that the function is defined (i.e. when it is evaluated by the interpreter). This might feel counter intuitive if you are coming from another language, like Ruby, where these kinds of defaults are evaluated at call time. This is unremarkable for scalar values like 4 or "fallback". It's much more interesting when your defaults are function calls.

What if our default is something like datetime.now()?

Here I've defined a Timer class that has a start and stop method. The stop method can be called with a specific datetime value otherwise it falls back to datetime.now() -- but when is now?

from datetime import datetime, timezone
import time


class Timer:
    def __init__(self):
        self._start = None
        self._stop = None

    def start(self):
        self._start = datetime.now(timezone.utc)
        self._stop = None

    def stop(self, at=datetime.now(timezone.utc)):
        print(f"now: {datetime.now(timezone.utc)}")
        print(f" at: {at}")
        self._stop = at

        elapsed = self._stop - self._start
        return elapsed

Here I instantiate a timer, call start, sleep for 5 seconds, and then call stop.

timer = Timer()
timer.start()

time.sleep(5)

print(f"Elapsed: {timer.stop()}")

Here is what gets printed to stdout:

now: 2026-05-22 00:45:05.654878+00:00
 at: 2026-05-22 00:45:00.649699+00:00
Elapsed: -1 day, 23:59:59.999875

Notice that the actual now (when the stop method is running) is about 5 seconds after the value of at. That is because at, which takes on the default argument value, is datetime.now() as evaluated at the time the function is interpreted. It is for that same reason that self._stop ends up being just a hair earlier than the call to start which sets self._start. Which explains why the elapsed time is a negative value.

To avoid this awkwardness all together, set the default as None and then override None at the start of the function:

def stop(self, at = None):
    if at == None:
        at = datetime.now(timezone.utc)

    # ...