Skip to content

Markdown

Records and results

These are what reads and writes give you back, and what you pass to write a file.

The examples below use tasks, made as shown here, and app, which is qb.app("bq1234567"). Job Tasks has Title, Status, Hours, Due Date, Code, which is unique, and Attachment. It starts with three records, and each example starts from those again. A live test runs every example with both clients and checks its output.

from pyquickbase import Quickbase

with Quickbase(
    realm="acme", token="your_user_token"
) as qb:
    tasks = qb.app("bq1234567").table("Job Tasks")
from pyquickbase import AsyncQuickbase

async with AsyncQuickbase(
    realm="acme", token="your_user_token"
) as qb:
    tasks = qb.app("bq1234567").table("Job Tasks")

Read results

For the full picture, see Reading records.

Record

One record. Read values by field label or ID: record["Status"] or record[7].

Iterating yields labels, so dict(record) gives {label: value}. Date fields are date objects and timestamp fields are timezone-aware datetime objects, or None when empty. A user field holds a User, a multiuser field a list of them, and a file field a FileAttachment, or None when empty.

Attributes

  • id int
    The Record ID#. Queries always select it, but a report may not include it.

Example

task = tasks.get(
    1, select=["Title", "Status", "Due Date"]
)
print(task.id, task["Title"], task[7])
print(task["Due Date"])
print(sorted(label for label in task))
task = await tasks.get(
    1, select=["Title", "Status", "Due Date"]
)
print(task.id, task["Title"], task[7])
print(task["Due Date"])
print(sorted(label for label in task))

Output

1 Pour slab Open
2027-01-15
['Due Date', 'Record ID#', 'Status', 'Title']

RecordIterator

Yields records from a query, fetching further pages as needed.

A class rather than an async generator, so stopping early with break needs no cleanup.

Example

records = tasks.query(select=["Title"], max_total=2)
for task in records:
    print(task.id, task["Title"])
records = tasks.query(select=["Title"], max_total=2)
async for task in records:
    print(task.id, task["Title"])

Output

1 Pour slab
2 Frame walls

Write records to a CSV file

Write the remaining records to a CSV file as they're fetched, with a header row.

Dates and timestamps are ISO 8601, with a timestamp's offset. A user is their email, or their ID if the client's policy hides emails, a file is its latest version's name, and a list's values are separated by ;. rows_from_file() reads the file back.

Parameters

  • destination FileLike, required
    A path, or a file open for writing text.
  • spreadsheet_safe bool, default True
    Write text that a spreadsheet would run as a formula, which starts with =, +, -, or @, with a leading ', so opening the file can't run it. rows_from_file() removes it again.
  • encoding str, default 'utf-8-sig'
    The text encoding for a path. The default writes a byte order mark, which Excel needs to read UTF-8.

Returns int

How many records were written.

to_csv(
    destination: FileLike,
    *,
    spreadsheet_safe: bool = True,
    encoding: str = "utf-8-sig",
) -> int

Write records to an Excel file

Write the remaining records to an Excel file, with a header row. Needs pyquickbase[excel].

Dates and numbers are Excel's own, and a timestamp is written in UTC, as Excel has no time zones. Text is always text, so a value starting with = is never a formula. Values are written as for to_csv().

Parameters

  • destination FileLike, required
    A path, or a file open for writing bytes.
  • sheet str, default 'Records'
    The worksheet's name.

Returns int

How many records were written.

to_excel(
    destination: FileLike, *, sheet: str = "Records"
) -> int

Fetch records into pandas

Fetch the remaining records into a pandas DataFrame, with a column for each field.

A user field holds the user's email, or their ID if the client's policy hides emails, and a file field holds the latest version's file name. Needs pyquickbase[pandas].

Returns pandas.DataFrame

A DataFrame, which has the columns even when nothing matched.

to_pandas() -> pandas.DataFrame

Fetch records into polars

Fetch the remaining records into a polars DataFrame. See to_pandas(). Needs pyquickbase[polars].

Returns polars.DataFrame

A DataFrame, which has the columns even when nothing matched.

to_polars() -> polars.DataFrame

Page

One page of query results.

