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.1.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.1-py3-none-any.whl (13.2 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: dictanykey-0.1.1.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.1.tar.gz
Algorithm Hash digest
SHA256 2cd21f361a6d32d9f6003b764c328c604662080a25e3617d3f1d575300054c67
MD5 00b083f5603cbae9537c05c968c43564
BLAKE2b-256 fe21978f60c14b82955f9064c69b637f460111a1e3e3220c16af59d2c336d480

See more details on using hashes here.

File details

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

File metadata

  • Download URL: dictanykey-0.1.1-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.1-py3-none-any.whl
Algorithm Hash digest
SHA256 796a8206fa102c88960ecacb8621b64f00126eb8a78302c5716f9311ebba2818
MD5 91edd582db89d76845c35dbde110fab1
BLAKE2b-256 c1a8d6580781175c3f7e2ec31b99a82393eb807d500adc805e5a896f2f709dbb

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