Metadata-Version: 2.4
Name: python-frozendict
Version: 0.1.0
Summary: Standalone back-port of Python 3.15's `frozendict` builtin.
Project-URL: Repository, https://github.com/pythonbackport/python-frozendict
License-Expression: MIT
License-File: LICENSE
Requires-Python: >=3.8
Description-Content-Type: text/markdown

# python-frozendict

A back-port of Python 3.15's `frozendict` builtin for older Python versions.

On Python 3.15+, you can write:

```python
SETTINGS = frozendict({"debug": True, "level": 3})
```

This package provides the same behaviour on every supported Python
version (3.8+) via a small, focused, dependency-free implementation.

## Why?

A `frozendict` is an **immutable, hashable** mapping.  It is useful
wherever you would reach for `dict` but want a value that:

- cannot be mutated by accident (or on purpose);
- is hashable and therefore usable as a `dict` key or a `set` member;
- serves as a clear, self-documenting signal that the data is fixed.

```python
from frozendict import frozendict

USER_ROLES = frozendict({
    "alice": "admin",
    "bob": "editor",
})

# `USER_ROLES` is a valid dict key, set member, function default, etc.
cache = {USER_ROLES: "loaded"}

# Attempting to mutate it raises `TypeError`:
USER_ROLES["carol"] = "viewer"
# -> TypeError: 'frozendict' object does not support item assignment
```

## Installation

```bash
pip install python-frozendict
```

## Usage

### Basic construction

```python
from frozendict import frozendict

empty = frozendict()
from_dict = frozendict({"a": 1, "b": 2})
from_pairs = frozendict([("a", 1), ("b", 2)])
from_mapping = frozendict(some_Mapping_instance)
copy_of = frozendict(from_dict)  # new object, same content
```

The constructor accepts at most one positional argument (a `dict`, a
`Mapping`, another `frozendict`, or any iterable of pairs).  Keyword
arguments are rejected, mirroring `dict` itself.

### Reading

`frozendict` supports the full read-only mapping interface:

```python
fd = frozendict({"a": 1, "b": 2, "c": 3})

fd["a"]              # 1
len(fd)              # 3
"a" in fd            # True
list(fd)             # ["a", "b", "c"]
list(fd.keys())      # ["a", "b", "c"]
list(fd.values())    # [1, 2, 3]
list(fd.items())     # [("a", 1), ("b", 2), ("c", 3)]
fd.get("z", 0)       # 0
list(reversed(fd))   # ["c", "b", "a"]
```

It is registered as a `collections.abc.Mapping` so it works with any
function that accepts a generic mapping.

### Hashing and equality

`frozendict` is hashable, *if* all of its values are hashable.  Two
`frozendict` objects with the same content compare equal and have the
same hash, independent of insertion order:

```python
a = frozendict({"a": 1, "b": 2})
b = frozendict({"b": 2, "a": 1})

a == b     # True
hash(a) == hash(b)   # True
```

A `frozendict` compares equal to a `dict` with the same content, and
is usable as a `dict` key or a `set` element:

```python
fd = frozendict({"a": 1})
{fd: "value"}            # {frozendict({'a': 1}): "value"}
{fd, frozendict({"a": 1})}   # a single-element set: {frozendict({'a': 1})}
```

If any value is unhashable, calling `hash(fd)` raises `TypeError` —
this mirrors the behaviour of `tuple`.

### Merging with `|`

The `|` operator returns a new `frozendict`; the operands are not
mutated:

```python
a = frozendict({"a": 1, "b": 2})
b = frozendict({"b": 99, "c": 3})

a | b           # frozendict({'a': 1, 'b': 99, 'c': 3})
{"x": 0} | a    # frozendict({'x': 0, 'a': 1, 'b': 2})
```

On key conflicts, the right-hand side wins — same as the built-in
`dict` merge.

### Immutability

Any attempt to mutate a `frozendict` raises `TypeError`:

```python
fd = frozendict({"a": 1})
fd["b"] = 2        # TypeError
del fd["a"]        # TypeError
fd.clear()         # TypeError
fd.update({"b": 2})  # TypeError
fd.setdefault("b", 2)  # TypeError
fd.pop("a")        # TypeError
fd.popitem()       # TypeError
```

### Copy and pickle

