Non-Static Arguments as Default
from time import sleep
from datetime import datetime
def log(message, when=datetime.now()):
print(f"{when}: {message}")
log('hello world 1')
sleep(0.1)
log('hello world 2')
In this example, when
argument is a non-static keyword argument.
Problem
As contrary to our assumption, when
argument is not evaludated each time the function log()
gets run.
Instead, when=datetime.now()
is evaluated only once when a program starts up.
>>> 2023-08-05 09:35:39.670258: hello world 1
>>> 2023-08-05 09:35:39.670258: hello world 1
The Convention to Avoid This Issue
The convention is to provide a default value of None
and to document the behaviour in the docstring.
def log(message, when=None):
"""
Log a message with a timetsamp.
Args:
message: Message to print.
when: datetime of when the message occurred.
Defaults to the present time.
"""
if when is None:
when = datetime.now()
print(f"{when}: {message}")
log('hello world 1')
sleep(0.1)
log('hello world 2')
By using None
as default value, now when we call log()
method, it has dynamic when
value.
>>> 2023-08-05 09:35:40.221349: hello world 1
>>> 2023-08-05 09:35:40.321349: hello world 1
Mutable Arguments as Default
import json
def decode(data, default={}):
try:
return json.loads(data)
except ValueError:
return default
Problem
The empty dictionary defined as default
is evaluated only once when the module is loaded.
For this reason, the default
dictionary will be shared by all function calls.
>>> foo = decode("bad data")
>>> foo['value error'] = 5
>>> bar = decode("bad data 2")
>>> bar["value error 2"] = 10
>>> print('foo:', foo)
foo: {'value error': 5, 'value error 2': 10}
>>> print('bar:', bar)
bar: {'value error': 5, 'value error 2': 10}
As contrary to the assumption, both value error
and value error 2
keys exist in both foo
and bar
.
The reason is because both foo
and bar
equal to the default
parameter, the same dictionary object.
The Convention to Avoid This Issue
We can set the default
parameter to None
.
def decode(data, default=None):
"""
Load JSON data from a string.
Args:
data: JSON data to decode.
default: Value to return if decoding fails.
Defaults to an empty dictionary.
"""
try:
return json.loads(data)
except ValueError:
if default is None:
default = {}
return default
With this change, the default dictionary for each function call has its own.
>>> foo = decode('bad data')
>>> foo['value error'] = 5
>>> bar = decode('also bad')
>>> bar['value error 2'] = 10
>>> print('foo:', foo)
foo: {'value error': 5}
>>> print('bar:', bar)
bar: {'value error 2': 10}
Type Annotations
We can also use type annotations to mark None
default value as optional.
from typing import Optional
def log_typed(message: str,
when: Optional[datetime]=None) -> None:
"""
Log a message with a timestamp.
Args:
message: Message to print.
when: datetime of when the message occurred.
Defaults to the present time.
"""
if when is None:
when = datetime.now()
print(f"{when}: {message}")
Optional[datetime]=None
allows when
to be either datetime
or None
.
Reference
- Effective Python: Item 24: Specify Dynamic Default Arguments in Docstrings