# PynamoDB: mapping Python attribute declarations onto DynamoDB tables

> An ORM-like layer for DynamoDB where a model class declares its hash and range keys as typed attributes, with automatic pagination, secondary indexes and a DynamoDB Local path.

**pynamodb/PynamoDB** — A pythonic interface to Amazon's DynamoDB

- Repository: https://github.com/pynamodb/PynamoDB
- Website: http://pynamodb.readthedocs.io
- Stars: 2,647 · Forks: 433
- Language: Python
- License: MIT
- Published: 2026-10-07 · Updated: 2026-10-07 · Language: en
- Canonical page: https://hysenlabs.com/projects/pynamodb-pynamodb

## Three install routes and one stale link

The README gives PyPI, GitHub and conda-forge as equal options:

```text
$ pip install pynamodb
$ pip install git+https://github.com/pynamodb/PynamoDB#egg=pynamodb
$ conda install -c conda-forge pynamodb
```

`setup.py` is short and tells you the real dependency picture. The only required runtime dependency is `botocore>=1.12.54`, plus `typing-extensions` when running below Python 3.11. There is an optional `signals` extra that pulls in `blinker>=1.3,<2.0`, which tells you model lifecycle signals are a separate opt-in rather than part of the default install.

One inconsistency is worth knowing about if you follow the packaging metadata rather than the README. The README's useful links point at pynamodb.readthedocs.io, and the GitHub repository records the same homepage, while the `url` field in `setup.py` is `http://jlafon.io/pynamodb.html`, an older personal page. Both point at real places, but only the readthedocs URL matches what the project documents. The package name is `pynamodb` on both PyPI and conda-forge, and the classifiers run from Python 3.7 through 3.12 with a production/stable status.

## A model class is the table schema in Python

The core idea is that you declare attributes and the library derives the key schema from them. Here is the README's own example:

```python
from pynamodb.models import Model
from pynamodb.attributes import UnicodeAttribute

class UserModel(Model):
    """
    A DynamoDB User
    """
    class Meta:
        table_name = "dynamodb-user"
    email = UnicodeAttribute(null=True)
    first_name = UnicodeAttribute(range_key=True)
    last_name = UnicodeAttribute(hash_key=True)
```

`last_name` is the hash key and `first_name` is the range key, which is why the constructor below takes the two positionally in that order even though the hash key is declared second. `null=True` on the email attribute means the value is omitted rather than written as an empty string, which is how you avoid storing attributes you do not have.

Creating the table is a separate call, and the README is direct about the ordering requirement: the table must exist before you can use it. Provisioning is explicit rather than implicit:

```python
UserModel.create_table(read_capacity_units=1, write_capacity_units=1)
```

That matches how most teams actually work, where infrastructure is managed outside the application process.

## Query filters, get, and the exception that tells you it is missing

Reading takes two forms. A `query` takes the hash key value and an optional set of conditions, so the README's examples are a prefix search and an exact match on the same key:

```python
for user in UserModel.query("Denver", UserModel.first_name.startswith("J")):
    print(user.first_name)
```

The conditions are written as attribute expressions rather than as a filter string, which means a typo is a Python attribute error at import time instead of a runtime surprise. For a single item by key, `get` raises a `DoesNotExist` exception the model defines:

```python
try:
    user = UserModel.get("John", "Denver")
    print(user)
except UserModel.DoesNotExist:
    print("User does not exist")
```

Writes are ordinary attribute assignment followed by `save()`. The attribute types listed in the feature list are Unicode, Binary, JSON, Number, Set and UTC Datetime, so anything outside that set needs either a custom attribute type or a JSON attribute.

One item deserves attention because it is a real compatibility hazard: the README carries an Upgrade Warning that the behaviour of `UnicodeSetAttribute` changed in backward-incompatible ways as of the 1.6.0 and 3.0.1 releases, with the instructions for migrating safely pointed at the release notes on the documentation site.

## Secondary indexes declared as nested classes

Indexes are defined by subclassing `GlobalSecondaryIndex` and attaching the instance as a model attribute, with key attributes marked inside the index class:

```python
from pynamodb.models import Model
from pynamodb.indexes import GlobalSecondaryIndex, AllProjection
from pynamodb.attributes import NumberAttribute, UnicodeAttribute

class ViewIndex(GlobalSecondaryIndex):
    class Meta:
        read_capacity_units = 2
        write_capacity_units = 1
        projection = AllProjection()
    view = NumberAttribute(default=0, hash_key=True)
```

