ORM

Defining Models

The Model class allows you create ORM-style classes for your Airtable tables.

from pyairtable.orm import Model, fields as F

class Contact(Model):
    first_name = F.TextField("First Name")
    last_name = F.TextField("Last Name")
    email = F.EmailField("Email")
    is_registered = F.CheckboxField("Registered")
    company = F.LinkField("Company", Company, lazy=False)

    class Meta:
        base_id = "appaPqizdsNHDvlEm"
        table_name = "Contact"
        api_key = "keyapikey"

Once you have a model, you can create new objects to represent your Airtable records. Call save() to save the newly created object to the Airtable API.

>>> contact = Contact(
...     first_name="Mike",
...     last_name="McDonalds",
...     email="mike@mcd.com",
...     is_registered=False
... )
>>> assert contact.id is None
>>> contact.exists()
False
>>> assert contact.save()
>>> contact.exists()
True
>>> contact.id
'recS6qSLw0OCA6Xul'

You can read and modify attributes, then call save() when you’re ready to save your changes to the API.

>>> contact = Contact.from_id("recS6qSLw0OCA6Xul")
>>> assert contact.is_registered is False
>>> contact.is_registered = True
>>> contact.save()

To refresh a record from the API, use fetch():

>>> contact.is_registered = False
>>> contact.fetch()
>>> contact.is_registered
True

Finally, you can use delete() to delete the record:

>>> contact.delete()
True

There are also batch_save() and batch_delete() for when you need to create, modify, or delete several records at once:

>>> contacts = Contact.all()
>>> contacts.append(Contact(first_name="Alice", email="alice@example.com"))
>>> Contact.batch_save(contacts)
>>> Contact.batch_delete(contacts)

Supported Field Types

The following grid maps each of the supported field types in pyAirtable to the Airtable field type. Any field with a lock icon is read-only by default. For more information on how the Airtable API represents each of its field types, read Field types and cell values.

ORM field class

Airtable field type(s)

AITextField 🔒

AI Text

AttachmentsField

Attachments

AutoNumberField 🔒

Auto number

BarcodeField

Barcode

ButtonField 🔒

Button

CheckboxField

Checkbox

CollaboratorField

Collaborator

CountField 🔒

Count

CreatedByField 🔒

Created by

CreatedTimeField 🔒

Created time

CurrencyField

Currency

DateField

Date

DatetimeField

Date and time

DurationField

Duration

EmailField

Email

ExternalSyncSourceField 🔒

Sync source

FloatField

Number

IntegerField

Number

LastModifiedByField 🔒

Last modified by

LastModifiedTimeField 🔒

Last modified time

LinkField

Link to another record

LookupField 🔒

Lookup

MultipleCollaboratorsField

Multiple Collaborators

MultipleSelectField

Multiple select

NumberField

Number

PercentField

Percent

PhoneNumberField

Phone

RatingField

Rating

RichTextField

Rich text

SelectField

Single select

TextField

Single line text, Long text

UrlField

Url

Formulas, Rollups, and Lookups

The data type of “formula”, “rollup”, and “lookup” fields will depend on the underlying fields they reference, and pyAirtable cannot easily guess at those fields’ types.

If you need to refer to one of these fields in the ORM, you need to know what type of value you expect it to contain. You can then declare that as a read-only field:

from pyairtable.orm import fields as F

class MyTable(Model):
    class Meta: ...

    formula_field = F.TextField("My Formula", readonly=True)
    rollup_field = F.IntegerField("Row Count", readonly=True)
    lookup_field = F.LookupField[str]("My Lookup", readonly=True)

Note

LookupField will always return a list of values, even if there is only a single value shown in the Airtable UI.

Error Values

Airtable will return special values to represent errors from invalid formulas, division by zero, or other sorts of issues. These will be returned by the ORM as-is. Read more at Common formula errors and how to fix them.

You can check for errors using the is_airtable_error() function:

>>> record = MyTable.from_id("recyhb9UNkEMaZtYA")
>>> record.formula_field
{'error': '#ERROR!'}
>>> record.rollup_field
{'specialValue': 'NaN'}
>>> record.lookup_field
[{'error': '#ERROR!'}]
>>> from pyairtable.api.types import is_airtable_error
>>> is_airtable_error(record.formula_field)
True
>>> is_airtable_error(record.rollup_field)
True
>>> is_airtable_error(record.lookup_field[0])
True

