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}")

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")

# 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"}

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)  # DictAnyKey({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)  # DictAnyKey({[1, 2]: 3, [3, 4]: 1})

📋 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.2.tar.gz (19.8 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.2-py3-none-any.whl (13.2 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: dictanykey-0.1.2.tar.gz
  • Upload date:
  • Size: 19.8 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.2.tar.gz
Algorithm Hash digest
SHA256 12ef1eb870bb6bb3835dcaa5d9981a56d2cf051b839e04cd84bf6abe053568d0
MD5 4df3066215d71c030cbb9e2b7a547331
BLAKE2b-256 34ffbd8f5825f8fff207cf7932b33ff027745acddbefcc67a531d49938c9caf2

See more details on using hashes here.

File details

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

File metadata

  • Download URL: dictanykey-0.1.2-py3-none-any.whl
  • Upload date:
  • Size: 13.2 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.2-py3-none-any.whl
Algorithm Hash digest
SHA256 3d5d45bce6823097797fe0142f97d38e90b42e8a38eb980abe8e8726aafd2ebe
MD5 4107bb3bb9a3ace22530673a5b5547b1
BLAKE2b-256 c2bf583d28f2dda3bda6fbf9d498d47c3795d4e9feacc2bb677405a66cca4c04

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