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.

3
  • 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". Commented Aug 31, 2023 at 17:46
  • 1
    If you're looking for a strong type system to protect you from yourself, Python is a poor choice. Commented Aug 31, 2023 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). Commented Aug 31, 2023 at 18:22

2 Answers 2

2

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?

Sign up to request clarification or add additional context in comments.

2 Comments

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.
Thanks, I didn't knew about MappingProxyType. PEP603 also looks like a very nice solution.
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.

1 Comment

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.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.