diff --git a/README.md b/README.md index 9770e22..9b69b2e 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ working across different projects via [VisualMode](https://www.visualmode.dev/). For a steady stream of TILs, [sign up for my newsletter](https://visualmode.kit.com/newsletter). -_1794 TILs and counting..._ +_1795 TILs and counting..._ See some of the other learning resources I work on: @@ -1056,6 +1056,7 @@ If you've learned something here, support my efforts writing daily TILs by - [Access Instance Variables](python/access-instance-variables.md) - [Access Most Recent Return Value In REPL](python/access-most-recent-return-value-in-repl.md) - [Access Variables Outside Loop Scope](python/access-variables-outside-loop-scope.md) +- [Argument Defaults Are Evaluated When Function Is Defined](python/argument-defaults-are-evaluated-when-function-is-defined.md) - [Assert Is Only A Development Check](python/assert-is-only-a-development-check.md) - [Avoid Modification With Frozen Dataclass](python/avoid-modification-with-frozen-dataclass.md) - [Break Debugger On First Line Of Program](python/break-debugger-on-first-line-of-program.md) diff --git a/python/argument-defaults-are-evaluated-when-function-is-defined.md b/python/argument-defaults-are-evaluated-when-function-is-defined.md new file mode 100644 index 0000000..de67ff3 --- /dev/null +++ b/python/argument-defaults-are-evaluated-when-function-is-defined.md @@ -0,0 +1,76 @@ +# 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_? + +```python +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`. + +```python +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: + +```python +def stop(self, at = None): + if at == None: + at = datetime.now(timezone.utc) + + # ... +```