Prodict
Prodict, what Python dict meant to be.
Install / Use
/learn @ramazanpolat/ProdictREADME
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
dictkeys liked['key']is error prone and ugly. -
Because it becomes uglier if it is nested, like
d['key1]['key2']['key3']. Compared['key1]['key2']['key3']tod.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
dictand deserializing fromdictin one line is awesome!
Features
- A class with dynamic properties, without defining it beforehand.
j = Prodict()
j.hi = 'there'
- Pass named arguments and all arguments will become properties.
p = Prodict(lang='Python', pros='Rocks!')
print(p.lang) # Python
print(p.pros) # Rocks!
print(p) # {'lang': 'Python', 'pros': 'Rocks!'}
- Instantiate from a
dict, getdictkeys as properties
p = Prodict.from_dict({'lang': 'Python', 'pros': 'Rocks!'})
print(p.lang) # Python
p.another_property = 'this is dynamically added'
- Pass a
dictas argument, get a nestedProdict!
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)) # <class 'prodict.Prodict'>
- Extend
Prodictand use type annotations for auto type conversion and auto code completion
class User(Prodict):
user_id: int
name: str
user = User(user_id="1", name="Ramazan")
type(user.user_id) # <class 'int'>
# 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.
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) # <class 'int'>
Notes on automatic type conversion:
- In the above example code,
user.user_idwill be anint, even if rest service responded withstr. - Same goes for all built-in types(int, str, float, bool, list, tuple), except
dict. Because by default, alldicttypes will be converted toProdict. - If you don't want any type conversion but still want to have auto code completion, use
Anyas type annotation, like theliteralattribute defined inUserclass. - If the annotated type of an attribute is sub-class of a
Prodict, the provideddictwill be instantiated as the instance of sub-class. Even if it isListof 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:
{
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:
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:
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.
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.
from prodict import Prodict
class Country(Prodict):
name: str
population: int
turkey = Country()
turkey.name = 'Turkey'
turkey.population = 79814871

Example 2: Auto type conversion
germany = Country(name='Germany', population='82175700', flag_colors=['black', 'red', 'yellow'])
print(germany.population) # 82175700
print(type(germany.population)) # <class 'int'> <-- 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)) # <class 'int'>
Example 3: Nested class instantiation
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)) # <class 'list'>
print(type(comp1.rams[0])) # <class 'Ram'> <-- 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.
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:
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_clas
Related Skills
node-connect
335.4kDiagnose OpenClaw node connection and pairing failures for Android, iOS, and macOS companion apps
claude-opus-4-5-migration
82.5kMigrate prompts and code from Claude Sonnet 4.0, Sonnet 4.5, or Opus 4.1 to Opus 4.5
frontend-design
82.5kCreate distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
model-usage
335.4kUse CodexBar CLI local cost usage to summarize per-model usage for Codex or Claude, including the current (most recent) model or a full model breakdown. Trigger when asked for model-level usage/cost data from codexbar, or when you need a scriptable per-model summary from codexbar cost JSON.