Attributes

  • records tuple[Record, ...]
    The Record objects on this page.
  • total int
    How many records match, not just the ones on this page.
  • skip int
    How many matching records came before this page.
  • columns tuple[str, ...]
    The labels of the fields it returned, in order, even when it has no records.
  • has_more bool
    Whether more matching records come after this page.
  • next_skip int
    The skip that fetches the following page.

Example

page = tasks.query_page(limit=2)
print(len(page.records), page.total, page.skip)
print(page.has_more, page.next_skip)
page = await tasks.query_page(limit=2)
print(len(page.records), page.total, page.skip)
print(page.has_more, page.next_skip)

Output

2 3 0
True 2

Write a page to a CSV file

Write this page's records to a CSV file, with a header row. See RecordIterator.to_csv().

Parameters

  • destination FileLike, required
    A path, or a file open for writing text.
  • spreadsheet_safe bool, default True
    Write text that a spreadsheet would run as a formula with a leading '.
  • encoding str, default 'utf-8-sig'
    The text encoding for a path.

Returns int

How many records were written.

to_csv(
    destination: FileLike,
    *,
    spreadsheet_safe: bool = True,
    encoding: str = "utf-8-sig",
) -> int

Write a page to an Excel file

Write this page's records to an Excel file, with a header row. See RecordIterator.to_excel(). Needs pyquickbase[excel].

Parameters

  • destination FileLike, required
    A path, or a file open for writing bytes.
  • sheet str, default 'Records'
    The worksheet's name.

Returns int

How many records were written.

to_excel(
    destination: FileLike, *, sheet: str = "Records"
) -> int

Turn a page into pandas

This page's records as a pandas DataFrame, with a column for each field returned.

A user field holds the user's email, or their ID if the client's policy hides emails, and a file field holds the latest version's file name. Needs pyquickbase[pandas].

Returns pandas.DataFrame

A DataFrame, which has the columns even when the page has no records.

to_pandas() -> pandas.DataFrame

Turn a page into polars

This page's records as a polars DataFrame. See to_pandas(). Needs pyquickbase[polars].

Returns polars.DataFrame

A DataFrame, which has the columns even when the page has no records.

to_polars() -> polars.DataFrame

User

A Quickbase user, as a user or multiuser field holds one.

repr leaves out the email address, because reprs end up in logs. Pass a User as a value to write it to a user field, or to compare with one in F.

Attributes

  • id str
    The user's ID.
  • name str
    The user's name, as Quickbase shows it.
  • email str | None
    The user's email address, or None if the client's policy leaves it out.
  • user_name str | None
    The user's Quickbase user name, if Quickbase sent one.

Example

from pyquickbase import User

task = tasks.get(1, select=["Record Owner"])
owner = task["Record Owner"]
print(isinstance(owner, User), bool(owner.email))
from pyquickbase import User

task = await tasks.get(1, select=["Record Owner"])
owner = task["Record Owner"]
print(isinstance(owner, User), bool(owner.email))

Output

True True

Write results

For the full picture, see Writing records.

WriteResult

What a write did. If rows fail, WriteError.result holds this, including what succeeded.

Attributes

  • created_ids tuple[int, ...]
    The Record ID# of each new record, in the order of the rows that succeeded. Use row_ids to match them to your rows.
  • updated_ids tuple[int, ...]
    The records whose values changed.
  • unchanged_ids tuple[int, ...]
    The records that already held these values.
  • errors tuple[RowError, ...]
    A RowError for each row that failed.
  • row_ids tuple[int | None, ...]
    For insert(), the new Record ID# of each row, in the order of your rows, or None for a row that failed. It stops short at a batch that wasn't sent, or whose response can't be matched to its rows, so compare its length with your rows. Empty for update() and upsert().

Example

result = tasks.upsert(
    [
        {"Code": "T-1", "Status": "Open"},
        {"Code": "T-2", "Status": "Done"},
        {"Code": "T-9", "Title": "Inspect site"},
    ],
    merge_on="Code",
)
print(result.created_ids)
print(result.updated_ids)
print(result.unchanged_ids)
print(result.errors)
result = await tasks.upsert(
    [
        {"Code": "T-1", "Status": "Open"},
        {"Code": "T-2", "Status": "Done"},
        {"Code": "T-9", "Title": "Inspect site"},
    ],
    merge_on="Code",
)
print(result.created_ids)
print(result.updated_ids)
print(result.unchanged_ids)
print(result.errors)

