pony Best Practices
Pony ORM simplifies database interactions with Pythonic syntax and powerful optimizations. Adhere to these rules for robust, high-performance applications.
1. Always Use db_session for Transaction Management
The db_session context manager is non-negotiable. It ensures proper transaction handling, automatic commits, and rollbacks, preventing resource leaks and data inconsistencies.
❌ BAD: Direct database operations without a session.
from pony.orm import *
from decimal import Decimal
db = Database('sqlite', ':memory:')
class Product(db.Entity):
name = Required(str)
price = Required(Decimal)
db.generate_mapping(create_tables=True)
# This will fail or lead to unexpected behavior in production
p = Product(name='Laptop', price=Decimal('1200.00'))
db.commit() # Manual commit is error-prone and easily forgotten
✅ GOOD: Encapsulate all database work within db_session.
from pony.orm import *
from decimal import Decimal
db = Database('sqlite', ':memory:')
class Product(db.Entity):
name = Required(str)
price = Required(Decimal)
db.generate_mapping(create_tables=True)
@db_session
def create_product(name: str, price: Decimal) -> Product:
p = Product(name=name, price=price)
return p # Auto-committed on exit if no exception
with db_session:
laptop = create_product('Laptop', Decimal('1200.00'))
print(f"Created product: {laptop.name}")
2. Leverage Pythonic Query Syntax for Optimization
Pony translates generator expressions and lambdas into optimized SQL, automatically handling N+1 problems and complex joins. Avoid manual SQL unless absolutely necessary.
❌ BAD: Manual filtering or inefficient data access.
@db_session
def get_expensive_products_bad():
all_products = list(select(p for p in Product)) # Fetches all products
expensive_products = [p for p in all_products if p.price > Decimal('1000')] # Filters in Python
return expensive_products
✅ GOOD: Let Pony generate optimized SQL.
@db_session
def get_expensive_products_good() -> list[Product]:
# Pony translates this directly to SQL: SELECT ... WHERE price > 1000
return select(p for p in Product if p.price > Decimal('1000'))[:] # Use [:] to fetch all results
3. Separate Data Access from Business Logic
Maintain a clean architecture by defining entities in a dedicated models.py and encapsulating data operations in a repositories.py or services.py module.
❌ BAD: Mixing entity definitions, database setup, and business logic in one file.
# app.py
from pony.orm import *
db = Database('sqlite', 'app.sqlite')
class User(db.Entity):
name = Required(str)
email = Required(str, unique=True)
db.generate_mapping(create_tables=True)
@db_session
def create_user_and_send_email(name, email):
user = User(name=name, email=email)
# ... send email logic here ...
✅ GOOD: Clear separation of concerns.
# models.py
from pony.orm import *
db = Database() # Bind later
class User(db.Entity):
id = PrimaryKey(int, auto=True)
name = Required(str)
email = Required(str, unique=True)
# database.py
from pony.orm import Database
from .models import db # Import db instance
def setup_database(provider: str, filename: str):
db.bind(provider=provider, filename=filename, create_db=True)
db.generate_mapping(create_tables=True)
# services.py
from pony.orm import db_session
from .models import User
from typing import List
@db_session
def create_user(name: str, email: str) -> User:
user = User(name=name, email=email)
return user
@db_session
def get_all_users() -> List[User]:
return list(select(u for u in User))
# main.py (or app.py)
from .database import setup_database
from .services import create_user, get_all_users
if __name__ == '__main__':
setup_database('sqlite', 'my_app.sqlite')
create_user('Alice', 'alice@example.com')
create_user('Bob', 'bob@example.com')
for user in get_all_users():
print(f"User: {user.name}, Email: {user.email}")
4. Apply Static Typing to Entities and Queries
Use type hints (typing module) for entity attributes and function signatures. This improves readability, enables IDE auto-completion, and catches errors early.
❌ BAD: Untyped entity attributes and function parameters.
class Order(db.Entity):
amount = Required(float) # float is imprecise for currency
customer = Required('Customer')
@db_session
def get_total_orders(customer_id): # No type hints
customer = Customer[customer_id]
return sum(o.amount for o in customer.orders)
✅ GOOD: Explicit type hints for clarity and correctness.
from decimal import Decimal
from typing import List
class Customer(db.Entity):
id = PrimaryKey(int, auto=True)
name = Required(str)
orders: List['Order'] # Forward reference for type hinting
class Order(db.Entity):
id = PrimaryKey(int, auto=True)
amount = Required(Decimal) # Use Decimal for currency
customer = Required(Customer)
@db_session
def get_customer_total_orders(customer_id: int) -> Decimal:
customer: Customer = Customer[customer_id]
# Pony handles aggregation efficiently
total_amount: Decimal = sum(o.amount for o in customer.orders)
return total_amount
5. Utilize Pony's Automatic Caching and IdentityMap
Pony automatically caches entities and query results within a db_session. Avoid manual caching layers for simple entity lookups to prevent stale data.
❌ BAD: Implementing a custom cache for entities within a session.
my_entity_cache = {} # Don't do this; it's redundant and error-prone with Pony
@db_session
def get_user_cached(user_id: int) -> User:
if user_id not in my_entity_cache:
user = User[user_id]
my_entity_cache[user_id] = user
return my_entity_cache[user_id]
✅ GOOD: Trust Pony's IdentityMap and session-level caching.
@db_session
def get_user_pony_cached(user_id: int) -> User:
# Pony's IdentityMap ensures that User[user_id] returns the same object
# within the session if it has already been fetched.
user1 = User[user_id]
user2 = User[user_id]
assert user1 is user2 # This assertion is True within the same db_session
return user1
6. Manage Schema with the Online Editor or Versioned Scripts
Keep your Pony model definitions in sync with the database schema. For complex projects, use version-controlled migration scripts. For rapid prototyping, Pony's online editor is excellent.
❌ BAD: Manually altering database tables without updating Pony models, or vice-versa.
# models.py
class Product(db.Entity):
name = Required(str)
# Forgot to add 'description' here, but added it to DB manually, leading to mismatches.
✅ GOOD: Use db.generate_mapping(create_tables=True) for initial setup and Pony's editor or custom migration scripts for changes.
# Initial setup
db.generate_mapping(create_tables=True)
# For schema changes, either use Pony's online editor to generate SQL
# or write explicit migration scripts (e.g., using Alembic or custom logic)
# to apply changes and then update your Pony entity definitions.