Dottify
Dottify is a lightweight Python library that wraps dictionaries in objects with attribute-style access.
Instead of writing:
data["user"]["name"]
you can write:
data.user.name
Dottify supports nested dictionaries, dictionary-style access, attribute assignment, merging, conversion back to standard dictionaries, and common dictionary operations while providing helpful errors and key suggestions.
Installation
Install Dottify from PyPI:
pip install dottify
Quick Start
from dottify import Dottify
data = Dottify({
"name": "Alice",
"age": 30,
"address": {
"city": "Paris",
"country": "France"
}
})
print(data.name) # Alice
print(data.age) # 30
print(data.address.city) # Paris
print(data["address"].city) # Paris
Nested dictionaries are automatically converted to Dottify instances.
Features
Attribute-style access
Access dictionary keys directly as attributes:
data = Dottify({
"name": "Alice",
"age": 30
})
print(data.name)
print(data.age)
Dictionary-style access remains available:
print(data["name"])
print(data["age"])
Nested dictionaries
Nested dictionaries are recursively converted:
data = Dottify({
"user": {
"name": "Alice",
"address": {
"city": "Paris"
}
}
})
print(data.user.name)
print(data.user.address.city)
Case-sensitive access
Attribute and direct key access are case-sensitive:
data = Dottify({
"Name": "Alice"
})
print(data.Name) # Alice
print(data["Name"]) # Alice
Using the wrong case raises a DottifyKNFError:
data.name
The exception includes a helpful suggestion when an appropriate key exists.
Key suggestions
When a key is missing, Dottify provides suggestions:
data = Dottify({
"username": "alice"
})
data.usernme
Dottify reports the missing key and suggests the closest matching key.
A default value can also be supplied:
print(data.get("unknown", "Not Found"))
Attribute assignment
Values can be modified using attribute syntax:
data = Dottify({
"name": "Alice"
})
data.name = "Bob"
print(data.name) # Bob
Dictionary-style assignment is also supported:
data["name"] = "Charlie"
Nested assignments are automatically converted when appropriate:
data = Dottify({})
data.user = {
"name": "Alice",
"age": 30
}
print(data.user.name)
Removing keys
Keys can be removed with .remove():
data = Dottify({
"name": "Alice",
"age": 30
})
data.remove("age")
Removal is case-sensitive and provides suggestions for incorrect key names.
Dictionary interface
Dottify supports common dictionary operations:
data = Dottify({
"name": "Alice",
"age": 30
})
print(len(data))
print(list(data.keys()))
print(list(data.values()))
print(list(data.items()))
You can also check whether a key exists:
print(data.has_key("name")) # True
print(data.has_key("Name")) # False
print(data.has_key("unknown")) # False
Standard dictionary-style operations such as get(), pop(), and setdefault() are also supported:
data = Dottify({
"name": "Alice"
})
print(data.get("name"))
data.setdefault("age", 30)
age = data.pop("age")
Index-based access
Integer indexes can be used to access values by their position:
people = Dottify({
"Alice": {"age": 30},
"Bob": {"age": 25},
"Charlie": {"age": 35}
})
print(people[0].age)
print(people[1].age)
Indexing is zero-based.
Lists and tuples
Dottify does not recursively convert dictionaries contained inside lists or tuples.
For example:
from dottify import Dottify
data = Dottify({
"users": [
{"name": "Alice"},
{"name": "Bob"}
]
})
print(type(data.users))
# <class 'list'>
print(type(data.users[0]))
# <class 'dict'>
Therefore, attribute-style access is not available for dictionaries inside the list:
print(data.users[0].name)
# AttributeError
To recursively convert dictionaries, including dictionaries contained inside lists or tuples, use convert() instead:
from dottify import convert
data = convert({
"users": [
{"name": "Alice"},
{"name": "Bob"}
]
})
print(data.users[0].name) # Alice
print(data.users[1].name) # Bob
convert() recursively converts dictionaries at any level while preserving the surrounding container types.
Wrapped functions
Dottify also provides the @wrapped decorator for automatically converting dictionaries created inside a function.
from dottify import wrapped
@wrapped
def context():
data = {
"users": [
{"name": "Alice"},
{"name": "Bob"}
]
}
print(type(data))
# <class 'dottify.core.Dottify'>
print(data.users[1].name)
# Bob
The decorator allows regular dictionary literals created inside the decorated function to be automatically converted to Dottify.
This is particularly useful when working with data structures containing lists of dictionaries, because the dictionaries inside those lists are also converted.
Without @wrapped, the same dictionary literal would remain a standard Python dict.
Dottify vs convert() vs @wrapped
These three approaches have different purposes:
| Approach | Nested dictionaries | Dicts inside lists/tuples | Usage |
|---|---|---|---|
Dottify(data) |
Yes | No | Explicitly wrap a dictionary |
convert(data) |
Yes | Yes | Recursively convert a data structure |
@wrapped |
Yes | Yes | Automatically convert dictionaries created inside a function |
For example:
from dottify import Dottify, convert, wrapped
# Direct wrapping
data = Dottify({
"user": {
"name": "Alice"
}
})
print(data.user.name)
# Recursive conversion
data = convert({
"users": [
{"name": "Alice"},
{"name": "Bob"}
]
})
print(data.users[0].name)
# Automatic conversion inside a function
@wrapped
def get_users():
data = {
"users": [
{"name": "Alice"},
{"name": "Bob"}
]
}
return data
data = get_users()
print(data.users[0].name)
Numeric keys
Dictionary keys are handled consistently by Dottify, including numeric keys:
data = Dottify({
1: "one",
2: "two"
})
Keys are normalized according to Dottify's internal key-handling rules.
Merging
Dottify supports merging with the + operator:
people = Dottify({
"Alice": {
"age": 30
}
})
new_person = {
"Bob": {
"age": 25
}
}
people = people + new_person
print(people.Bob.age)
The += operator can be used for in-place merging:
people += Dottify({
"Charlie": {
"age": 35
}
})
Conversion back to dict
A Dottify instance can be converted back to a standard Python dictionary:
data = Dottify({
"name": "Alice",
"address": {
"city": "Paris"
}
})
normal_dict = data.to_dict()
print(type(normal_dict))
# <class 'dict'>
Nested Dottify instances are converted back to standard dictionaries as well.
Copying and deep copying
Dottify supports both shallow and deep copying:
from copy import copy, deepcopy
data = Dottify({
"user": {
"name": "Alice"
}
})
shallow = copy(data)
deep = deepcopy(data)
Pickling
Dottify instances can be serialized and restored using Python's pickle module:
import pickle
data = Dottify({
"name": "Alice"
})
serialized = pickle.dumps(data)
restored = pickle.loads(serialized)
print(restored.name)
Complete Example
from dottify import Dottify
people = Dottify({
"Alice": {
"age": 30,
"city": "Paris",
"profession": "Engineer"
},
"Charlie": {
"age": 35,
"city": "Marseille",
"profession": "Doctor"
}
})
# Attribute access
print(people.Alice.age)
print(people.Charlie.city)
# Dictionary-style access
print(people["Alice"].profession)
# Merge
people += {
"Bob": {
"age": 28,
"city": "Lyon",
"profession": "Designer"
}
}
print(people.Bob.profession)
# Modify values
people.Alice.profession = "Developer"
people.Bob.age = 29
# Dictionary interface
print(len(people))
print(list(people.keys()))
# Check keys
print(people.has_key("Alice"))
print(people.has_key("alice"))
# Remove a key
people.remove("Bob")
# Convert back to a standard dictionary
normal_dict = people.to_dict()
Testing
Dottify uses pytest for its test suite.
Run all tests with:
pytest tests/
The current test suite covers:
- Initialization from dictionaries and keyword arguments
- Nested dictionary conversion
- Attribute and item access
- Integer indexing
- Missing-key errors and suggestions
- Case-sensitive access
- Case-insensitive
get() - Attribute and item assignment
- Nested assignment conversion
- Dictionary conversion
keys(),values(),items()len()get()pop()setdefault()- Key removal
+and+=operators- Lists, tuples, and nested values
- Numeric keys
- Empty dictionaries and special values
- Copy and deep copy
- Pickling
- Equality
- String representation
- Edge cases
Contributing
Contributions are welcome.
Please open an issue or submit a pull request on the GitHub repository.
License
Dottify is distributed under the MIT License. See LICENSE for more information.
Metadata
Release files for dottify 2.0.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| dottify-2.0.0.tar.gz | 12.3 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| dottify-2.0.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 21.7 kB
Release files / dottify-2.0.0.tar.gz
| Download URL | dottify-2.0.0.tar.gz |
|---|---|
| Size | 12.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
50366e5bb29953be028e8f431098f6dc72a4dfd6a7b08e169ed77f6ba32a8d1f
|
|
BLAKE2b-256 checksum How to use checksums |
adff1aa450045a2d880014806bc33c04aa9ef232ab5377eb5f63e05a8e4e0a1a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.7
|
Release files / dottify-2.0.0-py3-none-any.whl
| Download URL | dottify-2.0.0-py3-none-any.whl |
|---|---|
| Size | 9.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
fca60f6c16a10c4e560fe2b79650eb419ddd41d2c9c7fa6c916b97f7cbeb7329
|
|
BLAKE2b-256 checksum How to use checksums |
32c9afbc50e084a4c6c2d22c9ba0ef37d7e3b240cca31e26d28f9c533ca43131
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.7
|