Skip to main content

A dict that can use unhashable keys

Project description

DictAnyKey: Python Dictionary That Can Use Any Key

PyPI Latest Release Python Version License: MIT Code style: ruff

DictAnyKey is a modern Python package that provides dictionary-like objects capable of using unhashable keys (such as lists and dictionaries) while maintaining excellent performance for hashable keys.

✨ Key Features

  • 🔑 Any Key Type: Use lists, dictionaries, sets, or any unhashable object as keys
  • Optimized Performance: Hashable keys perform at native dict speed
  • 📊 Maintains Order: Preserves insertion order like Python 3.7+ dictionaries
  • 🧊 Immutable Variant: FrozenDictAnyKey for hashable, immutable dictionaries
  • 🎯 Default Values: DefaultDictAnyKey with customizable default factories
  • 📈 Value Counting: Built-in value_counts() function for frequency analysis
  • 🔒 Type Safe: Full type hints and mypy compliance
  • 🧪 Well Tested: Comprehensive test suite with 271+ tests

🚀 Quick Start

Installation

pip install dictanykey

Basic Usage

from dictanykey import DictAnyKey, FrozenDictAnyKey, DefaultDictAnyKey, value_counts

# Create a dictionary with mixed key types
d = DictAnyKey()

# Hashable keys (fast lookup)
d[1] = "one"
d["hello"] = "world"

# Unhashable keys (slower but supported)
d[[1, 2, 3]] = "list key"
d[{"nested": "dict"}] = "dict key"

print(d)  # {1: 'one', 'hello': 'world', [1, 2, 3]: 'list key', {'nested': 'dict'}: 'dict key'}

# All standard dictionary operations work
print(len(d))           # 4
print(1 in d)           # True
print([1, 2, 3] in d)   # True
print(d.get("missing", "default"))  # default

# Iteration preserves insertion order
for key, value in d.items():
    print(f"{key}: {value}")
# Output:
#   1: one
#   hello: world
#   [1, 2, 3]: list key
#   {'nested': 'dict'}: dict key

Advanced Features

FrozenDictAnyKey (Immutable)

# Create immutable dictionary
frozen = FrozenDictAnyKey({1: "one", (1, 2): "tuple"})

# Read operations work normally
print(frozen[1])  # one

# Mutation operations raise errors
try:
    frozen[2] = "two"  # Raises TypeError
except TypeError:
    print("Cannot modify frozen dictionary")  # Cannot modify frozen dictionary

# Can be used as dictionary keys (if all keys are hashable)
if all(isinstance(k, (int, str, tuple)) for k in frozen.keys()):
    other_dict = {frozen: "value"}
    print(other_dict)  # {FrozenDictAnyKey([(1, 'one'), ((1, 2), 'tuple')]): 'value'}

DefaultDictAnyKey

# Create with default factory
dd = DefaultDictAnyKey(list)  # Default to empty list

# Missing keys automatically get default value
dd["new_key"].append("item")
print(dd["new_key"])  # ['item']

# Works with any callable
dd_int = DefaultDictAnyKey(lambda: 0)
dd_int["count"] += 1
print(dd_int["count"])  # 1

Value Counting

# Count frequency of values
data = [1, 2, 2, 3, 3, 3, "a", "a", "b"]
counts = value_counts(data)

print(counts)  # {1: 1, 'b': 1, 2: 2, 'a': 2, 3: 3}

# Sort options
counts_asc = value_counts(data, ascending=True)   # Lowest to highest
counts_desc = value_counts(data, ascending=False)  # Highest to lowest
counts_unsorted = value_counts(data, sort=False)   # Preserve original order

print(counts_asc)    # {1: 1, 'b': 1, 2: 2, 'a': 2, 3: 3}
print(counts_desc)   # {3: 3, 2: 2, 'a': 2, 1: 1, 'b': 1}
print(counts_unsorted)  # {1: 1, 2: 2, 3: 3, 'a': 2, 'b': 1}

# Works with unhashable values too!
unhashable_data = [[1, 2], [1, 2], [3, 4], [1, 2]]
unhashable_counts = value_counts(unhashable_data)
print(unhashable_counts)  # {[3, 4]: 1, [1, 2]: 3}

📋 Requirements

  • Python: 3.9+ (supports 3.9, 3.10, 3.11, 3.12, 3.13)
  • Dependencies: None (pure Python)

🔧 API Reference

DictAnyKey

The main dictionary class supporting any key type.

class DictAnyKey(MutableMapping[Any, Any]):
    def __init__(self, data: Optional[Union[Iterable, Mapping]] = None) -> None
    def __getitem__(self, key: Any) -> Any
    def __setitem__(self, key: Any, value: Any) -> None
    def __delitem__(self, key: Any) -> None
    def __len__(self) -> int
    def __iter__(self) -> Iterator[Any]
    def __contains__(self, key: Any) -> bool
    def __eq__(self, other: object) -> bool
    
    # Standard dictionary methods
    def get(self, key: Any, default: Optional[Any] = None) -> Any
    def pop(self, key: Any, default: Optional[Any] = None) -> Any
    def popitem(self) -> tuple[Any, Any]
    def setdefault(self, key: Any, default: Optional[Any] = None) -> Any
    def update(self, data: Optional[Union[Iterable, Mapping]] = None) -> None
    def clear(self) -> None
    def copy(self) -> DictAnyKey
    def fromkeys(cls, keys: Iterable[Any], value: Optional[Any] = None) -> DictAnyKey
    
    # View objects
    def keys(self) -> DictKeys
    def values(self) -> DictValues
    def items(self) -> DictItems

