Gotchas of early-bound function argument defaults in Python
I was reading a tweet about it yesterday and that didn’t stop me from pushing a code change in production with the same rookie mistake today. Consider this function: # src.py from __future__ import annotations import logging import time from datetime import datetime def log( message: str, /, *, level: str, timestamp: str = datetime.utcnow().isoformat(), ) -> None: logger = getattr(logging, level) # Avoid f-string in logging as it's not lazy. logger("Timestamp: %s \nMessage: %s\n" % (timestamp, message)) if __name__ == "__main__": for _ in range(3): time.sleep(1) log("Reality can often be disappointing.", level="warning") Here, the function log has a parameter timestamp that computes its default value using the built-in datetime.utcnow().isoformat() method. I was under the impression that the timestamp parameter would be computed each time when the log function was called. However, that’s not what happens when you try to run it. If you run the above snippet, you’ll get this instead: ...