Output

(4,)
(2,)
(1,)
()

RowError

Why one row failed.

Attributes

  • index int
    The row's position in the rows you passed, starting from 0.
  • messages tuple[str, ...]
    Quickbase's wording, which can change, so use reason to decide what to do. For a rule failure, this holds the rule's message.
  • reason Literal['duplicate', 'permission', 'invalid_choice', 'rule', 'other']
    duplicate for a value already in a unique field, permission for a refusal by the token's role, invalid_choice for a value outside a field's choices, rule for a refusal by one of the table's custom data rules, and other for anything else, including wording this version doesn't recognize.
  • field_id int | None
    The field Quickbase named, for duplicate and invalid_choice.

Example

from pyquickbase import WriteError

try:
    tasks.insert(
        [
            {"Title": "New", "Code": "T-8"},
            {"Title": "Repeat", "Code": "T-1"},
        ]
    )
except WriteError as error:
    print(error.result.created_ids)
    for failure in error.result.errors:
        print(failure.index, failure.reason, failure.field_id)
from pyquickbase import WriteError

try:
    await tasks.insert(
        [
            {"Title": "New", "Code": "T-8"},
            {"Title": "Repeat", "Code": "T-1"},
        ]
    )
except WriteError as error:
    print(error.result.created_ids)
    for failure in error.result.errors:
        print(failure.index, failure.reason, failure.field_id)

Output

(4,)
1 duplicate 10

Files

For the full picture, see File attachments.

FileUpload

A file to write to a file attachment field, as a value in insert(), update(), or upsert().

repr leaves out the contents, because they may be sensitive and reprs end up in logs.

Parameters

  • file_name str, required
    The file's name. It can't contain a path or control characters, because Quickbase stores them as sent and could pass them to whoever downloads the file later.
  • data bytes, required
    The file's contents.

Raises

FileUpload(file_name: str, data: bytes)

Example

from pyquickbase import FileUpload

upload = FileUpload("spec.txt", b"Pour on Monday")
result = tasks.update(
    [{"Record ID#": 1, "Attachment": upload}]
)
print(result.updated_ids)
from pyquickbase import FileUpload

upload = FileUpload("spec.txt", b"Pour on Monday")
result = await tasks.update(
    [{"Record ID#": 1, "Attachment": upload}]
)
print(result.updated_ids)

Output

(1,)

FileDownload

A file downloaded from a file attachment field.

repr leaves out the contents, because they may be sensitive and reprs end up in logs.

Attributes

  • file_name str
    The file's name. Quickbase stores names as sent, such as ../../x, so don't use this as a path without cleaning it.
  • data bytes
    The file's contents.
  • content_type str | None
    The media type, if reported. It's based on the name rather than the contents, so it doesn't tell you what's in the file.

Example

from pyquickbase import FileUpload

upload = FileUpload("spec.txt", b"Pour on Monday")
tasks.update([{"Record ID#": 1, "Attachment": upload}])

download = tasks.download_file(1, "Attachment")
print(download.file_name, download.data)
from pyquickbase import FileUpload

upload = FileUpload("spec.txt", b"Pour on Monday")
await tasks.update([{"Record ID#": 1, "Attachment": upload}])

download = await tasks.download_file(1, "Attachment")
print(download.file_name, download.data)

Output

spec.txt b'Pour on Monday'

FileAttachment

A file attachment field's value, which holds each version Quickbase keeps.

An empty field reads as None, so a FileAttachment always has at least one version. Download the contents with table.download_file().

Attributes

  • url str
    The file's path in Quickbase, relative to the realm.
  • versions tuple[FileVersion, ...]
    Each version Quickbase keeps, oldest first.
  • latest FileVersion
    The newest version.

Example

from pyquickbase import FileUpload

upload = FileUpload("plan.txt", b"v1")
tasks.update([{"Record ID#": 2, "Attachment": upload}])
task = tasks.get(2, select=["Attachment"])
attachment = task["Attachment"]
print(attachment.latest.file_name, len(attachment.versions))
from pyquickbase import FileUpload

