Skip to main content

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! 🗝️

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.

Release history Release notifications | RSS feed

This release

0.1.3 This release

2 files

0.1.2

2 files

0.1.1

2 files

0.1.0

2 files

0.0.5

2 files

0.0.4

2 files

0.0.3

2 files

0.0.2

2 files

0.0.1

2 files

Supported by

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