0

It is generally, for good reason, considered unsafe to use mutable default arguments in python. On the other hand, it is quite annoying to always have to wrap everything in Optional and do the little unpacking dance at the start of the function.

In the situation when one want to allow passing **kwargs to a subfunction, it appears there is an alternative option:

def foo(
    x: int,
    subfunc_args: Sequence[Any] = (),
    subfunc_kwargs: Mapping[str, Any] = {},
) -> R:
    ...
    subfunc(*subfunc_args, **subfunc_kwargs)

Obviously, {} is a mutable default argument and hence considered unsafe. HOWEVER, since subfunc_kwargs is annotated as Mapping, and not dict or MutableMapping, a type-checker would raise an error if we do end up mutating.

The question is: would this be considered OK to do, or still a horrible idea?

It would be really nice not having to do the little subfunc_kwargs = {} if subfunc_kwargs is None else subfunc_kwargs dance and having neater signatures.

Note: **subfunc_kwargs is not an option since this potentially clashes with other keys and leads to issues if the kwargs of subfunc get changed.

wjandrea
  • 28,235
  • 9
  • 60
  • 81
Hyperplane
  • 1,422
  • 1
  • 14
  • 28
  • If you're just using the dictionary for kwargs, it's not usually mutated. So just default it the normal way, you don't need the "unpacking dance". – Barmar Aug 31 '23 at 17:46
  • 1
    If you're looking for a strong type system to protect you from yourself, Python is a poor choice. – Mark Ransom Aug 31 '23 at 18:11
  • Type annotations have **no effect** on the behaviour of the code at runtime, aside from assigning some `__annotations__` attributes. They are **only** tools for the use of third-party type checkers, which can only inhibit you from running the code, but cannot invalidate it. This is not a suitable question, in that it boils down to "is it 'okay' to have a mutable default argument if you don't actually mutate it?" That's ambiguous; either you mean "will a problem occur as written" (obviously not) or else you have in mind some subjective code-quality guideline (we don't do those). – Karl Knechtel Aug 31 '23 at 18:22

2 Answers2

1

You don't need to put yourself in this situation in the first place, where you're relying on a type checker, when you can put an actual immutable default argument:

from types import MappingProxyType

...
    subfunc_kwargs: Mapping[str, Any] = MappingProxyType({})

See What would a "frozen dict" be?

wjandrea
  • 28,235
  • 9
  • 60
  • 81
  • It just occurred to me that this doesn't fully address the question in the title, but I'm the one who set the new title and I may have made it too broad. – wjandrea Aug 31 '23 at 18:25
  • Thanks, I didn't knew about `MappingProxyType`. PEP603 also looks like a very nice solution. – Hyperplane Aug 31 '23 at 18:39
0

"It would be really nice not having to do the little subfunc_kwargs = {} if subfunc_kwargs is None else subfunc_kwargs dance and having neater signatures."

Why so? Setting the default to None is a very robust choice in Python, and I would think robustness and clarity are preferable to elegance (in cases when they conflict). It is also very maintainable in the sense that it will be accessible to all developers, and well-understood by all static code analyzers.

GabCaz
  • 99
  • 1
  • 11
  • It's just really annoying and repetitive boilerplate . Also, it can hurt readability as defaults have to be explained elsewhere instead of simply in the signature. – Hyperplane Aug 31 '23 at 19:35