upload = FileUpload("plan.txt", b"v1")
await tasks.update([{"Record ID#": 2, "Attachment": upload}])
task = await tasks.get(2, select=["Attachment"])
attachment = task["Attachment"]
print(attachment.latest.file_name, len(attachment.versions))

Output

plan.txt 1

FileVersion

One version of a file in a file attachment field.

Attributes

  • number int
    The version's number, starting at 1. Numbers aren't reused after a delete.
  • file_name str
    The file's name. Quickbase stores names as sent, such as ../../x, so don't use this as a path without cleaning it.
  • uploaded datetime.datetime | None
    When it was uploaded, or None if Quickbase didn't say.
  • creator User | None
    Who uploaded it, or None if the client's policy leaves it out.

Example

from pyquickbase import FileUpload

upload = FileUpload("notes.txt", b"v1")
tasks.update([{"Record ID#": 3, "Attachment": upload}])
task = tasks.get(3, select=["Attachment"])
latest = task["Attachment"].latest
print(latest.number, latest.file_name)
from pyquickbase import FileUpload

upload = FileUpload("notes.txt", b"v1")
await tasks.update([{"Record ID#": 3, "Attachment": upload}])
task = await tasks.get(3, select=["Attachment"])
latest = task["Attachment"].latest
print(latest.number, latest.file_name)

Output

1 notes.txt

CSV and Excel files

For the full picture, see CSV and Excel files.

FileRows

A file's rows, converted for a table's fields, from Table.rows_from_file().

Pass it to insert(), update(), upsert(), or preview_update(), which refuse it, writing nothing, while it has errors. Iterating it gives its rows.

repr shows only its size, because values may be sensitive and reprs end up in logs.

Attributes

  • rows tuple[Mapping[str, Any], ...]
    Each row that has a value, as {field label: value}. A blank cell is None, which leaves a field empty in insert(), and clears it in update() and upsert().
  • row_numbers tuple[int, ...]
    Each row's number in the file, as for FileRowError.row, so rows[0] came from row row_numbers[0]. A row whose cells are all blank is left out.
  • columns Mapping[str, str]
    Which field each column was read into, as {column: field label}.
  • skipped tuple[str, ...]
    The columns that weren't read.
  • errors tuple[FileRowError, ...]
    Every value that can't be written. Fix these in the file, or map or skip their columns.
  • ok bool
    Whether every value can be written.

Example

import io

text = io.StringIO('Title,Hours\nA,"1,234.5"\nB,lots\n')
rows = tasks.rows_from_file(text)
print(rows.rows[0]["Hours"], rows.ok)
print(rows.columns)
import io

text = io.StringIO('Title,Hours\nA,"1,234.5"\nB,lots\n')
rows = await tasks.rows_from_file(text)
print(rows.rows[0]["Hours"], rows.ok)
print(rows.columns)

Output

1234.5 False
{'Title': 'Title', 'Hours': 'Hours'}

Refuse a file with errors

Raise InvalidValueError listing the first errors, if there are any.

Raises

raise_for_errors() -> None

FileRowError

A value in a file that can't be written to its field.

Attributes

  • row int
    The row's number as a spreadsheet shows it, counting the header as row 1.
  • column str
    The file's column.
  • message str
    What's wrong, which never repeats the value.

Example

import io

text = io.StringIO("Title,Due Date\nA,next week\n")
rows = tasks.rows_from_file(text)
for error in rows.errors:
    print(error.row, error.column)
    print(error.message)
import io

text = io.StringIO("Title,Due Date\nA,next week\n")
rows = await tasks.rows_from_file(text)
for error in rows.errors:
    print(error.row, error.column)
    print(error.message)

Output

2 Due Date
isn't a date in the form YYYY-MM-DD. Set date_format for another.

FileFormat

A file's format: csv, including tab-separated files, or xlsx.

FileFormat = Literal["csv", "xlsx"]

FileLike

A path, or an open file: text for CSV, and binary for Excel.

FileLike = str | os.PathLike[str] | IO[str] | IO[bytes]

ColumnMap

Which field each file column goes in, by label or ID, or None to skip it.

ColumnMap = Mapping[str, str | int | None]

Formula results

For the full picture, see Formulas.

FormulaResult

The result of table.run_formula().

