1
0
mirror of https://github.com/jbranchaud/til synced 2026-07-02 15:49:44 +00:00

Add Argument Defaults Are Evaluated When Function Is Defined as a Python TIL

This commit is contained in:
jbranchaud
2026-05-21 21:53:46 -05:00
parent 49628a7849
commit 3eddb54053
2 changed files with 78 additions and 1 deletions
+2 -1
View File
@@ -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)
@@ -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)
# ...
```