Skip to main content

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 with active_index property
  • Active row access: Get the current active dictionary directly with the active_row property
  • Callback functions: Register callbacks to react to active index changes
  • 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

Active Row Property

The active_row property provides convenient access to the currently active dictionary:

lod = ListOfDicts(
    {'id': 1, 'name': 'Alice'},
    {'id': 2, 'name': 'Bob'},
    {'id': 3, 'name': 'Charlie'}
)

# Access the active row directly
print(lod.active_row)  # Output: {'id': 1, 'name': 'Alice'}

# Switch active index and get the new row
lod.active_index = 1
print(lod.active_row)  # Output: {'id': 2, 'name': 'Bob'}

# Empty list returns None
lod.clear()
print(lod.active_row)  # Output: None

# Useful for quick access to active dict data
lod = ListOfDicts({'a': 1, 'b': 2}, {'a': 10, 'b': 20})
lod.active_index = 0
print(lod.active_row['a'], lod.active_row['b'])  # Output: 1 2

# active_row is a dict object (not a ListOfDicts) but provides
# an alternate way to get to currently active data:
print(lod.active_row['a'] == lod.a) # True
print(lod.active_row['b'] == lod.b) # True
lod.c = 3  # add to the data
print(lod.active_row['c'] == lod.c) # True

Callback Functions

Register a callback function to be invoked whenever the active index changes. This is useful for reactive programming, logging, or triggering side effects:

lod = ListOfDicts(
    {'id': 1, 'status': 'not_started'},
    {'id': 2, 'status': 'pending'},
    {'id': 3, 'status': 'complete'}
)

# Define a callback function (receives the ListOfDicts instance)
def log_active_change(lod_instance):
    print(f" Active index changed to {lod_instance.active_index}")
    print(f" Active row: {lod_instance.active_row}")

# Register the callback
lod.callback_on_change(log_active_change)

# Change active index - callback is invoked
lod.active_index = 0  
# Output: 
#  Active index changed to 0
#  Active row: {'id': 1, 'status': 'not_started'}

lod.active_index = 1  
# Output: 
#  Active index changed to 1
#  Active row: {'id': 2, 'status': 'pending'}

lod.active_index = 2  
# Output: 
#  Active index changed to 2
#  Active row: {'id': 3, 'status': 'complete'}

Advanced Example: Reactive Updates

lod = ListOfDicts(
    {'value': 10},
    {'value': 20},
    {'value': 30}
)

result = None

def square_active_value(lod_instance):
    global result
    result = lod_instance.active_row['value'] ** 2

lod.callback_on_change(square_active_value)

lod.active_index = 0
print(result)  # Output: 100

lod.active_index = 1
print(result)  # Output: 400

lod.active_index = 2
print(result)  # Output: 900

lod.active_index = 1
print(result)  # Output: 400

lod.active_index = 0
print(result)  # Output: 100

Example: Enforcing Active Index Constraints

lod = ListOfDicts({'x': 1}, {'x': 2}, {'x': 3}, {'x': 4}, {'x': 5})

def always_stay_on_index_2(lod_instance):
    # Important: Check before reassigning to avoid infinite loops
    if lod_instance.active_index != 2:
        lod_instance.active_index = 2

lod.callback_on_change(always_stay_on_index_2)

lod.active_index = 0  # Callback enforces active_index = 2
print(lod.active_index)  # Output: 2

lod.active_index = 4  # Callback enforces active_index = 2
print(lod.active_index)  # Output: 2

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 integer
  • IndexError: If index is out of range
  • Negative indices wrap (e.g., -1 is the last item)
  • Returns None if the list is empty

active_row

Get the currently active dictionary without needing to use indexing.

current_dict = lod.active_row  # Returns self[active_index]
lod.active_index = 1
next_dict = lod.active_row  # Returns self[1]

Returns: The dictionary at active_index, or None if the list is empty

callback_on_change(func)

Register a callback function to be invoked whenever active_index changes.

def my_callback(lod_instance):
    print(f"Active index: {lod_instance.active_index}")
    
lod.callback_on_change(my_callback)
lod.active_index = 1  # Invokes callback

Parameters:

  • func: A callable that accepts one parameter (the ListOfDicts instance)

Notes:

  • The callback is called after active_index is set and synchronization occurs
  • Callbacks can modify the ListOfDicts instance (e.g., enforce index constraints)
  • To avoid infinite loops, always check the condition before modifying active_index within a callback

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

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":{...} } (both data and metadata keys 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 walkthrough
  • test_append_and_active_sync() - Attribute synchronization
  • test_make_datatypes_dbsafe_and_pyobj() - Datatype conversion
  • test_to_json_and_from_json() - JSON serialization
  • test_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:

  1. Fork the repository
  2. Create a feature branch
  3. Add tests for new functionality
  4. Ensure 100% test coverage
  5. Submit a pull request

Support

For issues and questions, please open a GitHub issue at: https://github.com/Stephen-Hilton/listofdicts/issues

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

listofdicts-2.1.6.tar.gz (18.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

listofdicts-2.1.6-py3-none-any.whl (10.1 kB view details)

Uploaded Python 3

File details

Details for the file listofdicts-2.1.6.tar.gz.

File metadata

  • Download URL: listofdicts-2.1.6.tar.gz
  • Upload date:
  • Size: 18.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.0

File hashes

Hashes for listofdicts-2.1.6.tar.gz
Algorithm Hash digest
SHA256 fc09ae4bac55afcf32e64025ecd18eb409751a91b445b844271ba40459e4cd7c
MD5 6287603ca604b440d33e631b8b1833e9
BLAKE2b-256 80dd46528d61c5149a8a949da973c17b70f4e15703da69b0b995da88f0493ac8

See more details on using hashes here.

File details

Details for the file listofdicts-2.1.6-py3-none-any.whl.

File metadata

  • Download URL: listofdicts-2.1.6-py3-none-any.whl
  • Upload date:
  • Size: 10.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.0

File hashes

Hashes for listofdicts-2.1.6-py3-none-any.whl
Algorithm Hash digest
SHA256 7cada9194670e0c6678323fb88a3e0b623442b867addcf132a7ec7f4cd582013
MD5 f3ffac3047e39c35a84ae9fdf5cd42c9
BLAKE2b-256 1819627564875248b808e5d6615f3502add27c90da6aebe77a7815dbb12776a3

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page