https://github.com/ramazanpolat/prodict
Prodict, what Python dict meant to be.
https://github.com/ramazanpolat/prodict
conversion dictionary dynamic-props hints ide json python python3 typehinting
Last synced: 4 months ago
JSON representation
Prodict, what Python dict meant to be.
- Host: GitHub
- URL: https://github.com/ramazanpolat/prodict
- Owner: ramazanpolat
- License: mit
- Created: 2018-01-25T17:56:04.000Z (almost 8 years ago)
- Default Branch: master
- Last Pushed: 2024-11-18T14:28:14.000Z (about 1 year ago)
- Last Synced: 2025-08-16T02:33:24.342Z (5 months ago)
- Topics: conversion, dictionary, dynamic-props, hints, ide, json, python, python3, typehinting
- Language: Python
- Homepage:
- Size: 150 KB
- Stars: 152
- Watchers: 4
- Forks: 15
- Open Issues: 21
-
Metadata Files:
- Readme: README.MD
- License: LICENSE.txt
Awesome Lists containing this project
README
# Prodict
Prodict = Dictionary with IDE friendly(auto code completion) and dot-accessible attributes **and more**.
# What it does
Ever wanted to use a `dict` like a class and access keys as attributes? **Prodict** does exactly this.
Although there are number of modules doing this, **Prodict** does a little bit **more**.
You can provide type hints and get auto code completion!
With type hints, you also get nested object instantiations, which will blow your mind.
You will never want to use `dict` again.
# Why?
* Because accessing `dict` keys like `d['key']` is error prone and ugly.
* Because it becomes uglier if it is nested, like `d['key1]['key2']['key3']`. Compare `d['key1]['key2']['key3']` to `d.key1.key2.key3`, which one looks better?
* Because since web technologies mostly talk with JSON, it should be much more easy to use JSON data(see sample use case below).
* Because auto code completion makes developers' life easier.
* Because serializing a Python class to `dict` and deserializing from `dict` in one line is awesome!
# Features
1) A class with dynamic properties, without defining it beforehand.
```python
j = Prodict()
j.hi = 'there'
```
2) Pass named arguments and all arguments will become properties.
```python
p = Prodict(lang='Python', pros='Rocks!')
print(p.lang) # Python
print(p.pros) # Rocks!
print(p) # {'lang': 'Python', 'pros': 'Rocks!'}
```
3) Instantiate from a `dict`, get `dict` keys as properties
```python
p = Prodict.from_dict({'lang': 'Python', 'pros': 'Rocks!'})
print(p.lang) # Python
p.another_property = 'this is dynamically added'
```
4) Pass a `dict` as argument, get a nested `Prodict`!
```python
p = Prodict(package='Prodict', makes='Python', rock={'even': 'more!'})
print(p) # {'package': 'Prodict', 'makes': 'Python', 'rock': {'even': 'more!'}}
print(p.rock.even) # 'more!'
print(type(p.rock)) #
```
5) Extend `Prodict` and use type annotations for auto type conversion and auto code completion
```python
class User(Prodict):
user_id: int
name: str
user = User(user_id="1", name="Ramazan")
type(user.user_id) #
# IDE will be able to auto complete 'user_id' and 'name' properties(see example 1 below)
```
Why type conversion? Because it will be useful if the incoming data doesn't have the desired type.
```python
class User(Prodict):
user_id: int
name: str
literal: Any
response = requests.get("https://some.restservice.com/user/1").json()
user: User = User.from_dict(response)
type(user.user_id) #
```
**Notes on automatic type conversion**:
* In the above example code, `user.user_id` will be an `int`, even if rest service responded with `str`.
* Same goes for all built-in types(int, str, float, bool, list, tuple), except `dict`. Because by default, all `dict` types will be converted to `Prodict`.
* If you don't want any type conversion but still want to have auto code completion, use `Any` as type annotation, like the `literal` attribute defined in `User` class.
* If the annotated type of an attribute is sub-class of a `Prodict`, the provided `dict` will be instantiated as the instance of sub-class. Even if it is `List` of the sub-class(see sample usa case below).
# Sample use case
Suppose that you are getting this JSON response from `https://some.restservice.com/user/1`:
```javascript
{
user_id: 1,
user_name: "rambo",
posts: [
{
title:"Hello World",
text:"This is my first blog post...",
date:"2018-01-02 03:04:05",
comments: [
{
user_id:2,
comment:"Good to see you blogging",
date:"2018-01-02 03:04:06"
},
{
user_id:3,
comment:"Good for you",
date:"2018-01-02 03:04:07"
}
]
},
{
title:"Leave the old behind",
text:"Stop using Python 2.x...",
date:"2018-02-03 04:05:06",
comments: [
{
user_id:4,
comment:"Python 2 is dead, long live Python",
date:"2018-02-03 04:05:07"
},
{
user_id:5,
comment:"You are god damn right :wears Heissenberg glasses:",
date:"2018-02-03 04:05:08"
}
]
}
]
}
```
Despite the fact that JSON being schemaless, most REST services will respond with a certain structure.
In the above example, the structure is something like this:
```
User
|--> user_id
|--> user_name
|--> posts [post]
|--> title
|--> text
|--> date
|--> comments [comment]
|--> user_id
|--> comment
|--> date
```
And you want to convert this to appropriate Python classes.
Without `Prodict`:
```python
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, text, date):
self.title = title
self.text = text
self.date = date
self.comments = []
class User:
def __init__(self, user_id, user_name):
self.user_id = user_id
self.user_name = user_name
self.posts = []
user_json = requests.get("https://some.restservice.com/user/1").json()
posts = [Post(post['title'], post['text'], post['date']) for post in user_json['posts']]
for post in posts:
post.comments = [[comment for comment in post['comments']] for post in user_json['posts']]
user = User(user_json['user_id'], user_json['user_name'])
user.posts = posts
for post in user.posts:
print(post.title)
```
With **Prodict** you just need to define the classes and let the prodict do the rest like this:
```python
class Comment(Prodict):
user_id: int
comment: str
date: str
class Post(Prodict):
title: str
text: str
date: str
comments: List[Comment]
class User(Prodict):
user_id: int
user_name: str
posts: List[Post]
user_json = requests.get("https://some.restservice.com/user/1").json()
user:User = User.from_dict(user_json)
# Don't forget to annotate the `user` with `User` type in order to get auto code completion.
```
See the difference?
Plus you can add new attributes to `User`, `Post` and `Comment` objects dynamically and access them as dot-accessible attributes.
# Examples
**Example 0**: Use it like regular `dict`, because **it is** a dict.
```python
from prodict import Prodict
d = dict(lang='Python', pros='Rocks!')
p = Prodict(lang='Python', pros='Rocks!')
print(d) # {'lang': 'Python', 'pros': 'Rocks!'}
print(p) # {'lang': 'Python', 'pros': 'Rocks!'}
print(d == p) # True
p2 = Prodict.from_dict({'Hello': 'world'})
print(p2) # {'Hello': 'world'}
print(issubclass(Prodict, dict)) # True
print(isinstance(p, dict)) # True
print(set(dir(dict)).issubset(dir(Prodict))) # True
```
**Example 1**: Accessing keys as attributes and auto completion.
```python
from prodict import Prodict
class Country(Prodict):
name: str
population: int
turkey = Country()
turkey.name = 'Turkey'
turkey.population = 79814871
```

