A recursive dictionary with safe attribute access and automatic conversion.
Project description
MagiDict
Do you find yourself chaining .get()'s like there's no tomorrow, then praying to the Gods of Safety that you didn't miss to add {} inside .get('key', {})?
Has your partner left you because whenever they ask you to do something, you always reply, "I'll try, except KeyError as e"?
Do your kids get annoyed with you because you've called them "None" one too many times.
And did your friends stop hanging out with you because every time you're together, you keep going to the bathroom to check your production logs for any TypeErrors named "real_friends"?
When you're out in public, do you constantly have the feeling that Keanu Reeves is judging you from behind the corner for your inability to elegantly access nested dictionary keys?
And when you go to sleep at night, do you lie awake thinking about how much better your life would be if you took that course in JavaScript that your friend gave you a voucher for, before they moved to a different country and you lost contact with them, so you could finally use optional chaining and nullish coalescing operators to safely access nested properties without all the drama?
If you answered "yes" to any of these questions - don't worry! There's finally a solution that doesn't involve learning a whole new programming language or changing your religion to JavaScript! It's called ✨MagiDict✨ and it's here to make your dicts work like magic!
MagiDict is a powerful Python dictionary subclass that provides simple, safe and convenient attribute-style access to nested data structures, with recursive conversion and graceful failure handling. Designed to ease working with complex, deeply nested dictionaries, it reduces errors and improves code readability. Optimized and memoized for better performance.
Stop chaining get()'s and brackets like it's 2003 and start living your best life, where Dicts.Just.Work!
Installation
You can install MagiDict via pip:
pip install magidict
Quick Start
from magidict import MagiDict
# Create from dict
md = MagiDict({'user': {'name': 'Keanu', 'nickname': None}})
# Dot notation access
print(md.user.name) # 'Keanu'
# Bracket access with dot notation
print(md['user.name']) # 'Keanu'
# Safe chaining - no errors!
print(md.user.settings.theme) # MagiDict({}) - not a KeyError!
print(md['user.settings.theme']) # None - safe!
# Works with None values too
print(md.user.nickname.stage_name) # MagiDict({}) - safe!
print(md['user.nickname.stage_name']) # None - safe!
Access Styles Map
d = MagiDict({})
┌───────────────────┐
│ Access Styles │
└─────────┬─────────┘
│
┌─────────┴─────────┐
│ │
▼ ▼
┌─────────────────┐ ┌────────────────┐
│ Attribute Style │ │ Bracket Style │
│ . │ │ [] │
└───────┬─────────┘ └────────┬───────┘
│ ┌──────┴──────┐
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌─────────┐
│ Safe │ │ Safe │ │ Strict │
└────┬─────┘ └─────┬────┘ └────┬────┘
│ │ │
▼ ▼ ▼
┌───────┐ ┌──────────────┐ ┌──────────┐
│ d.bar │ │ d["foo.bar"] │ │ d["foo"] │
└───────┘ └──────────────┘ └──────────┘
│ │ │
▼ ▼ ▼
MagiDict({}) None KeyError
Documentation
Full documentation available in the GitHub Wiki
Key Features
1. Attribute-Style Access
Access dictionary keys using dot notation instead of bracket notation. Missing keys and keys with None values return an empty MagiDict:
md = MagiDict({'user': {'name': 'Keanu', 'nickname': None}})
# Existing key:
print(md.user.name) # 'Keanu'
# None value key:
print(md.user.nickname) # MagiDict({})
# Missing key:
print(md.user.email) # MagiDict({})
# Chaining onto none value key:
print(md.user.nickname.stage_name) # MagiDict({})
# Chaining onto missing key:
print(md.user.email.address) # MagiDict({})
2. Dot Notation in Brackets
Access nested keys using dot notation within brackets. Supports list access via indices and invalid Python identifiers as keys.
Missing keys and keys with None values return None:
md = MagiDict({
'users': [
{'name': 'Keanu'},
{'name': 'Alice'}
],
'settings': {'theme': None, "key-with-dash": "value" , 1: "One"}
})
# Existing key:
print(md['users.0.name']) # 'Keanu'
# Index out of range:
print(md['users.2.name']) # None - safe!
# Missing key:
print(md['users.0.email']) # None
# None value:
print(md['settings.theme']) # None
# Chaining onto none value:
print(md['settings.theme.color']) # None
# Chaining onto missing key:
print(md['settings.language.code']) # None
# Invalid identifier keys
print(md['settings.key-with-dash']) # 'value'
print(md['settings.1']) # 'One'
# Direct access still raises KeyError as expected
print(md['nonexistent']) # KeyError
Automatic Type Conversion
When using dot notation in brackets, MagiDict intelligently converts key segments to their appropriate types. To prevent conversion and treat the segment as a string, enclose it in quotes, like you would with standard single key dictionary access.
md = MagiDict({'items': {0: 'int zero', True: 'yes', 3.14: 'pi', '0': 'string zero'}})
print(md['items.0']) # 'int zero'
print(md['items.True']) # 'yes'
# Use comma for float access
print(md['items.3,14']) # 'pi'
# Prevent conversion with quotes
print(md['items."0"']) # 'string zero' (stays as string)
Keys with Dots Parsing
Quotes can also be used to parse keys that contain dots:
md = MagiDict({"settings": {"config.version": "1.0","config": {"version": "2.0"}}})
print(md['settings."config.version"']) # '1.0'
print(md['settings.config.version']) # '2.0'
3. Recursive Conversion
Nested dictionaries are automatically converted to MagiDict instances:
4. Standard Dictionary Behavior Preserved
All standard dict methods and behaviors work as expected. For example missing keys with brackets raise KeyError as expected
5. Safe mget() Method
mget or mg is MagiDict's native get method. Unless a custom default is provided, it returns an empty MagiDict for missing keys or None values:
md = MagiDict({'key': 'value', 'second_key': None})
# Existing key
print(md.mget('key')) # 'value'
# Returns empty MagiDict for None values and missing keys
print(md.mget('second_key')) # MagiDict({})
print(md.mget('missing')) # MagiDict({})
# Custom default value
print(md.mget('missing', default='Not Found')) # 'Not Found'
6. Convert Back to Standard Dict
Use disenchant() to convert back to a standard Python dict:
md = MagiDict({'user': {'name': 'Keanu'}})
standard_dict = md.disenchant()
7. Convert empty MagiDict to None
Use none() to convert empty MagiDict instances that were created from None or missing keys back to None. Other values are returned unchanged:
md = MagiDict({'user': None, 'age': 25})
print(none(md.user)) # None
print(none(md.user.name)) # None
print(none(md.age)) # 25
API Reference
Constructor
MagiDict(\*args, **kwargs) - Creates a new instance. Accepts same arguments as built-in dict.
Core Methods
mget(key, default=...)/mg(key, default=...)- Safe get that returns emptyMagiDictfor missing keys orNonevalues (unless custom default provided)disenchant()- ConvertsMagiDictand all nested instances back to standarddict. Handles circular referencesfilter(function, drop_empty=False)- Returns newMagiDictwith items where function returnsTruesearch_key(key)- Finds first occurrence of key in nested structuressearch_keys(key)- Returns list of all values for key in nested structures
All standard dict methods are fully supported (get, update, copy, keys, values, items, etc.)
Utility Functions
enchant(d)- Converts standarddicttoMagiDictmagi_loads(s, **kwargs)- Deserializes JSON string toMagiDictmagi_load(fp, **kwargs)- Deserializes JSON file toMagiDictnone(obj)- Converts emptyMagiDict(fromNone/missing key) back toNone
Important Caveats
1. Key Conflicts with Dict Methods
Keys that conflict with standard dict methods must be accessed using brackets, mget or get:
md = MagiDict({'keys': 'my_value', 'items': 'another_value'})
# These return dict methods, not your values
print(md.keys) # <built-in method keys...>
# Use bracket access instead
print(md['keys']) # 'my_value'
# Or use mget()
print(md.mget('keys')) # 'my_value'
2. Invalid Python Identifiers and Non-String Keys
Keys that aren't valid Python identifiers must use bracket access or mget():
md = MagiDict({
'1-key': 'value1',
2: 'value2',
})
# Must use brackets or mget()
print(md['1-key']) # 'value1'
print(md[2]) # 'value2'
print(md.mget('1-key')) # 'value1'
# These won't work
print(md.1-key) # SyntaxError
print(md.2) # SyntaxError
3. Setting attributes
Setting or updating keys using dot notation is not supported. Use bracket notation instead like standard dicts. This is purposely restricted to avoid confusion and potential bugs.
Advanced Features
MagiDict supports:
- Pickling and unpickling
- Deep copying
- In-place updates with
|=operator (Python 3.9+) - Circular reference handling
- Auto-completion support in IPython, Jupyter and IDE's
Performance
Magidict's initialization and recursive conversion are very fast due to the core hooks being implemented in C.
Magidict is extensively tested with 800+ test cases and 98% code coverage.
Comparison with Alternatives
vs. Regular Dict
d = {'user': {'profile': {'name': 'Keanu'}}}
md = MagiDict(d)
# Regular dict
name = d.get('user', {}).get('profile', {}).get('name', 'Unknown')
# MagiDict
name = md.user.profile.name or 'Unknown'
vs. DotDict, Bunch, AttrDict and Similar Libraries
MagiDict provides additional features:
- Safe chaining with missing keys (returns empty
MagiDict) - Safe chaining with None values
- Dot notation in bracket access
- Built-in
mget() - Search and filter methods
- Protected empty instances
- Circular reference handling
- Memoization
- Type preservation for all non-dict values
- In-place mutation
Troubleshooting
If you encounter any issues or have questions, please check the Troubleshooting section in the Wiki or GitHub Issues
Contributing
Contributions are welcome and appreciated! Please see the CONTRIBUTING.md for more information.
License
MagiDict is licensed under the MIT License.
Links
For documentation and source code, visit the project on GitHub:
Documentation: GitHub Wiki
PyPI: magidict
Source Code: MagiDict
Issue Tracker: GitHub Issues
Benchmarks: Performance Results
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file magidict-0.2.0.tar.gz.
File metadata
- Download URL: magidict-0.2.0.tar.gz
- Upload date:
- Size: 75.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.5
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
47cf047e0190e7425c893c8a561d218b83c19e372b982250d94edd8914b5f80e
|
|
| MD5 |
5ecae8c342106777172cc501e1aa9d82
|
|
| BLAKE2b-256 |
729eae01423200914f308c5af9fa8b2badd1d3232f1381ee888e8b8ce077d372
|
File details
Details for the file magidict-0.2.0-cp313-cp313-win_amd64.whl.
File metadata
- Download URL: magidict-0.2.0-cp313-cp313-win_amd64.whl
- Upload date:
- Size: 25.7 kB
- Tags: CPython 3.13, Windows x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.5
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c781c9951157d7b6350f67c48244985475cab442f4ebdd0e8bea5c3d29fb2ef3
|
|
| MD5 |
208a830ba4d063c0185b24a758896615
|
|
| BLAKE2b-256 |
822500d7048f11d84fe0d3ad3405f11db41a231bfdca3f9f8c950af64005362e
|