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 elapsedHere 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)
# ...