Linked Records

In addition to standard data type fields, the LinkField class offers a special behaviour that can fetch linked records, so that you can traverse between related records.

from pyairtable.orm import Model, fields as F

class Company(Model):
    class Meta: ...

    name = F.TextField("Name")

class Person(Model):
    class Meta: ...

    name = F.TextField("Name")
    company = F.LinkField("Company", Company)
>>> person = Person.from_id("recZ6qSLw0OCA61ul")
>>> person.company
[<Company id='recqSk20OCrB13lZ7'>]
>>> person.company[0].name
'Acme Corp'

pyAirtable will not retrieve field values for a model’s linked records until the first time you access that field. So in the example above, the fields for Company were loaded when person.company was called for the first time. After that, the Company models are persisted, and won’t be refreshed until you call fetch().

Note

LinkField will always return a list of values, even if there is only a single value shown in the Airtable UI. It will not respect the prefersSingleRecordLink field configuration option, because the API will always return linked fields as a list of record IDs.

Comments

You can use Model.comments and Model.add_comment to interact with comments on a particular record, just like their Table equivalents:

>>> record = YourModel.from_id("recMNxslc6jG0XedV")
>>> comment = record.add_comment("Hello, @[usrVMNxslc6jG0Xed]!")
>>> record.comments()
[
    Comment(
        id='comdVMNxslc6jG0Xe',
        text='Hello, @[usrVMNxslc6jG0Xed]!',
        created_time='2023-06-07T17:46:24.435891',
        last_updated_time=None,
        mentioned={
            'usrVMNxslc6jG0Xed': Mentioned(
                display_name='Alice',
                email='alice@example.com',
                id='usrVMNxslc6jG0Xed',
                type='user'
            )
        },
        author=Collaborator(
            id='usr0000pyairtable',
            email='pyairtable@example.com',
            name='Your pyairtable access token'
        )
    )
]
>>> comment.text = "Never mind!"
>>> comment.save()
>>> record.comments()[0].text
'Never mind!'
>>> comment.delete()

ORM Limitations

Linked records don’t get saved automatically

pyAirtable will not attempt to recursively save any linked records. Because of this, you cannot save a record via ORM unless you’ve first created all of its linked records:

>>> alice = Person.from_id("recWcnG8712AqNuHw")
>>> alice.manager = [Person()]
>>> alice.save()
Traceback (most recent call last):
  ...
ValueError: Person.manager contains an unsaved record

Field values don’t get refreshed after saving a record

pyAirtable will not refresh models when calling save(), since certain field types (like LinkField) return lists of objects which you might not want pyAirtable to modify or discard. If you want to reload the values of all fields after saving (for example, to refresh the value of formula fields) then you need to call fetch().

For example:

class Person(Model):
    class Meta: ...

    name = F.TextField("Name")
    manager = F.LinkField["Person"]("Manager", "Person")
    # This field is a formula: {Manager} != BLANK()
    has_manager = F.IntegerField("Has Manager?", readonly=True)


bob = Person.from_id("rec2AqNuHwWcnG871")
assert bob.manager == []
assert bob.has_manager == 0

bob.manager = [alice]
bob.save()
assert bob.has_manager == 0

bob.fetch()
assert bob.has_manager == 1

Type annotations don’t account for possible formula errors

The ORM module does its best to give meaningful type annotations for each field. However, it is not feasible for the ORM to determine which fields’ underlying types might return an error code, and to annotate it accordingly.

Taking the same example as above…

class Person(Model):
    class Meta: ...

    name = F.TextField("Name")
    has_manager = F.IntegerField("Has Manager?", readonly=True)  # formula

…the type annotation of Person().has_manager will appear as int to mypy and to most type-aware code editors. It is nonetheless possible that if the formula becomes invalid, person.has_manager will return {'error': '#ERROR!'} (which is obviously not an int).

In most cases you probably want your code to fail quickly and loudly if there is an error value coming back from the Airtable API. In the unusual cases where you want to gracefully handle an error and move on, use is_airtable_error().