Skillversitas

Playhouse Utilities

Playhouse is Peewee's collection of utility modules - the functionality that is useful but not core, organized as importable extras alongside the migrate, pool, and signals modules the earlier chapters used. The extras cover the practical gaps: the ext module's convenience functions and field types, the csv_utils for the import/export case, the shortcuts for the one-line query helpers, the custom_fields for the value objects that map to a column, and the reflection for reading an existing schema. This chapter surveys the extras with the ones an app actually reaches for, and the rule for when a utility earns a place in the code.

The ext Module: Shortcuts and Extra Fields

from playhouse import ext

# convenience functions that wrap the common query shapes:
# ext.query(Model, **kwargs) - a select with keyword filters
rows = ext.query(Article, published=True, _order=-Article.views)

# the extra field types the core does not ship:
from playhouse.ext import *

class Profile(Model):
    # an IP address column (stored as a string, validated)
    last_ip = InetField(null=True)
    # a fixed-size decimal for money (the exact-arithmetic type)
    balance = DecimalField(max_digits=12, decimal_places=2,
                           default=0)
    # an array of values (the backend array type, where supported)
    roles = ArrayField(CharField, null=True)
    class Meta:
        database = db

The ext module is the grab-bag of the convenience layer: the query shortcut that builds a select from keyword filters (the _order and _limit kwargs shaping it), and the extra field types the core does not include - the InetField for an IP address, the DecimalField for the exact-arithmetic money column (the type the app should use for any currency value, because a FloatField is the rounding-bug factory), and the ArrayField for a backend array column. The DecimalField is the one to internalize: money is a Decimal in the database and a Decimal in Python, never a float, and the field is what keeps the column and the value type aligned. The shortcut functions are for the simple filter-and-order queries where the full expression is more typing than the logic deserves, and the escape is always the full select() when the query outgrows the kwargs.

Custom Fields: A Value Object as a Column

from playhouse.shortcuts import model_to_dict
from peewee import CharField

# a custom field: a Python value that maps to a column type,
# with its own db_value (what is stored) and python_value
# (what the model exposes)
class Point:
    def __init__(self, x, y):
        self.x, self.y = x, y
    def __repr__(self):
        return f"Point({self.x}, {self.y})"

# the pattern: a field that serializes the value to a string
# for storage and deserializes it back on read
from peewee import Field

class PointField(Field):
    def db_value(self, value):
        # the model has a Point; store it as "x,y"
        return f"{value.x},{value.y}" if value is not None else None

    def python_value(self, value):
        # the column has "x,y"; expose a Point
        if value is None:
            return None
        x, y = value.split(",")
        return Point(float(x), float(y))

class Marker(Model):
    position = PointField(null=True)
    class Meta:
        database = db

The custom field is the tool for the value object that lives in a column: a Point, a Money, a Color - a Python type that is not a native column type. The two methods are the contract: db_value(value) converts the model's value to what the column stores (the "x,y" string, the Decimal, the JSON text), and python_value(value) converts the column's stored value back to the Python type on read. The model exposes the rich type (marker.position.x), the database stores the simple one, and the field is the translation between them - the same separation as the property's backing store, applied to the column boundary. The JSONField the models chapter listed is itself an application of the pattern (the Python value serialized to a JSON string), which is the reference implementation to read when building a custom field. The rule: a custom field is for a value with a real type, not for a convenience - if the "conversion" is just a string format the caller could do, it belongs in the code, not the schema.

Reflection: Reading an Existing Schema

from playhouse.reflection import (read_database,
                                  object_list_from_database)

# read the tables that already exist in the database:
tables = read_database(db)
# a dict: table name -> {column name: column info}
print(list(tables.keys()))   # ['author', 'article', 'tag', ...]

# build MODEL CLASSES from the existing schema:
models = object_list_from_database(db)
# a list of Model subclasses, one per table, with the fields
# inferred from the columns - the ORM mapping of a legacy DB
for m in models:
    print(m.__name__, m._meta.table_name)

# the use: a database that predates the ORM, or a database
# owned by another tool - the models are generated, not written

Reflection is the reverse of the model-to-table mapping: instead of the model defining the schema, the schema defines the model. read_database(db) returns the database's tables and columns as a dict, and object_list_from_database(db) turns them into Model subclasses with the fields inferred from the columns - the ORM mapping of a database that already exists. The cases are the legacy database (the app is being added to a schema another tool owns) and the inspection (a developer reading what a database contains through the model language). The generated models are a starting point - the field types are inferred, the relationships are not, and the Meta is bare - so the reflection output is refined by hand (the FKs added, the types corrected) before it is the working model. The rule is the same as every generated code: it is the first draft, and the review is where the relationships and the intent go in.

The Working Rules

  • Reach for the utility when it matches the shape exactly - the DecimalField for money, the csv_utils for the file load, the query shortcut for the simple filter. A utility that needs contortion to fit is not the right tool; the core API is.
  • Money is a DecimalField, never a float - the exact-arithmetic column and the Decimal value are the pair that keeps the currency values correct.
  • A custom field is for a real value type - the db_value/python_value pair translates a Python object at the column boundary, and the JSONField is the reference for the pattern.
  • Reflection is a first draft - the generated models are the inferred mapping of an existing schema, refined by hand (the FKs, the types, the Meta) before they are the working model.
  • Import the module, not the namespace - from playhouse import ext and the specific names, so the code shows which utility is in use and the utility's version is the peewee version's.

Summary

Playhouse is the utility layer beside the core: migrate, pool, and signals (the earlier chapters) plus the extras - the ext module's query shortcut and its field types (the DecimalField for money, the InetField, the ArrayField), the custom-field pattern (db_value/python_value translating a value object at the column boundary, the JSONField as the reference), and the reflection tools (read_database, object_list_from_database) that map an existing schema into models as a first draft. The rule for every utility is the same: it earns its place when it matches the shape exactly - the DecimalField for the currency column, the reflection for the legacy database - and the core API is the answer when the utility needs contortion to fit. The extras are the practical layer that fills the gaps between the core's clean model and the mess the real databases and files have, and they are used by importing the specific module and names, so the code shows what it is relying on.