Attributes

  • text str
    Exactly what Quickbase sent, which is always a string.
  • type str
    Quickbase's name for its type, such as number, bool, or date.
  • value Any
    The result as a Python value, converted only where that's safe. A bool becomes True or False, and a number becomes a Decimal, or None if either is empty. A null becomes None. Anything else stays as text, including dates and timestamps, because they use the app's display format and have no time zone.

Example

result = tasks.run_formula("1 + 1")
print(result.type, result.text, result.value)
result = await tasks.run_formula("1 + 1")
print(result.type, result.text, result.value)

Output

number 2 2

Previews of writes

For the full picture, see Writing records.

UpdatePreview

What update() would do if the records stayed as they are now.

Attributes

  • changes tuple[RecordChange, ...]
    The records that would change, in the order of your rows.
  • unchanged_ids tuple[int, ...]
    The records that already hold these values.
  • missing_ids tuple[int, ...]
    The records that don't exist, or that the token can't see. update() would report these as failed rows.
  • events tuple[AppEvent, ...] | None
    The active webhooks, Quickbase actions, email notifications, and automations on the table, which the update may set off. Quickbase doesn't say which changes set each one off, so some may not run. None if they couldn't be read.

Example

preview = tasks.preview_update(
    [
        {"Record ID#": 1, "Status": "Done"},
        {"Record ID#": 2, "Status": "Open"},
        {"Record ID#": 999, "Status": "Done"},
    ]
)
print(len(preview.changes))
print(preview.unchanged_ids, preview.missing_ids)
preview = await tasks.preview_update(
    [
        {"Record ID#": 1, "Status": "Done"},
        {"Record ID#": 2, "Status": "Open"},
        {"Record ID#": 999, "Status": "Done"},
    ]
)
print(len(preview.changes))
print(preview.unchanged_ids, preview.missing_ids)

Output

1
(2,) (999,)

DeletePreview

What delete() would delete: a page of the matching records, and what may run.

Attributes

  • records tuple[Record, ...]
    The first records that would be deleted.
  • total int
    How many records would be deleted.
  • skip int
    How many matching records came before this page.
  • columns tuple[str, ...]
    The labels of the fields it returned, in order.
  • events tuple[AppEvent, ...] | None
    The active webhooks, Quickbase actions, email notifications, and automations on the table, which deleting records may set off. Quickbase doesn't say which changes set each one off, so some may not run. None if they couldn't be read.

Example

from pyquickbase import F

preview = tasks.preview_delete(F("Status") == "Done")
print(preview.total, preview.events)
from pyquickbase import F

preview = await tasks.preview_delete(F("Status") == "Done")
print(preview.total, preview.events)

Output

1 ()

RecordChange

What a write would change on one record.

Attributes

  • record_id int
    The record's Record ID#.
  • fields Mapping[str, FieldChange]
    What would change, as {field label: FieldChange}.

Example

preview = tasks.preview_update(
    [{"Record ID#": 1, "Status": "Done"}]
)
change = preview.changes[0]
print(change.record_id, sorted(change.fields))
preview = await tasks.preview_update(
    [{"Record ID#": 1, "Status": "Done"}]
)
change = preview.changes[0]
print(change.record_id, sorted(change.fields))

Output

1 ['Status']

FieldChange

One field's value before and after a write, with an empty field as None.

repr hides both values, because they may be sensitive and reprs end up in logs.

Attributes

  • before Any
    The value now. A file field shows the file's name, never its contents.
  • after Any
    The value the write would set. A file field shows the file's name here too.

Example

preview = tasks.preview_update(
    [{"Record ID#": 1, "Status": "Done"}]
)
status = preview.changes[0].fields["Status"]
print(status.before, status.after)
preview = await tasks.preview_update(
    [{"Record ID#": 1, "Status": "Done"}]
)
status = preview.changes[0].fields["Status"]
print(status.before, status.after)

Output

Open Done

Summaries

For the full picture, see Reading records.

SummaryRow

One group of records, from table.summarize(). Values are keyed by field label.

repr shows only the row's shape, because values may be sensitive and reprs end up in logs.