**Example 2**: Auto type conversion
```python
germany = Country(name='Germany', population='82175700', flag_colors=['black', 'red', 'yellow'])
print(germany.population) # 82175700
print(type(germany.population)) # <-- The type is `int` !
# If you don't want type conversion and still want to have auto code completion, use `Any` as type.
print(germany.flag_colors) # ['black', 'red', 'yellow']
print(type(germany.population)) #
```
**Example 3**: Nested class instantiation
```python
class Ram(Prodict):
capacity: int
unit: str
type: str
clock: int
class Computer(Prodict):
name: str
cpu_cores: int
rams: List[Ram]
def total_ram(self):
return sum([ram.capacity for ram in self.rams])
comp1 = Computer.from_dict(
{
'name': 'My Computer',
'cpu_cores': 4,
'rams': [
{'capacity': 4,
'unit': 'GB',
'type': 'DDR3',
'clock': 2400}
]
})
print(comp1.rams) # [{'capacity': 4, 'unit': 'GB', 'type': 'DDR3', 'clock': 2400}]
comp1.rams.append(Ram(capacity=8, type='DDR3'))
comp1.rams.append(Ram.from_dict({'capacity': 12, 'type': 'DDR3', 'clock': 2400}))
print(comp1.rams)
# [
# {'capacity': 4, 'unit': 'GB', 'type': 'DDR3', 'clock': 2400},
# {'capacity': 8, 'type': 'DDR3'},
# {'capacity': 12, 'type': 'DDR3', 'clock': 2400}
# ]
print(type(comp1.rams)) #
print(type(comp1.rams[0])) # <-- Mind the type !
```
**Example 4**: Provide default values
You can use `init` method to provide default values. Keep in mind that `init` is NOT `__init__` but `init` method will be called in `__init__` method.
Additionally, you can use `init` method instead of `__init__` without referring to `super`.
```python
class MyDataWithDefaults(Prodict):
an_int: int
a_str: str
def init(self):
self.an_int = 42
self.a_str = 'string'
data = MyDataWithDefaults(dynamic=43)
print(data)
# {'an_int':42, 'a_str':'string', 'dynamic':43}
```
# Class attributes vs Instance attributes
Prodict only works for instance attributes.
Even if you try to set an inherited class attribute, a new instance attribute is created and set.
Consider this example:
```python
from prodict import Prodict
class MyClass(Prodict):
class_attr: int = 42 # class_attr is a class attribute, not instance attribute
my_class = MyClass()
print(f"my_class.class_attr: {my_class.class_attr}") # 42
# There is no 'class_attr' defined as instance attribute, so class attribute will be returned (42).
print(f"MyClass.class_attr: {MyClass.class_attr}") # 42
# This is a class attribute, it will be returned as is.
# Now an instance attribute 'class_attr' is created and set to 77
my_class.class_attr = 77
print(f"my_class.class_attr: {my_class.class_attr}") # 42
# For this matter, avoid setting class_attribute with dot notation, use class name instead
MyClass.class_attr = 88
print(f"MyClass.class_attr: {my_class.class_attr}") # 88
# So where did 77 go? It is in instance attribute of the class and since it's name is colliding with
# the class attribute, you can't get it by dot notation. You can use .get tho.
print(f"my_class.get('class_attr'): {my_class.get('class_attr')}") # 77
```
# Installation
If your default Python is 3.7:
`pip install prodict`
If you have more than one Python versions installed:
`python3.7 -m pip install prodict`
# Limitations
- You cannot use `dict` method names as attribute names because of ambiguity.
- You cannot use `Prodict` method names as attribute names(I will change `Prodict` method names with dunder names to reduce the limitation).
- You must use valid variable names as `Prodict` attribute names(obviously). For example, while '1' cannot be an attribute for `Prodict`, it is perfectly valid for a `dict` to have '1' as a key. You can still use prodict.set_attribute('1',123) tho.
- Requires Python 3.7+
# Thanks
I would like to thank to [JetBrains](https://www.jetbrains.com/) for creating [PyCharm](https://www.jetbrains.com/pycharm/), the IDE that made my life better.