Welcome to Software Development on Codidact!
Will you help us build our independent community of developers helping developers? We're small and trying to grow. We welcome questions about all aspects of software development, from design to code to QA and more. Got questions? Got answers? Got code you'd like someone to review? Please join us.
Post History
Possible justifications It may make sense to use a mutable default argument in the following situations: For simplicity Consider for example an argument that should be some kind of mapping, wher...
Answer
#1: Initial revision
## Possible justifications It may make sense to use a mutable default argument in the following situations: ### For simplicity Consider for example an argument that should be some kind of mapping, where the function will only use it for lookup without actually mutating the provided object: ``` _default_config = {'value': 1} def display_value(config=_default_config): print(config['value']) ``` It's unwieldy to describe an "immutable dictionary" in Python, and the calling code is unlikely to take those extra steps anyway; so the implementation might as well use an ordinary `dict`. ### To create a unique sentinel Credit [to Martijn Pieters on Stack Overflow](https://stackoverflow.com/questions/50352516) for this interesting bit of trivia. For example, the standard library `copy.deepcopy` algorithm needs a *truly unique* sentinel object for certain parts of its logic that *cannot appear anywhere else in the program*. So it can't use `None` (that could be a valid value) or obvious sorts of immutable "empty" objects like `()` or `''` or `0` (since it could end up with an already-existing object with that value). By using `[]` (and then never actually mutating the object), it can be sure that other code will never have access to the same object (unless it deliberately "breaks the seal" by reaching into the function's internals). ### As a cache <section class="notice is-warning"> **There are usually better ways to accomplish this**. In particular, the standard library provides `functools.lru_cache` (and in 3.9 and up, `functools.cache`) for memoization. </section> However, for a quick-and-dirty approach, the "accumulating" effect of a mutable default argument can be used deliberately to keep track of results - for example, from a recursive helper function defined on the fly (it's hard to give a good example of this), or to implement the "registry" of a decorator used for registering functions. <details><summary>Example</summary> ``` registry = {} def invoke(name, registry=registry): return registry[name]() def register(func, registry=registry): registry[func.__name__] = func return func ``` Functions "registered" with the decorator in the normal way will use the global registry: ``` @register def test(): print('test function') invoke('test') ``` But it can also be used explicitly to register a function into a different registry: ``` my_registry = {} def example(): print('example function') register(example, my_registry) invoke('example', my_registry) ``` </details> ### Optimization <section class="notice is-warning"> **The benefits here are marginal at best, and a local assignment is almost as good.** </section> However, for performance-critical code, using a default argument that is "never supposed to be supplied explicitly" can be used to avoid repeatedly looking up a global name. <details><summary>Example</summary> ``` import math # The naive approach: def global_trigonometry(): return [math.sin(i) for i in range(1000000)] # Optimized: def default_trigonometry(sin=math.sin): return [sin(i) for i in range(1000000)] ``` On my machine, the optimization reduces the runtime by about 24% under Python 2.7 (which I keep around just for testing these sorts of legacy behaviours), 26% under Python 3.8, and 6% under Python 3.11. Of course, the difference is considerably smaller if `sin` is dumped directly into the global namespace. Simply making the same assignment inside the function also avoids the need for repeated lookup when the function is called (although it still needs to be looked up once per call). </details> ### To "bind" arguments or "partially apply" a function <section class="notice is-warning"> **This is a common and well-recognized idiom, but there are generally better ways.** Arguably, this technique uses one confusing "gotcha" to work around another, which some may find very inelegant. The standard library provides `functools.partial` which should normally be used instead. </section> Often, mutable default arguments are used to work around the default *late binding* of values from an outer scope. Usually, this technique does not actually use *mutable* default arguments, but it *does take advantage of the reason* why mutable default arguments work the way that they do. That is to say, default arguments are *early-binding*, so they are used as a way to avoid the usual late-binding result. <details><summary>Example</summary> This comes up when trying to use a loop to create callback functions, for example to define button behaviours in a GUI (using Tkinter or something similar). <section class="notice is-danger"> **People often naively expect each `Button` created this way to `print` the value that `i` had when the `Button` was created, but they don't:** ``` def make_buttons(window): for i in range(9): window.add(Button(command=lambda: print(i))) ``` </section> The problem is that `i` is not looked up until the button is actually clicked (and thus the callback provided as a `command` gets called). However, since default arguments are early-binding, the problem can be avoided by using `i` to set a default argument value. <section class="notice is-warning"> **This is a popular hack:** ``` def make_buttons(window): for i in range(9): window.add(Button(command=lambda i=i: print(i))) ``` </section> By adding `i=i`, the callback function gets a default value for its `i` parameter (which is a **separate name** from the local `i` defined in `make_buttons`). This default is never overridden; when the button is clicked, it uses the value of `i` that was determined ahead of time. <section class="notice is-success"> **The standard library `functools.partial` solves the problem more elegantly:** ``` from functools import partial def make_buttons(window): for i in range(9): window.add(Button(command=partial(print, i))) ``` </section> This way is explicit about the binding, and doesn't exploit a tricky feature of the language. It also doesn't needlessly expose a default argument that could in principle be overridden (but isn't supposed to be). </details>