Skip to content

Add default fallback for missing keys#107

Open
sachinsachdeva wants to merge 3 commits into
drgrib:masterfrom
sachinsachdeva:codex/fix-issue-74-missing-default
Open

Add default fallback for missing keys#107
sachinsachdeva wants to merge 3 commits into
drgrib:masterfrom
sachinsachdeva:codex/fix-issue-74-missing-default

Conversation

@sachinsachdeva

@sachinsachdeva sachinsachdeva commented Jun 8, 2026

Copy link
Copy Markdown

Fixes #74.

This adds an opt-in _default constructor argument for callers who want missing attributes to return a fallback value instead of autovivifying an empty DotMap. Existing behavior remains unchanged unless _default is provided.

Example:

m = DotMap({'city': 'abc', 'CountryCode': 101}, _default='')
assert m.zipCode == ''

Tests:

python3 -m unittest dotmap.test

@sachinsachdeva

Copy link
Copy Markdown
Author

@drgrib take a look when you get a chance

@FelixSchwarz

Copy link
Copy Markdown
Collaborator

Thank you for your PR. I'll try to have a closer look this week. Unfortunately time is pretty limited in this crazy time.

@FelixSchwarz FelixSchwarz left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you very much - solid PR which needs just a few changes.

Comment thread dotmap/test.py
Comment thread dotmap/__init__.py Outdated
Comment thread dotmap/test.py Outdated
Comment thread dotmap/__init__.py Outdated
@FelixSchwarz

Copy link
Copy Markdown
Collaborator

Oh, and if you rebase to/merge in the latest master branch, you should also get github actions running again.

@sachinsachdeva sachinsachdeva force-pushed the codex/fix-issue-74-missing-default branch from 2967e03 to 8a412af Compare June 18, 2026 10:40
- Raise ValueError when _default is provided with _dynamic=False instead
  of silently overriding _dynamic
- Nest the _default check inside the existing "k not in self._map" branch
  in __getitem__
- Preserve _default through copy()/deepcopy()
- Add tests for the _dynamic=False error, copy, and deepcopy; reorder the
  missing-key assertions to check setup before behavior

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@sachinsachdeva sachinsachdeva force-pushed the codex/fix-issue-74-missing-default branch from 7b967f1 to 2c76767 Compare June 18, 2026 10:59
@sachinsachdeva

Copy link
Copy Markdown
Author

@FelixSchwarz I think I got all your comments.

@FelixSchwarz FelixSchwarz left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm sorry that it took so long but I thought about the PR a bit and something felt "off" after my first review. After pondering this a bit more I think I now have a better picture in my mind.

This is what annoyed me:

>>> from dotmap import DotMap
>>> m = DotMap({}, _default=[])
>>> m.x
[]
>>> m.y
[]
>>> m.x.append(42)
>>> m.y
[42]

That is clearly wrong.

After a lot of back and forth I came up with two options:

  • return deepcopy(self._default) on each miss
  • replace _default with _default_factory, called once per miss. Similar to defaultdict, e.g. _default_factory=str instead of _default=''

The latter version is a bit more explicit but on the other hand causes a bit longer lines for each user of that feature. But using a _default_factory just simplify the internal code of dotmap and it fits an established pattern in Python to avoid silently shared mutable values.

It prevents the problem by construction, and it actually simplifies the diff. _default_provided can go away since None simply means "not set". A factory must be callable, we can raise a TypeError for non-callables.

What do you think?

Comment thread dotmap/test.py Outdated
Comment thread dotmap/test.py Outdated
@sachinsachdeva

Copy link
Copy Markdown
Author

I'm sorry that it took so long but I thought about the PR a bit and something felt "off" after my first review. After pondering this a bit more I think I now have a better picture in my mind.

This is what annoyed me:

>>> from dotmap import DotMap
>>> m = DotMap({}, _default=[])
>>> m.x
[]
>>> m.y
[]
>>> m.x.append(42)
>>> m.y
[42]

That is clearly wrong.

After a lot of back and forth I came up with two options:

  • return deepcopy(self._default) on each miss
  • replace _default with _default_factory, called once per miss. Similar to defaultdict, e.g. _default_factory=str instead of _default=''

The latter version is a bit more explicit but on the other hand causes a bit longer lines for each user of that feature. But using a _default_factory just simplify the internal code of dotmap and it fits an established pattern in Python to avoid silently shared mutable values.

It prevents the problem by construction, and it actually simplifies the diff. _default_provided can go away since None simply means "not set". A factory must be callable, we can raise a TypeError for non-callables.

What do you think?

I and Claude both like the default factory approach :-)

Per review feedback: a mutable _default value was shared across all
missing keys, so mutating one leaked into every other miss.

Following the collections.defaultdict pattern, _default_factory takes a
callable invoked on each miss; the result is stored under the key and
returned, giving every key its own independent value. Non-callables
raise TypeError, and _default_provided is gone since None now simply
means "not set".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Accessing any value which is not there in dictionary is printing "DotMap()"

2 participants