`copy.copy(fd)`, `copy.deepcopy(fd)`, and `fd.copy()` all return the
same object (there is no need to copy an immutable value).

Pickling and unpickling preserves content but not identity:

```python
import pickle
fd = frozendict({"a": 1, "b": 2})
restored = pickle.loads(pickle.dumps(fd))
restored == fd          # True
restored is fd          # False
```

### Subclassing

Subclassing is **not supported**.  Both `class Sub(frozendict): pass`
and `type("Sub", (frozendict,), {})` raise `TypeError`.

## API

### `frozendict(*[mapping_or_pairs]*)`

Construct a new `frozendict`.  At most one positional argument is
accepted:

| Argument                    | Behaviour                                 |
| --------------------------- | ----------------------------------------- |
| _omitted_                   | empty `frozendict`                        |
| `dict`                      | shallow copy of the dict                  |
| `frozendict`                | shallow copy                              |
| `collections.abc.Mapping`   | shallow copy via `dict(mapping)`          |
| iterable of (key, value)    | equivalent to `dict(iterable)`            |

Keyword arguments are rejected.  Passing more than one positional
argument raises `TypeError`.

### Methods and operators

| Member                              | Description                                          |
| ----------------------------------- | ---------------------------------------------------- |
| `fd[key]`                           | raise `KeyError` if missing                          |
| `key in fd`                         | membership test                                      |
| `len(fd)`                           | number of items                                      |
| `iter(fd)`                          | iterate over keys (insertion order)                  |
| `reversed(fd)`                      | iterate over keys in reverse                         |
| `fd.keys()` / `fd.values()` / `fd.items()` | mapping views (read-only)                    |
| `fd.get(key, default=None)`         | safe lookup                                          |
| `fd.copy()`                         | returns `fd` (immutable)                             |
| `fd == other`                       | content-based equality                               |
| `hash(fd)`                          | content-based hash (cached)                          |
| `fd \| other` / `other \| fd`       | merge, returns a new `frozendict`                    |
| `repr(fd)` / `str(fd)`              | `"frozendict({...})"`                                |
| `bool(fd)`                          | `False` for empty, `True` otherwise                  |
| `pickle.dumps/loads(fd)`            | content preserved                                    |
| `copy.copy(fd)` / `copy.deepcopy(fd)` | both return `fd`                                   |
| `dict(fd)`                          | convert to `dict`                                    |

### Forbidden operations

These all raise `TypeError`:

| Attempted            | Error message                                                  |
| -------------------- | -------------------------------------------------------------- |
| `fd[key] = value`    | `'frozendict' object does not support item assignment`         |
| `del fd[key]`        | `'frozendict' object does not support item deletion`           |
| `fd.clear()`         | `'frozendict' object does not support item assignment`         |
| `fd.pop(...)`        | `'frozendict' object does not support item deletion`           |
| `fd.popitem()`       | `'frozendict' object does not support item deletion`           |
| `fd.setdefault(...)` | `'frozendict' object does not support item assignment`         |
| `fd.update(...)`     | `'frozendict' object does not support item assignment`         |
| `class Sub(fd):`     | `subclassing frozendict is not supported`                      |
| `fd.foo = "bar"`     | `'frozendict' object has no attribute 'foo'`                   |

## Python 3.15+ native syntax

When you are running on Python 3.15+, `frozendict` is a true builtin.
The package's own API still works identically, so you can use either:

```python
# Native (Python 3.15+ only):
SETTINGS = frozendict({"debug": True})

# Cross-version equivalent via this package:
from frozendict import frozendict
SETTINGS = frozendict({"debug": True})
```

## Compatibility

- Python 3.8 through 3.15+ (uses `collections.abc.Mapping`; on 3.9+
  uses modern union / merge syntax in the docs only).
- No third-party dependencies — uses only the standard library.

## Running the tests

```bash
python -m unittest test_frozendict.py -v
```

## Retirement

This project will reach its end-of-life around **October 1, 2031** —
the official EOL date of Python 3.15 — and the exact timeline could be
slightly delayed.  We plan to ship the final stable release in
November 2031.  After this release, all support will cease and the
repository will be officially archived, as this library is developed
solely to bring `frozendict` compatibility to Python 3.15 and older
versions.

## License

MIT