The projection type is declared rather than inferred, with `AllProjection()` copying the whole item into the index. Querying is then a method on the index instance, and the results come back as models:

```python
for item in TestModel.view_index.query(0):
    print("Item queried from index: {0}".format(item))
```

Local secondary indexes are listed in the feature set alongside global ones. The thing to notice is that index capacity is declared in Python, in the same class body as everything else, so the cost model of the index is visible at the point where the index is defined rather than hidden in a template.

## DynamoDB Local, streams, and the rest of the API surface

Two capabilities are configured entirely through `Meta` options, which is the pattern that makes this library easy to test. A `host` attribute points the model at a local server, and `stream_view_type` enables table streams with the constant imported from `pynamodb.constants`:

```python
class Meta:
    table_name = "dynamodb-user"
    host = "http://localhost:8000"
    stream_view_type = STREAM_NEW_AND_OLD_IMAGE
```

Compatibility with DynamoDB Local is listed as a feature in its own right, which matters more than it sounds: the local server is how you run integration tests without touching a real table, and `tests/` in the tree contains an integration package that `setup.py` explicitly excludes from the distributed packages.

The feature list also claims support for the entire DynamoDB API, iterators that paginate automatically, and automatic pagination for bulk and batch operations. That last pair is the feature that changes how you write code: a query that returns more than one megabyte needs pagination, and the library handles the continuation tokens rather than making you request pages by hand.

Recent releases show the maintenance pattern. Version 6.1.0 added the ability to set or unset the boto retry configuration and a wait argument to `Model.delete_table`. Version 6.0.2 fixed a `datetime.utcfromtimestamp()` deprecation, and 6.0.1 returned the underlying item in `cancellation_reasons` for failed transactions and fixed a regression in the `extra_headers` feature used by proxies that strip headers. The last push was on 2026-05-29 and the project is MIT licensed with 2,646 stars.

## Conclusion

PynamoDB earns its place when a DynamoDB table is the shape of your data and the raw client API is the part you keep rewriting, because the model class collapses the key schema, the attribute types and the request shape into one declaration. The features that matter most in practice are the automatic pagination on queries, scans and batch operations, since unbounded reads are the classic DynamoDB mistake, and the `host` attribute that lets the same models run against DynamoDB Local. The README claims support for the entire DynamoDB API, which is a broad promise you should check against the documentation for the call you actually need. Start with the basic usage model in the README, then read the release notes, since that is where backward-incompatible changes such as the `UnicodeSetAttribute` behaviour change have landed.

## FAQ

### What is PynamoDB in Python?

It is a Pythonic interface to Amazon DynamoDB, described in its own README as a simple, elegant alternative to the verbose DynamoDB API. You declare a model class whose attributes are typed, mark which ones are the hash and range keys, and query, save and retrieve through that class instead of hand-building request dictionaries. The runtime dependency is botocore.

### How do I use PynamoDB with DynamoDB Local for testing?

Add a `host` attribute to the model's `Meta` class pointing at your local server, for example `http://localhost:8000`. The README shows exactly that in a model whose other options are unchanged, and compatibility with DynamoDB Local is listed as a supported feature rather than an afterthought. The same approach works with `stream_view_type` when the test needs table streams.

### Should I use PynamoDB or boto3 directly for DynamoDB?

boto3 is the general AWS SDK and gives you the raw DynamoDB client. PynamoDB sits on top of botocore, which is its only required dependency, and adds a model layer, typed attributes, attribute-expression filters, secondary index classes and automatic pagination. If your access pattern is fixed and your data is shaped like tables and indexes, the model layer removes a lot of repeated request construction; if you need operations outside that layer, the raw client remains available underneath.

## Sources

- [License: MIT](https://github.com/pynamodb/PynamoDB/blob/master/LICENSE)
- [Project website](http://pynamodb.readthedocs.io)
- [pynamodb/PynamoDB on GitHub](https://github.com/pynamodb/PynamoDB)
- [README](https://github.com/pynamodb/PynamoDB/blob/master/README.md)
- [Releases](https://github.com/pynamodb/PynamoDB/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/pynamodb-pynamodb
