Python Use None for Non-Static, Dynamic Default Args

created:

updated:

tags: python

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