FrozenDictAnyKey

Immutable version of DictAnyKey.

class FrozenDictAnyKey(DictAnyKey):
    def __hash__(self) -> int  # Raises TypeError if keys are unhashable
    # All mutation methods raise AttributeError

DefaultDictAnyKey

Dictionary with default value factory.

class DefaultDictAnyKey(DictAnyKey):
    def __init__(self, default_factory: Optional[Callable[[], Any]], 
                 data: Optional[Union[Iterable, Mapping]] = None) -> None
    # Inherits all DictAnyKey methods
    # Missing keys automatically get default_factory() value

Utility Functions

def value_counts(values: Iterable[Any], 
                sort: bool = True, 
                ascending: bool = True) -> DictAnyKey

🎯 Use Cases

Data Processing

# Group data by complex keys
groups = DictAnyKey()
for item in data:
    key = (item.category, item.subcategory, item.tags)
    if key not in groups:
        groups[key] = []
    groups[key].append(item)

Configuration Management

# Use nested structures as keys
config = DictAnyKey()
config[("database", "host")] = "localhost"
config[("database", "port")] = 5432
config[("cache", "redis", "host")] = "redis-server"

Scientific Computing

# Use arrays as keys for matrix operations
matrix_cache = DictAnyKey()
matrix_cache[tuple([1, 2, 3])] = compute_expensive_result([1, 2, 3])

⚡ Performance Characteristics

  • Hashable Keys: O(1) lookup, same performance as built-in dict
  • Unhashable Keys: O(n) lookup, where n is the number of unhashable keys
  • Memory: Slightly higher memory usage due to dual storage (hashmap + list)
  • Insertion Order: Always preserved, regardless of key type

🧪 Testing

The package includes comprehensive tests covering:

  • All dictionary operations with various key types
  • Edge cases (empty dictionaries, mutation during iteration)
  • Performance characteristics
  • Type safety and error handling
  • Immutable and default dictionary variants

Run tests with:

pytest tests/

🔍 Development

Code Quality Tools

The project uses modern Python tooling:

  • Ruff: Fast linting and formatting
  • MyPy: Static type checking
  • Pytest: Testing framework
  • Black: Code formatting (via ruff)
# Format code
ruff format

# Lint code
ruff check

# Type check
mypy dictanykey/

# Run tests
pytest tests/

Contributing

  1. Fork the repository
  2. Create a feature branch
  3. Make your changes
  4. Run the test suite
  5. Submit a pull request

📄 License

This project is licensed under the MIT License - see the LICENSE file for details.

🆕 Changelog

Latest Version

  • ✅ Fixed critical bugs in setdefault(), pop(), and popitem() methods
  • ✅ Added comprehensive test coverage (271+ tests)
  • ✅ Modernized to Python 3.9+ with full type hints
  • ✅ Added FrozenDictAnyKey hash support
  • ✅ Optimized performance for key lookups
  • ✅ Migrated to pyproject.toml configuration
  • ✅ Added value_counts() utility function
  • ✅ Improved documentation and examples

See CHANGELOG.md for detailed version history.

🤝 Support


DictAnyKey - Because sometimes you need a dictionary that accepts anything as a key! 🗝️

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

dictanykey-0.1.3.tar.gz (20.0 kB view details)

Uploaded Source

Built Distribution

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

dictanykey-0.1.3-py3-none-any.whl (13.3 kB view details)

Uploaded Python 3

File details

Details for the file dictanykey-0.1.3.tar.gz.

File metadata

  • Download URL: dictanykey-0.1.3.tar.gz
  • Upload date:
  • Size: 20.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.11.13

File hashes

Hashes for dictanykey-0.1.3.tar.gz
Algorithm Hash digest
SHA256 41199435ee377af6fca7be9cadf379b3fc62b1ba277a82153d248476bc46f307
MD5 54c504dd975b9c9b4296decbee4c2725
BLAKE2b-256 ee7a2a201510b609e6805e371acda49ddc332c72ae6e6388fade4416f1dbb9ae

See more details on using hashes here.

File details

Details for the file dictanykey-0.1.3-py3-none-any.whl.

File metadata

  • Download URL: dictanykey-0.1.3-py3-none-any.whl
  • Upload date:
  • Size: 13.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.11.13

File hashes

Hashes for dictanykey-0.1.3-py3-none-any.whl
Algorithm Hash digest
SHA256 bf00fbb2e38876f07b622b5091c523043f8a26e7adf1e0061b4731b3062710e6
MD5 933781c3ad6cc329c523f15742c18838
BLAKE2b-256 22d3f758e396511c538aa1fc25598dfdecb396e7796246750c7e0277ccf97dce

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