> I've written Python for 14 years and have never seen code like that.
Exactly because it's a footgun that everybody hits very early. I think the Python linters even flag this.
The fact that default arguments in Python get set to "None" is precisely because of this.
For this particular case, a better candidate is usually empty tuple () since it's actually iterable etc, so unless you need to mutate that argument...
The bigger problem is with dicts and sets because they don't have the equivalent concise representation for the immutable alternative.
Arguably the even bigger problem is that Python collection literals produce mutable collections by default. And orthogonal to that but contributing to the problem is that the taxonomy of collections is very disorganized. For example, an immutable equivalent of set is frozenset - well and good. But then you'd expect the immutable equivalent of list to be frozenlist, except it's tuple! And the immutable equivalent of dict isn't frozendict, it... doesn't actually exist at all in the Python stdlib (there's typing.MappingProxyType which provides a readonly wrapper around any mapping including dicts, but it will still reflect the changes done through the original dict instance, so to make an equivalent of frozenset you need to copy the dict first and then wrap it and discard all remaining references).
Most of this can be reasonably explained by piecemeal evolution of the language, but by now there's really no excuse to not have frozendict, nor to provide an equally concise syntax for all immutable collections, nor to provide better aliases and more uniform API (e.g. why do dicts have copy() but lists do not?).
At this point, the biggest problem simply seems to be that the size of "Python Core" has outstripped the number of maintainers.
I helped shepherd a bug fix into Python that was less than a dozen lines, dead simple, completely obvious, sorely needed and still took 3 years and a summoning of Guido, himself, to get it shoved through. Because there was no designated maintainer for that section of code, people were absolutely terrified of touching the code even though it was completely obvious that the fix was backwards compatible. It finally hit the latest Python and a bunch of other projects immediately removed their workarounds for the bug.
If it was that difficult to get a super small, super obvious bugfix through, trying to get a "frozendict" into the language is going to be a Sisyphean task.
There's a lot of changes going on in CPython in the past few releases, so a frozendict wouldn't be a big one comparatively speaking.
The bigger problem is that there was already a PEP (https://peps.python.org/pep-0416/) about that, and it was rejected for wonderful reasons such as "multiple threads can agree by convention not to mutate a shared dict, there’s no great need for enforcement" and "there are existing idioms for avoiding mutable default values".