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 a single {}?
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"?
How often do you seek imaginary guidance from Guido, begging him to teach you the mystical ways of safely navigating nested Python dictionaries?
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, you're not alone! But don't worry anymore, because 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 save your sanity!
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!
Overview
MagiDict extends Python's built-in dict to offer a more convenient and forgiving way to work with nested dictionaries. It's particularly useful when working with JSON data, API responses, configuration files, or any deeply nested data structures where safe navigation is important.
Installation
You can install MagiDict via pip:
pip install magidict
Importing MagiDict
Once installed, you can import it in several ways:
Import the entire package
import magidict
# Access the class directly
md = magidict.MagiDict({'user': {'name': 'Alice'}})
Import specific classes and helpers
from magidict import MagiDict, enchant, magi_loads, none
md = MagiDict({'user': {'name': 'Alice'}})
Key Features
1. Attribute-Style Access
Access dictionary keys using dot notation instead of bracket notation:
md = MagiDict({'user': {'name': 'Alice', 'age': 30}})
md.user.name # 'Alice'
md.user.age # 30
2. Dot Notation in Brackets
Use dot-separated strings for deep access, including list indices:
md = MagiDict({
'users': [
{'name': 'Alice', 'id': 1},
{'name': 'Keanu', 'id': 2}
]
})
md['users.0.name'] # 'Alice'
md['users.1.id'] # 2
3. Recursive Conversion
Nested dictionaries are automatically converted to MagiDict instances:
data = {
'company': {
'departments': {
'engineering': {
'employees': 50
}
}
}
}
md = MagiDict(data)
md.company.departments.engineering.employees # 50
4. Graceful Failure
Accessing non-existent keys returns an empty MagiDict instead of raising errors:
md = MagiDict({'user': {'name': 'Alice'}})
# No error, returns empty MagiDict
md.user.email.address.street # MagiDict({})
# Safe chaining
if md.settings.theme.dark_mode:
# This won't cause an error even if 'settings' doesn't exist
pass
5. Safe None Handling
Keys with None values can be safely chained:
md = MagiDict({'user': {'nickname': None}})
md.user.nickname.stage_name # MagiDict({})
# Bracket access returns the actual None value
md.user['nickname'] # None
6. Standard Dictionary Behavior Preserved
All standard dict methods and behaviors work as expected. For example missing keys with brackets raise KeyError as expected
7. Safe mget() Method
mget 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({'1-invalid': 'value', 'valid': None})
# Works with invalid identifiers
md.mget('1-invalid') # 'value'
# Returns empty MagiDict for missing keys
md.mget('missing') # MagiDict({})
# Shorthand version
md.mg('1-invalid') # 'value'
# Provide custom default
md.mget('missing', 'default') # 'default'
8. Convert Back to Standard Dict
Use disenchant() to convert back to a standard Python dict:
md = MagiDict({'user': {'name': 'Alice'}})
standard_dict = md.disenchant()
type(standard_dict) # <class 'dict'>
9. Convert empty MagiDict to None
Use none() to convert empty MagiDict instances that were created from None or missing keys back to None:
md = MagiDict({'user': None, 'age': 25})
none(md.user) # None
none(md.user.name) # None
none(md.age) # 25
API Reference
Constructor
MagiDict(*args, **kwargs)
Creates a new MagiDict instance. Accepts the same arguments as the built-in dict.
Examples:
MagiDict(*args, **kwargs)
or
d = {"key": "value"}
md = MagiDict(d)
Methods
mget(key, default=...)
Safe get method that mimics dict's get(), but returns an empty MagiDict for missing keys or None values unless a custom default is provided.
Parameters:
key: The key to retrievedefault: Value to return if key doesn't exist (optional)
Returns:
- The value if key exists and is not
None - Empty
MagiDictif key doesn't exist (unless custom default provided) - Empty
MagiDictif value isNone(unless default explicitly set toNone)
mg(key, default=...)
Shorthand alias for mget().
disenchant()
Converts the MagiDict and all nested MagiDict instances back to standard Python dictionaries. Handles circular references gracefully.
Returns: A standard Python dict
Example:
md = MagiDict({'nested': {'data': [1, 2, 3]}})
regular_dict = md.disenchant()
type(regular_dict) # <class 'dict'>
Standard Dict Methods
All standard dictionary methods are supported:
update()- Update with key-value pairscopy()- Return a shallow copysetdefault()- Get value or set defaultfromkeys()- Create dict from sequence of keyspop()- Remove and return valuepopitem()- Remove and return arbitrary itemclear()- Remove all itemskeys()- Return dict keysvalues()- Return dict valuesitems()- Return dict itemsget()- Get value with optional default__contains__()- Check if key exists (viain)- and more
Utility Functions
enchant(d)
Converts a standard dictionary into a MagiDict.
Parameters:
d: A standard Python dictionary
Returns: A MagiDict instance
magi_loads(s, **kwargs)
Deserializes a JSON string directly into a MagiDict instead of a standard dict.
Parameters:
s: JSON string to parse**kwargs: Additional arguments passed tojson.loads()
Returns: A MagiDict instance
Example:
json_string = '{"user": {"name": "Alice", "age": 30}}'
md = magi_loads(json_string)
md.user.name # 'Alice'
magi_load(fp, **kwargs)
Deserializes a JSON file-like object into a MagiDict instead of a standard dict.
Parameters:
fp: A file-like object containing JSON data**kwargs: Additional arguments passed tojson.load()
Returns: A MagiDict instance
none(obj)
Converts an empty MagiDict that was created from a None or missing key into None. Otherwise, returns the object as is.
Parameters:
obj: The object to check
Returns:
Noneifobjis an emptyMagiDictcreated fromNoneor missing keyobjotherwise
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
md.keys # <built-in method keys...>
md.items # <built-in method items...>
# Use bracket access instead
md['keys'] # 'my_value'
md['items'] # 'another_value'
# Or use mget()
md.mget('keys') # 'my_value'
Common conflicting keys: keys, values, items, get, pop, update, clear, copy, setdefault, fromkeys
2. Invalid Python Identifiers
Keys that aren't valid Python identifiers must use bracket access or mget():
md = MagiDict({
'1-key': 'value1',
'my key': 'value2',
'my-key': 'value3'
})
# Must use brackets or mget()
md['1-key'] # 'value1'
md.mget('my key') # 'value2'
md['my-key'] # 'value3'
# These won't work
print(md.1-key) # SyntaxError
print(md.my key) # SyntaxError
3. Non-String Keys
Non-string keys can only be accessed using standard bracket notation or mget():
md = MagiDict({1: 'one', (2, 3): 'tuple_key'})
md[1] # 'one'
md[(2, 3)] # 'tuple_key'
md.mget(1) # 'one'
print(md.1) # SyntaxError
4. Protected Empty MagiDicts
Empty MagiDict instances returned from missing keys or None values are protected from modification:
md = MagiDict({'user': None})
md.user["name"] = 'Alice' # TypeError
# Same for missing keys
md["missing"]["key"] = 'value' # TypeError
This protection prevents silent bugs where you might accidentally try to modify a non-existent path.
5. Setting attributes
Setting or updating keys using dot notation is not supported. Use bracket notation instead. As with standard dicts, this is purposely restricted to avoid confusion and potential bugs.
md = MagiDict({'user': {'name': 'Alice'}})
md.user.name = 'Keanu' # AttributeError
md.user.age = 30 # AttributeError
# Use bracket notation instead
md['user']['name'] = 'Keanu'
md['user']['age'] = 30
Advanced Features
Pickle Support
MagiDict supports pickling and unpickling:
md = MagiDict({'data': {'nested': 'value'}})
pickled = pickle.dumps(md)
restored = pickle.loads(pickled)
restored.data.nested # 'value'
Deep Copy Support
md1 = MagiDict({'user': {'name': 'Alice'}})
md2 = deepcopy(md1)
md2.user.name = 'Keanu'
md1.user.name # 'Alice' (unchanged)
md2.user.name # 'Keanu'
In-Place Updates with |= Operator
Python 3.9+ dict merge operator is supported:
md = MagiDict({'a': 1})
md |= {'b': 2, 'c': 3}
md # MagiDict({'a': 1, 'b': 2, 'c': 3})
Circular Reference Handling
MagiDict gracefully handles circular references:
md = MagiDict({'name': 'root'})
md['self'] = md # Circular reference
# Access works
md.self.name # 'root'
md.self.self.name # 'root'
# Safely converts back to dict
regular = md.disenchant()
Auto-completion Support
MagiDict provides intelligent auto-completion in IPython, Jupyter notebooks and IDE's.
Performance Considerations
Tested:
- All standard and custom functionality
- Circular and self references through pickle/deepcopy/disenchant
- Concurrent access patterns (multi-threaded reads/writes)
- Protected MagiDict mutation attempts
- Deep nesting with recursion limits and stack overflow prevention
- Type preservation through operations
Best Practices
Good use cases:
- Configuration files
- API response processing
- Data exploration
- One-time data transformations
- Interactive development
Avoid for:
- High-performance inner loops
- Large-scale data processing
- Memory-constrained environments
- When you need maximum speed
Optimization Tips
# If you need standard dict for performance-critical code
if need_speed:
regular_dict = md.disenchant()
# Use regular_dict in hot loop
# Convert back when done
md = enchant(regular_dict)
Comparison with Alternatives
vs. Regular Dict
# Regular dict - verbose and error-prone
regular = {'user': {'profile': {'name': 'Alice'}}}
name = regular.get('user', {}).get('profile', {}).get('name', 'Unknown')
# MagiDict - clean and safe
md = MagiDict({'user': {'profile': {'name': 'Alice'}}})
name = md.user.profile.name or 'Unknown'
vs. DotDict/AttrDict 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()for safe access - Protected empty instances
- Circular reference handling
Troubleshooting
KeyError on Dot Notation Access
md = MagiDict({'user': {'name': 'Alice'}})
email = md['user']['email'] #KeyError
email = md['user.email'] #KeyError
# This is safe
email = md.user.email or 'no-email'
Cannot Modify Error
md = MagiDict({'user': None})
md.user.name = 'Alice' #TypeError
Unexpected Empty MagiDict
md = MagiDict({'value': None})
md.value # MagiDict({})
# Use bracket access to get actual None
md['value'] # None
License
MagiDict is licensed under the MIT License. See the LICENSE file for details.
Links
For documentation and source code, visit the project on GitHub: https://github.com/hristokbonev/MagiDict
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.1.2.tar.gz.
File metadata
- Download URL: magidict-0.1.2.tar.gz
- Upload date:
- Size: 56.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.9.4
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2d6bb33dd7e859c760d3e9f1094618dbc85441c0d35a73726d73cd06d0f01fc6
|
|
| MD5 |
ca84313769b63c436e750c9cd1bb5db0
|
|
| BLAKE2b-256 |
87fcb19903853898580b3261674eddfef31a8d29f3810d50a80a1e9a4890bd24
|
File details
Details for the file magidict-0.1.2-py3-none-any.whl.
File metadata
- Download URL: magidict-0.1.2-py3-none-any.whl
- Upload date:
- Size: 11.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.9.4
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
31d26862084b50c2af7dd48632d48b8eb8ac158001caa6e7be25f6ba3d978b07
|
|
| MD5 |
2c4faa1daac66a4580b0e67371ebc59f
|
|
| BLAKE2b-256 |
e99b66fe9dac0479a5f4ca1f1524264a283aa5b6e937d4399ce9126694f119f7
|