Attributes

  • group Mapping[str, Any]
    The values that define this group, or empty without group_by. An empty value is None, and a user is their email.
  • count int
    How many records are in the group, including those with empty values.
  • sum Mapping[str, float]
    The sum of each sum field, which is 0 if every value is empty.
  • avg Mapping[str, float | None]
    The average of each avg field, or None if every value is empty.
  • median Mapping[str, float | None]
    The middle value of each median field, or the average of the two middle ones, or None if every value is empty.
  • std_dev Mapping[str, float | None]
    The sample standard deviation of each std_dev field, or None with fewer than two values.
  • min Mapping[str, Any]
    The lowest value of each min field, or None if every value is empty.
  • max Mapping[str, Any]
    The highest value of each max field, or None if every value is empty.
  • distinct_count Mapping[str, int]
    How many different values each distinct_count field has, leaving out empty ones.
  • change PeriodChange | None
    With compare_periods, how the group changed since the period before. None for the first period, and without compare_periods.

Example

rows = tasks.summarize(
    "Status", sum="Hours", median="Hours"
)
for row in rows:
    print(row.group["Status"], row.count)
    print(row.sum["Hours"], row.median["Hours"])
rows = await tasks.summarize(
    "Status", sum="Hours", median="Hours"
)
for row in rows:
    print(row.group["Status"], row.count)
    print(row.sum["Hours"], row.median["Hours"])

Output

Done 1
6.0 6.0
Open 2
20.0 10.0

Get one statistic's values

One statistic's values, by field label, such as row.statistic("sum").

Parameters

  • name StatisticName, required
    The statistic, such as sum or median.

Returns Mapping[str, Any]

Its values, by field label, which is empty if the summary didn't ask for it.

statistic(name: StatisticName) -> Mapping[str, Any]

PeriodChange

How a group changed since the period before it, from summarize(compare_periods=True).

Attributes

  • previous str
    The period it's compared with, such as 2026-02 for 2026-03.
  • count int
    How many more records there are, which is negative for fewer.
  • values Mapping[str, Mapping[str, float | None]]
    How much each numeric statistic rose, as {"sum": {"Hours": 4.0}}, or None where either period has no value. A period without records counts as 0 for sum and distinct_count, and has no value for the others.

Example

from pyquickbase import Period

rows = tasks.summarize(
    Period("Due Date", "month"),
    sum="Hours",
    compare_periods=True,
)
for row in rows:
    if row.change:
        print(row.group["Due Date"], row.change.previous)
        print(row.change.count, row.change.values["sum"])
from pyquickbase import Period

rows = await tasks.summarize(
    Period("Due Date", "month"),
    sum="Hours",
    compare_periods=True,
)
for row in rows:
    if row.change:
        print(row.group["Due Date"], row.change.previous)
        print(row.change.count, row.change.values["sum"])

Output

2027-02 2027-01
0 {'Hours': 4.0}

Period

Group by a date or timestamp field's day, week, month, quarter, or year.

Groups are labeled 2026-03-14, 2026-W11 (an ISO week), 2026-03, 2026-Q1, and 2026, so they sort in order.

Parameters

  • field str | int | F, required
    The date or timestamp field, by label, ID, or field object.
  • unit PeriodUnit, required
    day, week, month, quarter, or year.
  • tz datetime.tzinfo, default datetime.timezone.utc
    The time zone that decides a timestamp's date. It has no effect on a date field.

Raises

  • QueryError: unit isn't one of these, or field isn't a field.
Period(
    field: str | int | F,
    unit: PeriodUnit,
    tz: datetime.tzinfo = datetime.timezone.utc,
)

Example

from pyquickbase import Period

rows = tasks.summarize(Period("Due Date", "month"))
for row in rows:
    print(row.group["Due Date"], row.count)
from pyquickbase import Period

rows = await tasks.summarize(Period("Due Date", "month"))
for row in rows:
    print(row.group["Due Date"], row.count)

Output

2027-01 1
2027-02 1
None 1

PeriodUnit

How much time each group of a Period covers.

PeriodUnit = Literal[
    "day", "week", "month", "quarter", "year"
]

StatisticName

A value summarize() works out from one field's values in each group.

StatisticName = Literal[
    "sum",
    "avg",
    "median",
    "std_dev",
    "min",
    "max",
    "distinct_count",
]

SummaryGroups

What Table.summarize() can group by.

SummaryGroups = (
    str | int | F | Period | Sequence[str | int | F | Period]
)