Skip to main content

Prodict

PyPI Python versions CI License: MIT

Prodict is a dict with dot-accessible attributes, IDE auto-completion, and type conversion driven by annotations.

from typing import List
from prodict import Prodict

class Comment(Prodict):
    user_id: int
    text: str

class Post(Prodict):
    title: str
    comments: List[Comment]

post = Post.from_dict({'title': 'Hello', 'comments': [{'user_id': '2', 'text': 'Welcome!'}]})
post.comments[0].user_id   # 2, an int, and your IDE completes every name on the way

Why

  • d['key1']['key2']['key3'] is noisy and easy to get wrong; d.key1.key2.key3 is not.
  • Web services talk JSON. Turning a JSON response into objects you can navigate, with the types you expect, should take one line.
  • Declared attributes give you auto-completion in the IDE.
  • It is still a dict: json.dumps, ==, **kwargs and every dict method keep working.

Installation

pip install prodict
# or
uv add prodict

No dependencies. Requires Python 3.8 or later; tested on 3.8 through 3.15.

Features

Dynamic attributes. Keys are attributes and attributes are keys.

p = Prodict(lang='Python', pros='Rocks!')
p.lang          # 'Python'
p.year = 1991
p               # {'lang': 'Python', 'pros': 'Rocks!', 'year': 1991}
isinstance(p, dict)  # True

Nested dicts become Prodicts.

p = Prodict.from_dict({'package': 'Prodict', 'rock': {'even': 'more!'}})
p.rock.even     # 'more!'
type(p.rock)    # <class 'prodict.Prodict'>

Type conversion from annotations. Subclass Prodict, annotate attributes, and values are converted when they are set. Use Any to keep completion without conversion.

from typing import Any

class Country(Prodict):
    name: str
    population: int
    raw: Any

germany = Country(name='Germany', population='83200000', raw='83200000')
germany.population  # 83200000 (int)
germany.raw         # '83200000' (unchanged)
Country(name='Turkey')  # {'name': 'Turkey', 'population': None, 'raw': None}

Auto-completion in PyCharm

Nested models. Attributes annotated with a Prodict subclass, or List[...] of one, turn plain dicts into instances of that class.

class Ram(Prodict):
    capacity: int
    unit: str

class Computer(Prodict):
    name: str
    rams: List[Ram]

    def total_ram(self):
        return sum(ram.capacity for ram in self.rams)

computer = Computer.from_dict({'name': 'Workstation', 'rams': [{'capacity': '16', 'unit': 'GB'}]})
isinstance(computer.rams[0], Ram)  # True
computer.total_ram()    # 16

Defaults with init(). Override init(), not __init__; no super() call needed, and keyword arguments win over defaults.

class Settings(Prodict):
    retries: int
    timeout: float

    def init(self):
        self.retries = 3
        self.timeout = 10.0

Settings(retries='5')   # {'retries': 5, 'timeout': 10.0}

Serialization. to_dict() returns a plain dict and can drop None values and convert nested models and list items. copy.deepcopy and pickle keep model classes.

Country(name='Turkey').to_dict(exclude_none=True)  # {'name': 'Turkey'}

A complete example

Suppose https://example.com/api/users/1 returns:

{
  "user_id": 1,
  "user_name": "rambo",
  "posts": [
    {
      "title": "Hello World",
      "date": "2018-01-02 03:04:05",
      "comments": [
        {"user_id": 2, "comment": "Good to see you blogging", "date": "2018-01-02 03:04:06"}
      ]
    }
  ]
}

Without Prodict, you write the classes and the mapping by hand:

class Comment:
    def __init__(self, user_id, comment, date):
        self.user_id = user_id
        self.comment = comment
        self.date = date

class Post:
    def __init__(self, title, date, comments):
        self.title = title
        self.date = date
        self.comments = comments

class User:
    def __init__(self, user_id, user_name, posts):
        self.user_id = user_id
        self.user_name = user_name
        self.posts = posts

data = requests.get('https://example.com/api/users/1').json()
user = User(
    data['user_id'],
    data['user_name'],
    [
        Post(p['title'], p['date'], [Comment(c['user_id'], c['comment'], c['date']) for c in p['comments']])
        for p in data['posts']
    ],
)

With Prodict, you declare the shape and let it do the rest:

class Comment(Prodict):
    user_id: int
    comment: str
    date: str

class Post(Prodict):
    title: str
    date: str
    comments: List[Comment]

class User(Prodict):
    user_id: int
    user_name: str
    posts: List[Post]

user: User = User.from_dict(requests.get('https://example.com/api/users/1').json())
user.posts[0].comments[0].comment  # 'Good to see you blogging'

New keys can still be added to any of these objects at any time. A runnable version, including writing the data back out, is examples/05-json-api-client.

Limitations

  • Names of dict methods (items, keys, get, ...) cannot be attributes. Names of Prodict methods (to_dict, from_dict, init, ...) can be stored but are only readable with brackets, so avoid them.
  • Keys that are not valid Python identifiers ('1', 'first-name') work with brackets and set_attribute(), not with dots.
  • Values are converted for int, float, str, list, List[T], Tuple[...], Prodict and its subclasses, when set as keyword arguments, through from_dict() or by attribute assignment. bool, plain tuple and other types are stored as they come (a Prodict value for them raises AttributeError), and so is anything set with p['key'] = ... or update().
  • Pass data as keyword arguments or through from_dict(). A dict passed positionally (Model({...})) loses the values of annotated attributes.
  • Optional[...], Union[...], Dict[K, V], a bare dict, and built-in generics such as list[int] are not supported as annotations; use Any, Prodict or the typing forms (List[int]).
  • Prodict keeps data in the dict, not in instance attributes, so avoid giving class attributes values meant for instances.

The typed models guide covers each of these in detail.

Documentation

Every Python example in docs/ and examples/ is run by CI on each supported Python version.

Contributing

Bug reports and pull requests are welcome. See CONTRIBUTING.md for setup and tests, and CHANGELOG.md for what changed between releases.

License

MIT. See LICENSE.txt.

Thanks

To JetBrains for PyCharm, the IDE that made my life better.

JetBrains PyCharm

Metadata

Release files for prodict 0.8.23

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for prodict 0.8.23
File Size Uploaded Provenance Actions
prodict-0.8.23.tar.gz 84.7 kB Without provenance Details for prodict-0.8.23.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for prodict 0.8.23
File Interpreter ABI Platform Actions
prodict-0.8.23-py3-none-any.whl Python 3 none any Details for prodict-0.8.23-py3-none-any.whl

Total release size: 92.5 kB

Release files / prodict-0.8.23.tar.gz

Download URL prodict-0.8.23.tar.gz
Size 84.7 kB
Tags Source
SHA-256 checksum
How to use checksums
9f48b1ce0d3936894c2482f42fe3b6460c6a63599ed929afaf5490cabc75a7cd
BLAKE2b-256 checksum
How to use checksums
360c6e0214b67d75b4ae940a6115d99b71bed90cc5a8d06e46d29ecbe16e0651
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.15

Release files / prodict-0.8.23-py3-none-any.whl

Download URL prodict-0.8.23-py3-none-any.whl
Size 7.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
7a49a09e2ad97d73ad574765bfcf1ae92fad661290ca6770502b0b08438bbcfd
BLAKE2b-256 checksum
How to use checksums
d2953c3077dc05fe48102a97976ceb93f986632b085baad953d0d798281ab05b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.15

Security for prodict 0.8.23

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page