A ListOfDicts class for managing a list of dictionaries with enhanced functionality.
Project description
ListOfDicts
A Python list subclass that provides convenient attribute-based access to dictionary items, with seamless synchronization between active dict properties and object attributes. Includes JSON serialization, datatype conversion utilities, and full type validation.
Features
- Attribute-based access: Access active dictionary keys as object attributes
- Active index tracking: Track and switch between items in the list
- Automatic syncing: Changes to attributes sync to the active dictionary
- Type validation: All list items must be dictionaries
- JSON serialization: Serialize to/from JSON with metadata support
- Datatype conversion: Convert between Python objects (float, datetime) and database-safe types (Decimal, ISO strings)
- Class attribute exclusion: Subclass variables are automatically excluded from instance data
- 100% test coverage: Comprehensive test suite with pytest
Installation
pip install listofdicts
Or install from source:
git clone https://github.com/Stephen-Hilton/listofdicts.git
cd listofdicts
pip install -e .
Development Installation
For development, install with test dependencies:
pip install -e ".[dev]"
Quick Start
from listofdicts import ListOfDicts
# Create a new ListOfDicts
lod = ListOfDicts()
# Add dictionaries
lod.append({'a': 1, 'b': 2})
lod.append({'a': 3, 'b': 4})
# Access via active index
lod.active_index = 0
print(lod.a) # Output: 1
print(lod.b) # Output: 2
# Switch active index
lod.active_index = 1
print(lod.a) # Output: 3
# Modify via attribute (syncs to dict)
lod.b = 99
print(lod[1]['b']) # Output: 99
Usage
Creating and Populating
from listofdicts import ListOfDicts
# Empty list
lod = ListOfDicts()
# Initialize with dictionaries (variadic args or lists)
lod = ListOfDicts(
{'name': 'Alice', 'age': 30},
{'name': 'Bob', 'age': 25},
{'name': 'Charlie', 'age': 35}
)
# Standard list operations work normally
lod.append({'name': 'Diana', 'age': 28})
lod.insert(1, {'name': 'Eve', 'age': 27})
lod.extend([{'name': 'Frank', 'age': 32}])
# Type validation
try:
lod.append("not a dict") # Raises TypeError
except TypeError as e:
print(f"Error: {e}")
Active Index and Attribute Access
lod = ListOfDicts(
{'x': 1, 'y': 2},
{'x': 10, 'y': 20},
{'x': 100, 'y': 200}
)
# Set active index to access dict as attributes
lod.active_index = 0
print(lod.x, lod.y) # Output: 1 2
# Switch active index
lod.active_index = 1
print(lod.x, lod.y) # Output: 10 20
# Negative indices wrap around
lod.active_index = -1
print(lod.x, lod.y) # Output: 100 200 (last item)
# Modify attributes (syncs to active dict)
lod.y = 999
print(lod[2]['y']) # Output: 999
# Add new keys via attribute assignment
lod.z = 42
print(lod[2]['z']) # Output: 42
print('z' in lod[2]) # Output: True
Managing Keys
lod = ListOfDicts(
{'id': 1, 'name': 'Alice'},
{'id': 2, 'name': 'Bob'},
{'id': 3} # Missing 'status' key
)
# Add missing keys to all dicts
lod.addkey_if_missing('status', value_if_missing='active')
print(lod[0]) # {'id': 1, 'name': 'Alice', 'status': 'active'}
print(lod[2]) # {'id': 3, 'status': 'active'}
# Add multiple keys at once
lod.addkey_if_missing(['email', 'verified'], value_if_missing=None)
JSON Serialization
from datetime import datetime
import json
lod = ListOfDicts({'id': 1, 'created': datetime.now()})
lod.metadata = {'version': '1.0', 'author': 'Alice'}
# Serialize to JSON (datetimes become ISO strings, floats become Decimals)
json_string = lod.to_json()
print(json_string)
# Output:
# {
# "metadata": {
# "version": "1.0",
# "author": "Alice"
# },
# "data": [
# {
# "id": 1,
# "created": "2025-01-15T10:30:45.123456"
# }
# ]
# }
# Deserialize from JSON
loaded = ListOfDicts().from_json(json_string)
print(isinstance(loaded[0]['created'], datetime)) # Output: True
print(loaded.metadata) # Output: {'version': '1.0', 'author': 'Alice'}
Datatype Conversion
Convert between Python objects and database-safe types:
from datetime import datetime
from decimal import Decimal
lod = ListOfDicts({
'price': 19.99,
'quantity': 5,
'timestamp': datetime(2025, 1, 15, 10, 30, 45)
})
# Convert to database-safe types (float→Decimal, datetime→ISO string)
db_safe = lod.make_datatypes_dbsafe()
print(type(db_safe[0]['price'])) # Output: <class 'decimal.Decimal'>
print(type(db_safe[0]['timestamp'])) # Output: <class 'str'>
# Convert back to Python objects
py_obj = db_safe.make_datatypes_pyobj()
print(type(py_obj[0]['price'])) # Output: <class 'float'>
print(type(py_obj[0]['timestamp'])) # Output: <class 'datetime.datetime'>
# In-place conversion (modifies original)
lod.make_datatypes_dbsafe(inplace=True)
print(type(lod[0]['price'])) # Output: <class 'decimal.Decimal'>
Subclassing with Class Attributes
class Person(ListOfDicts):
# Class-level attributes are excluded from instance data
species: str = 'Homo sapiens'
def __init__(self, *args):
super().__init__(*args)
self.status = 'active' # Instance attribute (appears in data)
# Create instance
people = Person(
{'name': 'Alice', 'age': 30},
{'name': 'Bob', 'age': 25}
)
people.active_index = 0
print(people.name) # Output: 'Alice'
print(people.species) # Output: 'Homo sapiens' (class var, not in data)
print(people.status) # Output: 'active' (instance var, in data)
print('species' in people[0]) # Output: False
print('status' in people[0]) # Output: True
API Reference
Constructor
ListOfDicts(*args)
Create a new ListOfDicts. Args can be:
- Individual dicts:
ListOfDicts({'a': 1}, {'b': 2}) - A list/tuple:
ListOfDicts([{'a': 1}, {'b': 2}]) - Mixed:
ListOfDicts({'a': 1}, [{'b': 2}])
Properties
active_index
Get or set the active dictionary index. Setting triggers synchronization.
lod.active_index = 0 # Set active index
idx = lod.active_index # Get current active index
Raises:
TypeError: If value is not an integerIndexError: If index is out of range- Negative indices wrap (e.g.,
-1is the last item) - Returns
Noneif the list is empty
Methods
append(item)
Add a dictionary to the end of the list. First item added sets active_index to 0.
lod.append({'key': 'value'})
Raises: TypeError if item is not a dict
insert(index, item)
Insert a dictionary at the given index. Adjusts active_index if needed.
lod.insert(0, {'key': 'value'})
Raises: TypeError if item is not a dict
extend(items)
Add multiple dictionaries from an iterable.
lod.extend([{'a': 1}, {'b': 2}])
Raises: TypeError if any item is not a dict
addkey_if_missing(keys, value_if_missing=None)
Add keys to all dictionaries in the list if they don't exist.
lod.addkey_if_missing(['status', 'verified'], value_if_missing=None)
lod.addkey_if_missing('email', value_if_missing='')
Returns: self (chainable)
make_datatypes_dbsafe(inplace=False)
Convert float→Decimal and datetime→ISO string for database storage.
db_safe = lod.make_datatypes_dbsafe()
lod.make_datatypes_dbsafe(inplace=True) # Modify original
Returns: New ListOfDicts with converted types (or self if inplace=True, making it chainable). Note, new ListOfDicts does NOT carry over class properties, like metadata.
make_datatypes_pyobj(inplace=False)
Convert Decimal→float and ISO strings→datetime for Python use.
py_obj = lod.make_datatypes_pyobj()
lod.make_datatypes_pyobj(inplace=True) # Modify original
Returns: New ListOfDicts with converted types (or self if inplace=True, making it chainable). Note, new ListOfDicts does NOT carry over class properties, like metadata.
to_json()
Serialize to JSON string with metadata.
json_str = lod.to_json()
Returns: JSON string
from_json(json_content, metadata_to_props=[])
Load ListOfDicts from a JSON string. This is an instance method (not class method), and will replace all existing instance data and metadata.
lod = ListOfDicts().from_json(json_str)
Parameters:
json_content: JSON string in the format{"data":[...], "metadata":{...} }(bothdataandmetadatakeys optional)
Returns: ListOfDicts with deserialized data
clear()
Remove all items and reset active_index to None.
lod.clear()
pop(index=-1)
Remove and return item at index (default: last item).
last = lod.pop()
first = lod.pop(0)
Testing
The package includes 100% test coverage with pytest.
Running Tests
# Install test dependencies
pip install pytest coverage
# Run all tests
pytest test/test_listofdicts.py -v
# Run with coverage report
python -m coverage run -m pytest test/test_listofdicts.py
python -m coverage report -m
Test Examples
See test/test_listofdicts.py for comprehensive examples. Key test functions:
test_example_usage()- Complete usage walkthroughtest_append_and_active_sync()- Attribute synchronizationtest_make_datatypes_dbsafe_and_pyobj()- Datatype conversiontest_to_json_and_from_json()- JSON serializationtest_inheritance_and_classvar_exclusion()- Subclassing patterns
Coverage
Current test coverage: 100%
Name Stmts Miss Cover
------------------------------------------------------
src/ListOfDicts/__init__.py 0 0 100%
src/ListOfDicts/listofdicts.py 163 0 100%
test/test_listofdicts.py 224 0 100%
------------------------------------------------------
TOTAL 387 0 100%
Common Patterns
Filtering and Iteration
lod = ListOfDicts(
{'name': 'Alice', 'age': 30},
{'name': 'Bob', 'age': 25},
{'name': 'Charlie', 'age': 35}
)
# Iterate over all items
for item in lod:
print(item['name'], item['age'])
# Filter by condition
adults = [item for item in lod if item['age'] >= 30]
# Map with active index
for i, item in enumerate(lod):
lod.active_index = i
print(f"{lod.name} is {lod.age}")
Batch Operations
lod = ListOfDicts(
{'id': 1, 'status': 'pending'},
{'id': 2, 'status': 'pending'},
{'id': 3, 'status': 'pending'}
)
# Update all items at once
for i in range(len(lod)):
lod.active_index = i
lod.status = 'approved'
Working with Metadata
Metadata is a class variable, meaning it will never appear in the 'data' of the ListOfDicts. This also means that when creating a LOD object from data, the metadata isn't implicitly carried forward. For example:
lod = ListOfDicts(
{'id': 1, 'status': 'pending'},
{'id': 2, 'status': 'pending'},
{'id': 3, 'status': 'pending'}
)
lod.metadata = {'version':1.0}
newlod = lod[:]
assert newlod.metadata == {}
The lod[:] command creates a new data object, but doesn't contain the metadata (or any other class variable), so it's not carried over.
Metadata is the only class variable that is included in the .to_json() and .from_json() methods. This does allow you to serialize / deserialize using JSON, and persist metadata.
lod = ListOfDicts({'data': 'value'})
lod.metadata = {
'version': '1.0',
'created_by': 'Alice',
'timestamp': '2025-01-15T10:30:45'
}
# Access metadata
print(lod.metadata['version'])
# Persist with metadata
json_str = lod.to_json()
restored = ListOfDicts().from_json(json_str)
print(restored.metadata)
License
MIT License — see LICENSE file for details
Contributing
Contributions welcome! Please:
- Fork the repository
- Create a feature branch
- Add tests for new functionality
- Ensure 100% test coverage
- Submit a pull request
Support
For issues and questions, please open a GitHub issue at: https://github.com/Stephen-Hilton/listofdicts/issues
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 listofdicts-2.1.2.tar.gz.
File metadata
- Download URL: listofdicts-2.1.2.tar.gz
- Upload date:
- Size: 14.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.0
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9a612b0de8b68179da9146bbe0500000be198e24bd306a5fce58a595731a7bd6
|
|
| MD5 |
813d122e8e0e47986bc888900c38e2f1
|
|
| BLAKE2b-256 |
50b6e15cdb207f27e4b46147ff8cde6c29e61fe3433b85f09d769358f567361e
|
File details
Details for the file listofdicts-2.1.2-py3-none-any.whl.
File metadata
- Download URL: listofdicts-2.1.2-py3-none-any.whl
- Upload date:
- Size: 8.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.0
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d3161f19ba27f96bb18abe27b1301dd07b4ac5628ce6e234913419bc5a6242fe
|
|
| MD5 |
82b7e7060207d9a01ae70cc5468776ef
|
|
| BLAKE2b-256 |
e28069fb0a56128370a2c83c7146347fe18ebbb796262f225316181ee5a348be
|