https://github.com/astropenguin/readonlydict
Drop-in read-only dictionary with 100% typing and runtime compatibility
https://github.com/astropenguin/readonlydict
dictionary hashable immutable mapping python read-only typing
Last synced: 2 months ago
JSON representation
Drop-in read-only dictionary with 100% typing and runtime compatibility
- Host: GitHub
- URL: https://github.com/astropenguin/readonlydict
- Owner: astropenguin
- License: mit
- Created: 2026-03-21T13:40:05.000Z (3 months ago)
- Default Branch: main
- Last Pushed: 2026-04-11T08:10:40.000Z (2 months ago)
- Last Synced: 2026-04-11T09:27:53.730Z (2 months ago)
- Topics: dictionary, hashable, immutable, mapping, python, read-only, typing
- Language: Python
- Homepage: https://astropenguin.github.io/readonlydict/
- Size: 2.51 MB
- Stars: 1
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
- Code of conduct: .github/CODE_OF_CONDUCT.md
- Citation: CITATION.cff
Awesome Lists containing this project
README
# ReadonlyDict
[](https://pypi.org/project/readonlydict/)
[](https://pypi.org/project/readonlydict/)
[](https://pepy.tech/project/readonlydict)
[](https://doi.org/10.5281/zenodo.19187089)
[](https://github.com/astropenguin/readonlydict/actions)
Drop-in read-only dictionary with 100% typing and runtime compatibility
## Overview: Why ReadonlyDict?
This package is built strictly on the following formula:
``ReadonlyDict = (built-in dictionary features) - (in-place features) + (read-only features)``.
- **100% compatibility and zero custom API:** Our goal is to achieve flawless compatibility with Python's built-in dictionary in both static type checking (e.g., [mypy], [Pyright]) and runtime behavior. We simply removed in-place methods (e.g., ``pop()``, ``update()``). We do not introduce any custom methods.
- **True immutable semantics:** The only additions are those strictly required for a read-only data structure: it is fully hashable (only if all values are hashable), and shallow copies (i.e., ``self.copy()``, ``copy.copy(self)``) simply return itself to save memory.
- **When to use this package:** If you want extended read-only features, existing packages like [frozendict], [immutabledict], or [immutables] are better choices. However, if your priority is pure compatibility and perfect static type inference, ReadonlyDict should be the optimal choice.
## Installation
```bash
pip install readonlydict
```
## Basic Usage
It works exactly like a built-in dictionary, but raises an error if you try to modify it.
```python
from readonlydict import ReadonlyDict
# Initialization works just like the built-in dictionary:
>>> ro = ReadonlyDict(a=0, b=1)
>>> ro
ReadonlyDict({'a': 0, 'b': 1})
# It is fully hashable (can be used as a dictionary key or in a set):
>>> hash(ro)
-5925576189957013898
>>> {ro, ro}
{ReadonlyDict({'a': 0, 'b': 1})}
# Mutation is strictly prohibited (static type checkers will also warn you):
>>> ro["c"] = 2
TypeError: 'ReadonlyDict' object does not support item assignment
>>> ro.update(c=2)
AttributeError: 'ReadonlyDict' object has no attribute 'update'
```
## Advanced Usage: Subclassing with Type Hints
If you want to create your own custom read-only dictionary by subclassing ``ReadonlyDict``, you can maintain static type inference by utilizing ``TYPE_CHECKING`` and ``@overload``.
Here is the best-practice template for subclassing:
```python
# standard library
from collections.abc import Iterable, Mapping
from typing import TYPE_CHECKING, Any, TypeVar, overload
# dependencies
from readonlydict import ReadonlyDict, Tuples
# type variables
K = TypeVar("K")
V = TypeVar("V")
K2 = TypeVar("K2")
V2 = TypeVar("V2")
class CustomDict(ReadonlyDict[K, V]):
# Modify the return types to guarantee type inference:
if TYPE_CHECKING:
@overload
def __new__(cls, **kwargs: V) -> "CustomDict[str, V]": ...
@overload
def __new__(cls, mapping: Mapping[K, V], /, **kwargs: V2) -> "CustomDict[K | str, V | V2]": ...
@overload
def __new__(cls, iterable: Tuples[K, V], /, **kwargs: V2) -> "CustomDict[K | str, V | V2]": ...
def __new__(cls, *args: Any, **kwargs: Any) -> Any: ... # type: ignore[misc]
@overload
@classmethod
def fromkeys(cls, iterable: Iterable[K2], /) -> "CustomDict[K2, None]": ...
@overload
@classmethod
def fromkeys(cls, iterable: Iterable[K2], value: V2, /) -> "CustomDict[K2, V2]": ...
@classmethod
def fromkeys(cls, *args: Any, **kwargs: Any) -> Any: ...
def __or__(self, other: Mapping[K2, V2], /) -> "CustomDict[K | K2, V | V2]": ...
# Then add your custom properties or methods:
@property
def first(self) -> tuple[K, V]:
return next(iter(self.items()))
```
[frozendict]: https://github.com/Marco-Sulla/python-frozendict
[immutabledict]: https://immutabledict.corenting.fr
[immutables]: https://github.com/MagicStack/immutables
[mypy]: https://www.mypy-lang.org
[Pyright]: https://microsoft.github.io/pyright