Postgres and CoackroachDB have a concept called schemas.

It’s a way of grouping the tables in a database. To learn more:

To specify a table’s schema, do the following:

class Band(Table, schema="music"):

# The table will be created in the `music` schema.
# The music schema will also be created if it doesn't already exist.
>>> await Band.create_table()

If the schema argument isn’t specified, then the table is created in the public schema.

Migration support

Schemas are fully supported in database migrations. For example, if we change the schema argument:

class Band(Table, schema="music_2"):

Then create an automatic migration and run it, then the table will be moved to the new schema:

>>> piccolo migrations new my_app --auto
>>> piccolo migrations forwards my_app


The SchemaManager class is used internally by Piccolo to interact with schemas. You may find it useful if you want to write a script to interact with schemas (create / delete / list etc).


Sometimes Piccolo needs a succinct representation of a row - for example, when displaying a link in the Piccolo Admin. Rather than just displaying the row ID, we can specify something more user friendly using Readable.

from piccolo.table import Table
from piccolo.columns import Varchar
from piccolo.columns.readable import Readable

class Band(Table, tablename="music_band"):
    name = Varchar(length=100)

    def get_readable(cls):
        return Readable(template="%s", columns=[])

Specifying the get_readable classmethod isn’t just beneficial for Piccolo tooling - you can also use it your own queries.


Here is an example of a more complex Readable.

class Band(Table, tablename="music_band"):
    name = Varchar(length=100)

    def get_readable(cls):
        return Readable(template="Band %s - %s", columns=[,])

As you can see, the template can include multiple columns, and can contain your own text.

Table Tags

Table subclasses can be given tags. The tags can be used for filtering, for example with table_finder.

class Band(Table, tags=["music"]):
    name = Varchar(length=100)


If you’re frequently defining the same columns over and over again, you can use mixins to reduce the amount of repetition.

from piccolo.columns import Varchar, Boolean
from piccolo.table import Table

class FavouriteMixin:
    favourite = Boolean(default=False)

class Manager(FavouriteMixin, Table):
    name = Varchar()


You can specify choices for a column, using Python’s Enum support.

from enum import Enum

from piccolo.columns import Varchar
from piccolo.table import Table

class Shirt(Table):
    class Size(str, Enum):
        small = 's'
        medium = 'm'
        large = 'l'

    size = Varchar(length=1, choices=Size)

We can then use the Enum in our queries.

>>> await Shirt(size=Shirt.Size.large).save()

>>> await
[{'id': 1, 'size': 'l'}]

Note how the value stored in the database is the Enum value (in this case 'l').

You can also use the Enum in where clauses, and in most other situations where a query requires a value.

>>> await Shirt.insert(
...     Shirt(size=Shirt.Size.small),
...     Shirt(size=Shirt.Size.medium)
... )

>>> await == Shirt.Size.small)
[{'id': 1, 'size': 's'}]


By using choices, you get the following benefits:

  • Signalling to other programmers what values are acceptable for the column.

  • Improved storage efficiency (we can store 'l' instead of 'large').

  • Piccolo Admin support

Array columns

You can also use choices with Array columns.

class Ticket(Table):
    class Extras(str, enum.Enum):
        drink = "drink"
        snack = "snack"
        program = "program"

    extras = Array(Varchar(), choices=Extras)

Note how you pass choices to Array, and not the base_column:

Array(Varchar(), choices=Extras)


We can then use the Enum in our queries:

>>> await Ticket.insert(
...     Ticket(extras=[Extras.drink, Extras.snack]),
...     Ticket(extras=[Extras.program]),
... )


This is a very advanced feature, which is only required for specialist use cases. Currently, just Postgres is supported.

Instead of writing your Table definitions in a file, Piccolo can dynamically create them at run time, by inspecting the database. These Table classes are then stored in memory, using a singleton object called TableStorage.

Some example use cases:

  • You have a very dynamic database, where new tables are being created constantly, so updating a is impractical.

  • You use Piccolo on the command line to explore databases.

Full reflection

Here’s an example, where we reflect the entire schema:

from piccolo.table_reflection import TableStorage

storage = TableStorage()
await storage.reflect(schema_name="music")

Table objects are accessible from TableStorage.tables:

>>> storage.tables
{"music.Band": <class 'Band'>, ... }

>>> Band = storage.tables["music.Band"]

Then you can use them like your normal Table classes:

>>> await
[{'id': 1, 'name': 'Pythonistas', 'manager': 1}, ...]

Partial reflection

Full schema reflection can be a heavy process based on the size of your schema. You can use include, exclude and keep_existing parameters of the reflect method to limit the overhead dramatically.

Only reflect the needed table(s):

from piccolo.table_reflection import TableStorage

storage = TableStorage()
await storage.reflect(schema_name="music", include=['band', ...])

Exclude table(s):

await storage.reflect(schema_name="music", exclude=['band', ...])

If you set keep_existing=True, only new tables on the database will be reflected and the existing tables in TableStorage will be left intact.

await storage.reflect(schema_name="music", keep_existing=True)


TableStorage has a helper method named get_table. If the table is already present in the TableStorage, this will return it and if the table is not present, it will be reflected and returned.

Band = storage.get_table(tablename='band')


Reflection will automatically create Table classes for referenced tables too. For example, if Table1 references Table2, then Table2 will automatically be added to TableStorage.

How to create custom column types

Sometimes, the column types shipped with Piccolo don’t meet your requirements, and you will need to define your own column types.

Generally there are two ways to define your own column types:

  • Create a subclass of an existing column type; or

  • Directly subclass the Column class.

Try to use the first method whenever possible because it is more straightforward and can often save you some work. Otherwise, subclass Column.


In this example, we create a column type called MyColumn, which is fundamentally an Integer type but has a custom attribute custom_attr:

from piccolo.columns import Integer

class MyColumn(Integer):
    def __init__(self, *args, custom_attr: str = '', **kwargs):
        self.custom_attr = custom_attr
        super().__init__(*args, **kwargs)

    def column_type(self):
        return 'INTEGER'


It is important to specify the column_type property, which tells the database engine the actual storage type of the custom column.

Now we can use MyColumn in our table:

from piccolo.table import Table

class MyTable(Table):
    my_col = MyColumn(custom_attr='foo')

And later we can retrieve the value of the attribute:

>>> MyTable.my_col.custom_attr